From c29ee4d0c3abe5e00c54bde183baf449d134197a 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: Fri, 20 Feb 2026 22:36:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=B2=BE=E7=AE=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/sync-medusa/route.ts | 143 ++++++------------- src/app/api/sync-product/route.ts | 225 ++++++++++++++++++++++++++++++ src/lib/cors.ts | 36 +++++ 3 files changed, 303 insertions(+), 101 deletions(-) create mode 100644 src/app/api/sync-product/route.ts create mode 100644 src/lib/cors.ts diff --git a/src/app/api/sync-medusa/route.ts b/src/app/api/sync-medusa/route.ts index 84dae42..be7bd7d 100644 --- a/src/app/api/sync-medusa/route.ts +++ b/src/app/api/sync-medusa/route.ts @@ -7,6 +7,15 @@ import { getMedusaProductsPaginated, getProductCollection, } from '@/lib/medusa' +import { addCorsHeaders, handleCorsOptions } from '@/lib/cors' + +/** + * 处理 CORS 预检请求 + */ +export async function OPTIONS(request: Request) { + const origin = request.headers.get('origin') + return handleCorsOptions(origin) +} /** * 通过 seedId 或 medusaId 查找产品(优先使用 seedId) @@ -99,11 +108,14 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole /** * 同步 Medusa 商品到 Payload CMS - * GET /api/sync-medusa - * GET /api/sync-medusa?medusaId=prod_xxx (通过 Medusa ID 同步单个商品) - * GET /api/sync-medusa?payloadId=123 (通过 Payload ID 同步单个商品) + * 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') + try { // 可选的 API Key 验证 const authHeader = request.headers.get('authorization') @@ -113,53 +125,45 @@ export async function GET(request: Request) { if (payloadApiKey && authHeader) { const token = authHeader.replace('Bearer ', '') if (token !== payloadApiKey) { - return NextResponse.json( + const response = NextResponse.json( { success: false, error: 'Invalid API key', }, { status: 401 }, ) + return addCorsHeaders(response, origin) } } const { searchParams } = new URL(request.url) const medusaId = searchParams.get('medusaId') - const payloadId = searchParams.get('payloadId') const collection = searchParams.get('collection') as 'products' | 'preorder-products' | null const forceUpdate = searchParams.get('forceUpdate') === 'true' const payload = await getPayload({ config }) - // 同步单个商品(通过 Medusa ID) + // 同步单个商品 if (medusaId) { const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined) - return NextResponse.json(result) - } - - // 同步单个商品(通过 Payload ID) - if (payloadId) { - const result = await syncSingleProductByPayloadId( - payload, - payloadId, - collection || '', - forceUpdate, - ) - return NextResponse.json(result) + const response = NextResponse.json(result) + return addCorsHeaders(response, origin) } // 同步所有商品 const result = await syncAllProducts(payload, forceUpdate) - return NextResponse.json(result) + const response = NextResponse.json(result) + return addCorsHeaders(response, origin) } catch (error) { - console.error('Sync error:', error) - return NextResponse.json( + console.error('[Sync API] ❌ 请求处理失败:', error) + const response = NextResponse.json( { success: false, error: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 }, ) + return addCorsHeaders(response, origin) } } @@ -316,73 +320,6 @@ async function syncSingleProductByMedusaId( } } -/** - * 通过 Payload ID 同步单个商品 - */ -async function syncSingleProductByPayloadId( - payload: any, - payloadId: string, - collection: string, - forceUpdate: boolean, -) { - try { - // 如果未指定 collection,尝试在两个 collections 中查找 - let product: any = null - let foundCollection: string = collection - - if (collection) { - product = await payload.findByID({ - collection, - id: payloadId, - }) - } else { - // 尝试 preorder-products - try { - product = await payload.findByID({ - collection: 'preorder-products', - id: payloadId, - }) - foundCollection = 'preorder-products' - } catch { - // 最后尝试旧的 products - product = await payload.findByID({ - collection: 'products', - id: payloadId, - }) - foundCollection = 'products' - } - } - - if (!product) { - return { - success: false, - action: 'not_found', - message: `未找到商品 ID: ${payloadId}`, - } - } - - // 如果没有 medusaId,无法同步 - if (!product.medusaId) { - return { - success: false, - action: 'no_medusa_id', - message: `商品 ${product.title} 没有关联的 Medusa ID,无法同步`, - } - } - - // 使用 medusaId 同步(保持在当前 collection) - return await syncSingleProductByMedusaId(payload, product.medusaId, forceUpdate, foundCollection as 'products' | 'preorder-products') - } catch (error) { - console.error(`Error syncing product by Payload ID ${payloadId}:`, error) - return { - success: false, - action: 'error', - message: `同步商品 ID ${payloadId} 失败`, - error: error instanceof Error ? error.message : 'Unknown error', - } - } -} - /** * 同步所有商品 */ @@ -555,41 +492,45 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) { /** * POST /api/sync-medusa - * 触发手动同步(需要认证) + * 手动触发同步(与 GET 参数相同) + * Body: { medusaId?, collection?, forceUpdate? } */ export async function POST(request: Request) { + const origin = request.headers.get('origin') + try { const payload = await getPayload({ config }) // 可以在这里添加认证检查 // const { user } = await payload.auth({ headers: request.headers }) // if (!user) { - // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // const response = NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // return addCorsHeaders(response, origin) // } const body = await request.json() - const { medusaId, payloadId, collection = '', forceUpdate = true } = body + const { medusaId, collection, forceUpdate = true } = body + // 同步单个商品 if (medusaId) { - const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined) - return NextResponse.json(result) - } - - if (payloadId) { - const result = await syncSingleProductByPayloadId(payload, payloadId, collection, forceUpdate) - return NextResponse.json(result) + const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection) + const response = NextResponse.json(result) + return addCorsHeaders(response, origin) } + // 同步所有商品 const result = await syncAllProducts(payload, forceUpdate) - return NextResponse.json(result) + const response = NextResponse.json(result) + return addCorsHeaders(response, origin) } catch (error) { - console.error('Sync error:', error) - return NextResponse.json( + console.error('[Sync API] ❌ POST 请求处理失败:', error) + const response = NextResponse.json( { success: false, error: error instanceof 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 new file mode 100644 index 0000000..30d44fe --- /dev/null +++ b/src/app/api/sync-product/route.ts @@ -0,0 +1,225 @@ +import { getPayload } from 'payload' +import config from '@payload-config' +import { NextResponse } from 'next/server' +import { + getAllMedusaProducts, + transformMedusaProductToPayload, + getProductCollection, +} from '@/lib/medusa' +import { addCorsHeaders, handleCorsOptions } from '@/lib/cors' + +/** + * 处理 CORS 预检请求 + */ +export async function OPTIONS(request: Request) { + const origin = request.headers.get('origin') + return handleCorsOptions(origin) +} + +/** + * 同步单个产品并返回完整的产品数据 + * POST /api/sync-product + * Body: { medusaId: string, collection?: 'products' | 'preorder-products', forceUpdate?: boolean } + * + * 返回: { success: true, product: {...完整的产品数据}, action: 'created' | 'updated' | 'skipped' } + */ +export async function POST(request: Request) { + console.log('[Sync Product API] 📥 收到同步请求') + + const origin = request.headers.get('origin') + + try { + const body = await request.json() + const { medusaId, collection: preferredCollection, forceUpdate = false } = body + + if (!medusaId) { + const response = NextResponse.json( + { + success: false, + error: 'medusaId is required', + }, + { status: 400 }, + ) + return addCorsHeaders(response, origin) + } + + console.log('[Sync Product API] 🎯 参数:', { medusaId, preferredCollection, forceUpdate }) + + const payload = await getPayload({ config }) + + // 从 Medusa 获取商品数据 + const medusaProducts = await getAllMedusaProducts() + const medusaProduct = medusaProducts.find((p) => p.id === medusaId) + + if (!medusaProduct) { + console.error(`[Sync Product API] ❌ Medusa 中未找到商品: ${medusaId}`) + const response = NextResponse.json( + { + success: false, + error: `Medusa 中未找到商品 ${medusaId}`, + }, + { status: 404 }, + ) + return addCorsHeaders(response, origin) + } + + console.log(`[Sync Product API] ✅ 找到 Medusa 产品: ${medusaProduct.title}`) + + // 确定目标 collection + const targetCollection = preferredCollection || getProductCollection(medusaProduct) + console.log(`[Sync Product API] 🎯 目标 collection: ${targetCollection}`) + + // 转换数据 + const productData = transformMedusaProductToPayload(medusaProduct) + const seedId = productData.seedId + + // 查找现有产品(优先通过 seedId) + let existingProduct: any = null + let existingCollection: 'products' | 'preorder-products' | null = null + + // 先通过 seedId 查找 + if (seedId) { + for (const coll of ['products', 'preorder-products'] as const) { + const result = await payload.find({ + collection: coll, + where: { seedId: { equals: seedId } }, + limit: 1, + }) + if (result.docs[0]) { + existingProduct = result.docs[0] + existingCollection = coll + console.log(`[Sync Product API] 🔍 通过 seedId 找到产品 (${coll}): ${existingProduct.id}`) + break + } + } + } + + // 如果没找到,通过 medusaId 查找 + if (!existingProduct) { + for (const coll of ['products', 'preorder-products'] as const) { + const result = await payload.find({ + collection: coll, + where: { medusaId: { equals: medusaId } }, + limit: 1, + }) + if (result.docs[0]) { + existingProduct = result.docs[0] + existingCollection = coll + console.log(`[Sync Product API] 🔍 通过 medusaId 找到产品 (${coll}): ${existingProduct.id}`) + break + } + } + } + + let action: 'created' | 'updated' | 'updated_partial' | 'moved' | 'skipped' = 'created' + let finalProduct: any + + // 如果在错误的 collection 中,需要移动 + if (existingProduct && existingCollection && existingCollection !== targetCollection) { + console.log(`[Sync Product API] 🚚 移动产品: ${existingCollection} -> ${targetCollection}`) + + await payload.delete({ + collection: existingCollection, + id: existingProduct.id, + }) + + finalProduct = await payload.create({ + collection: targetCollection, + data: productData, + }) + + action = 'moved' + console.log(`[Sync Product API] ✅ 移动成功, 新 ID: ${finalProduct.id}`) + } + // 如果找到了并且在正确的 collection 中 + else if (existingProduct) { + if (!forceUpdate) { + // 只更新空字段 + const mergedData: any = { + lastSyncedAt: productData.lastSyncedAt, + medusaId: productData.medusaId, + } + + if (!existingProduct.seedId && productData.seedId) { + mergedData.seedId = productData.seedId + } + if (!existingProduct.title) mergedData.title = productData.title + if (!existingProduct.handle) mergedData.handle = productData.handle + if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail + if (!existingProduct.status) mergedData.status = productData.status + + // 如果没有需要更新的字段,跳过 + if (Object.keys(mergedData).length <= 2) { + console.log(`[Sync Product API] ⏭️ 跳过: 所有字段都有值`) + action = 'skipped' + finalProduct = existingProduct + } else { + console.log(`[Sync Product API] 📝 更新字段: ${Object.keys(mergedData).join(', ')}`) + finalProduct = await payload.update({ + collection: targetCollection, + id: existingProduct.id, + data: mergedData, + }) + action = 'updated_partial' + } + } else { + // 强制更新所有字段 + console.log(`[Sync Product API] ⚡ 强制更新所有字段`) + finalProduct = await payload.update({ + collection: targetCollection, + id: existingProduct.id, + data: productData, + }) + action = 'updated' + } + } + // 不存在,创建新产品 + else { + console.log(`[Sync Product API] ✨ 创建新产品`) + finalProduct = await payload.create({ + collection: targetCollection, + data: productData, + }) + action = 'created' + } + + console.log(`[Sync Product API] ✅ 同步完成: ${action}`) + + // 返回完整的产品数据 + const response = NextResponse.json({ + success: true, + action, + collection: targetCollection, + product: { + id: finalProduct.id, + title: finalProduct.title, + medusaId: finalProduct.medusaId, + seedId: finalProduct.seedId, + thumbnail: finalProduct.thumbnail, + status: finalProduct.status, + lastSyncedAt: finalProduct.lastSyncedAt, + // 如果是预购产品,包含预购信息 + ...(targetCollection === 'preorder-products' && { + preorderType: finalProduct.preorderType, + preorderEndDate: finalProduct.preorderEndDate, + fundingGoal: finalProduct.fundingGoal, + orderCount: finalProduct.orderCount, + fakeOrderCount: finalProduct.fakeOrderCount, + }), + }, + message: `产品已${action === 'created' ? '创建' : action === 'moved' ? '移动' : action === 'skipped' ? '跳过' : '更新'}于 ${targetCollection}`, + }) + + return addCorsHeaders(response, origin) + } catch (error) { + console.error('[Sync Product API] ❌ 同步失败:', error) + const errorResponse = NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 }, + ) + return addCorsHeaders(errorResponse, origin) + } +} diff --git a/src/lib/cors.ts b/src/lib/cors.ts new file mode 100644 index 0000000..8d06abe --- /dev/null +++ b/src/lib/cors.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' + +/** + * CORS 配置 + * 允许 Medusa 开发服务器访问 Payload API + */ +const ALLOWED_ORIGINS = [ + 'http://localhost:9000', // Medusa 开发服务器 + 'http://localhost:7001', // Medusa Admin 默认端口 + 'http://localhost:7000', // Storefront 默认 端口 + process.env.MEDUSA_URL, + process.env.ADMIN_URL, +].filter(Boolean) as string[] + +/** + * 添加 CORS 头部到响应 + */ +export function addCorsHeaders(response: NextResponse, origin?: string | null): NextResponse { + // 检查 origin 是否在允许列表中 + const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0] + + response.headers.set('Access-Control-Allow-Origin', allowedOrigin) + response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization') + response.headers.set('Access-Control-Allow-Credentials', 'true') + + return response +} + +/** + * 处理 OPTIONS 预检请求 + */ +export function handleCorsOptions(origin?: string | null): NextResponse { + const response = NextResponse.json({}, { status: 200 }) + return addCorsHeaders(response, origin) +}