Compare commits
2 Commits
f5621a3aa1
...
9a47af76ce
| Author | SHA1 | Date |
|---|---|---|
|
|
9a47af76ce | |
|
|
1f78d88d10 |
|
|
@ -25,10 +25,12 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e
|
||||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||||
import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton'
|
import { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton'
|
||||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||||
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
||||||
|
import { TaobaoLinkPreview as TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b } from '../../../components/fields/TaobaoLinkPreview'
|
||||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
||||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||||
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||||
|
|
@ -62,10 +64,12 @@ export const importMap = {
|
||||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||||
"/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08,
|
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f,
|
||||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
||||||
|
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b,
|
||||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
||||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量同步选中的产品到 Medusa
|
||||||
|
* POST /api/batch-sync-medusa
|
||||||
|
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate: boolean }
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { ids, collection, forceUpdate = false } = body
|
||||||
|
|
||||||
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'No product IDs provided',
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection || !['products', 'preorder-products'].includes(collection)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid collection',
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
total: ids.length,
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
details: [] as any[],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐个同步选中的产品
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
// 获取产品信息
|
||||||
|
const product = await payload.findByID({
|
||||||
|
collection: collection as 'products' | 'preorder-products',
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!product || !product.medusaId) {
|
||||||
|
results.failed++
|
||||||
|
results.details.push({
|
||||||
|
id,
|
||||||
|
title: product?.title || 'Unknown',
|
||||||
|
status: 'failed',
|
||||||
|
error: 'No Medusa ID',
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用单个产品同步 API
|
||||||
|
const syncResponse = await fetch(
|
||||||
|
`${request.url.replace('/api/batch-sync-medusa', '/api/sync-medusa')}?medusaId=${product.medusaId}&forceUpdate=${forceUpdate}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: request.headers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const syncResult = await syncResponse.json()
|
||||||
|
|
||||||
|
if (syncResult.success) {
|
||||||
|
results.success++
|
||||||
|
results.details.push({
|
||||||
|
id,
|
||||||
|
medusaId: product.medusaId,
|
||||||
|
title: product.title,
|
||||||
|
status: 'success',
|
||||||
|
action: syncResult.action,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results.failed++
|
||||||
|
results.details.push({
|
||||||
|
id,
|
||||||
|
medusaId: product.medusaId,
|
||||||
|
title: product.title,
|
||||||
|
status: 'failed',
|
||||||
|
error: syncResult.error || syncResult.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed++
|
||||||
|
results.details.push({
|
||||||
|
id,
|
||||||
|
status: 'failed',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `批量同步完成: ${results.success} 成功, ${results.failed} 失败`,
|
||||||
|
results,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Batch sync error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { getAllMedusaProducts } from '@/lib/medusa'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊断 API - 检查 Medusa 产品的 metadata
|
||||||
|
* GET /api/diagnose-products
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const products = await getAllMedusaProducts()
|
||||||
|
|
||||||
|
const diagnosis = products.map((product) => ({
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
handle: product.handle,
|
||||||
|
metadata: product.metadata,
|
||||||
|
is_preorder_value: product.metadata?.is_preorder,
|
||||||
|
is_preorder_type: typeof product.metadata?.is_preorder,
|
||||||
|
should_be_in_collection:
|
||||||
|
product.metadata?.is_preorder === true || product.metadata?.is_preorder === 'true'
|
||||||
|
? 'preorder-products'
|
||||||
|
: 'products',
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
total: products.length,
|
||||||
|
preorder_count: diagnosis.filter((d) => d.should_be_in_collection === 'preorder-products')
|
||||||
|
.length,
|
||||||
|
regular_count: diagnosis.filter((d) => d.should_be_in_collection === 'products').length,
|
||||||
|
products: diagnosis,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预购产品的订单列表(从 Medusa 获取)
|
||||||
|
* GET /api/preorders/:id/orders
|
||||||
|
*/
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { id } = params
|
||||||
|
|
||||||
|
// 获取预购产品
|
||||||
|
let product: any = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
product = await payload.findByID({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{ medusaId: { equals: id } },
|
||||||
|
{ seedId: { equals: id } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
product = result.docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Preorder product not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.medusaId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Product has no Medusa ID, cannot fetch orders' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Medusa 获取订单数据
|
||||||
|
const medusaUrl = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const medusaResponse = await fetch(`${medusaUrl}/admin/orders`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!medusaResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch orders from Medusa')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orders } = await medusaResponse.json()
|
||||||
|
|
||||||
|
// 筛选包含此产品的订单
|
||||||
|
const productOrders = (orders || [])
|
||||||
|
.filter((order: any) => {
|
||||||
|
return order?.items?.some((item: any) => item.product_id === product.medusaId)
|
||||||
|
})
|
||||||
|
.map((order: any) => {
|
||||||
|
// 提取此产品的订单项
|
||||||
|
const items = order.items.filter((item: any) => item.product_id === product.medusaId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: order.id,
|
||||||
|
display_id: order.display_id,
|
||||||
|
status: order.status,
|
||||||
|
payment_status: order.payment_status,
|
||||||
|
fulfillment_status: order.fulfillment_status,
|
||||||
|
customer_id: order.customer_id,
|
||||||
|
email: order.email,
|
||||||
|
total: order.total,
|
||||||
|
currency_code: order.currency_code,
|
||||||
|
created_at: order.created_at,
|
||||||
|
updated_at: order.updated_at,
|
||||||
|
items: items.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
variant_id: item.variant_id,
|
||||||
|
title: item.title,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit_price: item.unit_price,
|
||||||
|
total: item.total,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按创建时间倒序排序
|
||||||
|
productOrders.sort((a: any, b: any) =>
|
||||||
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
orders: productOrders,
|
||||||
|
count: productOrders.length,
|
||||||
|
product: {
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
medusa_id: product.medusaId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Payload Preorder Orders API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch preorder orders', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新计算预购产品的订单统计(从 Medusa 获取实际订单数据)
|
||||||
|
* POST /api/preorders/:id/recalculate
|
||||||
|
*/
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { id } = params
|
||||||
|
|
||||||
|
// 获取预购产品
|
||||||
|
let product: any = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
product = await payload.findByID({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{ medusaId: { equals: id } },
|
||||||
|
{ seedId: { equals: id } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
product = result.docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Preorder product not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.medusaId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Product has no Medusa ID, cannot recalculate' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Medusa 获取订单数据
|
||||||
|
const medusaUrl = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const medusaResponse = await fetch(`${medusaUrl}/admin/orders`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!medusaResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch orders from Medusa')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orders } = await medusaResponse.json()
|
||||||
|
|
||||||
|
// 统计每个变体的订单数量
|
||||||
|
const variantCounts: Record<string, number> = {}
|
||||||
|
let totalCount = 0
|
||||||
|
|
||||||
|
for (const order of orders || []) {
|
||||||
|
if (!order?.items) continue
|
||||||
|
|
||||||
|
for (const item of order.items) {
|
||||||
|
if (!item || item.product_id !== product.medusaId) continue
|
||||||
|
|
||||||
|
const variantId = item.variant_id
|
||||||
|
if (variantId) {
|
||||||
|
variantCounts[variantId] = (variantCounts[variantId] || 0) + (item.quantity || 0)
|
||||||
|
totalCount += item.quantity || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Medusa 获取产品变体列表
|
||||||
|
const productResponse = await fetch(`${medusaUrl}/admin/products/${product.medusaId}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!productResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch product from Medusa')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { product: medusaProduct } = await productResponse.json()
|
||||||
|
const variants = medusaProduct.variants || []
|
||||||
|
|
||||||
|
// 更新每个变体的 metadata(在 Medusa 中)
|
||||||
|
const updatePromises = variants.map(async (variant: any) => {
|
||||||
|
const count = variantCounts[variant.id] || 0
|
||||||
|
|
||||||
|
await fetch(`${medusaUrl}/admin/product-variants/${variant.id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
metadata: {
|
||||||
|
...(variant.metadata || {}),
|
||||||
|
current_orders: String(count),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
variant_id: variant.id,
|
||||||
|
count,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedVariants = await Promise.all(updatePromises)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
product_id: product.id,
|
||||||
|
total_orders: totalCount,
|
||||||
|
variants: updatedVariants,
|
||||||
|
message: `Recalculated orders for ${variants.length} variant(s)`,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Payload Preorder Recalculate API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to recalculate orders', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个预购产品详情
|
||||||
|
* GET /api/preorders/:id
|
||||||
|
*/
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { id } = params
|
||||||
|
|
||||||
|
// 尝试通过 Payload ID 查找
|
||||||
|
let product: any = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
product = await payload.findByID({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// 如果不是 Payload ID,尝试通过 medusaId 或 seedId 查找
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{ medusaId: { equals: id } },
|
||||||
|
{ seedId: { equals: id } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
product = result.docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Preorder product not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化变体数据
|
||||||
|
const variants = (product.variants || []).map((variant: any) => {
|
||||||
|
const currentOrders = parseInt(variant.currentOrders || '0', 10) || 0
|
||||||
|
const maxOrders = parseInt(variant.maxOrders || '0', 10) || 0
|
||||||
|
const availableSlots = maxOrders > 0 ? maxOrders - currentOrders : 0
|
||||||
|
const soldOut = maxOrders > 0 && currentOrders >= maxOrders
|
||||||
|
const utilization = maxOrders > 0 ? Math.round((currentOrders / maxOrders) * 100) : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: variant.id,
|
||||||
|
title: variant.title,
|
||||||
|
sku: variant.sku,
|
||||||
|
current_orders: currentOrders,
|
||||||
|
max_orders: maxOrders,
|
||||||
|
available_slots: availableSlots,
|
||||||
|
sold_out: soldOut,
|
||||||
|
utilization_percentage: utilization,
|
||||||
|
prices: variant.prices || [],
|
||||||
|
options: variant.options || {},
|
||||||
|
metadata: variant.metadata || {},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const totalOrders = variants.reduce((sum: number, v: any) => sum + v.current_orders, 0)
|
||||||
|
const totalMaxOrders = variants.reduce((sum: number, v: any) => sum + v.max_orders, 0)
|
||||||
|
const totalAvailable = variants.reduce((sum: number, v: any) => sum + v.available_slots, 0)
|
||||||
|
|
||||||
|
const fundingGoal = parseInt(product.fundingGoal || '0', 10) || totalMaxOrders
|
||||||
|
const completionPercentage = fundingGoal > 0
|
||||||
|
? Math.round((totalOrders / fundingGoal) * 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const allSoldOut = variants.every((v: any) => v.sold_out)
|
||||||
|
const someSoldOut = variants.some((v: any) => v.sold_out)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
preorder: {
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
handle: product.handle,
|
||||||
|
description: product.description,
|
||||||
|
status: product._status,
|
||||||
|
thumbnail: product.thumbnail,
|
||||||
|
images: product.images || [],
|
||||||
|
|
||||||
|
// IDs
|
||||||
|
seed_id: product.seedId || product.medusaId,
|
||||||
|
medusa_id: product.medusaId,
|
||||||
|
|
||||||
|
// 预购元数据(从 Payload 管理)
|
||||||
|
is_preorder: true,
|
||||||
|
preorder_type: product.preorderType || 'standard',
|
||||||
|
estimated_ship_date: product.estimatedShipDate || null,
|
||||||
|
preorder_end_date: product.preorderEndDate || null,
|
||||||
|
funding_goal: fundingGoal,
|
||||||
|
min_order_quantity: product.minOrderQuantity || 1,
|
||||||
|
max_order_quantity: product.maxOrderQuantity || 0,
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
current_orders: totalOrders,
|
||||||
|
total_max_orders: totalMaxOrders,
|
||||||
|
total_available_slots: totalAvailable,
|
||||||
|
completion_percentage: completionPercentage,
|
||||||
|
|
||||||
|
// 可用性状态
|
||||||
|
all_variants_sold_out: allSoldOut,
|
||||||
|
some_variants_sold_out: someSoldOut,
|
||||||
|
is_available: !allSoldOut && totalAvailable > 0,
|
||||||
|
|
||||||
|
// 详细信息
|
||||||
|
variants,
|
||||||
|
variants_count: variants.length,
|
||||||
|
categories: product.categories || [],
|
||||||
|
collection: product.collection || null,
|
||||||
|
metadata: product.metadata || {},
|
||||||
|
|
||||||
|
// 时间戳
|
||||||
|
created_at: product.createdAt,
|
||||||
|
updated_at: product.updatedAt,
|
||||||
|
last_synced_at: product.lastSyncedAt,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Payload Preorder Detail API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch preorder product', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新预购产品
|
||||||
|
* PATCH /api/preorders/:id
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - variant_id?: string - 如果提供,更新变体计数
|
||||||
|
* - current_orders?: number - 直接设置订单数
|
||||||
|
* - max_orders?: number - 更新最大订单数
|
||||||
|
* - increment?: number - 增加订单数
|
||||||
|
* - decrement?: number - 减少订单数
|
||||||
|
*
|
||||||
|
* - estimated_ship_date?: string - 更新预估发货日期
|
||||||
|
* - preorder_end_date?: string - 更新预购结束日期
|
||||||
|
* - funding_goal?: number - 更新众筹目标
|
||||||
|
* - preorder_type?: string - 更新预购类型
|
||||||
|
* - min_order_quantity?: number - 最小起订量
|
||||||
|
* - max_order_quantity?: number - 最大购买数量
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { id } = params
|
||||||
|
const body = await req.json()
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant_id,
|
||||||
|
current_orders,
|
||||||
|
max_orders,
|
||||||
|
increment,
|
||||||
|
decrement,
|
||||||
|
estimated_ship_date,
|
||||||
|
preorder_end_date,
|
||||||
|
funding_goal,
|
||||||
|
preorder_type,
|
||||||
|
min_order_quantity,
|
||||||
|
max_order_quantity,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
// 获取产品
|
||||||
|
let product: any = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
product = await payload.findByID({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{ medusaId: { equals: id } },
|
||||||
|
{ seedId: { equals: id } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
depth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
product = result.docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Preorder product not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式1: 更新变体预购计数
|
||||||
|
if (variant_id) {
|
||||||
|
// 预购变体数据存储在 Medusa 中,直接更新 Medusa
|
||||||
|
const medusaUrl = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
|
||||||
|
// 获取当前变体数据
|
||||||
|
const variantResponse = await fetch(`${medusaUrl}/admin/product-variants/${variant_id}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!variantResponse.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Variant not found in Medusa' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { variant } = await variantResponse.json()
|
||||||
|
const currentMeta = variant.metadata || {}
|
||||||
|
let newCurrentOrders = parseInt(currentMeta.current_orders || '0', 10) || 0
|
||||||
|
let newMaxOrders = parseInt(currentMeta.max_orders || '0', 10) || 0
|
||||||
|
|
||||||
|
// 处理更新逻辑
|
||||||
|
if (typeof current_orders === 'number') {
|
||||||
|
newCurrentOrders = Math.max(0, current_orders)
|
||||||
|
} else if (typeof increment === 'number') {
|
||||||
|
newCurrentOrders = Math.max(0, newCurrentOrders + increment)
|
||||||
|
} else if (typeof decrement === 'number') {
|
||||||
|
newCurrentOrders = Math.max(0, newCurrentOrders - decrement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof max_orders === 'number') {
|
||||||
|
newMaxOrders = Math.max(0, max_orders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 Medusa 变体 metadata
|
||||||
|
const updateResponse = await fetch(`${medusaUrl}/admin/product-variants/${variant_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
metadata: {
|
||||||
|
...currentMeta,
|
||||||
|
current_orders: String(newCurrentOrders),
|
||||||
|
max_orders: String(newMaxOrders),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!updateResponse.ok) {
|
||||||
|
throw new Error('Failed to update variant in Medusa')
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
variant_id,
|
||||||
|
current_orders: newCurrentOrders,
|
||||||
|
max_orders: newMaxOrders,
|
||||||
|
available_slots: newMaxOrders - newCurrentOrders,
|
||||||
|
sold_out: newMaxOrders > 0 && newCurrentOrders >= newMaxOrders,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式2: 更新产品级别预购元数据(在 Payload 中管理)
|
||||||
|
const updateData: any = {}
|
||||||
|
|
||||||
|
if (estimated_ship_date !== undefined) {
|
||||||
|
updateData.estimatedShipDate = estimated_ship_date
|
||||||
|
}
|
||||||
|
if (preorder_end_date !== undefined) {
|
||||||
|
updateData.preorderEndDate = preorder_end_date
|
||||||
|
}
|
||||||
|
if (funding_goal !== undefined) {
|
||||||
|
updateData.fundingGoal = String(funding_goal)
|
||||||
|
}
|
||||||
|
if (preorder_type !== undefined) {
|
||||||
|
updateData.preorderType = preorder_type
|
||||||
|
}
|
||||||
|
if (min_order_quantity !== undefined) {
|
||||||
|
updateData.minOrderQuantity = min_order_quantity
|
||||||
|
}
|
||||||
|
if (max_order_quantity !== undefined) {
|
||||||
|
updateData.maxOrderQuantity = max_order_quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await payload.update({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id: product.id,
|
||||||
|
data: updateData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Preorder product updated successfully',
|
||||||
|
updated_fields: Object.keys(updateData),
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Payload Preorder Update API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to update preorder product', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预购产品列表
|
||||||
|
* GET /api/preorders
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* - seed_id: 按 seed_id 筛选
|
||||||
|
* - status: 按状态筛选 (draft|published)
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const seed_id = searchParams.get('seed_id')
|
||||||
|
const status = searchParams.get('status')
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const where: any = {}
|
||||||
|
|
||||||
|
if (seed_id) {
|
||||||
|
where.seedId = { equals: seed_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
where._status = { equals: status }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询预购产品集合
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where,
|
||||||
|
depth: 2,
|
||||||
|
limit: 100,
|
||||||
|
sort: '-createdAt',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化数据 - 以预购为主,展示完整变体信息
|
||||||
|
const formattedProducts = result.docs.map((product: any) => {
|
||||||
|
// 计算变体预购统计
|
||||||
|
const variants = (product.variants || []).map((variant: any) => {
|
||||||
|
const currentOrders = parseInt(variant.currentOrders || '0', 10) || 0
|
||||||
|
const maxOrders = parseInt(variant.maxOrders || '0', 10) || 0
|
||||||
|
const availableSlots = maxOrders > 0 ? maxOrders - currentOrders : 0
|
||||||
|
const soldOut = maxOrders > 0 && currentOrders >= maxOrders
|
||||||
|
const utilization = maxOrders > 0 ? Math.round((currentOrders / maxOrders) * 100) : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: variant.id,
|
||||||
|
title: variant.title,
|
||||||
|
sku: variant.sku,
|
||||||
|
current_orders: currentOrders,
|
||||||
|
max_orders: maxOrders,
|
||||||
|
available_slots: availableSlots,
|
||||||
|
sold_out: soldOut,
|
||||||
|
utilization_percentage: utilization,
|
||||||
|
prices: variant.prices || [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 产品级别统计
|
||||||
|
const totalOrders = variants.reduce((sum: number, v: any) => sum + v.current_orders, 0)
|
||||||
|
const totalMaxOrders = variants.reduce((sum: number, v: any) => sum + v.max_orders, 0)
|
||||||
|
const totalAvailable = variants.reduce((sum: number, v: any) => sum + v.available_slots, 0)
|
||||||
|
|
||||||
|
const fundingGoal = parseInt(product.fundingGoal || '0', 10) || totalMaxOrders
|
||||||
|
const completionPercentage = fundingGoal > 0
|
||||||
|
? Math.round((totalOrders / fundingGoal) * 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const allSoldOut = variants.every((v: any) => v.sold_out)
|
||||||
|
const someSoldOut = variants.some((v: any) => v.sold_out)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
handle: product.handle,
|
||||||
|
status: product._status,
|
||||||
|
thumbnail: product.thumbnail,
|
||||||
|
description: product.description,
|
||||||
|
|
||||||
|
// Seed ID
|
||||||
|
seed_id: product.seedId || product.medusaId,
|
||||||
|
medusa_id: product.medusaId,
|
||||||
|
|
||||||
|
// 预购元数据
|
||||||
|
preorder_type: product.preorderType || 'standard',
|
||||||
|
estimated_ship_date: product.estimatedShipDate || null,
|
||||||
|
funding_goal: fundingGoal,
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
current_orders: totalOrders,
|
||||||
|
total_max_orders: totalMaxOrders,
|
||||||
|
total_available_slots: totalAvailable,
|
||||||
|
completion_percentage: completionPercentage,
|
||||||
|
|
||||||
|
// 可用性状态
|
||||||
|
all_variants_sold_out: allSoldOut,
|
||||||
|
some_variants_sold_out: someSoldOut,
|
||||||
|
is_available: !allSoldOut && totalAvailable > 0,
|
||||||
|
|
||||||
|
// 变体详情
|
||||||
|
variants,
|
||||||
|
variants_count: variants.length,
|
||||||
|
|
||||||
|
// 时间戳
|
||||||
|
created_at: product.createdAt,
|
||||||
|
updated_at: product.updatedAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 排序:有库存优先 → 完成度高优先
|
||||||
|
formattedProducts.sort((a: any, b: any) => {
|
||||||
|
if (a.is_available !== b.is_available) {
|
||||||
|
return a.is_available ? -1 : 1
|
||||||
|
}
|
||||||
|
if (a.completion_percentage !== b.completion_percentage) {
|
||||||
|
return b.completion_percentage - a.completion_percentage
|
||||||
|
}
|
||||||
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
preorders: formattedProducts,
|
||||||
|
count: formattedProducts.length,
|
||||||
|
summary: {
|
||||||
|
total: formattedProducts.length,
|
||||||
|
available: formattedProducts.filter((p: any) => p.is_available).length,
|
||||||
|
sold_out: formattedProducts.filter((p: any) => p.all_variants_sold_out).length,
|
||||||
|
total_orders: formattedProducts.reduce((sum: number, p: any) => sum + p.current_orders, 0),
|
||||||
|
total_slots: formattedProducts.reduce((sum: number, p: any) => sum + p.total_max_orders, 0),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Payload Preorders API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch preorder products', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,95 @@ import {
|
||||||
getProductCollection,
|
getProductCollection,
|
||||||
} from '@/lib/medusa'
|
} from '@/lib/medusa'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 seedId 或 medusaId 查找产品(优先使用 seedId)
|
||||||
|
* 会在两个 collection 中查找
|
||||||
|
* @returns { product, collection } 或 null
|
||||||
|
*/
|
||||||
|
async function findProductBySeedIdOrMedusaId(
|
||||||
|
payload: any,
|
||||||
|
seedId: string | null,
|
||||||
|
medusaId: string,
|
||||||
|
): Promise<{ product: any; collection: 'products' | 'preorder-products' } | null> {
|
||||||
|
const collections: Array<'products' | 'preorder-products'> = ['products', 'preorder-products']
|
||||||
|
|
||||||
|
// 优先通过 seedId 查找
|
||||||
|
if (seedId) {
|
||||||
|
for (const collection of collections) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection,
|
||||||
|
where: {
|
||||||
|
seedId: { equals: seedId },
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs[0]) {
|
||||||
|
return { product: result.docs[0], collection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果通过 seedId 没找到,使用 medusaId 查找
|
||||||
|
for (const collection of collections) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection,
|
||||||
|
where: {
|
||||||
|
medusaId: { equals: medusaId },
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs[0]) {
|
||||||
|
return { product: result.docs[0], collection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并产品数据 - 只更新 Payload 中为空的字段
|
||||||
|
* @param existingProduct Payload 中现有的产品数据
|
||||||
|
* @param newData 从 Medusa 转换来的新数据
|
||||||
|
* @param forceUpdate 是否强制更新所有字段
|
||||||
|
* @returns 合并后的数据
|
||||||
|
*/
|
||||||
|
function mergeProductData(existingProduct: any, newData: any, forceUpdate: boolean): any {
|
||||||
|
if (forceUpdate) {
|
||||||
|
// 强制更新模式:使用所有新数据
|
||||||
|
return { ...newData }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只填充空值模式:只更新空字段
|
||||||
|
const mergedData: any = {}
|
||||||
|
|
||||||
|
// 总是更新这些字段
|
||||||
|
mergedData.lastSyncedAt = newData.lastSyncedAt
|
||||||
|
mergedData.medusaId = newData.medusaId
|
||||||
|
|
||||||
|
// 如果 seedId 为空,更新它
|
||||||
|
if (!existingProduct.seedId && newData.seedId) {
|
||||||
|
mergedData.seedId = newData.seedId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在字段为空时更新
|
||||||
|
if (!existingProduct.title) {
|
||||||
|
mergedData.title = newData.title
|
||||||
|
}
|
||||||
|
if (!existingProduct.handle) {
|
||||||
|
mergedData.handle = newData.handle
|
||||||
|
}
|
||||||
|
if (!existingProduct.thumbnail) {
|
||||||
|
mergedData.thumbnail = newData.thumbnail
|
||||||
|
}
|
||||||
|
if (!existingProduct.status) {
|
||||||
|
mergedData.status = newData.status
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedData
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步 Medusa 商品到 Payload CMS
|
* 同步 Medusa 商品到 Payload CMS
|
||||||
* GET /api/sync-medusa
|
* GET /api/sync-medusa
|
||||||
|
|
@ -16,6 +105,24 @@ import {
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
// 可选的 API Key 验证
|
||||||
|
const authHeader = request.headers.get('authorization')
|
||||||
|
const storeApiKey = process.env.STORE_API_KEY
|
||||||
|
|
||||||
|
// 如果配置了 STORE_API_KEY,则验证请求
|
||||||
|
if (storeApiKey && authHeader) {
|
||||||
|
const token = authHeader.replace('Bearer ', '')
|
||||||
|
if (token !== storeApiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid API key',
|
||||||
|
},
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 payloadId = searchParams.get('payloadId')
|
||||||
|
|
@ -78,31 +185,18 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
||||||
const otherCollection =
|
const otherCollection =
|
||||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
||||||
|
|
||||||
// 在目标 collection 中检查是否已存在
|
// 转换数据
|
||||||
const existingInTarget = await payload.find({
|
|
||||||
collection: targetCollection,
|
|
||||||
where: {
|
|
||||||
medusaId: { equals: medusaId },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 在另一个 collection 中检查是否存在(产品类型可能改变)
|
|
||||||
const existingInOther = await payload.find({
|
|
||||||
collection: otherCollection,
|
|
||||||
where: {
|
|
||||||
medusaId: { equals: medusaId },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||||
|
const seedId = productData.seedId
|
||||||
|
|
||||||
// 如果在另一个 collection 中存在,需要删除并在正确的 collection 中创建
|
// 使用新的查找函数(优先 seedId)
|
||||||
if (existingInOther.docs[0]) {
|
const found = await findProductBySeedIdOrMedusaId(payload, seedId, medusaId)
|
||||||
|
|
||||||
|
// 如果在另一个 collection 中找到,需要移动
|
||||||
|
if (found && found.collection !== targetCollection) {
|
||||||
await payload.delete({
|
await payload.delete({
|
||||||
collection: otherCollection,
|
collection: found.collection,
|
||||||
id: existingInOther.docs[0].id,
|
id: found.product.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const created = await payload.create({
|
const created = await payload.create({
|
||||||
|
|
@ -113,27 +207,50 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'moved',
|
action: 'moved',
|
||||||
message: `商品 ${medusaId} 已从 ${otherCollection} 移动到 ${targetCollection}`,
|
message: `商品 ${medusaId} 已从 ${found.collection} 移动到 ${targetCollection}`,
|
||||||
productId: created.id,
|
productId: created.id,
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingProduct = existingInTarget.docs[0]
|
// 如果在目标 collection 中找到
|
||||||
|
if (found) {
|
||||||
|
const existingProduct = found.product
|
||||||
|
|
||||||
// 如果存在且不强制更新,跳过
|
// 如果存在且不强制更新,只更新空字段
|
||||||
if (existingProduct && !forceUpdate) {
|
if (!forceUpdate) {
|
||||||
|
// 合并数据(只更新空字段)
|
||||||
|
const mergedData = mergeProductData(existingProduct, productData, false)
|
||||||
|
|
||||||
|
// 如果没有需要更新的字段,跳过
|
||||||
|
if (Object.keys(mergedData).length <= 2) {
|
||||||
|
// 只有 lastSyncedAt 和 medusaId
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'skipped',
|
action: 'skipped',
|
||||||
message: `商品 ${medusaId} 已存在于 ${targetCollection}`,
|
message: `商品 ${medusaId} 已存在于 ${targetCollection},且所有字段都有值`,
|
||||||
productId: existingProduct.id,
|
productId: existingProduct.id,
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingProduct) {
|
// 更新(只更新空字段)
|
||||||
// 更新现有商品
|
const updated = await payload.update({
|
||||||
|
collection: targetCollection,
|
||||||
|
id: existingProduct.id,
|
||||||
|
data: mergedData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'updated_partial',
|
||||||
|
message: `商品 ${medusaId} 已部分更新(仅空字段)于 ${targetCollection}`,
|
||||||
|
productId: updated.id,
|
||||||
|
collection: targetCollection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制更新所有字段
|
||||||
const updated = await payload.update({
|
const updated = await payload.update({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
id: existingProduct.id,
|
id: existingProduct.id,
|
||||||
|
|
@ -147,8 +264,9 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
||||||
productId: updated.id,
|
productId: updated.id,
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// 创建新商品
|
|
||||||
|
// 不存在,创建新商品
|
||||||
const created = await payload.create({
|
const created = await payload.create({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
data: productData,
|
data: productData,
|
||||||
|
|
@ -161,7 +279,6 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
||||||
productId: created.id,
|
productId: created.id,
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error syncing product ${medusaId}:`, error)
|
console.error(`Error syncing product ${medusaId}:`, error)
|
||||||
return {
|
return {
|
||||||
|
|
@ -252,6 +369,8 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
total: 0,
|
total: 0,
|
||||||
created: 0,
|
created: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
|
updated_partial: 0,
|
||||||
|
moved: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
errors: 0,
|
errors: 0,
|
||||||
details: [] as any[],
|
details: [] as any[],
|
||||||
|
|
@ -273,34 +392,23 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
try {
|
try {
|
||||||
// 确定应该同步到哪个 collection
|
// 确定应该同步到哪个 collection
|
||||||
const targetCollection = getProductCollection(medusaProduct)
|
const targetCollection = getProductCollection(medusaProduct)
|
||||||
const otherCollection =
|
|
||||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
|
||||||
|
|
||||||
// 在目标 collection 中检查是否已存在
|
|
||||||
const existingInTarget = await payload.find({
|
|
||||||
collection: targetCollection,
|
|
||||||
where: {
|
|
||||||
medusaId: { equals: medusaProduct.id },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 在另一个 collection 中检查是否存在(产品类型可能改变)
|
|
||||||
const existingInOther = await payload.find({
|
|
||||||
collection: otherCollection,
|
|
||||||
where: {
|
|
||||||
medusaId: { equals: medusaProduct.id },
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 转换数据
|
||||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||||
|
const seedId = productData.seedId
|
||||||
|
|
||||||
|
// 使用新的查找函数(优先 seedId)
|
||||||
|
const found = await findProductBySeedIdOrMedusaId(
|
||||||
|
payload,
|
||||||
|
seedId,
|
||||||
|
medusaProduct.id,
|
||||||
|
)
|
||||||
|
|
||||||
// 如果在错误的 collection 中,移动它
|
// 如果在错误的 collection 中,移动它
|
||||||
if (existingInOther.docs[0]) {
|
if (found && found.collection !== targetCollection) {
|
||||||
await payload.delete({
|
await payload.delete({
|
||||||
collection: otherCollection,
|
collection: found.collection,
|
||||||
id: existingInOther.docs[0].id,
|
id: found.product.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
await payload.create({
|
await payload.create({
|
||||||
|
|
@ -308,24 +416,33 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
data: productData,
|
data: productData,
|
||||||
})
|
})
|
||||||
|
|
||||||
results.updated++
|
results.moved++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
medusaId: medusaProduct.id,
|
medusaId: medusaProduct.id,
|
||||||
|
seedId: seedId,
|
||||||
title: medusaProduct.title,
|
title: medusaProduct.title,
|
||||||
action: 'moved',
|
action: 'moved',
|
||||||
from: otherCollection,
|
from: found.collection,
|
||||||
to: targetCollection,
|
to: targetCollection,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingProduct = existingInTarget.docs[0]
|
// 如果在目标 collection 中找到
|
||||||
|
if (found) {
|
||||||
|
const existingProduct = found.product
|
||||||
|
|
||||||
// 如果存在且不强制更新,跳过
|
// 如果不强制更新,只更新空字段
|
||||||
if (existingProduct && !forceUpdate) {
|
if (!forceUpdate) {
|
||||||
|
const mergedData = mergeProductData(existingProduct, productData, false)
|
||||||
|
|
||||||
|
// 如果没有需要更新的字段,跳过
|
||||||
|
if (Object.keys(mergedData).length <= 2) {
|
||||||
|
// 只有 lastSyncedAt 和 medusaId
|
||||||
results.skipped++
|
results.skipped++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
medusaId: medusaProduct.id,
|
medusaId: medusaProduct.id,
|
||||||
|
seedId: seedId,
|
||||||
title: medusaProduct.title,
|
title: medusaProduct.title,
|
||||||
action: 'skipped',
|
action: 'skipped',
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
|
|
@ -333,8 +450,25 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingProduct) {
|
// 更新(只更新空字段)
|
||||||
// 更新
|
await payload.update({
|
||||||
|
collection: targetCollection,
|
||||||
|
id: existingProduct.id,
|
||||||
|
data: mergedData,
|
||||||
|
})
|
||||||
|
|
||||||
|
results.updated_partial++
|
||||||
|
results.details.push({
|
||||||
|
medusaId: medusaProduct.id,
|
||||||
|
seedId: seedId,
|
||||||
|
title: medusaProduct.title,
|
||||||
|
action: 'updated_partial',
|
||||||
|
collection: targetCollection,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制更新
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
id: existingProduct.id,
|
id: existingProduct.id,
|
||||||
|
|
@ -343,12 +477,13 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
results.updated++
|
results.updated++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
medusaId: medusaProduct.id,
|
medusaId: medusaProduct.id,
|
||||||
|
seedId: seedId,
|
||||||
title: medusaProduct.title,
|
title: medusaProduct.title,
|
||||||
action: 'updated',
|
action: 'updated',
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 创建
|
// 创建新商品
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
data: productData,
|
data: productData,
|
||||||
|
|
@ -356,6 +491,7 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
results.created++
|
results.created++
|
||||||
results.details.push({
|
results.details.push({
|
||||||
medusaId: medusaProduct.id,
|
medusaId: medusaProduct.id,
|
||||||
|
seedId: seedId,
|
||||||
title: medusaProduct.title,
|
title: medusaProduct.title,
|
||||||
action: 'created',
|
action: 'created',
|
||||||
collection: targetCollection,
|
collection: targetCollection,
|
||||||
|
|
@ -382,7 +518,7 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `同步完成: ${results.created} 个创建, ${results.updated} 个更新, ${results.skipped} 个跳过, ${results.errors} 个错误`,
|
message: `同步完成: ${results.created} 个创建, ${results.updated} 个更新, ${results.updated_partial} 个部分更新, ${results.moved} 个移动, ${results.skipped} 个跳过, ${results.errors} 个错误`,
|
||||||
results,
|
results,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||||
},
|
},
|
||||||
beforeListTable: [
|
beforeListTable: [
|
||||||
'/components/sync/SyncMedusaButton#SyncMedusaButton',
|
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||||
'/components/list/ProductGridStyler',
|
'/components/list/ProductGridStyler',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -51,7 +51,7 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
type: 'tabs',
|
type: 'tabs',
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
label: '基本信息',
|
label: 'ℹ️ 基本信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'row',
|
type: 'row',
|
||||||
|
|
@ -84,6 +84,16 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'seedId',
|
||||||
|
type: 'text',
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Seed ID (从 Medusa 同步,用于数据绑定)',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
@ -123,7 +133,84 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '商品描述',
|
label: '⚙️ 预购设置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'preorderType',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'standard',
|
||||||
|
options: [
|
||||||
|
{ label: '标准预购', value: 'standard' },
|
||||||
|
{ label: '众筹预购', value: 'crowdfunding' },
|
||||||
|
{ label: '限量预购', value: 'limited' },
|
||||||
|
],
|
||||||
|
admin: {
|
||||||
|
description: '预购类型',
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fundingGoal',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
admin: {
|
||||||
|
description: '众筹目标数量(0 表示以变体 max_orders 总和为准)',
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'estimatedShipDate',
|
||||||
|
type: 'date',
|
||||||
|
admin: {
|
||||||
|
description: '预计发货日期',
|
||||||
|
date: {
|
||||||
|
displayFormat: 'yyyy-MM-dd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preorderEndDate',
|
||||||
|
type: 'date',
|
||||||
|
admin: {
|
||||||
|
description: '预购结束日期(可选)',
|
||||||
|
date: {
|
||||||
|
displayFormat: 'yyyy-MM-dd HH:mm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'minOrderQuantity',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: 1,
|
||||||
|
admin: {
|
||||||
|
description: '最小起订量',
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxOrderQuantity',
|
||||||
|
type: 'number',
|
||||||
|
admin: {
|
||||||
|
description: '最大购买数量(0 表示不限制)',
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '📝 商品描述',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
|
|
@ -183,7 +270,7 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '相关商品',
|
label: '🔗 相关商品',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relatedProducts',
|
name: 'relatedProducts',
|
||||||
|
|
@ -199,6 +286,84 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '📦 订单信息',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'ordersDisplay',
|
||||||
|
type: 'ui',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: '/components/fields/PreorderOrdersField#PreorderOrdersField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '🛒 淘宝链接',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'taobaoLinks',
|
||||||
|
type: 'array',
|
||||||
|
label: '淘宝采购链接列表',
|
||||||
|
admin: {
|
||||||
|
description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)',
|
||||||
|
initCollapsed: false,
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => !!user,
|
||||||
|
update: ({ req: { user } }) => !!user,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
label: '🔗 淘宝链接',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
placeholder: 'https://item.taobao.com/...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
label: '📝 标题',
|
||||||
|
admin: {
|
||||||
|
placeholder: '链接标题或商品名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
type: 'text',
|
||||||
|
label: '🖼️ 缩略图 URL',
|
||||||
|
admin: {
|
||||||
|
placeholder: 'https://...',
|
||||||
|
description: '淘宝商品图片地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'note',
|
||||||
|
type: 'textarea',
|
||||||
|
label: '📄 备注',
|
||||||
|
admin: {
|
||||||
|
placeholder: '其他备注信息...',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ui',
|
||||||
|
name: 'linkPreview',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export const Products: CollectionConfig = {
|
||||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||||
},
|
},
|
||||||
beforeListTable: [
|
beforeListTable: [
|
||||||
'/components/sync/SyncMedusaButton#SyncMedusaButton',
|
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||||
'/components/list/ProductGridStyler',
|
'/components/list/ProductGridStyler',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -51,7 +51,7 @@ export const Products: CollectionConfig = {
|
||||||
type: 'tabs',
|
type: 'tabs',
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
label: '基本信息',
|
label: 'ℹ️ 基本信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'row',
|
type: 'row',
|
||||||
|
|
@ -90,6 +90,16 @@ export const Products: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'seedId',
|
||||||
|
type: 'text',
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Seed ID (恥从 Medusa 同步,用于数据绑定)',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
@ -131,7 +141,7 @@ export const Products: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '商品详情',
|
label: '📄 商品详情',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
|
|
@ -183,7 +193,7 @@ export const Products: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关联信息',
|
label: '🔗 关联信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relatedProducts',
|
name: 'relatedProducts',
|
||||||
|
|
@ -199,6 +209,70 @@ export const Products: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '🛒 淘宝链接',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'taobaoLinks',
|
||||||
|
type: 'array',
|
||||||
|
label: '淘宝采购链接列表',
|
||||||
|
admin: {
|
||||||
|
description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)',
|
||||||
|
initCollapsed: false,
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => !!user,
|
||||||
|
update: ({ req: { user } }) => !!user,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
label: '🔗 淘宝链接',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
placeholder: 'https://item.taobao.com/...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
label: '📝 标题',
|
||||||
|
admin: {
|
||||||
|
placeholder: '链接标题或商品名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
type: 'text',
|
||||||
|
label: '🖼️ 缩略图 URL',
|
||||||
|
admin: {
|
||||||
|
placeholder: 'https://...',
|
||||||
|
description: '淘宝商品图片地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'note',
|
||||||
|
type: 'textarea',
|
||||||
|
label: '📄 备注',
|
||||||
|
admin: {
|
||||||
|
placeholder: '其他备注信息...',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ui',
|
||||||
|
name: 'linkPreview',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useField, useFormFields } from '@payloadcms/ui'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string
|
||||||
|
display_id: number
|
||||||
|
status: string
|
||||||
|
payment_status: string
|
||||||
|
email: string
|
||||||
|
total: number
|
||||||
|
currency_code: string
|
||||||
|
created_at: string
|
||||||
|
items: Array<{
|
||||||
|
title: string
|
||||||
|
quantity: number
|
||||||
|
unit_price: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreorderOrdersField: React.FC = () => {
|
||||||
|
const { value: medusaId } = useField<string>({ path: 'medusaId' })
|
||||||
|
const { value: seedId } = useField<string>({ path: 'seedId' })
|
||||||
|
|
||||||
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [stats, setStats] = useState<{ total: number; totalAmount: number } | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (medusaId || seedId) {
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
}, [medusaId, seedId])
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
const productId = seedId || medusaId
|
||||||
|
if (!productId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/preorders/${productId}/orders`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch orders')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setOrders(data.orders || [])
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const totalAmount = data.orders?.reduce((sum: number, order: Order) => sum + order.total, 0) || 0
|
||||||
|
setStats({
|
||||||
|
total: data.count || 0,
|
||||||
|
totalAmount,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to fetch orders:', err)
|
||||||
|
setError(err.message || 'Failed to load orders')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number, currency: string) => {
|
||||||
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency.toUpperCase(),
|
||||||
|
}).format(amount / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!medusaId && !seedId) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||||
|
<p style={{ margin: 0, color: '#666' }}>
|
||||||
|
产品尚未同步到 Medusa,无法查看订单
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||||
|
<p style={{ margin: 0 }}>加载订单中...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem', background: '#fee', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #fcc' }}>
|
||||||
|
<p style={{ margin: 0, color: '#c00' }}>加载失败: {error}</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchOrders}
|
||||||
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||||
|
<p style={{ margin: 0, color: '#666' }}>暂无订单</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchOrders}
|
||||||
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
{/* 统计信息 */}
|
||||||
|
{stats && (
|
||||||
|
<div style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: '#e3f2fd',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||||
|
gap: '1rem',
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总数</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总额</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>
|
||||||
|
{formatCurrency(stats.totalAmount, orders[0]?.currency_code || 'CNY')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={fetchOrders}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid #1976d2',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#1976d2',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔄 刷新订单
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 订单列表 */}
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f5f5f5', borderBottom: '2px solid #e0e0e0' }}>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>订单号</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>客户</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>商品</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'right', fontSize: '0.875rem', fontWeight: 600 }}>金额</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'center', fontSize: '0.875rem', fontWeight: 600 }}>状态</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map((order, index) => (
|
||||||
|
<tr
|
||||||
|
key={order.id}
|
||||||
|
style={{
|
||||||
|
borderBottom: index < orders.length - 1 ? '1px solid #e0e0e0' : 'none',
|
||||||
|
background: index % 2 === 0 ? '#fff' : '#fafafa',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>#{order.display_id}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#999' }}>{order.id.slice(0, 8)}</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{order.email}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{order.items.map((item, i) => (
|
||||||
|
<div key={i} style={{ marginBottom: i < order.items.length - 1 ? '0.25rem' : 0 }}>
|
||||||
|
{item.title} × {item.quantity}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem', textAlign: 'right', fontWeight: 600 }}>
|
||||||
|
{formatCurrency(order.total, order.currency_code)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
background: order.status === 'completed' ? '#e8f5e9' : '#fff3e0',
|
||||||
|
color: order.status === 'completed' ? '#2e7d32' : '#f57c00',
|
||||||
|
}}>
|
||||||
|
{order.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{formatDate(order.created_at)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useField, useFormFields } from '@payloadcms/ui'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 淘宝链接预览组件
|
||||||
|
* 显示在每个淘宝链接数组项中
|
||||||
|
*/
|
||||||
|
export const TaobaoLinkPreview: React.FC = () => {
|
||||||
|
const { value: url } = useField<string>({ path: 'url' })
|
||||||
|
const { value: thumbnail } = useField<string>({ path: 'thumbnail' })
|
||||||
|
|
||||||
|
const openLink = () => {
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url && !thumbnail) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
padding: '0.75rem',
|
||||||
|
background: '#f7f9fb',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{thumbnail && (
|
||||||
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#666', marginBottom: '0.25rem' }}>
|
||||||
|
预览:
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={thumbnail}
|
||||||
|
alt="淘宝商品预览"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).style.display = 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{url && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openLink}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#ff6700',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
width: 'fit-content',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔗 打开淘宝链接
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,13 @@ import { useDocumentInfo } from '@payloadcms/ui'
|
||||||
* 用于在商品编辑页面强制更新该商品的信息
|
* 用于在商品编辑页面强制更新该商品的信息
|
||||||
*/
|
*/
|
||||||
export function ForceSyncButton() {
|
export function ForceSyncButton() {
|
||||||
const { id } = useDocumentInfo()
|
const { id, collectionSlug } = useDocumentInfo()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
const handleForceSync = async () => {
|
const handleForceSync = async () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setMessage('无法获取商品 ID')
|
setMessage('❌ 无法获取商品 ID')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,38 +26,62 @@ export function ForceSyncButton() {
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/sync-medusa?payloadId=${id}&forceUpdate=true`, {
|
const response = await fetch(
|
||||||
|
`/api/sync-medusa?payloadId=${id}&collection=${collectionSlug}&forceUpdate=true`,
|
||||||
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setMessage(data.message || '强制同步成功!')
|
setMessage('✅ ' + (data.message || '强制同步成功!'))
|
||||||
// 刷新页面显示更新后的数据
|
// 刷新页面显示更新后的数据
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
} else {
|
} else {
|
||||||
setMessage(`同步失败: ${data.error || data.message}`)
|
setMessage(`❌ 同步失败: ${data.error || data.message}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
setMessage(`❌ 同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<div style={{
|
||||||
<Button onClick={handleForceSync} disabled={loading} buttonStyle="secondary">
|
marginTop: '1rem',
|
||||||
{loading ? '同步中...' : '从 Medusa 强制更新'}
|
padding: '1rem',
|
||||||
|
background: 'var(--theme-elevation-50)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-100)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: '1.25rem' }}>🔄</span>
|
||||||
|
从 Medusa 同步数据
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleForceSync}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
>
|
||||||
|
⚡ {loading ? '同步中...' : '強制更新此商品'}
|
||||||
</Button>
|
</Button>
|
||||||
{message && (
|
{message && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.75rem',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
message.includes('失败') || message.includes('出错')
|
message.includes('❌')
|
||||||
? 'var(--theme-error-50)'
|
? 'var(--theme-error-50)'
|
||||||
: 'var(--theme-success-50)',
|
: 'var(--theme-success-50)',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
|
@ -67,6 +91,13 @@ export function ForceSyncButton() {
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div style={{
|
||||||
|
marginTop: '0.75rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'var(--theme-elevation-400)',
|
||||||
|
}}>
|
||||||
|
💡 此操作将从 Medusa 获取最新数据并覆盖当前商品信息
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
'use client'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Button } from '@payloadcms/ui'
|
|
||||||
|
|
||||||
export function SyncMedusaButton() {
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [message, setMessage] = useState('')
|
|
||||||
const [showConfirmInput, setShowConfirmInput] = useState(false)
|
|
||||||
const [confirmText, setConfirmText] = useState('')
|
|
||||||
|
|
||||||
const handleSync = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
setMessage('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/sync-medusa?forceUpdate=false', {
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setMessage(data.message || '同步成功!')
|
|
||||||
// 刷新页面显示新商品
|
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
|
||||||
} else {
|
|
||||||
setMessage(`同步失败: ${data.error || data.message || '未知错误'}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleForceUpdateAll = () => {
|
|
||||||
setShowConfirmInput(true)
|
|
||||||
setMessage('')
|
|
||||||
setConfirmText('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirmForceUpdate = async () => {
|
|
||||||
if (confirmText !== 'FORCE_UPDATE_ALL') {
|
|
||||||
setMessage('确认字符不正确,请输入: FORCE_UPDATE_ALL')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
setMessage('')
|
|
||||||
setShowConfirmInput(false)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/sync-medusa?forceUpdate=true', {
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setMessage(data.message || '强制更新成功!')
|
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
|
||||||
} else {
|
|
||||||
setMessage(`同步失败: ${data.error || data.message || '未知错误'}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setConfirmText('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelForceUpdate = () => {
|
|
||||||
setShowConfirmInput(false)
|
|
||||||
setConfirmText('')
|
|
||||||
setMessage('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '1rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
|
||||||
<h3 style={{ marginBottom: '1rem' }}>Medusa 商品同步</h3>
|
|
||||||
|
|
||||||
{showConfirmInput ? (
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
padding: '0.75rem',
|
|
||||||
backgroundColor: 'var(--theme-warning-50)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: '0 0 0.5rem 0',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'var(--theme-warning-900)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
⚠️ 危险操作
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem' }}>
|
|
||||||
这将强制更新所有已存在的商品,覆盖所有本地修改。
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: 0, fontSize: '0.875rem' }}>
|
|
||||||
请输入{' '}
|
|
||||||
<code
|
|
||||||
style={{
|
|
||||||
padding: '0.125rem 0.25rem',
|
|
||||||
backgroundColor: 'var(--theme-elevation-100)',
|
|
||||||
borderRadius: '2px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
FORCE_UPDATE_ALL
|
|
||||||
</code>{' '}
|
|
||||||
确认:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={confirmText}
|
|
||||||
onChange={(e) => setConfirmText(e.target.value)}
|
|
||||||
placeholder="输入 FORCE_UPDATE_ALL"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.5rem',
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
border: '1px solid var(--theme-elevation-400)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleConfirmForceUpdate}
|
|
||||||
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
|
||||||
>
|
|
||||||
{loading ? '更新中...' : '确认强制更新'}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCancelForceUpdate} disabled={loading} buttonStyle="secondary">
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1rem' }}>
|
|
||||||
<Button onClick={handleSync} disabled={loading}>
|
|
||||||
{loading ? '同步中...' : '同步新商品'}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleForceUpdateAll} disabled={loading} buttonStyle="secondary">
|
|
||||||
强制更新全部
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '0.75rem',
|
|
||||||
backgroundColor:
|
|
||||||
message.includes('失败') || message.includes('出错') || message.includes('不正确')
|
|
||||||
? 'var(--theme-error-50)'
|
|
||||||
: 'var(--theme-success-50)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!showConfirmInput && (
|
|
||||||
<div
|
|
||||||
style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-400)' }}
|
|
||||||
>
|
|
||||||
<p style={{ marginBottom: '0.5rem' }}>
|
|
||||||
• <strong>同步新商品</strong>: 从 Medusa 导入尚未同步的商品,不会更新已存在的商品。
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: 0 }}>
|
|
||||||
• <strong>强制更新全部</strong>: 更新所有商品,覆盖本地修改(需要确认)。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button, useSelection } from '@payloadcms/ui'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的同步按钮组件
|
||||||
|
* 整合所有同步功能,布局更紧凑
|
||||||
|
*/
|
||||||
|
export function UnifiedSyncButton() {
|
||||||
|
const { getQueryParams, toggleAll } = useSelection()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [showForceAllConfirm, setShowForceAllConfirm] = useState(false)
|
||||||
|
const [confirmText, setConfirmText] = useState('')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 获取当前页面的 collection slug
|
||||||
|
const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
|
||||||
|
const collectionSlug = pathname.includes('preorder-products')
|
||||||
|
? 'preorder-products'
|
||||||
|
: 'products'
|
||||||
|
|
||||||
|
// 同步新商品
|
||||||
|
const handleSyncNew = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/sync-medusa?forceUpdate=false', {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('✅ ' + (data.message || '同步成功!'))
|
||||||
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
|
} else {
|
||||||
|
setMessage('❌ 同步失败: ' + (data.error || data.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('❌ 同步出错: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量同步选中
|
||||||
|
const handleBatchSync = async (forceUpdate: boolean = false) => {
|
||||||
|
try {
|
||||||
|
const queryParams = getQueryParams()
|
||||||
|
let selectedIds: string[] = []
|
||||||
|
|
||||||
|
if (queryParams && typeof queryParams === 'object') {
|
||||||
|
const whereCondition = (queryParams as any).where
|
||||||
|
if (whereCondition?.id?.in) {
|
||||||
|
selectedIds = whereCondition.id.in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedIds || selectedIds.length === 0) {
|
||||||
|
setMessage('⚠️ 请先勾选要同步的商品(使用列表左侧的复选框)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
forceUpdate &&
|
||||||
|
!confirm(`确定要强制更新选中的 ${selectedIds.length} 个商品吗?这将覆盖本地修改。`)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
const response = await fetch('/api/batch-sync-medusa', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: selectedIds,
|
||||||
|
collection: collectionSlug,
|
||||||
|
forceUpdate,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('✅ ' + (data.message || '批量同步成功!'))
|
||||||
|
if (toggleAll) {
|
||||||
|
toggleAll()
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
router.refresh()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
setMessage('❌ 批量同步失败: ' + (data.error || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('❌ 批量同步出错: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制更新全部
|
||||||
|
const handleForceUpdateAll = async () => {
|
||||||
|
if (confirmText !== 'FORCE_UPDATE_ALL') {
|
||||||
|
setMessage('❌ 确认字符不正确,请输入: FORCE_UPDATE_ALL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
setShowForceAllConfirm(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/sync-medusa?forceUpdate=true', {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('✅ ' + (data.message || '强制更新成功!'))
|
||||||
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
|
} else {
|
||||||
|
setMessage('❌ 同步失败: ' + (data.error || data.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('❌ 同步出错: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setConfirmText('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span style={{ fontSize: '1.25rem' }}>🔄</span>
|
||||||
|
Medusa 商品同步管理
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{showForceAllConfirm ? (
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: 'var(--theme-warning-50)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: '0 0 0.5rem 0',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'var(--theme-warning-900)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚠️ 危险操作
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem' }}>
|
||||||
|
这将强制更新所有已存在的商品,覆盖所有本地修改。
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: 0, fontSize: '0.875rem' }}>
|
||||||
|
请输入{' '}
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
padding: '0.125rem 0.25rem',
|
||||||
|
backgroundColor: 'var(--theme-elevation-100)',
|
||||||
|
borderRadius: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
FORCE_UPDATE_ALL
|
||||||
|
</code>{' '}
|
||||||
|
确认:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={confirmText}
|
||||||
|
onChange={(e) => setConfirmText(e.target.value)}
|
||||||
|
placeholder="输入 FORCE_UPDATE_ALL"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleForceUpdateAll}
|
||||||
|
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||||
|
>
|
||||||
|
{loading ? '更新中...' : '✅ 确认强制更新'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowForceAllConfirm(false)
|
||||||
|
setConfirmText('')
|
||||||
|
setMessage('')
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
>
|
||||||
|
❌ 取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||||
|
gap: '0.75rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 第一行:基础同步功能 */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSyncNew}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="primary"
|
||||||
|
>
|
||||||
|
📥 {loading ? '同步中...' : '同步新商品'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => handleBatchSync(false)}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
>
|
||||||
|
🔄 {loading ? '同步中...' : '同步选中商品'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 第二行:强制更新功能 */}
|
||||||
|
<Button
|
||||||
|
onClick={() => handleBatchSync(true)}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
>
|
||||||
|
⚡ {loading ? '更新中...' : '强制更新选中'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowForceAllConfirm(true)
|
||||||
|
setMessage('')
|
||||||
|
setConfirmText('')
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
>
|
||||||
|
🔥 强制更新全部
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: message.includes('❌')
|
||||||
|
? 'var(--theme-error-50)'
|
||||||
|
: message.includes('⚠️')
|
||||||
|
? 'var(--theme-warning-50)'
|
||||||
|
: 'var(--theme-success-50)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showForceAllConfirm && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'var(--theme-elevation-400)',
|
||||||
|
lineHeight: '1.5',
|
||||||
|
background: 'var(--theme-elevation-50)',
|
||||||
|
padding: '0.75rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: '0.5rem', fontWeight: 600 }}>💡 功能说明:</div>
|
||||||
|
<div style={{ display: 'grid', gap: '0.25rem' }}>
|
||||||
|
<div>
|
||||||
|
<strong>📥 同步新商品</strong>: 从 Medusa 导入尚未同步的商品
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>🔄 同步选中商品</strong>: 只更新选中商品的空字段
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>⚡ 强制更新选中</strong>: 覆盖选中商品的所有字段
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>🔥 强制更新全部</strong>: 更新所有商品(需要确认)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,8 @@ interface MedusaProduct {
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
metadata?: Record<string, any> & {
|
metadata?: Record<string, any> & {
|
||||||
is_preorder?: boolean
|
is_preorder?: boolean | string
|
||||||
|
seed_id?: string
|
||||||
}
|
}
|
||||||
images?: Array<{
|
images?: Array<{
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -177,10 +178,12 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断产品是否为预售商品
|
* 判断产品是否为预售商品
|
||||||
* 检查 metadata.is_preorder
|
* 检查 metadata.is_preorder (支持布尔值和字符串)
|
||||||
*/
|
*/
|
||||||
export function isPreorderProduct(product: MedusaProduct): boolean {
|
export function isPreorderProduct(product: MedusaProduct): boolean {
|
||||||
return product.metadata?.is_preorder === true
|
const isPreorder = product.metadata?.is_preorder
|
||||||
|
// 支持布尔值 true 或字符串 "true"
|
||||||
|
return isPreorder === true || isPreorder === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -204,6 +207,7 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
medusaId: product.id,
|
medusaId: product.id,
|
||||||
|
seedId: product.metadata?.seed_id || null,
|
||||||
title: product.title,
|
title: product.title,
|
||||||
handle: product.handle,
|
handle: product.handle,
|
||||||
thumbnail: thumbnailUrl || null,
|
thumbnail: thumbnailUrl || null,
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,10 @@ export interface Product {
|
||||||
* 商品详情状态
|
* 商品详情状态
|
||||||
*/
|
*/
|
||||||
status: 'draft' | 'published';
|
status: 'draft' | 'published';
|
||||||
|
/**
|
||||||
|
* Seed ID (恥从 Medusa 同步,用于数据绑定)
|
||||||
|
*/
|
||||||
|
seedId?: string | null;
|
||||||
/**
|
/**
|
||||||
* 商品标题(从 Medusa 同步)
|
* 商品标题(从 Medusa 同步)
|
||||||
*/
|
*/
|
||||||
|
|
@ -251,6 +255,21 @@ export interface Product {
|
||||||
}
|
}
|
||||||
)[]
|
)[]
|
||||||
| null;
|
| null;
|
||||||
|
/**
|
||||||
|
* 💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)
|
||||||
|
*/
|
||||||
|
taobaoLinks?:
|
||||||
|
| {
|
||||||
|
url: string;
|
||||||
|
title?: string | null;
|
||||||
|
/**
|
||||||
|
* 淘宝商品图片地址
|
||||||
|
*/
|
||||||
|
thumbnail?: string | null;
|
||||||
|
note?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +289,10 @@ export interface PreorderProduct {
|
||||||
* 商品详情状态
|
* 商品详情状态
|
||||||
*/
|
*/
|
||||||
status: 'draft' | 'published';
|
status: 'draft' | 'published';
|
||||||
|
/**
|
||||||
|
* Seed ID (从 Medusa 同步,用于数据绑定)
|
||||||
|
*/
|
||||||
|
seedId?: string | null;
|
||||||
/**
|
/**
|
||||||
* 商品标题(从 Medusa 同步)
|
* 商品标题(从 Medusa 同步)
|
||||||
*/
|
*/
|
||||||
|
|
@ -286,6 +309,30 @@ export interface PreorderProduct {
|
||||||
* 上次同步时间
|
* 上次同步时间
|
||||||
*/
|
*/
|
||||||
lastSyncedAt?: string | null;
|
lastSyncedAt?: string | null;
|
||||||
|
/**
|
||||||
|
* 预购类型
|
||||||
|
*/
|
||||||
|
preorderType: 'standard' | 'crowdfunding' | 'limited';
|
||||||
|
/**
|
||||||
|
* 众筹目标数量(0 表示以变体 max_orders 总和为准)
|
||||||
|
*/
|
||||||
|
fundingGoal: number;
|
||||||
|
/**
|
||||||
|
* 预计发货日期
|
||||||
|
*/
|
||||||
|
estimatedShipDate?: string | null;
|
||||||
|
/**
|
||||||
|
* 预购结束日期(可选)
|
||||||
|
*/
|
||||||
|
preorderEndDate?: string | null;
|
||||||
|
/**
|
||||||
|
* 最小起订量
|
||||||
|
*/
|
||||||
|
minOrderQuantity?: number | null;
|
||||||
|
/**
|
||||||
|
* 最大购买数量(0 表示不限制)
|
||||||
|
*/
|
||||||
|
maxOrderQuantity?: number | null;
|
||||||
/**
|
/**
|
||||||
* 预售商品的详细描述(支持富文本编辑)
|
* 预售商品的详细描述(支持富文本编辑)
|
||||||
*/
|
*/
|
||||||
|
|
@ -319,6 +366,21 @@ export interface PreorderProduct {
|
||||||
}
|
}
|
||||||
)[]
|
)[]
|
||||||
| null;
|
| null;
|
||||||
|
/**
|
||||||
|
* 💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)
|
||||||
|
*/
|
||||||
|
taobaoLinks?:
|
||||||
|
| {
|
||||||
|
url: string;
|
||||||
|
title?: string | null;
|
||||||
|
/**
|
||||||
|
* 淘宝商品图片地址
|
||||||
|
*/
|
||||||
|
thumbnail?: string | null;
|
||||||
|
note?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -762,12 +824,22 @@ 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;
|
||||||
status?: T;
|
status?: T;
|
||||||
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
handle?: T;
|
handle?: T;
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
relatedProducts?: T;
|
relatedProducts?: T;
|
||||||
|
taobaoLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
title?: T;
|
||||||
|
thumbnail?: T;
|
||||||
|
note?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|
@ -778,12 +850,28 @@ 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;
|
||||||
status?: T;
|
status?: T;
|
||||||
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
handle?: T;
|
handle?: T;
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
|
preorderType?: T;
|
||||||
|
fundingGoal?: T;
|
||||||
|
estimatedShipDate?: T;
|
||||||
|
preorderEndDate?: T;
|
||||||
|
minOrderQuantity?: T;
|
||||||
|
maxOrderQuantity?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
relatedProducts?: T;
|
relatedProducts?: T;
|
||||||
|
taobaoLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
title?: T;
|
||||||
|
thumbnail?: T;
|
||||||
|
note?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue