同步确定

This commit is contained in:
龟男日记\www 2026-02-20 04:18:24 +08:00
parent 9a47af76ce
commit efb3f2c727
19 changed files with 531 additions and 235 deletions

View File

@ -4,14 +4,16 @@ DATABASE_URL=postgresql://user:password@localhost:5432/database
# Payload # Payload
PAYLOAD_SECRET=YOUR_SECRET_HERE PAYLOAD_SECRET=YOUR_SECRET_HERE
# Redis 配置 # Redis Configuration
REDIS_HOST=localhost REDIS_URL=redis://localhost:6379
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Store API Key用于访问 hero-slider 和 product-recommendations 接口) # API Keys (统一使用 PAYLOAD_API_KEY)
STORE_API_KEY=your-store-api-key-here PAYLOAD_API_KEY=your-payload-api-key-here
# Medusa 配置
MEDUSA_BACKEND_URL=http://localhost:9000
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=your-publishable-key-here
PAYLOAD_API_KEY=your-payload-api-key-here
# Cloudflare R2 配置 # Cloudflare R2 配置
CLOUDFLARE_R2_BUCKET=your-bucket CLOUDFLARE_R2_BUCKET=your-bucket

View File

@ -25,12 +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 { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton' import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } 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 { 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 { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
import { RefreshOrderCountButton as RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e } from '../../../components/sync/RefreshOrderCountButton'
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'
@ -64,12 +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/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f, "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
"/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/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
"/components/sync/RefreshOrderCountButton#RefreshOrderCountButton": RefreshOrderCountButton_f6ce1bfc16a20083ee4c6ceb7022839e,
"/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,

View File

@ -28,6 +28,7 @@ export async function GET(request: Request) {
const results = { const results = {
products: 0, products: 0,
preorderProducts: 0,
announcements: 0, announcements: 0,
articles: 0, articles: 0,
errors: [] as string[], errors: [] as string[],
@ -47,6 +48,20 @@ export async function GET(request: Request) {
results.errors.push(errorMsg) results.errors.push(errorMsg)
} }
// 清理 Preorder Products
try {
const deletedPreorderProducts = await payload.delete({
collection: 'preorder-products',
where: {},
})
results.preorderProducts = deletedPreorderProducts.docs?.length || 0
console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`)
} catch (error) {
const errorMsg = `清理 Preorder Products 失败: ${error instanceof Error ? error.message : '未知错误'}`
console.error('❌', errorMsg)
results.errors.push(errorMsg)
}
// 清理 Announcements // 清理 Announcements
try { try {
const deletedAnnouncements = await payload.delete({ const deletedAnnouncements = await payload.delete({
@ -77,7 +92,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `数据清理完成!已删除 ${results.products} 个商品、${results.announcements} 个公告、${results.articles} 个文章`, message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`,
results, results,
}) })
} catch (error) { } catch (error) {

View File

@ -0,0 +1,50 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
export async function POST() {
try {
const payload = await getPayload({ config })
// Delete all products
const products = await payload.find({
collection: 'products',
limit: 1000,
})
for (const product of products.docs) {
await payload.delete({
collection: 'products',
id: product.id,
})
}
// Delete all preorder products
const preorderProducts = await payload.find({
collection: 'preorder-products',
limit: 1000,
})
for (const product of preorderProducts.docs) {
await payload.delete({
collection: 'preorder-products',
id: product.id,
})
}
return NextResponse.json({
success: true,
message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`,
deleted: {
products: products.docs.length,
preorderProducts: preorderProducts.docs.length,
},
})
} catch (error) {
console.error('[clear-payload-collections] Error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : String(error),
}, { status: 500 })
}
}

View File

@ -0,0 +1,32 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
export async function GET() {
try {
const payload = await getPayload({ config })
const products = await payload.find({
collection: 'products',
limit: 0, // Just get count
})
const preorderProducts = await payload.find({
collection: 'preorder-products',
limit: 0, // Just get count
})
return NextResponse.json({
success: true,
products: products.totalDocs,
preorderProducts: preorderProducts.totalDocs,
total: products.totalDocs + preorderProducts.totalDocs,
})
} catch (error) {
console.error('[payload-stats] Error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : String(error),
}, { status: 500 })
}
}

View File

@ -89,7 +89,6 @@ export async function GET(
preorder: { preorder: {
id: product.id, id: product.id,
title: product.title, title: product.title,
handle: product.handle,
description: product.description, description: product.description,
status: product._status, status: product._status,
thumbnail: product.thumbnail, thumbnail: product.thumbnail,
@ -102,11 +101,13 @@ export async function GET(
// 预购元数据(从 Payload 管理) // 预购元数据(从 Payload 管理)
is_preorder: true, is_preorder: true,
preorder_type: product.preorderType || 'standard', preorder_type: product.preorderType || 'standard',
estimated_ship_date: product.estimatedShipDate || null,
preorder_end_date: product.preorderEndDate || null, preorder_end_date: product.preorderEndDate || null,
funding_goal: fundingGoal, funding_goal: fundingGoal,
min_order_quantity: product.minOrderQuantity || 1,
max_order_quantity: product.maxOrderQuantity || 0, // 订单计数
order_count: parseInt(product.orderCount || '0', 10) || 0,
fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0,
total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0),
// 统计数据 // 统计数据
current_orders: totalOrders, current_orders: totalOrders,
@ -152,12 +153,10 @@ export async function GET(
* - increment?: number - * - increment?: number -
* - decrement?: number - * - decrement?: number -
* *
* - estimated_ship_date?: string -
* - preorder_end_date?: string - * - preorder_end_date?: string -
* - funding_goal?: number - * - funding_goal?: number -
* - preorder_type?: string - * - preorder_type?: string -
* - min_order_quantity?: number - * - fake_order_count?: number - Fake
* - max_order_quantity?: number -
*/ */
export async function PATCH( export async function PATCH(
req: NextRequest, req: NextRequest,
@ -174,12 +173,10 @@ export async function PATCH(
max_orders, max_orders,
increment, increment,
decrement, decrement,
estimated_ship_date,
preorder_end_date, preorder_end_date,
funding_goal, funding_goal,
preorder_type, preorder_type,
min_order_quantity, fake_order_count,
max_order_quantity,
} = body } = body
// 获取产品 // 获取产品
@ -285,9 +282,6 @@ export async function PATCH(
// 模式2: 更新产品级别预购元数据(在 Payload 中管理) // 模式2: 更新产品级别预购元数据(在 Payload 中管理)
const updateData: any = {} const updateData: any = {}
if (estimated_ship_date !== undefined) {
updateData.estimatedShipDate = estimated_ship_date
}
if (preorder_end_date !== undefined) { if (preorder_end_date !== undefined) {
updateData.preorderEndDate = preorder_end_date updateData.preorderEndDate = preorder_end_date
} }
@ -297,11 +291,8 @@ export async function PATCH(
if (preorder_type !== undefined) { if (preorder_type !== undefined) {
updateData.preorderType = preorder_type updateData.preorderType = preorder_type
} }
if (min_order_quantity !== undefined) { if (fake_order_count !== undefined) {
updateData.minOrderQuantity = min_order_quantity updateData.fakeOrderCount = Math.max(0, fake_order_count)
}
if (max_order_quantity !== undefined) {
updateData.maxOrderQuantity = max_order_quantity
} }
if (Object.keys(updateData).length > 0) { if (Object.keys(updateData).length > 0) {

View File

@ -76,7 +76,6 @@ export async function GET(req: NextRequest) {
return { return {
id: product.id, id: product.id,
title: product.title, title: product.title,
handle: product.handle,
status: product._status, status: product._status,
thumbnail: product.thumbnail, thumbnail: product.thumbnail,
description: product.description, description: product.description,
@ -87,9 +86,13 @@ export async function GET(req: NextRequest) {
// 预购元数据 // 预购元数据
preorder_type: product.preorderType || 'standard', preorder_type: product.preorderType || 'standard',
estimated_ship_date: product.estimatedShipDate || null,
funding_goal: fundingGoal, funding_goal: fundingGoal,
// 订单计数
order_count: parseInt(product.orderCount || '0', 10) || 0,
fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0,
total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0),
// 统计数据 // 统计数据
current_orders: totalOrders, current_orders: totalOrders,
total_max_orders: totalMaxOrders, total_max_orders: totalMaxOrders,

View File

@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
try { try {
// 验证 API Key // 验证 API Key
const apiKey = req.headers.get('x-store-api-key') const apiKey = req.headers.get('x-store-api-key')
const validApiKey = process.env.STORE_API_KEY const validApiKey = process.env.PAYLOAD_API_KEY
if (!apiKey || !validApiKey || apiKey !== validApiKey) { if (!apiKey || !validApiKey || apiKey !== validApiKey) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })

View File

@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
try { try {
// 验证 API Key // 验证 API Key
const apiKey = req.headers.get('x-store-api-key') const apiKey = req.headers.get('x-store-api-key')
const validApiKey = process.env.STORE_API_KEY const validApiKey = process.env.PAYLOAD_API_KEY
if (!apiKey || !validApiKey || apiKey !== validApiKey) { if (!apiKey || !validApiKey || apiKey !== validApiKey) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })

View File

@ -0,0 +1,163 @@
import { NextRequest, NextResponse } from 'next/server'
import payload from 'payload'
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || ''
/**
*
* POST /api/refresh-order-counts
*
* Body:
* - productIds?: string[] - ID Payload ID
* - refreshAll?: boolean -
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { productIds, refreshAll } = body
if (!productIds && !refreshAll) {
return NextResponse.json(
{ success: false, error: '请提供 productIds 或设置 refreshAll' },
{ status: 400 }
)
}
let products: any[] = []
if (refreshAll) {
// 获取所有预购商品
const result = await payload.find({
collection: 'preorder-products',
limit: 1000,
where: {
medusaId: {
exists: true,
},
},
})
products = result.docs
} else {
// 获取指定的商品
const result = await payload.find({
collection: 'preorder-products',
limit: productIds.length,
where: {
id: {
in: productIds,
},
medusaId: {
exists: true,
},
},
})
products = result.docs
}
if (products.length === 0) {
return NextResponse.json(
{ success: false, error: '没有找到要刷新的商品' },
{ status: 404 }
)
}
// 统计更新结果
let successCount = 0
let failCount = 0
const errors: string[] = []
// 为每个商品刷新订单计数
for (const product of products) {
try {
const orderCount = await fetchProductOrderCount(product.medusaId)
await payload.update({
collection: 'preorder-products',
id: product.id,
data: {
orderCount,
},
})
successCount++
} catch (error) {
failCount++
const errorMsg = `商品 ${product.title} (${product.medusaId}): ${
error instanceof Error ? error.message : '未知错误'
}`
errors.push(errorMsg)
console.error('刷新订单计数失败:', errorMsg)
}
}
return NextResponse.json({
success: true,
message: `刷新完成!成功: ${successCount},失败: ${failCount}`,
details: {
total: products.length,
success: successCount,
failed: failCount,
errors: errors.length > 0 ? errors : undefined,
},
})
} catch (error) {
console.error('刷新订单计数 API 错误:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : '未知错误',
},
{ status: 500 }
)
}
}
/**
* Medusa
*/
async function fetchProductOrderCount(productId: string): Promise<number> {
try {
// 使用 Medusa Admin API 查询订单
// 注意:这里需要使用 Query API 或者 Admin Orders API
// 查询所有包含该商品的已完成订单
const response = await fetch(
`${MEDUSA_BACKEND_URL}/admin/orders?fields=id,items&items[product_id]=${productId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '',
...(MEDUSA_ADMIN_API_KEY && { 'Authorization': `Bearer ${MEDUSA_ADMIN_API_KEY}` }),
},
}
)
if (!response.ok) {
throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`)
}
const data = await response.json()
// 计算订单中该商品的总数量
let totalQuantity = 0
if (data.orders && Array.isArray(data.orders)) {
for (const order of data.orders) {
if (order.items && Array.isArray(order.items)) {
for (const item of order.items) {
if (item.product_id === productId) {
totalQuantity += item.quantity || 0
}
}
}
}
}
return totalQuantity
} catch (error) {
console.error(`获取商品 ${productId} 订单数量失败:`, error)
throw error
}
}

View File

@ -107,12 +107,12 @@ export async function GET(request: Request) {
try { try {
// 可选的 API Key 验证 // 可选的 API Key 验证
const authHeader = request.headers.get('authorization') const authHeader = request.headers.get('authorization')
const storeApiKey = process.env.STORE_API_KEY const payloadApiKey = process.env.PAYLOAD_API_KEY
// 如果配置了 STORE_API_KEY则验证请求 // 如果配置了 PAYLOAD_API_KEY则验证请求
if (storeApiKey && authHeader) { if (payloadApiKey && authHeader) {
const token = authHeader.replace('Bearer ', '') const token = authHeader.replace('Bearer ', '')
if (token !== storeApiKey) { if (token !== payloadApiKey) {
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@ -29,16 +29,14 @@ export const PreorderProducts: CollectionConfig = {
useAsTitle: 'title', useAsTitle: 'title',
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'], defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
description: '管理预售商品的详细内容和描述', description: '管理预售商品的详细内容和描述',
listSearchableFields: ['title', 'medusaId', 'handle'], listSearchableFields: ['title', 'medusaId'],
pagination: { pagination: {
defaultLimit: 25, defaultLimit: 25,
}, },
components: { components: {
edit: {
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
},
beforeListTable: [ beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/sync/UnifiedSyncButton#UnifiedSyncButton',
'/components/sync/RefreshOrderCountButton#RefreshOrderCountButton',
'/components/list/ProductGridStyler', '/components/list/ProductGridStyler',
], ],
}, },
@ -103,20 +101,15 @@ export const PreorderProducts: CollectionConfig = {
readOnly: true, readOnly: true,
}, },
}, },
{
name: 'handle',
type: 'text',
admin: {
description: '商品 URL handle从 Medusa 同步)',
readOnly: true,
},
},
{ {
name: 'thumbnail', name: 'thumbnail',
type: 'text', type: 'text',
admin: { admin: {
description: '商品缩略图 URL从 Medusa 同步)', description: '商品封面 URL支持上传或输入 URL',
readOnly: true, components: {
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
Field: '/components/fields/ThumbnailField#ThumbnailField',
},
}, },
}, },
{ {
@ -165,16 +158,6 @@ export const PreorderProducts: CollectionConfig = {
}, },
], ],
}, },
{
name: 'estimatedShipDate',
type: 'date',
admin: {
description: '预计发货日期',
date: {
displayFormat: 'yyyy-MM-dd',
},
},
},
{ {
name: 'preorderEndDate', name: 'preorderEndDate',
type: 'date', type: 'date',
@ -189,19 +172,21 @@ export const PreorderProducts: CollectionConfig = {
type: 'row', type: 'row',
fields: [ fields: [
{ {
name: 'minOrderQuantity', name: 'orderCount',
type: 'number', type: 'number',
defaultValue: 1, defaultValue: 0,
admin: { admin: {
description: '最小起订量', description: '真实订单计数(从 Medusa 同步)',
readOnly: true,
width: '50%', width: '50%',
}, },
}, },
{ {
name: 'maxOrderQuantity', name: 'fakeOrderCount',
type: 'number', type: 'number',
defaultValue: 0,
admin: { admin: {
description: '最大购买数量0 表示不限制', description: 'Fake 订单计数(手动设置',
width: '50%', width: '50%',
}, },
}, },

View File

@ -29,14 +29,11 @@ export const Products: CollectionConfig = {
useAsTitle: 'title', useAsTitle: 'title',
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'], defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
description: '管理 Medusa 商品的详细内容和描述', description: '管理 Medusa 商品的详细内容和描述',
listSearchableFields: ['title', 'medusaId', 'handle'], listSearchableFields: ['title', 'medusaId'],
pagination: { pagination: {
defaultLimit: 25, defaultLimit: 25,
}, },
components: { components: {
edit: {
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
},
beforeListTable: [ beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/sync/UnifiedSyncButton#UnifiedSyncButton',
'/components/list/ProductGridStyler', '/components/list/ProductGridStyler',
@ -109,13 +106,6 @@ export const Products: CollectionConfig = {
readOnly: true, readOnly: true,
}, },
}, },
{
name: 'handle',
type: 'text',
admin: {
hidden: true, // 隐藏字段,但保留用于搜索
},
},
{ {
name: 'thumbnail', name: 'thumbnail',
type: 'text', type: 'text',

View File

@ -1,7 +1,8 @@
// 这是为了覆盖 Payload 默认表格样式的 SCSS // 这是为了覆盖 Payload 默认表格样式的 SCSS
// 我们使用 CSS Grid 强制改变表格布局从而实现 Grid 视图同时保留 Payload 所有原生功能 // 我们使用 CSS Grid 强制改变表格布局从而实现 Grid 视图同时保留 Payload 所有原生功能
.collection-list.collection-list--products { .collection-list.collection-list--products,
.collection-list.collection-list--preorder-products {
table { table {
display: block !important; display: block !important;

View File

@ -1,103 +0,0 @@
'use client'
import { useState } from 'react'
import { Button } from '@payloadcms/ui'
import { useDocumentInfo } from '@payloadcms/ui'
/**
*
*
*/
export function ForceSyncButton() {
const { id, collectionSlug } = useDocumentInfo()
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const handleForceSync = async () => {
if (!id) {
setMessage('❌ 无法获取商品 ID')
return
}
if (!confirm('确定要从 Medusa 强制更新此商品吗?这将覆盖当前的商品信息。')) {
return
}
setLoading(true)
setMessage('')
try {
const response = await fetch(
`/api/sync-medusa?payloadId=${id}&collection=${collectionSlug}&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)
}
}
return (
<div style={{
marginTop: '1rem',
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>
{message && (
<div
style={{
marginTop: '0.75rem',
padding: '0.75rem',
backgroundColor:
message.includes('❌')
? 'var(--theme-error-50)'
: 'var(--theme-success-50)',
borderRadius: '4px',
fontSize: '0.875rem',
}}
>
{message}
</div>
)}
<div style={{
marginTop: '0.75rem',
fontSize: '0.75rem',
color: 'var(--theme-elevation-400)',
}}>
💡 Medusa
</div>
</div>
)
}

View File

@ -0,0 +1,167 @@
'use client'
import { useState } from 'react'
import { Button, useSelection } from '@payloadcms/ui'
import { useRouter } from 'next/navigation'
/**
*
* Medusa orderCount
*/
export function RefreshOrderCountButton() {
const { getQueryParams, toggleAll } = useSelection()
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const router = useRouter()
// 刷新所有商品的订单计数
const handleRefreshAll = async () => {
if (!confirm('确定要刷新所有预购商品的订单计数吗?')) {
return
}
setLoading(true)
setMessage('')
try {
const response = await fetch('/api/refresh-order-counts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refreshAll: true,
}),
})
const data = await response.json()
if (data.success) {
setMessage(`${data.message || '订单计数刷新成功!'}`)
setTimeout(() => {
router.refresh()
}, 1500)
} else {
setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`)
}
} catch (error) {
setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误'))
} finally {
setLoading(false)
}
}
// 刷新选中商品的订单计数
const handleRefreshSelected = async () => {
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
}
setLoading(true)
setMessage('')
const response = await fetch('/api/refresh-order-counts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: selectedIds,
}),
})
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)
}
}
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>
</h3>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.5rem' }}>
<Button
onClick={handleRefreshSelected}
disabled={loading}
buttonStyle="secondary"
size="small"
>
{loading ? '刷新中...' : '刷新选中商品'}
</Button>
<Button
onClick={handleRefreshAll}
disabled={loading}
buttonStyle="secondary"
size="small"
>
{loading ? '刷新中...' : '刷新全部订单计数'}
</Button>
</div>
{message && (
<div
style={{
padding: '0.75rem',
marginTop: '0.5rem',
borderRadius: '4px',
backgroundColor: message.startsWith('✅')
? 'var(--theme-success-50)'
: message.startsWith('⚠️')
? 'var(--theme-warning-50)'
: 'var(--theme-error-50)',
color: message.startsWith('✅')
? 'var(--theme-success-900)'
: message.startsWith('⚠️')
? 'var(--theme-warning-900)'
: 'var(--theme-error-900)',
fontSize: '0.875rem',
}}
>
{message}
</div>
)}
<div style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-500)' }}>
<p style={{ margin: '0.25rem 0' }}>
💡 <strong></strong> = + Fake计数
</p>
<p style={{ margin: '0.25rem 0' }}>
<strong></strong> Medusa
</p>
<p style={{ margin: '0.25rem 0' }}>
<strong>Fake计数</strong>
</p>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useEffect } from 'react'
import { Button, useSelection } from '@payloadcms/ui' import { Button, useSelection } from '@payloadcms/ui'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
@ -13,13 +13,19 @@ export function UnifiedSyncButton() {
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [showForceAllConfirm, setShowForceAllConfirm] = useState(false) const [showForceAllConfirm, setShowForceAllConfirm] = useState(false)
const [confirmText, setConfirmText] = useState('') const [confirmText, setConfirmText] = useState('')
const [collectionSlug, setCollectionSlug] = useState('products')
const router = useRouter() const router = useRouter()
// 获取当前页面的 collection slug // 在客户端确定 collection slug避免 hydration 错误
const pathname = typeof window !== 'undefined' ? window.location.pathname : '' useEffect(() => {
const collectionSlug = pathname.includes('preorder-products') if (typeof window !== 'undefined') {
const pathname = window.location.pathname
const slug = pathname.includes('preorder-products')
? 'preorder-products' ? 'preorder-products'
: 'products' : 'products'
setCollectionSlug(slug)
}
}, [])
// 同步新商品 // 同步新商品
const handleSyncNew = async () => { const handleSyncNew = async () => {

View File

@ -4,6 +4,8 @@
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
// Payload API key - 用于同步产品数据(包括 metadata
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || ''
interface MedusaProduct { interface MedusaProduct {
id: string id: string
@ -47,25 +49,28 @@ interface MedusaResponse<T> {
} }
/** /**
* Medusa * Medusa 使 admin API metadata
*/ */
export async function getAllMedusaProducts(): Promise<MedusaProduct[]> { export async function getAllMedusaProducts(): Promise<MedusaProduct[]> {
try { try {
const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products`, { const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products-sync`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY, 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
'x-payload-api-key': PAYLOAD_API_KEY,
}, },
}) })
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch products: ${response.statusText}`) const errorText = await response.text()
console.error(`[getAllMedusaProducts] Error response:`, errorText)
throw new Error(`Failed to fetch products: ${response.status} ${response.statusText} - ${errorText}`)
} }
const data: MedusaResponse<MedusaProduct> = await response.json() const data: MedusaResponse<MedusaProduct> = await response.json()
return data.products || [] return data.products || []
} catch (error) { } catch (error) {
console.error('Error fetching Medusa products:', error) console.error('[getAllMedusaProducts] Error fetching Medusa products:', error)
throw error throw error
} }
} }
@ -99,30 +104,22 @@ export async function getMedusaProduct(productId: string): Promise<MedusaProduct
/** /**
* Medusa * Medusa
* 使
*/ */
export async function getMedusaProductsPaginated( export async function getMedusaProductsPaginated(
offset: number = 0, offset: number = 0,
limit: number = 100, limit: number = 100,
): Promise<{ products: MedusaProduct[]; count: number }> { ): Promise<{ products: MedusaProduct[]; count: number }> {
try { try {
const response = await fetch( // 获取所有产品
`${MEDUSA_BACKEND_URL}/store/products?offset=${offset}&limit=${limit}`, const allProducts = await getAllMedusaProducts()
{
headers: {
'Content-Type': 'application/json',
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
},
},
)
if (!response.ok) { // 在内存中分页
throw new Error(`Failed to fetch products: ${response.statusText}`) const products = allProducts.slice(offset, offset + limit)
}
const data: MedusaResponse<MedusaProduct> = await response.json()
return { return {
products: data.products || [], products,
count: data.count || 0, count: allProducts.length,
} }
} catch (error) { } catch (error) {
console.error('Error fetching Medusa products (paginated):', error) console.error('Error fetching Medusa products (paginated):', error)
@ -178,12 +175,21 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis
/** /**
* *
* metadata.is_preorder () * metadata.is_preorder metadata.preorder ()
*/ */
export function isPreorderProduct(product: MedusaProduct): boolean { export function isPreorderProduct(product: MedusaProduct): boolean {
const isPreorder = product.metadata?.is_preorder const isPreorderValue = product.metadata?.is_preorder
// 支持布尔值 true 或字符串 "true" const preorderValue = product.metadata?.preorder
return isPreorder === true || isPreorder === 'true'
// 尝试两个字段,支持多种类型
const value = isPreorderValue !== undefined ? isPreorderValue : preorderValue
// 支持布尔值 true、字符串 "true"、数字 1、字符串 "1"
if (value === true || value === 'true' || value === '1' || value === 1) {
return true
}
return false
} }
/** /**

View File

@ -213,7 +213,6 @@ export interface Product {
* Medusa * Medusa
*/ */
title: string; title: string;
handle?: string | null;
/** /**
* URL URL * URL URL
*/ */
@ -298,11 +297,7 @@ export interface PreorderProduct {
*/ */
title: string; title: string;
/** /**
* URL handle Medusa * URL URL
*/
handle?: string | null;
/**
* URL Medusa
*/ */
thumbnail?: string | null; thumbnail?: string | null;
/** /**
@ -317,22 +312,18 @@ export interface PreorderProduct {
* 0 max_orders * 0 max_orders
*/ */
fundingGoal: number; fundingGoal: number;
/**
*
*/
estimatedShipDate?: string | null;
/** /**
* *
*/ */
preorderEndDate?: string | null; preorderEndDate?: string | null;
/** /**
* * Medusa
*/ */
minOrderQuantity?: number | null; orderCount?: number | null;
/** /**
* 0 * Fake
*/ */
maxOrderQuantity?: number | null; fakeOrderCount?: number | null;
/** /**
* *
*/ */
@ -826,7 +817,6 @@ export interface ProductsSelect<T extends boolean = true> {
status?: T; status?: T;
seedId?: T; seedId?: T;
title?: T; title?: T;
handle?: T;
thumbnail?: T; thumbnail?: T;
lastSyncedAt?: T; lastSyncedAt?: T;
content?: T; content?: T;
@ -852,15 +842,13 @@ export interface PreorderProductsSelect<T extends boolean = true> {
status?: T; status?: T;
seedId?: T; seedId?: T;
title?: T; title?: T;
handle?: T;
thumbnail?: T; thumbnail?: T;
lastSyncedAt?: T; lastSyncedAt?: T;
preorderType?: T; preorderType?: T;
fundingGoal?: T; fundingGoal?: T;
estimatedShipDate?: T;
preorderEndDate?: T; preorderEndDate?: T;
minOrderQuantity?: T; orderCount?: T;
maxOrderQuantity?: T; fakeOrderCount?: T;
description?: T; description?: T;
relatedProducts?: T; relatedProducts?: T;
taobaoLinks?: taobaoLinks?: