商品同步

This commit is contained in:
龟男日记\www 2026-02-21 01:58:49 +08:00
parent c29ee4d0c3
commit 41f3eb5adf
6 changed files with 146 additions and 100 deletions

View File

@ -89,7 +89,7 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole
mergedData.seedId = newData.seedId mergedData.seedId = newData.seedId
} }
// 只在字段为空时更新 // 只在字段为空时更新基础字段
if (!existingProduct.title) { if (!existingProduct.title) {
mergedData.title = newData.title mergedData.title = newData.title
} }
@ -103,6 +103,23 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole
mergedData.status = newData.status mergedData.status = newData.status
} }
// Medusa 属性字段:总是更新(以 Medusa 为准)
mergedData.tags = newData.tags
mergedData.type = newData.type
mergedData.collection = newData.collection
mergedData.category = newData.category
// 物理属性:总是更新
mergedData.height = newData.height
mergedData.width = newData.width
mergedData.length = newData.length
mergedData.weight = newData.weight
// 海关与物流:总是更新
mergedData.midCode = newData.midCode
mergedData.hsCode = newData.hsCode
mergedData.countryOfOrigin = newData.countryOfOrigin
return mergedData return mergedData
} }
@ -241,25 +258,12 @@ async function syncSingleProductByMedusaId(
// 如果存在且不强制更新,只更新空字段 // 如果存在且不强制更新,只更新空字段
if (!forceUpdate) { if (!forceUpdate) {
console.log(`[Sync API] 🔄 模式: 只填充空字段`) console.log(`[Sync API] 🔄 模式: 只填充空字段,但 Medusa 属性总是更新`)
// 合并数据(只曹新空字段 // 合并数据(只更新空字段,但 Medusa 属性总是更新
const mergedData = mergeProductData(existingProduct, productData, false) const mergedData = mergeProductData(existingProduct, productData, false)
// 如果没有需要更新的字段,跳过 console.log(`[Sync API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
if (Object.keys(mergedData).length <= 2) { // 更新(只更新空字段 + Medusa 属性)
// 只有 lastSyncedAt 和 medusaId
console.log(`[Sync API] ⏭️ 跳过: 所有字段都有值`)
return {
success: true,
action: 'skipped',
message: `商品 ${medusaId} 已存在于 ${targetCollection},且所有字段都有值`,
productId: existingProduct.id,
collection: targetCollection,
}
}
console.log(`[Sync API] 📝 更新字段: ${Object.keys(mergedData).join(', ')}`)
// 更新(只更新空字段)
const updated = await payload.update({ const updated = await payload.update({
collection: targetCollection, collection: targetCollection,
id: existingProduct.id, id: existingProduct.id,
@ -395,25 +399,11 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
if (found) { if (found) {
const existingProduct = found.product const existingProduct = found.product
// 如果不强制更新,只更新空字段 // 如果不强制更新,只更新空字段,但 Medusa 属性总是更新
if (!forceUpdate) { if (!forceUpdate) {
const mergedData = mergeProductData(existingProduct, productData, false) const mergedData = mergeProductData(existingProduct, productData, false)
// 如果没有需要更新的字段,跳过 // 更新(只更新空字段 + Medusa 属性)
if (Object.keys(mergedData).length <= 2) {
// 只有 lastSyncedAt 和 medusaId
results.skipped++
results.details.push({
medusaId: medusaProduct.id,
seedId: seedId,
title: medusaProduct.title,
action: 'skipped',
collection: targetCollection,
})
continue
}
// 更新(只更新空字段)
await payload.update({ await payload.update({
collection: targetCollection, collection: targetCollection,
id: existingProduct.id, id: existingProduct.id,

View File

@ -111,7 +111,7 @@ export async function POST(request: Request) {
} }
} }
let action: 'created' | 'updated' | 'updated_partial' | 'moved' | 'skipped' = 'created' let action: 'created' | 'updated' | 'updated_partial' | 'moved' = 'created'
let finalProduct: any let finalProduct: any
// 如果在错误的 collection 中,需要移动 // 如果在错误的 collection 中,需要移动
@ -134,12 +134,13 @@ export async function POST(request: Request) {
// 如果找到了并且在正确的 collection 中 // 如果找到了并且在正确的 collection 中
else if (existingProduct) { else if (existingProduct) {
if (!forceUpdate) { if (!forceUpdate) {
// 只更新空字段 // 只更新空字段,但 Medusa 属性字段总是更新
const mergedData: any = { const mergedData: any = {
lastSyncedAt: productData.lastSyncedAt, lastSyncedAt: productData.lastSyncedAt,
medusaId: productData.medusaId, medusaId: productData.medusaId,
} }
// 基础字段:只更新空字段
if (!existingProduct.seedId && productData.seedId) { if (!existingProduct.seedId && productData.seedId) {
mergedData.seedId = productData.seedId mergedData.seedId = productData.seedId
} }
@ -148,20 +149,30 @@ export async function POST(request: Request) {
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
if (!existingProduct.status) mergedData.status = productData.status if (!existingProduct.status) mergedData.status = productData.status
// 如果没有需要更新的字段,跳过 // Medusa 属性字段:总是更新(以 Medusa 为准)
if (Object.keys(mergedData).length <= 2) { mergedData.tags = productData.tags
console.log(`[Sync Product API] ⏭️ 跳过: 所有字段都有值`) mergedData.type = productData.type
action = 'skipped' mergedData.collection = productData.collection
finalProduct = existingProduct mergedData.category = productData.category
} else {
console.log(`[Sync Product API] 📝 更新字段: ${Object.keys(mergedData).join(', ')}`) // 物理属性:总是更新
mergedData.height = productData.height
mergedData.width = productData.width
mergedData.length = productData.length
mergedData.weight = productData.weight
// 海关与物流:总是更新
mergedData.midCode = productData.midCode
mergedData.hsCode = productData.hsCode
mergedData.countryOfOrigin = productData.countryOfOrigin
console.log(`[Sync Product API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
finalProduct = await payload.update({ finalProduct = await payload.update({
collection: targetCollection, collection: targetCollection,
id: existingProduct.id, id: existingProduct.id,
data: mergedData, data: mergedData,
}) })
action = 'updated_partial' action = 'updated_partial'
}
} else { } else {
// 强制更新所有字段 // 强制更新所有字段
console.log(`[Sync Product API] ⚡ 强制更新所有字段`) console.log(`[Sync Product API] ⚡ 强制更新所有字段`)
@ -207,7 +218,7 @@ export async function POST(request: Request) {
fakeOrderCount: finalProduct.fakeOrderCount, fakeOrderCount: finalProduct.fakeOrderCount,
}), }),
}, },
message: `产品已${action === 'created' ? '创建' : action === 'moved' ? '移动' : action === 'skipped' ? '跳过' : '更新'}${targetCollection}`, message: `产品已${action === 'created' ? '创建' : action === 'moved' ? '移动' : '更新'}${targetCollection}`,
}) })
return addCorsHeaders(response, origin) return addCorsHeaders(response, origin)

View File

@ -92,43 +92,48 @@ export const ProductBaseFields: Field[] = [
/** /**
* Medusa * Medusa
* Medusa * Medusa
* Medusa Payload
*/ */
export const MedusaAttributesFields: Field[] = [ export const MedusaAttributesFields: Field[] = [
{ {
name: 'tags', name: 'tags',
type: 'text', type: 'text',
admin: { admin: {
description: '产品标签(逗号分隔', description: '产品标签(逗号分隔,从 Medusa 同步',
placeholder: '例如: 热门, 新品, 限量', placeholder: '例如: 热门, 新品, 限量',
readOnly: true,
}, },
}, },
{ {
name: 'type', name: 'type',
type: 'text', type: 'text',
admin: { admin: {
description: '产品类型', description: '产品类型(从 Medusa 同步)',
placeholder: '例如: 外壳, PCB, 工具', placeholder: '例如: 外壳, PCB, 工具',
readOnly: true,
}, },
}, },
{ {
name: 'collection', name: 'collection',
type: 'text', type: 'text',
admin: { admin: {
description: '产品系列', description: '产品系列(从 Medusa 同步)',
placeholder: '例如: Shell, Cartridge', placeholder: '例如: Shell, Cartridge',
readOnly: true,
}, },
}, },
{ {
name: 'category', name: 'category',
type: 'text', type: 'text',
admin: { admin: {
description: '产品分类', description: '产品分类(从 Medusa 同步)',
placeholder: '例如: GBA, GBC, GB', placeholder: '例如: GBA, GBC, GB',
readOnly: true,
}, },
}, },
{ {
type: 'collapsible', type: 'collapsible',
label: '物理属性', label: '物理属性(只读)',
fields: [ fields: [
{ {
type: 'row', type: 'row',
@ -137,32 +142,36 @@ export const MedusaAttributesFields: Field[] = [
name: 'height', name: 'height',
type: 'number', type: 'number',
admin: { admin: {
description: '高度 (cm)', description: '高度 (cm),从 Medusa 同步',
width: '25%', width: '25%',
readOnly: true,
}, },
}, },
{ {
name: 'width', name: 'width',
type: 'number', type: 'number',
admin: { admin: {
description: '宽度 (cm)', description: '宽度 (cm),从 Medusa 同步',
width: '25%', width: '25%',
readOnly: true,
}, },
}, },
{ {
name: 'length', name: 'length',
type: 'number', type: 'number',
admin: { admin: {
description: '长度 (cm)', description: '长度 (cm),从 Medusa 同步',
width: '25%', width: '25%',
readOnly: true,
}, },
}, },
{ {
name: 'weight', name: 'weight',
type: 'number', type: 'number',
admin: { admin: {
description: '重量 (g)', description: '重量 (g),从 Medusa 同步',
width: '25%', width: '25%',
readOnly: true,
}, },
}, },
], ],
@ -171,7 +180,7 @@ export const MedusaAttributesFields: Field[] = [
}, },
{ {
type: 'collapsible', type: 'collapsible',
label: '海关与物流', label: '海关与物流(只读)',
fields: [ fields: [
{ {
type: 'row', type: 'row',
@ -180,27 +189,30 @@ export const MedusaAttributesFields: Field[] = [
name: 'midCode', name: 'midCode',
type: 'text', type: 'text',
admin: { admin: {
description: 'MID 代码(制造商识别码', description: 'MID 代码(制造商识别码,从 Medusa 同步',
placeholder: '例如: 1234567890', placeholder: '例如: 1234567890',
width: '33%', width: '33%',
readOnly: true,
}, },
}, },
{ {
name: 'hsCode', name: 'hsCode',
type: 'text', type: 'text',
admin: { admin: {
description: 'HS 代码(海关编码', description: 'HS 代码(海关编码,从 Medusa 同步',
placeholder: '例如: 8523.49.00', placeholder: '例如: 8523.49.00',
width: '33%', width: '33%',
readOnly: true,
}, },
}, },
{ {
name: 'countryOfOrigin', name: 'countryOfOrigin',
type: 'text', type: 'text',
admin: { admin: {
description: '原产国', description: '原产国(从 Medusa 同步)',
placeholder: '例如: CN, US, JP', placeholder: '例如: CN, US, JP',
width: '34%', width: '34%',
readOnly: true,
}, },
}, },
], ],

View File

@ -6,8 +6,7 @@ import { NextResponse } from 'next/server'
*/ */
const ALLOWED_ORIGINS = [ const ALLOWED_ORIGINS = [
'http://localhost:9000', // Medusa 开发服务器 'http://localhost:9000', // Medusa 开发服务器
'http://localhost:7001', // Medusa Admin 默认端口 'http://localhost:8000', // Storefront 默认 端口
'http://localhost:7000', // Storefront 默认 端口
process.env.MEDUSA_URL, process.env.MEDUSA_URL,
process.env.ADMIN_URL, process.env.ADMIN_URL,
].filter(Boolean) as string[] ].filter(Boolean) as string[]

View File

@ -36,6 +36,31 @@ interface MedusaProduct {
id: string id: string
value: string value: string
}> }>
// 完整的关联对象
collection?: {
id: string
title: string
handle: string
}
type?: {
id: string
value: string
}
categories?: Array<{
id: string
name: string
handle: string
}>
// 物理属性
height?: number
width?: number
length?: number
weight?: number
// 海关与物流
mid_code?: string
hs_code?: string
origin_country?: string
// 兼容旧字段
collection_id?: string collection_id?: string
type_id?: string type_id?: string
} }
@ -214,6 +239,15 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
// 提取 tags逗号分隔 // 提取 tags逗号分隔
const tags = product.tags?.map(tag => tag.value).join(', ') || null const tags = product.tags?.map(tag => tag.value).join(', ') || null
// 提取 type优先使用 type.value否则使用 type_id 或 metadata
const type = product.type?.value || product.type_id || product.metadata?.type || null
// 提取 collection优先使用 collection.title否则使用 collection_id 或 metadata
const collection = product.collection?.title || product.collection_id || product.metadata?.collection || null
// 提取 category从 categories 数组或 metadata
const category = product.categories?.[0]?.name || product.metadata?.category || null
return { return {
medusaId: product.id, medusaId: product.id,
seedId: product.metadata?.seed_id || null, seedId: product.metadata?.seed_id || null,
@ -225,20 +259,20 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
// Medusa 默认属性 // Medusa 默认属性
tags: tags, tags: tags,
type: product.type_id || null, type: type,
collection: product.collection_id || null, collection: collection,
category: product.metadata?.category || null, category: category,
// 物理属性(从 metadata 中提取) // 物理属性(优先使用直接字段,否则从 metadata 中提取)
height: product.metadata?.height ? Number(product.metadata.height) : null, height: product.height || (product.metadata?.height ? Number(product.metadata.height) : null),
width: product.metadata?.width ? Number(product.metadata.width) : null, width: product.width || (product.metadata?.width ? Number(product.metadata.width) : null),
length: product.metadata?.length ? Number(product.metadata.length) : null, length: product.length || (product.metadata?.length ? Number(product.metadata.length) : null),
weight: product.metadata?.weight ? Number(product.metadata.weight) : null, weight: product.weight || (product.metadata?.weight ? Number(product.metadata.weight) : null),
// 海关与物流 // 海关与物流(优先使用直接字段,否则从 metadata 中提取)
midCode: product.metadata?.mid_code || product.metadata?.midCode || null, midCode: product.mid_code || product.metadata?.mid_code || product.metadata?.midCode || null,
hsCode: product.metadata?.hs_code || product.metadata?.hsCode || null, hsCode: product.hs_code || product.metadata?.hs_code || product.metadata?.hsCode || null,
countryOfOrigin: product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null, countryOfOrigin: product.origin_country || product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null,
} }
} }

View File

@ -255,47 +255,47 @@ export interface Product {
)[] )[]
| null; | null;
/** /**
* * Medusa
*/ */
tags?: string | null; tags?: string | null;
/** /**
* * Medusa
*/ */
type?: string | null; type?: string | null;
/** /**
* * Medusa
*/ */
collection?: string | null; collection?: string | null;
/** /**
* * Medusa
*/ */
category?: string | null; category?: string | null;
/** /**
* (cm) * (cm) Medusa
*/ */
height?: number | null; height?: number | null;
/** /**
* (cm) * (cm) Medusa
*/ */
width?: number | null; width?: number | null;
/** /**
* (cm) * (cm) Medusa
*/ */
length?: number | null; length?: number | null;
/** /**
* (g) * (g) Medusa
*/ */
weight?: number | null; weight?: number | null;
/** /**
* MID * MID Medusa
*/ */
midCode?: string | null; midCode?: string | null;
/** /**
* HS * HS Medusa
*/ */
hsCode?: string | null; hsCode?: string | null;
/** /**
* * Medusa
*/ */
countryOfOrigin?: string | null; countryOfOrigin?: string | null;
/** /**
@ -406,47 +406,47 @@ export interface PreorderProduct {
)[] )[]
| null; | null;
/** /**
* * Medusa
*/ */
tags?: string | null; tags?: string | null;
/** /**
* * Medusa
*/ */
type?: string | null; type?: string | null;
/** /**
* * Medusa
*/ */
collection?: string | null; collection?: string | null;
/** /**
* * Medusa
*/ */
category?: string | null; category?: string | null;
/** /**
* (cm) * (cm) Medusa
*/ */
height?: number | null; height?: number | null;
/** /**
* (cm) * (cm) Medusa
*/ */
width?: number | null; width?: number | null;
/** /**
* (cm) * (cm) Medusa
*/ */
length?: number | null; length?: number | null;
/** /**
* (g) * (g) Medusa
*/ */
weight?: number | null; weight?: number | null;
/** /**
* MID * MID Medusa
*/ */
midCode?: string | null; midCode?: string | null;
/** /**
* HS * HS Medusa
*/ */
hsCode?: string | null; hsCode?: string | null;
/** /**
* * Medusa
*/ */
countryOfOrigin?: string | null; countryOfOrigin?: string | null;
/** /**