同步精简并优化

This commit is contained in:
龟男日记\www 2026-02-22 19:29:15 +08:00
parent a424a5c1a9
commit c8de57af22
11 changed files with 224 additions and 23 deletions

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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%',
},
},
],

View File

@ -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',

View File

@ -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<T extends boolean = true> {
*/
export interface ProductsSelect<T extends boolean = true> {
medusaId?: T;
handle?: T;
status?: T;
seedId?: T;
title?: T;
@ -960,6 +969,7 @@ export interface ProductsSelect<T extends boolean = true> {
*/
export interface PreorderProductsSelect<T extends boolean = true> {
medusaId?: T;
handle?: T;
status?: T;
seedId?: T;
title?: T;