diff --git a/src/app/api/admin/batch-sync-medusa/route.ts b/src/app/api/admin/batch-sync-medusa/route.ts index 997f6c2..a9dcc2d 100644 --- a/src/app/api/admin/batch-sync-medusa/route.ts +++ b/src/app/api/admin/batch-sync-medusa/route.ts @@ -1,7 +1,7 @@ import { getPayload } from 'payload' import config from '@payload-config' import { NextResponse } from 'next/server' -import { getAllMedusaProducts } from '@/lib/medusa' +import { getAllMedusaProducts, transformMedusaProductToPayload } from '@/lib/medusa' /** * Batch Sync Selected Products @@ -74,13 +74,32 @@ export async function POST(request: Request) { continue } - // Update basic fields from Medusa - const updateData: any = { - lastSyncedAt: new Date().toISOString(), - } + // 使用统一 transform,保持与 sync/product 逻辑一致 + const productData = transformMedusaProductToPayload(medusaProduct) - if (forceUpdate || !product.title) updateData.title = medusaProduct.title - if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail + const updateData: any = { + lastSyncedAt: productData.lastSyncedAt, + medusaId: productData.medusaId, + seedId: productData.seedId, + // 始终从 Medusa 同步的字段 + title: productData.title, + handle: productData.handle, + description: productData.description, + startPrice: productData.startPrice, + tags: productData.tags, + type: productData.type, + collection: productData.collection, + category: productData.category, + height: productData.height, + width: productData.width, + length: productData.length, + weight: productData.weight, + midCode: productData.midCode, + hsCode: productData.hsCode, + countryOfOrigin: productData.countryOfOrigin, + // thumbnail:强制更新时覆盖,否则只在为空时同步 + ...((forceUpdate || !product.thumbnail) && { thumbnail: productData.thumbnail }), + } await payload.update({ collection: collection as 'products' | 'preorder-products', diff --git a/src/app/api/admin/reset-data/route.ts b/src/app/api/admin/reset-data/route.ts index 68479af..a052319 100644 --- a/src/app/api/admin/reset-data/route.ts +++ b/src/app/api/admin/reset-data/route.ts @@ -42,6 +42,7 @@ export async function POST(request: NextRequest) { method: 'POST', headers: { 'Content-Type': 'application/json', + 'x-payload-api-key': process.env.PAYLOAD_API_KEY || '', }, }) @@ -76,6 +77,7 @@ export async function POST(request: NextRequest) { method: 'POST', headers: { 'Content-Type': 'application/json', + 'x-payload-api-key': process.env.PAYLOAD_API_KEY || '', }, }) diff --git a/src/app/api/home/route.ts b/src/app/api/home/route.ts index d08db05..7cd7772 100644 --- a/src/app/api/home/route.ts +++ b/src/app/api/home/route.ts @@ -71,6 +71,7 @@ export async function GET(req: NextRequest) { const baseInfo = { id: product.id, medusaId: product.medusaId, + handle: product.handle || null, seedId: product.seedId, title: product.title, thumbnail: product.thumbnail, @@ -82,25 +83,28 @@ export async function GET(req: NextRequest) { // 如果是预购产品,添加预购特有字段 if (productRef.relationTo === 'preorder-products') { + const realOrderCount = product.orderCount || 0 + const fakeOrderCount = product.fakeOrderCount || 0 + const totalCount = realOrderCount + fakeOrderCount return { ...baseInfo, relationTo: 'preorder-products', preorder: { type: product.preorderType || 'standard', fundingGoal: product.fundingGoal || 0, - orderCount: product.orderCount || 0, + orderCount: totalCount, startDate: product.preorderStartDate, endDate: product.preorderEndDate, - // 计算进度百分比 + // 计算进度百分比(含 fakeOrderCount,用于展示) progress: product.fundingGoal > 0 - ? Math.min(Math.round((product.orderCount / product.fundingGoal) * 100), 100) + ? Math.min(Math.round((totalCount / product.fundingGoal) * 100), 100) : 0, // 计算剩余天数 daysLeft: product.preorderEndDate ? Math.max(0, Math.ceil((new Date(product.preorderEndDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : null, - // 支持者数量(使用 orderCount) - backers: product.orderCount || 0, + // 支持者数量(含 fakeOrderCount,用于展示) + backers: totalCount, }, } } diff --git a/src/app/api/preorders/[id]/orders/route.ts b/src/app/api/preorders/[id]/orders/route.ts index 28f77e7..0a2d591 100644 --- a/src/app/api/preorders/[id]/orders/route.ts +++ b/src/app/api/preorders/[id]/orders/route.ts @@ -8,11 +8,11 @@ import config from '@payload-config' */ export async function GET( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const payload = await getPayload({ config }) - const { id } = params + const { id } = await params // 获取预购产品 let product: any = null diff --git a/src/app/api/preorders/[id]/recalculate/route.ts b/src/app/api/preorders/[id]/recalculate/route.ts index fd50f12..070e7f9 100644 --- a/src/app/api/preorders/[id]/recalculate/route.ts +++ b/src/app/api/preorders/[id]/recalculate/route.ts @@ -8,11 +8,11 @@ import config from '@payload-config' */ export async function POST( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const payload = await getPayload({ config }) - const { id } = params + const { id } = await params // 获取预购产品 let product: any = null diff --git a/src/app/api/preorders/[id]/route.ts b/src/app/api/preorders/[id]/route.ts index 25b5370..722f160 100644 --- a/src/app/api/preorders/[id]/route.ts +++ b/src/app/api/preorders/[id]/route.ts @@ -8,11 +8,11 @@ import config from '@payload-config' */ export async function GET( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const payload = await getPayload({ config }) - const { id } = params + const { id } = await params // 尝试通过 Payload ID 查找 let product: any = null @@ -160,11 +160,11 @@ export async function GET( */ export async function PATCH( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const payload = await getPayload({ config }) - const { id } = params + const { id } = await params const body = await req.json() const { diff --git a/src/app/api/sync/medusa/route.ts b/src/app/api/sync/medusa/route.ts new file mode 100644 index 0000000..207e467 --- /dev/null +++ b/src/app/api/sync/medusa/route.ts @@ -0,0 +1,155 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' +import { + getAllMedusaProducts, + transformMedusaProductToPayload, + getProductCollection, +} from '@/lib/medusa' +import { addCorsHeaders, handleCorsOptions } from '@/lib/cors' + +export async function OPTIONS(request: Request) { + const origin = request.headers.get('origin') + return handleCorsOptions(origin) +} + +/** + * GET /api/sync/medusa + * 从 Medusa 同步商品到 Payload + * + * 参数: + * ?forceUpdate=true/false 强制更新所有字段 (默认 false,只更新空字段) + * ?medusaId=xxx 只同步单个商品(由 Medusa subscriber 调用) + * ?collection=xxx 指定 collection(配合 medusaId 使用) + */ +export async function GET(request: NextRequest) { + const origin = request.headers.get('origin') + + try { + const { searchParams } = new URL(request.url) + const forceUpdate = searchParams.get('forceUpdate') === 'true' + const singleMedusaId = searchParams.get('medusaId') + const preferredCollection = searchParams.get('collection') as + | 'products' + | 'preorder-products' + | null + + const payload = await getPayload({ config }) + const allMedusaProducts = await getAllMedusaProducts() + + // 单商品模式(由 Medusa subscriber 调用) + const targetProducts = singleMedusaId + ? allMedusaProducts.filter((p) => p.id === singleMedusaId) + : allMedusaProducts + + if (singleMedusaId && targetProducts.length === 0) { + const response = NextResponse.json( + { success: false, error: `Medusa 中未找到商品 ${singleMedusaId}` }, + { status: 404 }, + ) + return addCorsHeaders(response, origin) + } + + const results = { + total: targetProducts.length, + created: 0, + updated: 0, + skipped: 0, + failed: 0, + } + + for (const medusaProduct of targetProducts) { + try { + const productData = transformMedusaProductToPayload(medusaProduct) + const targetCollection = + preferredCollection || getProductCollection(medusaProduct) + + // 查找现有产品(seedId 优先,否则 medusaId) + let existingProduct: any = null + let existingCollection: 'products' | 'preorder-products' | null = null + + if (productData.seedId) { + for (const coll of ['products', 'preorder-products'] as const) { + const result = await payload.find({ + collection: coll, + where: { seedId: { equals: productData.seedId } }, + limit: 1, + }) + if (result.docs[0]) { + existingProduct = result.docs[0] + existingCollection = coll + break + } + } + } + + if (!existingProduct) { + for (const coll of ['products', 'preorder-products'] as const) { + const result = await payload.find({ + collection: coll, + where: { medusaId: { equals: medusaProduct.id } }, + limit: 1, + }) + if (result.docs[0]) { + existingProduct = result.docs[0] + existingCollection = coll + break + } + } + } + + // 在非 forceUpdate 模式下,跳过已存在的产品(只同步新产品) + if (!forceUpdate && existingProduct) { + results.skipped++ + continue + } + + if (existingProduct) { + // 强制更新:更新所有 Medusa 同步字段 + const updateData: any = { + ...productData, + // thumbnail 保留 Payload 已有值(除非 forceUpdate 或为空) + thumbnail: existingProduct.thumbnail || productData.thumbnail, + } + + // 如果需要跨 collection 移动 + if (existingCollection && existingCollection !== targetCollection) { + await payload.delete({ collection: existingCollection, id: existingProduct.id }) + await payload.create({ collection: targetCollection, data: updateData }) + } else { + await payload.update({ + collection: targetCollection, + id: existingProduct.id, + data: updateData, + }) + } + results.updated++ + } else { + // 新建 + await payload.create({ + collection: targetCollection, + data: { ...productData }, + }) + results.created++ + } + } catch (err) { + console.error(`[sync/medusa] ❌ 同步失败 ${medusaProduct.id}:`, err) + results.failed++ + } + } + + const response = NextResponse.json({ + success: true, + message: `同步完成:新建 ${results.created},更新 ${results.updated},跳过 ${results.skipped},失败 ${results.failed}`, + results, + }) + return addCorsHeaders(response, origin) + } catch (error: any) { + console.error('[sync/medusa] ❌ 错误:', error) + const response = NextResponse.json( + { success: false, error: error.message || 'Unknown error' }, + { status: 500 }, + ) + return addCorsHeaders(response, origin) + } +} diff --git a/src/app/api/sync/product/route.ts b/src/app/api/sync/product/route.ts index 8d1e0a3..8cfee61 100644 --- a/src/app/api/sync/product/route.ts +++ b/src/app/api/sync/product/route.ts @@ -159,6 +159,7 @@ export async function POST(request: Request) { // 基础字段:Medusa 来源的字段总是更新 mergedData.seedId = productData.seedId mergedData.title = productData.title + mergedData.handle = productData.handle mergedData.status = productData.status // thumbnail 只在为空时同步(Payload 编辑优先) if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail diff --git a/src/collections/base/ProductBase.ts b/src/collections/base/ProductBase.ts index 5ac9dc2..fcfdc23 100644 --- a/src/collections/base/ProductBase.ts +++ b/src/collections/base/ProductBase.ts @@ -27,7 +27,17 @@ export const ProductBaseFields: Field[] = [ admin: { description: 'Medusa 商品 ID', readOnly: true, - width: '60%', + width: '40%', + }, + }, + { + name: 'handle', + type: 'text', + index: true, + admin: { + description: 'Medusa 商品 Handle(用于前台 URL,从 Medusa 同步)', + readOnly: true, + width: '30%', }, }, { @@ -41,7 +51,7 @@ export const ProductBaseFields: Field[] = [ ], admin: { description: '商品详情状态', - width: '40%', + width: '30%', }, }, ], diff --git a/src/components/sync/UnifiedSyncButton.tsx b/src/components/sync/UnifiedSyncButton.tsx index 97366f3..9706593 100644 --- a/src/components/sync/UnifiedSyncButton.tsx +++ b/src/components/sync/UnifiedSyncButton.tsx @@ -80,7 +80,7 @@ export function UnifiedSyncButton() { setLoading(true) setMessage('') - const response = await fetch('/api/sync/batch-medusa', { + const response = await fetch('/api/admin/batch-sync-medusa', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/payload-types.ts b/src/payload-types.ts index a61c50e..590bb40 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -201,6 +201,10 @@ export interface Product { * Medusa 商品 ID */ medusaId: string; + /** + * Medusa 商品 Handle(用于前台 URL,从 Medusa 同步) + */ + handle?: string | null; /** * 商品详情状态 */ @@ -336,6 +340,10 @@ export interface PreorderProduct { * Medusa 商品 ID */ medusaId: string; + /** + * Medusa 商品 Handle(用于前台 URL,从 Medusa 同步) + */ + handle?: string | null; /** * 商品详情状态 */ @@ -922,6 +930,7 @@ export interface MediaSelect { */ export interface ProductsSelect { medusaId?: T; + handle?: T; status?: T; seedId?: T; title?: T; @@ -960,6 +969,7 @@ export interface ProductsSelect { */ export interface PreorderProductsSelect { medusaId?: T; + handle?: T; status?: T; seedId?: T; title?: T;