同步精简并优化
This commit is contained in:
parent
a424a5c1a9
commit
c8de57af22
|
|
@ -1,7 +1,7 @@
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { getAllMedusaProducts } from '@/lib/medusa'
|
import { getAllMedusaProducts, transformMedusaProductToPayload } from '@/lib/medusa'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Sync Selected Products
|
* Batch Sync Selected Products
|
||||||
|
|
@ -74,13 +74,32 @@ export async function POST(request: Request) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update basic fields from Medusa
|
// 使用统一 transform,保持与 sync/product 逻辑一致
|
||||||
const updateData: any = {
|
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||||
lastSyncedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceUpdate || !product.title) updateData.title = medusaProduct.title
|
const updateData: any = {
|
||||||
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail
|
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({
|
await payload.update({
|
||||||
collection: collection as 'products' | 'preorder-products',
|
collection: collection as 'products' | 'preorder-products',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export async function POST(request: NextRequest) {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'x-payload-api-key': process.env.PAYLOAD_API_KEY || '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export async function GET(req: NextRequest) {
|
||||||
const baseInfo = {
|
const baseInfo = {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
medusaId: product.medusaId,
|
medusaId: product.medusaId,
|
||||||
|
handle: product.handle || null,
|
||||||
seedId: product.seedId,
|
seedId: product.seedId,
|
||||||
title: product.title,
|
title: product.title,
|
||||||
thumbnail: product.thumbnail,
|
thumbnail: product.thumbnail,
|
||||||
|
|
@ -82,25 +83,28 @@ export async function GET(req: NextRequest) {
|
||||||
|
|
||||||
// 如果是预购产品,添加预购特有字段
|
// 如果是预购产品,添加预购特有字段
|
||||||
if (productRef.relationTo === 'preorder-products') {
|
if (productRef.relationTo === 'preorder-products') {
|
||||||
|
const realOrderCount = product.orderCount || 0
|
||||||
|
const fakeOrderCount = product.fakeOrderCount || 0
|
||||||
|
const totalCount = realOrderCount + fakeOrderCount
|
||||||
return {
|
return {
|
||||||
...baseInfo,
|
...baseInfo,
|
||||||
relationTo: 'preorder-products',
|
relationTo: 'preorder-products',
|
||||||
preorder: {
|
preorder: {
|
||||||
type: product.preorderType || 'standard',
|
type: product.preorderType || 'standard',
|
||||||
fundingGoal: product.fundingGoal || 0,
|
fundingGoal: product.fundingGoal || 0,
|
||||||
orderCount: product.orderCount || 0,
|
orderCount: totalCount,
|
||||||
startDate: product.preorderStartDate,
|
startDate: product.preorderStartDate,
|
||||||
endDate: product.preorderEndDate,
|
endDate: product.preorderEndDate,
|
||||||
// 计算进度百分比
|
// 计算进度百分比(含 fakeOrderCount,用于展示)
|
||||||
progress: product.fundingGoal > 0
|
progress: product.fundingGoal > 0
|
||||||
? Math.min(Math.round((product.orderCount / product.fundingGoal) * 100), 100)
|
? Math.min(Math.round((totalCount / product.fundingGoal) * 100), 100)
|
||||||
: 0,
|
: 0,
|
||||||
// 计算剩余天数
|
// 计算剩余天数
|
||||||
daysLeft: product.preorderEndDate
|
daysLeft: product.preorderEndDate
|
||||||
? Math.max(0, Math.ceil((new Date(product.preorderEndDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
|
? Math.max(0, Math.ceil((new Date(product.preorderEndDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
|
||||||
: null,
|
: null,
|
||||||
// 支持者数量(使用 orderCount)
|
// 支持者数量(含 fakeOrderCount,用于展示)
|
||||||
backers: product.orderCount || 0,
|
backers: totalCount,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import config from '@payload-config'
|
||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const { id } = params
|
const { id } = await params
|
||||||
|
|
||||||
// 获取预购产品
|
// 获取预购产品
|
||||||
let product: any = null
|
let product: any = null
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import config from '@payload-config'
|
||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const { id } = params
|
const { id } = await params
|
||||||
|
|
||||||
// 获取预购产品
|
// 获取预购产品
|
||||||
let product: any = null
|
let product: any = null
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import config from '@payload-config'
|
||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const { id } = params
|
const { id } = await params
|
||||||
|
|
||||||
// 尝试通过 Payload ID 查找
|
// 尝试通过 Payload ID 查找
|
||||||
let product: any = null
|
let product: any = null
|
||||||
|
|
@ -160,11 +160,11 @@ export async function GET(
|
||||||
*/
|
*/
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const { id } = params
|
const { id } = await params
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -159,6 +159,7 @@ export async function POST(request: Request) {
|
||||||
// 基础字段:Medusa 来源的字段总是更新
|
// 基础字段:Medusa 来源的字段总是更新
|
||||||
mergedData.seedId = productData.seedId
|
mergedData.seedId = productData.seedId
|
||||||
mergedData.title = productData.title
|
mergedData.title = productData.title
|
||||||
|
mergedData.handle = productData.handle
|
||||||
mergedData.status = productData.status
|
mergedData.status = productData.status
|
||||||
// thumbnail 只在为空时同步(Payload 编辑优先)
|
// thumbnail 只在为空时同步(Payload 编辑优先)
|
||||||
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,17 @@ export const ProductBaseFields: Field[] = [
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Medusa 商品 ID',
|
description: 'Medusa 商品 ID',
|
||||||
readOnly: true,
|
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: {
|
admin: {
|
||||||
description: '商品详情状态',
|
description: '商品详情状态',
|
||||||
width: '40%',
|
width: '30%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export function UnifiedSyncButton() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
||||||
const response = await fetch('/api/sync/batch-medusa', {
|
const response = await fetch('/api/admin/batch-sync-medusa', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,10 @@ export interface Product {
|
||||||
* Medusa 商品 ID
|
* Medusa 商品 ID
|
||||||
*/
|
*/
|
||||||
medusaId: string;
|
medusaId: string;
|
||||||
|
/**
|
||||||
|
* Medusa 商品 Handle(用于前台 URL,从 Medusa 同步)
|
||||||
|
*/
|
||||||
|
handle?: string | null;
|
||||||
/**
|
/**
|
||||||
* 商品详情状态
|
* 商品详情状态
|
||||||
*/
|
*/
|
||||||
|
|
@ -336,6 +340,10 @@ export interface PreorderProduct {
|
||||||
* Medusa 商品 ID
|
* Medusa 商品 ID
|
||||||
*/
|
*/
|
||||||
medusaId: string;
|
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> {
|
export interface ProductsSelect<T extends boolean = true> {
|
||||||
medusaId?: T;
|
medusaId?: T;
|
||||||
|
handle?: T;
|
||||||
status?: T;
|
status?: T;
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
|
|
@ -960,6 +969,7 @@ export interface ProductsSelect<T extends boolean = true> {
|
||||||
*/
|
*/
|
||||||
export interface PreorderProductsSelect<T extends boolean = true> {
|
export interface PreorderProductsSelect<T extends boolean = true> {
|
||||||
medusaId?: T;
|
medusaId?: T;
|
||||||
|
handle?: T;
|
||||||
status?: T;
|
status?: T;
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue