商品同步

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
}
// 只在字段为空时更新
// 只在字段为空时更新基础字段
if (!existingProduct.title) {
mergedData.title = newData.title
}
@ -103,6 +103,23 @@ function mergeProductData(existingProduct: any, newData: any, forceUpdate: boole
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
}
@ -241,25 +258,12 @@ async function syncSingleProductByMedusaId(
// 如果存在且不强制更新,只更新空字段
if (!forceUpdate) {
console.log(`[Sync API] 🔄 模式: 只填充空字段`)
// 合并数据(只曹新空字段
console.log(`[Sync API] 🔄 模式: 只填充空字段,但 Medusa 属性总是更新`)
// 合并数据(只更新空字段,但 Medusa 属性总是更新
const mergedData = mergeProductData(existingProduct, productData, false)
// 如果没有需要更新的字段,跳过
if (Object.keys(mergedData).length <= 2) {
// 只有 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(', ')}`)
// 更新(只更新空字段)
console.log(`[Sync API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
// 更新(只更新空字段 + Medusa 属性)
const updated = await payload.update({
collection: targetCollection,
id: existingProduct.id,
@ -395,25 +399,11 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
if (found) {
const existingProduct = found.product
// 如果不强制更新,只更新空字段
// 如果不强制更新,只更新空字段,但 Medusa 属性总是更新
if (!forceUpdate) {
const mergedData = mergeProductData(existingProduct, productData, false)
// 如果没有需要更新的字段,跳过
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
}
// 更新(只更新空字段)
// 更新(只更新空字段 + Medusa 属性)
await payload.update({
collection: targetCollection,
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
// 如果在错误的 collection 中,需要移动
@ -134,12 +134,13 @@ export async function POST(request: Request) {
// 如果找到了并且在正确的 collection 中
else if (existingProduct) {
if (!forceUpdate) {
// 只更新空字段
// 只更新空字段,但 Medusa 属性字段总是更新
const mergedData: any = {
lastSyncedAt: productData.lastSyncedAt,
medusaId: productData.medusaId,
}
// 基础字段:只更新空字段
if (!existingProduct.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.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(', ')}`)
// Medusa 属性字段:总是更新(以 Medusa 为准)
mergedData.tags = productData.tags
mergedData.type = productData.type
mergedData.collection = productData.collection
mergedData.category = productData.category
// 物理属性:总是更新
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({
collection: targetCollection,
id: existingProduct.id,
data: mergedData,
})
action = 'updated_partial'
}
} else {
// 强制更新所有字段
console.log(`[Sync Product API] ⚡ 强制更新所有字段`)
@ -207,7 +218,7 @@ export async function POST(request: Request) {
fakeOrderCount: finalProduct.fakeOrderCount,
}),
},
message: `产品已${action === 'created' ? '创建' : action === 'moved' ? '移动' : action === 'skipped' ? '跳过' : '更新'}${targetCollection}`,
message: `产品已${action === 'created' ? '创建' : action === 'moved' ? '移动' : '更新'}${targetCollection}`,
})
return addCorsHeaders(response, origin)

View File

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

View File

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

View File

@ -36,6 +36,31 @@ interface MedusaProduct {
id: 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
type_id?: string
}
@ -214,6 +239,15 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
// 提取 tags逗号分隔
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 {
medusaId: product.id,
seedId: product.metadata?.seed_id || null,
@ -225,20 +259,20 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
// Medusa 默认属性
tags: tags,
type: product.type_id || null,
collection: product.collection_id || null,
category: product.metadata?.category || null,
type: type,
collection: collection,
category: category,
// 物理属性(从 metadata 中提取)
height: product.metadata?.height ? Number(product.metadata.height) : null,
width: product.metadata?.width ? Number(product.metadata.width) : null,
length: product.metadata?.length ? Number(product.metadata.length) : null,
weight: product.metadata?.weight ? Number(product.metadata.weight) : null,
// 物理属性(优先使用直接字段,否则从 metadata 中提取)
height: product.height || (product.metadata?.height ? Number(product.metadata.height) : null),
width: product.width || (product.metadata?.width ? Number(product.metadata.width) : null),
length: product.length || (product.metadata?.length ? Number(product.metadata.length) : null),
weight: product.weight || (product.metadata?.weight ? Number(product.metadata.weight) : null),
// 海关与物流
midCode: product.metadata?.mid_code || product.metadata?.midCode || null,
hsCode: product.metadata?.hs_code || product.metadata?.hsCode || null,
countryOfOrigin: product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null,
// 海关与物流(优先使用直接字段,否则从 metadata 中提取)
midCode: product.mid_code || product.metadata?.mid_code || product.metadata?.midCode || null,
hsCode: product.hs_code || product.metadata?.hs_code || product.metadata?.hsCode || null,
countryOfOrigin: product.origin_country || product.metadata?.country_of_origin || product.metadata?.countryOfOrigin || null,
}
}

View File

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