From 1860affd6960986cc8508748c97650133298f954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Sat, 21 Feb 2026 03:28:58 +0800 Subject: [PATCH] =?UTF-8?q?api=20=E7=BB=93=E6=9E=84=E5=A4=A7=E7=B2=BE?= =?UTF-8?q?=E7=AE=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/{delete-logs => admin/log}/route.ts | 2 +- .../restore-recommendations-seed/route.ts | 2 +- src/app/api/home/route.ts | 74 +++++++ .../preorders/refresh-order-counts/route.ts | 190 ++++++++++++++++++ src/app/api/public/hero-slider/route.ts | 67 ------ src/app/api/public/homepage/route.ts | 131 ------------ .../public/product-recommendations/route.ts | 100 --------- src/app/api/public/products/[id]/route.ts | 111 ---------- src/app/api/public/products/route.ts | 120 ----------- src/app/api/refresh-order-counts/route.ts | 163 --------------- .../batch-medusa}/route.ts | 5 +- .../api/{sync-medusa => sync/medusa}/route.ts | 10 +- .../{sync-product => sync/product}/route.ts | 2 +- .../fields/RefreshOrderCountField.tsx | 2 +- .../seed/RestoreRecommendationsSeedButton.tsx | 2 +- .../sync/RefreshOrderCountButton.tsx | 4 +- src/components/sync/UnifiedSyncButton.tsx | 6 +- src/components/views/LogsManagerView.tsx | 2 +- src/endpoints/homepage.ts | 72 ------- src/payload.config.ts | 2 - 20 files changed, 282 insertions(+), 785 deletions(-) rename src/app/api/{delete-logs => admin/log}/route.ts (97%) rename src/app/api/{ => admin}/restore-recommendations-seed/route.ts (96%) create mode 100644 src/app/api/home/route.ts create mode 100644 src/app/api/preorders/refresh-order-counts/route.ts delete mode 100644 src/app/api/public/hero-slider/route.ts delete mode 100644 src/app/api/public/homepage/route.ts delete mode 100644 src/app/api/public/product-recommendations/route.ts delete mode 100644 src/app/api/public/products/[id]/route.ts delete mode 100644 src/app/api/public/products/route.ts delete mode 100644 src/app/api/refresh-order-counts/route.ts rename src/app/api/{batch-sync-medusa => sync/batch-medusa}/route.ts (95%) rename src/app/api/{sync-medusa => sync/medusa}/route.ts (98%) rename src/app/api/{sync-product => sync/product}/route.ts (99%) delete mode 100644 src/endpoints/homepage.ts diff --git a/src/app/api/delete-logs/route.ts b/src/app/api/admin/log/route.ts similarity index 97% rename from src/app/api/delete-logs/route.ts rename to src/app/api/admin/log/route.ts index 68dc5e4..83dfb18 100644 --- a/src/app/api/delete-logs/route.ts +++ b/src/app/api/admin/log/route.ts @@ -4,7 +4,7 @@ import { NextRequest } from 'next/server' /** * 批量删除日志 API - * DELETE /api/delete-logs?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD + * DELETE /api/admin/log?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD */ export async function DELETE(req: NextRequest) { try { diff --git a/src/app/api/restore-recommendations-seed/route.ts b/src/app/api/admin/restore-recommendations-seed/route.ts similarity index 96% rename from src/app/api/restore-recommendations-seed/route.ts rename to src/app/api/admin/restore-recommendations-seed/route.ts index 4c29652..7b02227 100644 --- a/src/app/api/restore-recommendations-seed/route.ts +++ b/src/app/api/admin/restore-recommendations-seed/route.ts @@ -4,7 +4,7 @@ import config from '@payload-config' /** * API Route: Restore Product Recommendations from Seed - * POST /api/restore-recommendations-seed + * POST /api/admin/restore-recommendations-seed * * This server-side route uses Payload's local API to update the global config * which requires proper authentication context that client-side fetch doesn't have. diff --git a/src/app/api/home/route.ts b/src/app/api/home/route.ts new file mode 100644 index 0000000..1512d92 --- /dev/null +++ b/src/app/api/home/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' + +/** + * GET /api/home + * 获取首页所有数据:公告 + Hero Slider + 产品推荐 + */ +export async function GET(req: NextRequest) { + try { + const payload = await getPayload({ config }) + + // 获取首页公告(已发布且在首页显示) + const announcements = await payload.find({ + collection: 'announcements', + where: { + and: [ + { + status: { + equals: 'published', + }, + }, + { + showOnHomepage: { + equals: true, + }, + }, + ], + }, + sort: '-priority', + limit: 10, + }) + + // 获取 Hero Slider + const heroSlider = await payload.findGlobal({ + slug: 'hero-slider', + }) + + // 获取产品推荐 + const productRecommendations = await payload.findGlobal({ + slug: 'product-recommendations', + }) + + // 构建响应数据 + const response = { + announcements: announcements.docs.map((announcement) => ({ + id: announcement.id, + title: announcement.title, + type: announcement.type, + summary: announcement.summary, + priority: announcement.priority, + publishedAt: announcement.publishedAt, + })), + heroSlider: { + slides: heroSlider.slides || [], + }, + productRecommendations: { + enabled: productRecommendations.enabled || false, + lists: productRecommendations.lists || [], + }, + } + + return NextResponse.json(response, { status: 200 }) + } catch (error: any) { + console.error('Error fetching homepage data:', error) + return NextResponse.json( + { + error: 'Failed to fetch homepage data', + message: error.message, + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/preorders/refresh-order-counts/route.ts b/src/app/api/preorders/refresh-order-counts/route.ts new file mode 100644 index 0000000..9cd1c8f --- /dev/null +++ b/src/app/api/preorders/refresh-order-counts/route.ts @@ -0,0 +1,190 @@ +import { NextRequest, NextResponse } from 'next/server' +import payload from 'payload' + +const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' +const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || '' + +/** + * 刷新预购商品的订单计数 + * POST /api/preorders/refresh-order-counts + * + * Body: + * - productIds?: string[] - 要刷新的商品 ID 列表(Payload ID) + * - refreshAll?: boolean - 是否刷新所有预购商品 + */ +export async function POST(req: NextRequest) { + try { + const body = await req.json() + const { productIds, refreshAll } = body + + if (!productIds && !refreshAll) { + return NextResponse.json( + { success: false, error: '请提供 productIds 或设置 refreshAll' }, + { status: 400 } + ) + } + + let products: any[] = [] + + if (refreshAll) { + // 获取所有预购商品 + const result = await payload.find({ + collection: 'preorder-products', + limit: 1000, + where: { + medusaId: { + exists: true, + }, + }, + }) + products = result.docs + } else { + // 获取指定的商品 + const result = await payload.find({ + collection: 'preorder-products', + limit: productIds.length, + where: { + id: { + in: productIds, + }, + medusaId: { + exists: true, + }, + }, + }) + products = result.docs + } + + if (products.length === 0) { + return NextResponse.json( + { success: false, error: '没有找到要刷新的商品' }, + { status: 404 } + ) + } + + // 统计更新结果 + let successCount = 0 + let failCount = 0 + const errors: string[] = [] + + // 为每个商品刷新订单计数 + for (const product of products) { + try { + const medusaId = product.medusaId + + // 从 Medusa 获取订单数据 + const response = await fetch( + `${MEDUSA_BACKEND_URL}/admin/orders?product_id=${medusaId}&limit=1000`, + { + headers: { + 'x-medusa-access-token': MEDUSA_ADMIN_API_KEY, + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + const orders = data.orders || [] + + // 计算真实订单数 + let realOrderCount = 0 + + // 筛选有效订单(排除取消的) + const validOrders = orders.filter((order: any) => + order.status !== 'canceled' && order.payment_status !== 'not_paid' + ) + + // 遍历有效订单,统计该商品的数量 + for (const order of validOrders) { + const items = order.items || [] + for (const item of items) { + if (item.product_id === medusaId) { + realOrderCount += item.quantity || 1 + } + } + } + + // 更新 Payload 中的订单计数 + await payload.update({ + collection: 'preorder-products', + id: product.id, + data: { + orderCount: realOrderCount, + }, + }) + + successCount++ + } catch (error) { + console.error(`刷新商品 ${product.id} (${product.title}) 失败:`, error) + failCount++ + errors.push(`${product.title}: ${error instanceof Error ? error.message : '未知错误'}`) + } + } + + return NextResponse.json({ + success: true, + message: `刷新完成: ${successCount} 个成功, ${failCount} 个失败`, + successCount, + failCount, + errors, + }) + } catch (error) { + console.error('刷新订单计数失败:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : '未知错误', + }, + { status: 500 } + ) + } +} + +/** + * 获取单个商品的订单计数(不更新数据库) + * GET /api/preorders/refresh-order-counts?productId=xxx + */ +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const productId = searchParams.get('productId') + + if (!productId) { + return NextResponse.json( + { success: false, error: '请提供 productId' }, + { status: 400 } + ) + } + + const product = await payload.findByID({ + collection: 'preorder-products', + id: productId, + }) + + if (!product || !product.medusaId) { + return NextResponse.json( + { success: false, error: '商品不存在或没有 Medusa ID' }, + { status: 404 } + ) + } + + return NextResponse.json({ + success: true, + currentCount: product.orderCount || 0, + medusaId: product.medusaId, + }) + } catch (error) { + console.error('获取订单计数失败:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : '未知错误', + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/public/hero-slider/route.ts b/src/app/api/public/hero-slider/route.ts deleted file mode 100644 index 805dc03..0000000 --- a/src/app/api/public/hero-slider/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getPayload } from 'payload' -import config from '@payload-config' -import { getCache, setCache } from '@/lib/redis' - -/** - * GET /api/public/hero-slider - * 获取首页幻灯片数据(带缓存) - * 需要 x-store-api-key 验证 - */ -export async function GET(req: NextRequest) { - try { - // 验证 API Key - const apiKey = req.headers.get('x-store-api-key') - const validApiKey = process.env.PAYLOAD_API_KEY - - if (!apiKey || !validApiKey || apiKey !== validApiKey) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) - } - - // 生成缓存 key - const cacheKey = 'hero-slider:data' - - // 尝试从缓存获取 - const cached = await getCache(cacheKey) - if (cached) { - return NextResponse.json({ - success: true, - data: cached, - cached: true, - }) - } - - // 从数据库获取 - const payload = await getPayload({ config }) - const result = await payload.findGlobal({ - slug: 'hero-slider' as any, - depth: 2, // 填充图片关联数据 - }) - - // 获取所有幻灯片 - const slides = (result as any).slides || [] - - const responseData = { - slides, - totalSlides: slides.length, - } - - // 缓存结果(1 小时) - await setCache(cacheKey, responseData, 3600) - - return NextResponse.json({ - success: true, - data: responseData, - cached: false, - }) - } catch (error) { - console.error('Hero slider API error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch hero slider', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/public/homepage/route.ts b/src/app/api/public/homepage/route.ts deleted file mode 100644 index aecc623..0000000 --- a/src/app/api/public/homepage/route.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getPayload } from 'payload' -import config from '@payload-config' -import { getCache, setCache } from '@/lib/redis' - -/** - * GET /api/public/homepage - * 获取首页所有数据:幻灯片 + 商品推荐列表(带缓存) - * - * 这个 API 组合了多个数据源,提供完整的首页内容 - */ -export async function GET(req: NextRequest) { - try { - // 生成缓存 key - const cacheKey = 'homepage:data' - - // 尝试从缓存获取 - const cached = await getCache(cacheKey) - if (cached) { - return NextResponse.json({ - success: true, - data: cached, - cached: true, - }) - } - - // 从数据库获取数据 - const payload = await getPayload({ config }) - - // 并行获取所有数据 - const [heroSliderResult, productRecommendationsResult] = await Promise.all([ - // 获取幻灯片数据 - payload - .findGlobal({ - slug: 'hero-slider' as any, - depth: 2, - }) - .catch((error) => { - console.error('Failed to fetch hero slider:', error) - return null - }), - - // 获取商品推荐数据 - payload - .findGlobal({ - slug: 'product-recommendations' as any, - depth: 3, - }) - .catch((error) => { - console.error('Failed to fetch product recommendations:', error) - return null - }), - ]) - - // 处理幻灯片数据 - let heroSlider = { - slides: [], - totalSlides: 0, - } - - if (heroSliderResult) { - const slides = (heroSliderResult as any).slides || [] - - heroSlider = { - slides, - totalSlides: slides.length, - } - } - - // 处理商品推荐数据 - let productRecommendations = { - enabled: false, - lists: [], - totalLists: 0, - } - - if (productRecommendationsResult && (productRecommendationsResult as any).enabled) { - // 获取所有列表 - const lists = (productRecommendationsResult as any).lists || [] - - // 处理每个列表 - const processedLists = lists.map((list: any) => { - const products = Array.isArray(list.products) - ? list.products.filter((product: any) => product && product.status === 'published') - : [] - - return { - id: list.id, - title: list.title, - subtitle: list.subtitle, - products, - totalProducts: products.length, - } - }) - - productRecommendations = { - enabled: (productRecommendationsResult as any).enabled, - lists: processedLists, - totalLists: processedLists.length, - } - } - - // 组合响应数据 - const responseData = { - heroSlider, - productRecommendations, - meta: { - timestamp: new Date().toISOString(), - version: '1.0', - }, - } - - // 缓存结果(1 小时) - await setCache(cacheKey, responseData, 3600) - - return NextResponse.json({ - success: true, - data: responseData, - cached: false, - }) - } catch (error) { - console.error('Homepage API error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch homepage data', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/public/product-recommendations/route.ts b/src/app/api/public/product-recommendations/route.ts deleted file mode 100644 index 674db0e..0000000 --- a/src/app/api/public/product-recommendations/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getPayload } from 'payload' -import config from '@payload-config' -import { getCache, setCache } from '@/lib/redis' - -/** - * GET /api/public/product-recommendations - * 获取商品推荐列表数据(带缓存) - * 需要 x-store-api-key 验证 - */ -export async function GET(req: NextRequest) { - try { - // 验证 API Key - const apiKey = req.headers.get('x-store-api-key') - const validApiKey = process.env.PAYLOAD_API_KEY - - if (!apiKey || !validApiKey || apiKey !== validApiKey) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) - } - - // 生成缓存 key - const cacheKey = 'product-recommendations:data' - - // 尝试从缓存获取 - const cached = await getCache(cacheKey) - if (cached) { - return NextResponse.json({ - success: true, - data: cached, - cached: true, - }) - } - - // 从数据库获取 - const payload = await getPayload({ config }) - const result = await payload.findGlobal({ - slug: 'product-recommendations' as any, - depth: 3, // 填充商品和图片关联数据 - }) - - // 如果功能未启用,返回空数据 - if (!(result as any).enabled) { - const emptyData = { - enabled: false, - lists: [], - } - - // 缓存空数据(较短时间) - await setCache(cacheKey, emptyData, 600) // 10 分钟 - - return NextResponse.json({ - success: true, - data: emptyData, - cached: false, - }) - } - - // 获取所有列表 - const lists = (result as any).lists || [] - - // 处理每个列表 - const processedLists = lists.map((list: any) => { - const products = Array.isArray(list.products) - ? list.products.filter((product: any) => product && product.status === 'published') - : [] - - return { - id: list.id, - title: list.title, - subtitle: list.subtitle, - products, - totalProducts: products.length, - } - }) - - const responseData = { - enabled: (result as any).enabled, - lists: processedLists, - totalLists: processedLists.length, - } - - // 缓存结果(1 小时) - await setCache(cacheKey, responseData, 3600) - - return NextResponse.json({ - success: true, - data: responseData, - cached: false, - }) - } catch (error) { - console.error('Product recommendations API error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch product recommendations', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/public/products/[id]/route.ts b/src/app/api/public/products/[id]/route.ts deleted file mode 100644 index 9ff7306..0000000 --- a/src/app/api/public/products/[id]/route.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getPayload } from 'payload' -import config from '@payload-config' -import { getCache, setCache } from '@/lib/redis' - -/** - * GET /api/public/products/[id] - * 获取单个产品详情(带缓存) - * 支持参数: - * - collection: 'preorder-products' | 'products' (可选,如不指定则自动搜索) - */ -export async function GET(req: NextRequest, { params }: { params: { id: string } }) { - try { - const { id } = params - const searchParams = req.nextUrl.searchParams - const collection = searchParams.get('collection') - - // 生成缓存 key - const cacheKey = `products:detail:${id}:collection=${collection || 'auto'}` - - // 尝试从缓存获取 - const cached = await getCache(cacheKey) - if (cached) { - return NextResponse.json({ - success: true, - data: cached, - cached: true, - }) - } - - const payload = await getPayload({ config }) - let result: any = null - let foundCollection: string = '' - - if (collection) { - // 如果指定了 collection,直接查询 - try { - result = await payload.findByID({ - collection: collection as any, - id, - depth: 2, - }) - foundCollection = collection - } catch { - // 找不到 - } - } else { - // 自动搜索各个 collection - const collections = ['preorder-products', 'products'] - - for (const col of collections) { - try { - result = await payload.findByID({ - collection: col as any, - id, - depth: 2, - }) - foundCollection = col - break - } catch { - // 继续尝试下一个 - } - } - } - - if (!result) { - return NextResponse.json( - { - success: false, - error: 'Product not found', - }, - { status: 404 }, - ) - } - - // 只有已发布的产品才返回 - if (result.status !== 'published') { - return NextResponse.json( - { - success: false, - error: 'Product not found or not published', - }, - { status: 404 }, - ) - } - - // 添加 collection 信息 - const resultWithMeta = { - ...result, - _collection: foundCollection, - } - - // 缓存结果(1 小时) - await setCache(cacheKey, resultWithMeta, 3600) - - return NextResponse.json({ - success: true, - data: resultWithMeta, - cached: false, - }) - } catch (error) { - console.error('Product detail API error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch product', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/public/products/route.ts b/src/app/api/public/products/route.ts deleted file mode 100644 index 35b4d4d..0000000 --- a/src/app/api/public/products/route.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getPayload } from 'payload' -import config from '@payload-config' -import { getCache, setCache } from '@/lib/redis' - -/** - * GET /api/public/products - * 获取产品列表(带缓存) - * 支持参数: - * - type: 'preorder' | 'order' | 'all' (默认 'all') - * - page: 页码 - * - limit: 每页数量 - * - status: 'draft' | 'published' (默认 'published') - */ -export async function GET(req: NextRequest) { - try { - const searchParams = req.nextUrl.searchParams - const page = parseInt(searchParams.get('page') || '1', 10) - const limit = parseInt(searchParams.get('limit') || '10', 10) - const status = searchParams.get('status') || 'published' - const type = searchParams.get('type') || 'all' - - // 生成缓存 key - const cacheKey = `products:list:type=${type}:page=${page}:limit=${limit}:status=${status}` - - // 尝试从缓存获取 - const cached = await getCache(cacheKey) - if (cached) { - return NextResponse.json({ - success: true, - data: cached, - cached: true, - }) - } - - const payload = await getPayload({ config }) - const where = { status: { equals: status } } - - let result - - if (type === 'all') { - // 查询所有类型 - const [preorders, products] = await Promise.all([ - payload.find({ - collection: 'preorder-products', - where, - page, - limit, - depth: 1, - }), - payload.find({ - collection: 'products', - where, - page, - limit, - depth: 1, - }), - ]) - - // 合并结果 - result = { - docs: [ - ...preorders.docs.map((doc) => ({ ...doc, _type: 'preorder-products' })), - ...products.docs.map((doc) => ({ ...doc, _type: 'products' })), - ], - totalDocs: preorders.totalDocs + products.totalDocs, - limit, - page, - totalPages: Math.ceil((preorders.totalDocs + products.totalDocs) / limit), - hasNextPage: page < Math.ceil((preorders.totalDocs + products.totalDocs) / limit), - hasPrevPage: page > 1, - } - } else if (type === 'preorder') { - // 只查询预售商品 - result = await payload.find({ - collection: 'preorder-products', - where, - page, - limit, - depth: 1, - }) - } else if (type === 'order') { - // 只查询现货商品 - result = await payload.find({ - collection: 'products', - where, - page, - limit, - depth: 1, - }) - } else { - // 旧的 products collection - result = await payload.find({ - collection: 'products', - where, - page, - limit, - depth: 1, - }) - } - - // 缓存结果(1 小时) - await setCache(cacheKey, result, 3600) - - return NextResponse.json({ - success: true, - data: result, - cached: false, - }) - } catch (error) { - console.error('Products API error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch products', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/refresh-order-counts/route.ts b/src/app/api/refresh-order-counts/route.ts deleted file mode 100644 index d6ea79e..0000000 --- a/src/app/api/refresh-order-counts/route.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import payload from 'payload' - -const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' -const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || '' - -/** - * 刷新预购商品的订单计数 - * POST /api/refresh-order-counts - * - * Body: - * - productIds?: string[] - 要刷新的商品 ID 列表(Payload ID) - * - refreshAll?: boolean - 是否刷新所有预购商品 - */ -export async function POST(req: NextRequest) { - try { - const body = await req.json() - const { productIds, refreshAll } = body - - if (!productIds && !refreshAll) { - return NextResponse.json( - { success: false, error: '请提供 productIds 或设置 refreshAll' }, - { status: 400 } - ) - } - - let products: any[] = [] - - if (refreshAll) { - // 获取所有预购商品 - const result = await payload.find({ - collection: 'preorder-products', - limit: 1000, - where: { - medusaId: { - exists: true, - }, - }, - }) - products = result.docs - } else { - // 获取指定的商品 - const result = await payload.find({ - collection: 'preorder-products', - limit: productIds.length, - where: { - id: { - in: productIds, - }, - medusaId: { - exists: true, - }, - }, - }) - products = result.docs - } - - if (products.length === 0) { - return NextResponse.json( - { success: false, error: '没有找到要刷新的商品' }, - { status: 404 } - ) - } - - // 统计更新结果 - let successCount = 0 - let failCount = 0 - const errors: string[] = [] - - // 为每个商品刷新订单计数 - for (const product of products) { - try { - const orderCount = await fetchProductOrderCount(product.medusaId) - - await payload.update({ - collection: 'preorder-products', - id: product.id, - data: { - orderCount, - }, - }) - - successCount++ - } catch (error) { - failCount++ - const errorMsg = `商品 ${product.title} (${product.medusaId}): ${ - error instanceof Error ? error.message : '未知错误' - }` - errors.push(errorMsg) - console.error('刷新订单计数失败:', errorMsg) - } - } - - return NextResponse.json({ - success: true, - message: `刷新完成!成功: ${successCount},失败: ${failCount}`, - details: { - total: products.length, - success: successCount, - failed: failCount, - errors: errors.length > 0 ? errors : undefined, - }, - }) - } catch (error) { - console.error('刷新订单计数 API 错误:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : '未知错误', - }, - { status: 500 } - ) - } -} - -/** - * 从 Medusa 获取指定商品的订单数量 - */ -async function fetchProductOrderCount(productId: string): Promise { - try { - // 使用 Medusa Admin API 查询订单 - // 注意:这里需要使用 Query API 或者 Admin Orders API - // 查询所有包含该商品的已完成订单 - - const response = await fetch( - `${MEDUSA_BACKEND_URL}/admin/orders?fields=id,items&items[product_id]=${productId}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '', - ...(MEDUSA_ADMIN_API_KEY && { 'Authorization': `Bearer ${MEDUSA_ADMIN_API_KEY}` }), - }, - } - ) - - if (!response.ok) { - throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`) - } - - const data = await response.json() - - // 计算订单中该商品的总数量 - let totalQuantity = 0 - - if (data.orders && Array.isArray(data.orders)) { - for (const order of data.orders) { - if (order.items && Array.isArray(order.items)) { - for (const item of order.items) { - if (item.product_id === productId) { - totalQuantity += item.quantity || 0 - } - } - } - } - } - - return totalQuantity - } catch (error) { - console.error(`获取商品 ${productId} 订单数量失败:`, error) - throw error - } -} diff --git a/src/app/api/batch-sync-medusa/route.ts b/src/app/api/sync/batch-medusa/route.ts similarity index 95% rename from src/app/api/batch-sync-medusa/route.ts rename to src/app/api/sync/batch-medusa/route.ts index b0ce594..47e5f94 100644 --- a/src/app/api/batch-sync-medusa/route.ts +++ b/src/app/api/sync/batch-medusa/route.ts @@ -5,7 +5,7 @@ import { getAllMedusaProducts } from '@/lib/medusa' /** * Batch Sync Selected Products - * POST /api/batch-sync-medusa + * POST /api/sync/batch-medusa * Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean } */ export async function POST(request: Request) { @@ -80,8 +80,7 @@ export async function POST(request: Request) { } if (forceUpdate || !product.title) updateData.title = medusaProduct.title - if (forceUpdate || !product.handle) updateData.handle = medusaProduct.handle - if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail?.url + if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail await payload.update({ collection: collection as 'products' | 'preorder-products', diff --git a/src/app/api/sync-medusa/route.ts b/src/app/api/sync/medusa/route.ts similarity index 98% rename from src/app/api/sync-medusa/route.ts rename to src/app/api/sync/medusa/route.ts index 5f59604..4e6d72e 100644 --- a/src/app/api/sync-medusa/route.ts +++ b/src/app/api/sync/medusa/route.ts @@ -125,10 +125,10 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole /** * 同步 Medusa 商品到 Payload CMS - * GET /api/sync-medusa - 同步所有商品 - * GET /api/sync-medusa?medusaId=prod_xxx - 同步单个商品 - * GET /api/sync-medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection - * GET /api/sync-medusa?forceUpdate=true - 强制更新所有字段 + * GET /api/sync/medusa - 同步所有商品 + * GET /api/sync/medusa?medusaId=prod_xxx - 同步单个商品 + * GET /api/sync/medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection + * GET /api/sync/medusa?forceUpdate=true - 强制更新所有字段 */ export async function GET(request: Request) { const origin = request.headers.get('origin') @@ -481,7 +481,7 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) { } /** - * POST /api/sync-medusa + * POST /api/sync/medusa * 手动触发同步(与 GET 参数相同) * Body: { medusaId?, collection?, forceUpdate? } */ diff --git a/src/app/api/sync-product/route.ts b/src/app/api/sync/product/route.ts similarity index 99% rename from src/app/api/sync-product/route.ts rename to src/app/api/sync/product/route.ts index 0c361d3..3228c68 100644 --- a/src/app/api/sync-product/route.ts +++ b/src/app/api/sync/product/route.ts @@ -18,7 +18,7 @@ export async function OPTIONS(request: Request) { /** * 同步单个产品并返回完整的产品数据 - * POST /api/sync-product + * POST /api/sync/product * Body: { medusaId: string, collection?: 'products' | 'preorder-products', forceUpdate?: boolean } * * 返回: { success: true, product: {...完整的产品数据}, action: 'created' | 'updated' | 'skipped' } diff --git a/src/components/fields/RefreshOrderCountField.tsx b/src/components/fields/RefreshOrderCountField.tsx index 082887d..ac03063 100644 --- a/src/components/fields/RefreshOrderCountField.tsx +++ b/src/components/fields/RefreshOrderCountField.tsx @@ -23,7 +23,7 @@ export function RefreshOrderCountField() { setMessage('') try { - const response = await fetch('/api/refresh-order-counts', { + const response = await fetch('/api/preorders/refresh-order-counts', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/seed/RestoreRecommendationsSeedButton.tsx b/src/components/seed/RestoreRecommendationsSeedButton.tsx index 73a259b..344e491 100644 --- a/src/components/seed/RestoreRecommendationsSeedButton.tsx +++ b/src/components/seed/RestoreRecommendationsSeedButton.tsx @@ -105,7 +105,7 @@ export function RestoreRecommendationsSeedButton({ className }: Props) { setMessage('💾 Updating configuration...') // Update product-recommendations global via server-side API - const updateResponse = await fetch('/api/restore-recommendations-seed', { + const updateResponse = await fetch('/api/admin/restore-recommendations-seed', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/sync/RefreshOrderCountButton.tsx b/src/components/sync/RefreshOrderCountButton.tsx index 162d3c0..431e215 100644 --- a/src/components/sync/RefreshOrderCountButton.tsx +++ b/src/components/sync/RefreshOrderCountButton.tsx @@ -23,7 +23,7 @@ export function RefreshOrderCountButton() { setMessage('') try { - const response = await fetch('/api/refresh-order-counts', { + const response = await fetch('/api/preorders/refresh-order-counts', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -71,7 +71,7 @@ export function RefreshOrderCountButton() { setLoading(true) setMessage('') - const response = await fetch('/api/refresh-order-counts', { + const response = await fetch('/api/preorders/refresh-order-counts', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/sync/UnifiedSyncButton.tsx b/src/components/sync/UnifiedSyncButton.tsx index 5c09351..97366f3 100644 --- a/src/components/sync/UnifiedSyncButton.tsx +++ b/src/components/sync/UnifiedSyncButton.tsx @@ -33,7 +33,7 @@ export function UnifiedSyncButton() { setMessage('') try { - const response = await fetch('/api/sync-medusa?forceUpdate=false', { + const response = await fetch('/api/sync/medusa?forceUpdate=false', { method: 'GET', }) @@ -80,7 +80,7 @@ export function UnifiedSyncButton() { setLoading(true) setMessage('') - const response = await fetch('/api/batch-sync-medusa', { + const response = await fetch('/api/sync/batch-medusa', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -124,7 +124,7 @@ export function UnifiedSyncButton() { setShowForceAllConfirm(false) try { - const response = await fetch('/api/sync-medusa?forceUpdate=true', { + const response = await fetch('/api/sync/medusa?forceUpdate=true', { method: 'GET', }) diff --git a/src/components/views/LogsManagerView.tsx b/src/components/views/LogsManagerView.tsx index 21c8404..3ca795b 100644 --- a/src/components/views/LogsManagerView.tsx +++ b/src/components/views/LogsManagerView.tsx @@ -115,7 +115,7 @@ export default function LogsManagerView() { try { const response = await fetch( - `/api/delete-logs?startDate=${deleteStartDate}&endDate=${deleteEndDate}`, + `/api/admin/log?startDate=${deleteStartDate}&endDate=${deleteEndDate}`, { method: 'DELETE', }, diff --git a/src/endpoints/homepage.ts b/src/endpoints/homepage.ts deleted file mode 100644 index 4b31d4c..0000000 --- a/src/endpoints/homepage.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Endpoint } from 'payload' - -export const homepageDataEndpoint: Endpoint = { - path: '/homepage-data', - method: 'get', - handler: async (req) => { - try { - const payload = req.payload - - // 获取首页公告(已发布且在首页显示) - const announcements = await payload.find({ - collection: 'announcements', - where: { - and: [ - { - status: { - equals: 'published', - }, - }, - { - showOnHomepage: { - equals: true, - }, - }, - ], - }, - sort: '-priority', - limit: 10, - }) - - // 获取 Hero Slider - const heroSlider = await payload.findGlobal({ - slug: 'hero-slider', - }) - - // 获取产品推荐 - const productRecommendations = await payload.findGlobal({ - slug: 'product-recommendations', - }) - - // 构建响应数据 - const response = { - announcements: announcements.docs.map((announcement) => ({ - id: announcement.id, - title: announcement.title, - type: announcement.type, - summary: announcement.summary, - priority: announcement.priority, - publishedAt: announcement.publishedAt, - })), - heroSlider: { - slides: heroSlider.slides || [], - }, - productRecommendations: { - enabled: productRecommendations.enabled || false, - lists: productRecommendations.lists || [], - }, - } - - return Response.json(response, { status: 200 }) - } catch (error: any) { - req.payload.logger.error('Error fetching homepage data:', error) - return Response.json( - { - error: 'Failed to fetch homepage data', - message: error.message, - }, - { status: 500 } - ) - } - }, -} diff --git a/src/payload.config.ts b/src/payload.config.ts index 3d8d3c0..c7dda05 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -20,7 +20,6 @@ import { SiteAccess } from './globals/SiteAccess' import { s3Storage } from '@payloadcms/storage-s3' import { en } from '@payloadcms/translations/languages/en' import { zh } from '@payloadcms/translations/languages/zh' -import { homepageDataEndpoint } from './endpoints/homepage' // 导入自定义翻译 import enProducts from './translations/en/products.json' @@ -51,7 +50,6 @@ export default buildConfig({ }, collections: [Users, Media, Products, PreorderProducts, Announcements, Articles, Logs], globals: [AdminSettings, LogsManager, HeroSlider, ProductRecommendations, SiteAccess], - endpoints: [homepageDataEndpoint], editor: lexicalEditor(), secret: process.env.PAYLOAD_SECRET || '', typescript: {