批量同步

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

View File

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

View File

@ -103,6 +103,21 @@ export const ProductRecommendations: GlobalConfig = {
rows: 2, 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', name: 'products',
type: 'relationship', type: 'relationship',

View File

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