批量同步
This commit is contained in:
parent
1860affd69
commit
4def0e2c0b
|
|
@ -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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue