批量同步

This commit is contained in:
龟男日记\www 2026-02-21 18:17:39 +08:00
parent 1860affd69
commit 4def0e2c0b
5 changed files with 165 additions and 15 deletions

View File

@ -0,0 +1,123 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
import { getAllMedusaProducts } from '@/lib/medusa'
/**
* Batch Sync Selected Products
* POST /api/admin/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 })
// Get all Medusa products once
const medusaProducts = await getAllMedusaProducts()
const medusaProductMap = new Map(medusaProducts.map(p => [p.id, p]))
const results = {
total: ids.length,
success: 0,
failed: 0,
skipped: 0,
details: [] as any[],
}
// Sync each selected product
for (const id of ids) {
try {
const product = await payload.findByID({
collection: collection as 'products' | 'preorder-products',
id,
})
if (!product || !product.medusaId) {
results.skipped++
results.details.push({
id,
title: product?.title || 'Unknown',
status: 'skipped',
reason: 'No Medusa ID',
})
continue
}
const medusaProduct = medusaProductMap.get(product.medusaId)
if (!medusaProduct) {
results.failed++
results.details.push({
id,
medusaId: product.medusaId,
title: product.title,
status: 'failed',
error: 'Product not found in Medusa',
})
continue
}
// Update basic fields from Medusa
const updateData: any = {
lastSyncedAt: new Date().toISOString(),
}
if (forceUpdate || !product.title) updateData.title = medusaProduct.title
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail
await payload.update({
collection: collection as 'products' | 'preorder-products',
id,
data: updateData,
})
results.success++
results.details.push({
id,
medusaId: product.medusaId,
title: product.title,
status: 'success',
})
} catch (error) {
results.failed++
results.details.push({
id,
status: 'failed',
error: error instanceof Error ? error.message : 'Unknown error',
})
}
}
return NextResponse.json({
success: true,
message: `Batch sync completed: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`,
results,
})
} catch (error) {
console.error('[batch-sync-medusa] Error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
)
}
}

View File

@ -22,35 +22,38 @@ export function RestoreRecommendationsSeedButton({ className }: Props) {
*/
const findProductsBySeedIds = async (
seedIds: string[],
isPreorder: boolean = false,
): Promise<Array<{ relationTo: string; value: string }>> => {
const products: Array<{ relationTo: string; value: string }> = []
const primaryCollection = isPreorder ? 'preorder-products' : 'products'
const fallbackCollection = isPreorder ? 'products' : 'preorder-products'
for (const seedId of seedIds) {
try {
// Try products collection first
const productsResponse = await fetch(
`/api/products?where[seedId][equals]=${seedId}&limit=1`,
// Try primary collection first based on preorder flag
const primaryResponse = await fetch(
`/api/${primaryCollection}?where[seedId][equals]=${seedId}&limit=1`,
)
const productsData = await productsResponse.json()
const primaryData = await primaryResponse.json()
if (productsData.docs && productsData.docs.length > 0) {
if (primaryData.docs && primaryData.docs.length > 0) {
products.push({
relationTo: 'products',
value: productsData.docs[0].id,
relationTo: primaryCollection,
value: primaryData.docs[0].id,
})
continue
}
// Try preorder-products if not found
const preorderResponse = await fetch(
`/api/preorder-products?where[seedId][equals]=${seedId}&limit=1`,
// Try fallback collection if not found
const fallbackResponse = await fetch(
`/api/${fallbackCollection}?where[seedId][equals]=${seedId}&limit=1`,
)
const preorderData = await preorderResponse.json()
const fallbackData = await fallbackResponse.json()
if (preorderData.docs && preorderData.docs.length > 0) {
if (fallbackData.docs && fallbackData.docs.length > 0) {
products.push({
relationTo: 'preorder-products',
value: preorderData.docs[0].id,
relationTo: fallbackCollection,
value: fallbackData.docs[0].id,
})
} else {
console.warn(`Product not found for seedId: ${seedId}`)
@ -84,10 +87,11 @@ export function RestoreRecommendationsSeedButton({ className }: Props) {
// Find all product IDs in polymorphic relationship format
const listsWithProductIds = await Promise.all(
seed.lists.map(async (list) => {
const products = await findProductsBySeedIds(list.productSeedIds)
const products = await findProductsBySeedIds(list.productSeedIds, list.preorder || false)
return {
title: list.title,
subtitle: list.subtitle || '',
preorder: list.preorder || false,
products: products,
}
}),

View File

@ -7,6 +7,7 @@
export interface RecommendationListSeed {
title: string
subtitle?: string
preorder?: boolean
productSeedIds: string[]
}
@ -26,6 +27,7 @@ export const BATCH_02_RECOMMENDATIONS: RecommendationsSeed = {
{
title: 'PreGame - Preorder Games',
subtitle: 'Selected indie games, now available for preorder! Support indie developers and get early access to new releases.',
preorder: true,
productSeedIds: [
'game-urcicus', // Urcicus - GBA Game
'game-mikoto-nikki', // Mikoto Nikki - GBA Game
@ -36,6 +38,7 @@ export const BATCH_02_RECOMMENDATIONS: RecommendationsSeed = {
{
title: 'PreMod - Preorder Modifications',
subtitle: 'Premium custom shells and consoles, full metal construction, artisan craftsmanship. Limited preorder available!',
preorder: true,
productSeedIds: [
'shell-gba-sp-metal-unhinged', // Metal Shell - GBA SP (Unhinged Mod)
'shell-gba-metal', // Metal Shell - GBA

View File

@ -103,6 +103,21 @@ export const ProductRecommendations: GlobalConfig = {
rows: 2,
},
},
{
name: 'preorder',
type: 'checkbox',
label: {
en: 'Preorder Products',
zh: '预购商品',
},
defaultValue: false,
admin: {
description: {
en: 'Check if this list contains preorder products',
zh: '勾选表示此列表包含预购商品',
},
},
},
{
name: 'products',
type: 'relationship',

View File

@ -1214,6 +1214,10 @@ export interface ProductRecommendation {
| {
title: string;
subtitle?: string | null;
/**
* Check if this list contains preorder products
*/
preorder?: boolean | null;
/**
* Select and drag to reorder products
*/
@ -1312,6 +1316,7 @@ export interface ProductRecommendationsSelect<T extends boolean = true> {
| {
title?: T;
subtitle?: T;
preorder?: T;
products?: T;
id?: T;
};