同步精简

This commit is contained in:
龟男日记\www 2026-02-20 22:36:13 +08:00
parent dae1b2704f
commit c29ee4d0c3
3 changed files with 303 additions and 101 deletions

View File

@ -7,6 +7,15 @@ import {
getMedusaProductsPaginated, getMedusaProductsPaginated,
getProductCollection, getProductCollection,
} from '@/lib/medusa' } 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 * seedId medusaId 使 seedId
@ -99,11 +108,14 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole
/** /**
* Medusa Payload CMS * Medusa Payload CMS
* GET /api/sync-medusa * GET /api/sync-medusa -
* GET /api/sync-medusa?medusaId=prod_xxx ( Medusa ID ) * GET /api/sync-medusa?medusaId=prod_xxx -
* GET /api/sync-medusa?payloadId=123 ( Payload ID ) * GET /api/sync-medusa?medusaId=prod_xxx&collection=preorder-products - collection
* GET /api/sync-medusa?forceUpdate=true -
*/ */
export async function GET(request: Request) { export async function GET(request: Request) {
const origin = request.headers.get('origin')
try { try {
// 可选的 API Key 验证 // 可选的 API Key 验证
const authHeader = request.headers.get('authorization') const authHeader = request.headers.get('authorization')
@ -113,53 +125,45 @@ export async function GET(request: Request) {
if (payloadApiKey && authHeader) { if (payloadApiKey && authHeader) {
const token = authHeader.replace('Bearer ', '') const token = authHeader.replace('Bearer ', '')
if (token !== payloadApiKey) { if (token !== payloadApiKey) {
return NextResponse.json( const response = NextResponse.json(
{ {
success: false, success: false,
error: 'Invalid API key', error: 'Invalid API key',
}, },
{ status: 401 }, { status: 401 },
) )
return addCorsHeaders(response, origin)
} }
} }
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const medusaId = searchParams.get('medusaId') const medusaId = searchParams.get('medusaId')
const payloadId = searchParams.get('payloadId')
const collection = searchParams.get('collection') as 'products' | 'preorder-products' | null const collection = searchParams.get('collection') as 'products' | 'preorder-products' | null
const forceUpdate = searchParams.get('forceUpdate') === 'true' const forceUpdate = searchParams.get('forceUpdate') === 'true'
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// 同步单个商品(通过 Medusa ID // 同步单个商品
if (medusaId) { if (medusaId) {
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined) const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined)
return NextResponse.json(result) const response = NextResponse.json(result)
} return addCorsHeaders(response, origin)
// 同步单个商品(通过 Payload ID
if (payloadId) {
const result = await syncSingleProductByPayloadId(
payload,
payloadId,
collection || '',
forceUpdate,
)
return NextResponse.json(result)
} }
// 同步所有商品 // 同步所有商品
const result = await syncAllProducts(payload, forceUpdate) const result = await syncAllProducts(payload, forceUpdate)
return NextResponse.json(result) const response = NextResponse.json(result)
return addCorsHeaders(response, origin)
} catch (error) { } catch (error) {
console.error('Sync error:', error) console.error('[Sync API] ❌ 请求处理失败:', error)
return NextResponse.json( const response = NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
}, },
{ status: 500 }, { 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 * POST /api/sync-medusa
* * GET
* Body: { medusaId?, collection?, forceUpdate? }
*/ */
export async function POST(request: Request) { export async function POST(request: Request) {
const origin = request.headers.get('origin')
try { try {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// 可以在这里添加认证检查 // 可以在这里添加认证检查
// const { user } = await payload.auth({ headers: request.headers }) // const { user } = await payload.auth({ headers: request.headers })
// if (!user) { // 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 body = await request.json()
const { medusaId, payloadId, collection = '', forceUpdate = true } = body const { medusaId, collection, forceUpdate = true } = body
// 同步单个商品
if (medusaId) { if (medusaId) {
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined) const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection)
return NextResponse.json(result) const response = NextResponse.json(result)
} return addCorsHeaders(response, origin)
if (payloadId) {
const result = await syncSingleProductByPayloadId(payload, payloadId, collection, forceUpdate)
return NextResponse.json(result)
} }
// 同步所有商品
const result = await syncAllProducts(payload, forceUpdate) const result = await syncAllProducts(payload, forceUpdate)
return NextResponse.json(result) const response = NextResponse.json(result)
return addCorsHeaders(response, origin)
} catch (error) { } catch (error) {
console.error('Sync error:', error) console.error('[Sync API] ❌ POST 请求处理失败:', error)
return NextResponse.json( const response = NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
}, },
{ status: 500 }, { status: 500 },
) )
return addCorsHeaders(response, origin)
} }
} }

View File

@ -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)
}
}

36
src/lib/cors.ts Normal file
View File

@ -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)
}