diff --git a/.env.example b/.env.example index 9e72b50..2eb8069 100644 --- a/.env.example +++ b/.env.example @@ -4,14 +4,16 @@ DATABASE_URL=postgresql://user:password@localhost:5432/database # Payload PAYLOAD_SECRET=YOUR_SECRET_HERE -# Redis 配置 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= -REDIS_DB=0 +# Redis Configuration +REDIS_URL=redis://localhost:6379 -# Store API Key(用于访问 hero-slider 和 product-recommendations 接口) -STORE_API_KEY=your-store-api-key-here +# API Keys (统一使用 PAYLOAD_API_KEY) +PAYLOAD_API_KEY=your-payload-api-key-here + +# Medusa 配置 +MEDUSA_BACKEND_URL=http://localhost:9000 +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=your-publishable-key-here +PAYLOAD_API_KEY=your-payload-api-key-here # Cloudflare R2 配置 CLOUDFLARE_R2_BUCKET=your-bucket diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index bc2f82a..aa2ab4e 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -25,12 +25,12 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField' -import { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton' +import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview' +import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton' import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler' -import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton' -import { TaobaoLinkPreview as TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b } from '../../../components/fields/TaobaoLinkPreview' import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField' +import { RefreshOrderCountButton as RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e } from '../../../components/sync/RefreshOrderCountButton' import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel' import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' @@ -64,12 +64,12 @@ export const importMap = { "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426, - "/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f, + "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959, + "/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523, "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, - "/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c, - "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f, + "/components/sync/RefreshOrderCountButton#RefreshOrderCountButton": RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, diff --git a/src/app/api/clear-data/route.ts b/src/app/api/clear-data/route.ts index 07e95e0..5dc782d 100644 --- a/src/app/api/clear-data/route.ts +++ b/src/app/api/clear-data/route.ts @@ -28,6 +28,7 @@ export async function GET(request: Request) { const results = { products: 0, + preorderProducts: 0, announcements: 0, articles: 0, errors: [] as string[], @@ -47,6 +48,20 @@ export async function GET(request: Request) { results.errors.push(errorMsg) } + // 清理 Preorder Products + try { + const deletedPreorderProducts = await payload.delete({ + collection: 'preorder-products', + where: {}, + }) + results.preorderProducts = deletedPreorderProducts.docs?.length || 0 + console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`) + } catch (error) { + const errorMsg = `清理 Preorder Products 失败: ${error instanceof Error ? error.message : '未知错误'}` + console.error('❌', errorMsg) + results.errors.push(errorMsg) + } + // 清理 Announcements try { const deletedAnnouncements = await payload.delete({ @@ -77,7 +92,7 @@ export async function GET(request: Request) { return NextResponse.json({ success: true, - message: `数据清理完成!已删除 ${results.products} 个商品、${results.announcements} 个公告、${results.articles} 个文章`, + message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`, results, }) } catch (error) { diff --git a/src/app/api/clear-payload-collections/route.ts b/src/app/api/clear-payload-collections/route.ts new file mode 100644 index 0000000..8dea8d4 --- /dev/null +++ b/src/app/api/clear-payload-collections/route.ts @@ -0,0 +1,50 @@ +import { getPayload } from 'payload' +import config from '@payload-config' +import { NextResponse } from 'next/server' + +export async function POST() { + try { + const payload = await getPayload({ config }) + + // Delete all products + const products = await payload.find({ + collection: 'products', + limit: 1000, + }) + + for (const product of products.docs) { + await payload.delete({ + collection: 'products', + id: product.id, + }) + } + + // Delete all preorder products + const preorderProducts = await payload.find({ + collection: 'preorder-products', + limit: 1000, + }) + + for (const product of preorderProducts.docs) { + await payload.delete({ + collection: 'preorder-products', + id: product.id, + }) + } + + return NextResponse.json({ + success: true, + message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`, + deleted: { + products: products.docs.length, + preorderProducts: preorderProducts.docs.length, + }, + }) + } catch (error) { + console.error('[clear-payload-collections] Error:', error) + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : String(error), + }, { status: 500 }) + } +} diff --git a/src/app/api/payload-stats/route.ts b/src/app/api/payload-stats/route.ts new file mode 100644 index 0000000..0c9e347 --- /dev/null +++ b/src/app/api/payload-stats/route.ts @@ -0,0 +1,32 @@ +import { getPayload } from 'payload' +import config from '@payload-config' +import { NextResponse } from 'next/server' + +export async function GET() { + try { + const payload = await getPayload({ config }) + + const products = await payload.find({ + collection: 'products', + limit: 0, // Just get count + }) + + const preorderProducts = await payload.find({ + collection: 'preorder-products', + limit: 0, // Just get count + }) + + return NextResponse.json({ + success: true, + products: products.totalDocs, + preorderProducts: preorderProducts.totalDocs, + total: products.totalDocs + preorderProducts.totalDocs, + }) + } catch (error) { + console.error('[payload-stats] Error:', error) + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : String(error), + }, { status: 500 }) + } +} diff --git a/src/app/api/preorders/[id]/route.ts b/src/app/api/preorders/[id]/route.ts index 93c9343..25b5370 100644 --- a/src/app/api/preorders/[id]/route.ts +++ b/src/app/api/preorders/[id]/route.ts @@ -89,7 +89,6 @@ export async function GET( preorder: { id: product.id, title: product.title, - handle: product.handle, description: product.description, status: product._status, thumbnail: product.thumbnail, @@ -102,11 +101,13 @@ export async function GET( // 预购元数据(从 Payload 管理) is_preorder: true, preorder_type: product.preorderType || 'standard', - estimated_ship_date: product.estimatedShipDate || null, preorder_end_date: product.preorderEndDate || null, funding_goal: fundingGoal, - min_order_quantity: product.minOrderQuantity || 1, - max_order_quantity: product.maxOrderQuantity || 0, + + // 订单计数 + order_count: parseInt(product.orderCount || '0', 10) || 0, + fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0, + total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0), // 统计数据 current_orders: totalOrders, @@ -152,12 +153,10 @@ export async function GET( * - increment?: number - 增加订单数 * - decrement?: number - 减少订单数 * - * - estimated_ship_date?: string - 更新预估发货日期 * - preorder_end_date?: string - 更新预购结束日期 * - funding_goal?: number - 更新众筹目标 * - preorder_type?: string - 更新预购类型 - * - min_order_quantity?: number - 最小起订量 - * - max_order_quantity?: number - 最大购买数量 + * - fake_order_count?: number - 更新 Fake 订单计数 */ export async function PATCH( req: NextRequest, @@ -174,12 +173,10 @@ export async function PATCH( max_orders, increment, decrement, - estimated_ship_date, preorder_end_date, funding_goal, preorder_type, - min_order_quantity, - max_order_quantity, + fake_order_count, } = body // 获取产品 @@ -285,9 +282,6 @@ export async function PATCH( // 模式2: 更新产品级别预购元数据(在 Payload 中管理) const updateData: any = {} - if (estimated_ship_date !== undefined) { - updateData.estimatedShipDate = estimated_ship_date - } if (preorder_end_date !== undefined) { updateData.preorderEndDate = preorder_end_date } @@ -297,11 +291,8 @@ export async function PATCH( if (preorder_type !== undefined) { updateData.preorderType = preorder_type } - if (min_order_quantity !== undefined) { - updateData.minOrderQuantity = min_order_quantity - } - if (max_order_quantity !== undefined) { - updateData.maxOrderQuantity = max_order_quantity + if (fake_order_count !== undefined) { + updateData.fakeOrderCount = Math.max(0, fake_order_count) } if (Object.keys(updateData).length > 0) { diff --git a/src/app/api/preorders/route.ts b/src/app/api/preorders/route.ts index a0c8f82..d5879aa 100644 --- a/src/app/api/preorders/route.ts +++ b/src/app/api/preorders/route.ts @@ -76,7 +76,6 @@ export async function GET(req: NextRequest) { return { id: product.id, title: product.title, - handle: product.handle, status: product._status, thumbnail: product.thumbnail, description: product.description, @@ -87,9 +86,13 @@ export async function GET(req: NextRequest) { // 预购元数据 preorder_type: product.preorderType || 'standard', - estimated_ship_date: product.estimatedShipDate || null, funding_goal: fundingGoal, + // 订单计数 + order_count: parseInt(product.orderCount || '0', 10) || 0, + fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0, + total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0), + // 统计数据 current_orders: totalOrders, total_max_orders: totalMaxOrders, diff --git a/src/app/api/public/hero-slider/route.ts b/src/app/api/public/hero-slider/route.ts index 886945d..805dc03 100644 --- a/src/app/api/public/hero-slider/route.ts +++ b/src/app/api/public/hero-slider/route.ts @@ -12,7 +12,7 @@ export async function GET(req: NextRequest) { try { // 验证 API Key const apiKey = req.headers.get('x-store-api-key') - const validApiKey = process.env.STORE_API_KEY + const validApiKey = process.env.PAYLOAD_API_KEY if (!apiKey || !validApiKey || apiKey !== validApiKey) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) diff --git a/src/app/api/public/product-recommendations/route.ts b/src/app/api/public/product-recommendations/route.ts index 8f8c623..674db0e 100644 --- a/src/app/api/public/product-recommendations/route.ts +++ b/src/app/api/public/product-recommendations/route.ts @@ -12,7 +12,7 @@ export async function GET(req: NextRequest) { try { // 验证 API Key const apiKey = req.headers.get('x-store-api-key') - const validApiKey = process.env.STORE_API_KEY + const validApiKey = process.env.PAYLOAD_API_KEY if (!apiKey || !validApiKey || apiKey !== validApiKey) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) diff --git a/src/app/api/refresh-order-counts/route.ts b/src/app/api/refresh-order-counts/route.ts new file mode 100644 index 0000000..d6ea79e --- /dev/null +++ b/src/app/api/refresh-order-counts/route.ts @@ -0,0 +1,163 @@ +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/sync-medusa/route.ts b/src/app/api/sync-medusa/route.ts index 5e3969f..fb8fbba 100644 --- a/src/app/api/sync-medusa/route.ts +++ b/src/app/api/sync-medusa/route.ts @@ -107,12 +107,12 @@ export async function GET(request: Request) { try { // 可选的 API Key 验证 const authHeader = request.headers.get('authorization') - const storeApiKey = process.env.STORE_API_KEY + const payloadApiKey = process.env.PAYLOAD_API_KEY - // 如果配置了 STORE_API_KEY,则验证请求 - if (storeApiKey && authHeader) { + // 如果配置了 PAYLOAD_API_KEY,则验证请求 + if (payloadApiKey && authHeader) { const token = authHeader.replace('Bearer ', '') - if (token !== storeApiKey) { + if (token !== payloadApiKey) { return NextResponse.json( { success: false, diff --git a/src/collections/PreorderProducts.ts b/src/collections/PreorderProducts.ts index 203c22d..762904e 100644 --- a/src/collections/PreorderProducts.ts +++ b/src/collections/PreorderProducts.ts @@ -29,16 +29,14 @@ export const PreorderProducts: CollectionConfig = { useAsTitle: 'title', defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'], description: '管理预售商品的详细内容和描述', - listSearchableFields: ['title', 'medusaId', 'handle'], + listSearchableFields: ['title', 'medusaId'], pagination: { defaultLimit: 25, }, components: { - edit: { - PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton', - }, beforeListTable: [ '/components/sync/UnifiedSyncButton#UnifiedSyncButton', + '/components/sync/RefreshOrderCountButton#RefreshOrderCountButton', '/components/list/ProductGridStyler', ], }, @@ -103,20 +101,15 @@ export const PreorderProducts: CollectionConfig = { readOnly: true, }, }, - { - name: 'handle', - type: 'text', - admin: { - description: '商品 URL handle(从 Medusa 同步)', - readOnly: true, - }, - }, { name: 'thumbnail', type: 'text', admin: { - description: '商品缩略图 URL(从 Medusa 同步)', - readOnly: true, + description: '商品封面 URL(支持上传或输入 URL)', + components: { + Cell: '/components/cells/ThumbnailCell#ThumbnailCell', + Field: '/components/fields/ThumbnailField#ThumbnailField', + }, }, }, { @@ -165,16 +158,6 @@ export const PreorderProducts: CollectionConfig = { }, ], }, - { - name: 'estimatedShipDate', - type: 'date', - admin: { - description: '预计发货日期', - date: { - displayFormat: 'yyyy-MM-dd', - }, - }, - }, { name: 'preorderEndDate', type: 'date', @@ -189,19 +172,21 @@ export const PreorderProducts: CollectionConfig = { type: 'row', fields: [ { - name: 'minOrderQuantity', + name: 'orderCount', type: 'number', - defaultValue: 1, + defaultValue: 0, admin: { - description: '最小起订量', + description: '真实订单计数(从 Medusa 同步)', + readOnly: true, width: '50%', }, }, { - name: 'maxOrderQuantity', + name: 'fakeOrderCount', type: 'number', + defaultValue: 0, admin: { - description: '最大购买数量(0 表示不限制)', + description: 'Fake 订单计数(手动设置)', width: '50%', }, }, diff --git a/src/collections/Products.ts b/src/collections/Products.ts index 54a0f82..b3ead68 100644 --- a/src/collections/Products.ts +++ b/src/collections/Products.ts @@ -29,14 +29,11 @@ export const Products: CollectionConfig = { useAsTitle: 'title', defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'], description: '管理 Medusa 商品的详细内容和描述', - listSearchableFields: ['title', 'medusaId', 'handle'], + listSearchableFields: ['title', 'medusaId'], pagination: { defaultLimit: 25, }, components: { - edit: { - PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton', - }, beforeListTable: [ '/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/list/ProductGridStyler', @@ -109,13 +106,6 @@ export const Products: CollectionConfig = { readOnly: true, }, }, - { - name: 'handle', - type: 'text', - admin: { - hidden: true, // 隐藏字段,但保留用于搜索 - }, - }, { name: 'thumbnail', type: 'text', diff --git a/src/components/list/product-grid-styler.scss b/src/components/list/product-grid-styler.scss index 10e1e81..fdc6c8d 100644 --- a/src/components/list/product-grid-styler.scss +++ b/src/components/list/product-grid-styler.scss @@ -1,7 +1,8 @@ // 这是为了覆盖 Payload 默认表格样式的 SCSS // 我们使用 CSS Grid 强制改变表格布局,从而实现 Grid 视图,同时保留 Payload 所有原生功能 -.collection-list.collection-list--products { +.collection-list.collection-list--products, +.collection-list.collection-list--preorder-products { table { display: block !important; diff --git a/src/components/sync/ForceSyncButton.tsx b/src/components/sync/ForceSyncButton.tsx deleted file mode 100644 index 2d93a50..0000000 --- a/src/components/sync/ForceSyncButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client' -import { useState } from 'react' -import { Button } from '@payloadcms/ui' -import { useDocumentInfo } from '@payloadcms/ui' - -/** - * 单个商品强制同步按钮 - * 用于在商品编辑页面强制更新该商品的信息 - */ -export function ForceSyncButton() { - const { id, collectionSlug } = useDocumentInfo() - const [loading, setLoading] = useState(false) - const [message, setMessage] = useState('') - - const handleForceSync = async () => { - if (!id) { - setMessage('❌ 无法获取商品 ID') - return - } - - if (!confirm('确定要从 Medusa 强制更新此商品吗?这将覆盖当前的商品信息。')) { - return - } - - setLoading(true) - setMessage('') - - try { - const response = await fetch( - `/api/sync-medusa?payloadId=${id}&collection=${collectionSlug}&forceUpdate=true`, - { - method: 'GET', - }, - ) - - const data = await response.json() - - if (data.success) { - setMessage('✅ ' + (data.message || '强制同步成功!')) - // 刷新页面显示更新后的数据 - setTimeout(() => window.location.reload(), 1500) - } else { - setMessage(`❌ 同步失败: ${data.error || data.message}`) - } - } catch (error) { - setMessage(`❌ 同步出错: ${error instanceof Error ? error.message : '未知错误'}`) - } finally { - setLoading(false) - } - } - - return ( -
-
- 🔄 - 从 Medusa 同步数据 -
- - {message && ( -
- {message} -
- )} -
- 💡 此操作将从 Medusa 获取最新数据并覆盖当前商品信息 -
-
- ) -} diff --git a/src/components/sync/RefreshOrderCountButton.tsx b/src/components/sync/RefreshOrderCountButton.tsx new file mode 100644 index 0000000..162d3c0 --- /dev/null +++ b/src/components/sync/RefreshOrderCountButton.tsx @@ -0,0 +1,167 @@ +'use client' +import { useState } from 'react' +import { Button, useSelection } from '@payloadcms/ui' +import { useRouter } from 'next/navigation' + +/** + * 刷新预购商品订单计数按钮 + * 从 Medusa 拉取最新订单数据并更新 orderCount + */ +export function RefreshOrderCountButton() { + const { getQueryParams, toggleAll } = useSelection() + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const router = useRouter() + + // 刷新所有商品的订单计数 + const handleRefreshAll = async () => { + if (!confirm('确定要刷新所有预购商品的订单计数吗?')) { + return + } + + setLoading(true) + setMessage('') + + try { + const response = await fetch('/api/refresh-order-counts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refreshAll: true, + }), + }) + + const data = await response.json() + + if (data.success) { + setMessage(`✅ ${data.message || '订单计数刷新成功!'}`) + setTimeout(() => { + router.refresh() + }, 1500) + } else { + setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`) + } + } catch (error) { + setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setLoading(false) + } + } + + // 刷新选中商品的订单计数 + const handleRefreshSelected = async () => { + try { + const queryParams = getQueryParams() + let selectedIds: string[] = [] + + if (queryParams && typeof queryParams === 'object') { + const whereCondition = (queryParams as any).where + if (whereCondition?.id?.in) { + selectedIds = whereCondition.id.in + } + } + + if (!selectedIds || selectedIds.length === 0) { + setMessage('⚠️ 请先勾选要刷新的商品(使用列表左侧的复选框)') + return + } + + setLoading(true) + setMessage('') + + const response = await fetch('/api/refresh-order-counts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + productIds: selectedIds, + }), + }) + + const data = await response.json() + + if (data.success) { + setMessage(`✅ ${data.message || '订单计数刷新成功!'}`) + if (toggleAll) { + toggleAll() + } + setTimeout(() => { + router.refresh() + }, 1500) + } else { + setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`) + } + } catch (error) { + setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setLoading(false) + } + } + + return ( +
+

+ 📊 + 订单计数管理 +

+ +
+ + + +
+ + {message && ( +
+ {message} +
+ )} + +
+

+ 💡 说明:订单计数 = 真实订单计数 + Fake计数 +

+

+ • 真实订单计数:从 Medusa 订单系统同步,只读 +

+

+ • Fake计数:可手动编辑,用于调整显示的进度 +

+
+
+ ) +} diff --git a/src/components/sync/UnifiedSyncButton.tsx b/src/components/sync/UnifiedSyncButton.tsx index 387ad9f..5c09351 100644 --- a/src/components/sync/UnifiedSyncButton.tsx +++ b/src/components/sync/UnifiedSyncButton.tsx @@ -1,5 +1,5 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Button, useSelection } from '@payloadcms/ui' import { useRouter } from 'next/navigation' @@ -13,13 +13,19 @@ export function UnifiedSyncButton() { const [message, setMessage] = useState('') const [showForceAllConfirm, setShowForceAllConfirm] = useState(false) const [confirmText, setConfirmText] = useState('') + const [collectionSlug, setCollectionSlug] = useState('products') const router = useRouter() - // 获取当前页面的 collection slug - const pathname = typeof window !== 'undefined' ? window.location.pathname : '' - const collectionSlug = pathname.includes('preorder-products') - ? 'preorder-products' - : 'products' + // 在客户端确定 collection slug,避免 hydration 错误 + useEffect(() => { + if (typeof window !== 'undefined') { + const pathname = window.location.pathname + const slug = pathname.includes('preorder-products') + ? 'preorder-products' + : 'products' + setCollectionSlug(slug) + } + }, []) // 同步新商品 const handleSyncNew = async () => { diff --git a/src/lib/medusa.ts b/src/lib/medusa.ts index f422efd..f9bd895 100644 --- a/src/lib/medusa.ts +++ b/src/lib/medusa.ts @@ -4,6 +4,8 @@ const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' +// Payload API key - 用于同步产品数据(包括 metadata) +const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || '' interface MedusaProduct { id: string @@ -47,25 +49,28 @@ interface MedusaResponse { } /** - * 获取所有 Medusa 商品 + * 获取所有 Medusa 商品(使用 admin API 以获取 metadata) */ export async function getAllMedusaProducts(): Promise { try { - const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products`, { + const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products-sync`, { headers: { 'Content-Type': 'application/json', 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY, + 'x-payload-api-key': PAYLOAD_API_KEY, }, }) - + if (!response.ok) { - throw new Error(`Failed to fetch products: ${response.statusText}`) + const errorText = await response.text() + console.error(`[getAllMedusaProducts] Error response:`, errorText) + throw new Error(`Failed to fetch products: ${response.status} ${response.statusText} - ${errorText}`) } const data: MedusaResponse = await response.json() return data.products || [] } catch (error) { - console.error('Error fetching Medusa products:', error) + console.error('[getAllMedusaProducts] Error fetching Medusa products:', error) throw error } } @@ -99,30 +104,22 @@ export async function getMedusaProduct(productId: string): Promise { try { - const response = await fetch( - `${MEDUSA_BACKEND_URL}/store/products?offset=${offset}&limit=${limit}`, - { - headers: { - 'Content-Type': 'application/json', - 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY, - }, - }, - ) - - if (!response.ok) { - throw new Error(`Failed to fetch products: ${response.statusText}`) - } - - const data: MedusaResponse = await response.json() + // 获取所有产品 + const allProducts = await getAllMedusaProducts() + + // 在内存中分页 + const products = allProducts.slice(offset, offset + limit) + return { - products: data.products || [], - count: data.count || 0, + products, + count: allProducts.length, } } catch (error) { console.error('Error fetching Medusa products (paginated):', error) @@ -178,12 +175,21 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis /** * 判断产品是否为预售商品 - * 检查 metadata.is_preorder (支持布尔值和字符串) + * 检查 metadata.is_preorder 和 metadata.preorder (支持布尔值和字符串) */ export function isPreorderProduct(product: MedusaProduct): boolean { - const isPreorder = product.metadata?.is_preorder - // 支持布尔值 true 或字符串 "true" - return isPreorder === true || isPreorder === 'true' + const isPreorderValue = product.metadata?.is_preorder + const preorderValue = product.metadata?.preorder + + // 尝试两个字段,支持多种类型 + const value = isPreorderValue !== undefined ? isPreorderValue : preorderValue + + // 支持布尔值 true、字符串 "true"、数字 1、字符串 "1" + if (value === true || value === 'true' || value === '1' || value === 1) { + return true + } + + return false } /** diff --git a/src/payload-types.ts b/src/payload-types.ts index c7066c3..5b52bd9 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -213,7 +213,6 @@ export interface Product { * 商品标题(从 Medusa 同步) */ title: string; - handle?: string | null; /** * 商品封面 URL(支持上传或输入 URL) */ @@ -298,11 +297,7 @@ export interface PreorderProduct { */ title: string; /** - * 商品 URL handle(从 Medusa 同步) - */ - handle?: string | null; - /** - * 商品缩略图 URL(从 Medusa 同步) + * 商品封面 URL(支持上传或输入 URL) */ thumbnail?: string | null; /** @@ -317,22 +312,18 @@ export interface PreorderProduct { * 众筹目标数量(0 表示以变体 max_orders 总和为准) */ fundingGoal: number; - /** - * 预计发货日期 - */ - estimatedShipDate?: string | null; /** * 预购结束日期(可选) */ preorderEndDate?: string | null; /** - * 最小起订量 + * 真实订单计数(从 Medusa 同步) */ - minOrderQuantity?: number | null; + orderCount?: number | null; /** - * 最大购买数量(0 表示不限制) + * Fake 订单计数(手动设置) */ - maxOrderQuantity?: number | null; + fakeOrderCount?: number | null; /** * 预售商品的详细描述(支持富文本编辑) */ @@ -826,7 +817,6 @@ export interface ProductsSelect { status?: T; seedId?: T; title?: T; - handle?: T; thumbnail?: T; lastSyncedAt?: T; content?: T; @@ -852,15 +842,13 @@ export interface PreorderProductsSelect { status?: T; seedId?: T; title?: T; - handle?: T; thumbnail?: T; lastSyncedAt?: T; preorderType?: T; fundingGoal?: T; - estimatedShipDate?: T; preorderEndDate?: T; - minOrderQuantity?: T; - maxOrderQuantity?: T; + orderCount?: T; + fakeOrderCount?: T; description?: T; relatedProducts?: T; taobaoLinks?: