From 4def0e2c0b7aaabd45f0180f6b70a7556372b3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Sat, 21 Feb 2026 18:17:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/admin/batch-sync-medusa/route.ts | 123 ++++++++++++++++++ .../seed/RestoreRecommendationsSeedButton.tsx | 34 ++--- .../20260221-product-recommendations-seeds.ts | 3 + src/globals/ProductRecommendations.ts | 15 +++ src/payload-types.ts | 5 + 5 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 src/app/api/admin/batch-sync-medusa/route.ts diff --git a/src/app/api/admin/batch-sync-medusa/route.ts b/src/app/api/admin/batch-sync-medusa/route.ts new file mode 100644 index 0000000..997f6c2 --- /dev/null +++ b/src/app/api/admin/batch-sync-medusa/route.ts @@ -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 }, + ) + } +} diff --git a/src/components/seed/RestoreRecommendationsSeedButton.tsx b/src/components/seed/RestoreRecommendationsSeedButton.tsx index 344e491..b435ae0 100644 --- a/src/components/seed/RestoreRecommendationsSeedButton.tsx +++ b/src/components/seed/RestoreRecommendationsSeedButton.tsx @@ -22,35 +22,38 @@ export function RestoreRecommendationsSeedButton({ className }: Props) { */ const findProductsBySeedIds = async ( seedIds: string[], + isPreorder: boolean = false, ): Promise> => { 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, } }), diff --git a/src/components/seed/data/20260221-product-recommendations-seeds.ts b/src/components/seed/data/20260221-product-recommendations-seeds.ts index 7b102ba..8ee8b06 100644 --- a/src/components/seed/data/20260221-product-recommendations-seeds.ts +++ b/src/components/seed/data/20260221-product-recommendations-seeds.ts @@ -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 diff --git a/src/globals/ProductRecommendations.ts b/src/globals/ProductRecommendations.ts index a0e4e75..7dd07cc 100644 --- a/src/globals/ProductRecommendations.ts +++ b/src/globals/ProductRecommendations.ts @@ -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', diff --git a/src/payload-types.ts b/src/payload-types.ts index bef1a34..b3bae83 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -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 { | { title?: T; subtitle?: T; + preorder?: T; products?: T; id?: T; };