diff --git a/src/app/api/admin/reset-data/route.ts b/src/app/api/admin/reset-data/route.ts index a052319..2fa20ff 100644 --- a/src/app/api/admin/reset-data/route.ts +++ b/src/app/api/admin/reset-data/route.ts @@ -6,39 +6,47 @@ import config from '@payload-config' * API Route: Reset All Data * POST /api/admin/reset-data * - * 执行完整的数据重置流程: - * 1. 清理 Payload CMS 数据 - * 2. 清理 Medusa 数据 - * 3. 导入 Medusa seed 数据 + * Body: { mode?: 'full' | 'medusa-only' } + * + * full (默认): 清理 Payload + 清理 Medusa + 导入 Medusa seed 数据 + * medusa-only: 仅清理 Medusa + 导入 Medusa seed 数据(不动 Payload) */ export async function POST(request: NextRequest) { try { + const body = await request.json().catch(() => ({})) + const mode: 'full' | 'medusa-only' = body.mode === 'medusa-only' ? 'medusa-only' : 'full' const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' const results: any = { steps: [], success: true, + mode, } - // ==================== 步骤 1: 清理 Payload 数据 ==================== - console.log('🧹 [1/3] 开始清理 Payload CMS 数据...') - const payloadResult = await cleanPayloadData() - results.steps.push({ - step: 1, - name: 'Clean Payload', - success: payloadResult.success, - deleted: payloadResult.totalDeleted, - details: payloadResult.details, - }) + // ==================== 步骤 1: 清理 Payload 数据(full 模式才执行)==================== + if (mode === 'full') { + console.log('🧹 [1/3] 开始清理 Payload CMS 数据...') + const payloadResult = await cleanPayloadData() + results.steps.push({ + step: 1, + name: 'Clean Payload', + success: payloadResult.success, + deleted: payloadResult.totalDeleted, + details: payloadResult.details, + }) - if (!payloadResult.success) { - results.success = false - return NextResponse.json(results, { status: 500 }) + if (!payloadResult.success) { + results.success = false + return NextResponse.json(results, { status: 500 }) + } + } else { + console.log('⏭️ [1/3] medusa-only 模式,跳过 Payload 清理') + results.steps.push({ step: 1, name: 'Clean Payload', success: true, skipped: true }) } // ==================== 步骤 2: 清理 Medusa 数据 ==================== console.log('🧹 [2/3] 开始清理 Medusa 数据...') try { - const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/clean`, { + const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/clean`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -47,7 +55,8 @@ export async function POST(request: NextRequest) { }) if (!cleanResponse.ok) { - throw new Error(`Medusa clean failed: ${cleanResponse.statusText}`) + const bodyText = await cleanResponse.text().catch(() => '') + throw new Error(`Medusa clean failed (${cleanResponse.status}): ${bodyText || cleanResponse.statusText}`) } const cleanData = await cleanResponse.json() @@ -59,30 +68,34 @@ export async function POST(request: NextRequest) { }) console.log('✅ Medusa 数据清理完成') } catch (error) { - console.error('❌ Medusa 清理失败:', error) + const errMsg = error instanceof Error ? error.message : 'Unknown error' + console.error('❌ Medusa 清理失败:', errMsg) results.steps.push({ step: 2, name: 'Clean Medusa', success: false, - error: error instanceof Error ? error.message : 'Unknown error', + error: errMsg, }) results.success = false + results.error = `[步骤2] ${errMsg}` return NextResponse.json(results, { status: 500 }) } // ==================== 步骤 3: Seed Medusa 数据 ==================== console.log('🌱 [3/3] 开始导入 Medusa 数据...') try { - const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/seed-pro`, { + const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/seed-pro`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-payload-api-key': process.env.PAYLOAD_API_KEY || '', }, + // seed:pro 可能需要较长时间 }) if (!seedResponse.ok) { - throw new Error(`Medusa seed failed: ${seedResponse.statusText}`) + const bodyText = await seedResponse.text().catch(() => '') + throw new Error(`Medusa seed failed (${seedResponse.status}): ${bodyText || seedResponse.statusText}`) } const seedData = await seedResponse.json() @@ -94,20 +107,24 @@ export async function POST(request: NextRequest) { }) console.log('✅ Medusa 数据导入完成') } catch (error) { - console.error('❌ Medusa seed 失败:', error) + const errMsg = error instanceof Error ? error.message : 'Unknown error' + console.error('❌ Medusa seed 失败:', errMsg) results.steps.push({ step: 3, name: 'Seed Medusa', success: false, - error: error instanceof Error ? error.message : 'Unknown error', + error: errMsg, }) results.success = false + results.error = `[步骤3] ${errMsg}` return NextResponse.json(results, { status: 500 }) } // ==================== 完成 ==================== console.log('✨ 数据重置完成!') - results.message = '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。' + results.message = mode === 'medusa-only' + ? 'Medusa 数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。' + : '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。' return NextResponse.json(results, { status: 200 }) } catch (error) { diff --git a/src/app/api/sync/medusa/route.ts b/src/app/api/sync/medusa/route.ts index 207e467..83cb33c 100644 --- a/src/app/api/sync/medusa/route.ts +++ b/src/app/api/sync/medusa/route.ts @@ -98,18 +98,31 @@ export async function GET(request: NextRequest) { } } - // 在非 forceUpdate 模式下,跳过已存在的产品(只同步新产品) - if (!forceUpdate && existingProduct) { - results.skipped++ - continue - } - if (existingProduct) { - // 强制更新:更新所有 Medusa 同步字段 + // 构建更新数据:forceUpdate 时覆盖所有字段,否则只更新 Medusa 来源字段(保留 Payload 编辑内容) const updateData: any = { - ...productData, - // thumbnail 保留 Payload 已有值(除非 forceUpdate 或为空) - thumbnail: existingProduct.thumbnail || productData.thumbnail, + lastSyncedAt: productData.lastSyncedAt, + medusaId: productData.medusaId, + seedId: productData.seedId, + title: productData.title, + handle: productData.handle, + description: productData.description, + startPrice: productData.startPrice, + tags: productData.tags, + type: productData.type, + collection: productData.collection, + category: productData.category, + height: productData.height, + width: productData.width, + length: productData.length, + weight: productData.weight, + midCode: productData.midCode, + hsCode: productData.hsCode, + countryOfOrigin: productData.countryOfOrigin, + // thumbnail: forceUpdate 时覆盖,否则保留 Payload 已有值 + thumbnail: forceUpdate + ? (productData.thumbnail || existingProduct.thumbnail) + : (existingProduct.thumbnail || productData.thumbnail), } // 如果需要跨 collection 移动 diff --git a/src/components/sync/ResetDataButton.tsx b/src/components/sync/ResetDataButton.tsx index 0e30a8b..4e585b8 100644 --- a/src/components/sync/ResetDataButton.tsx +++ b/src/components/sync/ResetDataButton.tsx @@ -9,69 +9,74 @@ interface Props { /** * Reset Data Button * 一键重置所有数据:清理 Payload + 清理 Medusa + Seed Medusa + * 或仅重置 Medusa:清理 Medusa + Seed Medusa(不动 Payload) */ export function ResetDataButton({ className }: Props) { - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState<'full' | 'medusa-only' | null>(null) const [message, setMessage] = useState('') const [details, setDetails] = useState(null) - const handleResetData = async () => { - if (!confirm( - '⚠️ 危险操作:重置所有数据\n\n' + - '此操作将:\n' + - '1. 清理所有 Payload CMS 数据(保留用户)\n' + - '2. 清理所有 Medusa 数据\n' + - '3. 重新导入 Medusa seed 数据\n\n' + - '⚠️ 此操作不可撤销!\n\n' + - '确认要继续吗?' - )) { - return - } + const handleReset = async (mode: 'full' | 'medusa-only') => { + const confirmMsg = mode === 'medusa-only' + ? '⚠️ 重置 Medusa 数据\n\n此操作将:\n1. 清理所有 Medusa 数据\n2. 重新导入 Medusa seed 数据\n\nPayload CMS 数据不受影响。\n\n⚠️ 此操作不可撤销!确认继续吗?' + : '⚠️ 危险操作:重置所有数据\n\n此操作将:\n1. 清理所有 Payload CMS 数据(保留用户)\n2. 清理所有 Medusa 数据\n3. 重新导入 Medusa seed 数据\n\n⚠️ 此操作不可撤销!确认要继续吗?' - setLoading(true) + if (!confirm(confirmMsg)) return + + setLoading(mode) setMessage('🔄 开始数据重置流程...') setDetails(null) try { const response = await fetch('/api/admin/reset-data', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mode }), }) const result = await response.json() if (!result.success) { - throw new Error(result.error || 'Reset failed') + // 优先显示顶级 error,否则找第一个失败步骤的错误 + const stepError = result.steps?.find((s: any) => !s.success && !s.skipped && s.error)?.error + throw new Error(result.error || stepError || 'Reset failed') } setDetails(result) setMessage( - '✅ 数据重置完成!\n\n' + - '下一步:\n' + - '1. 同步 Medusa 商品到 Payload CMS\n' + - '2. 设置 ProductRecommendations\n' + - '3. 配置 PreorderProducts 的预购设置' + mode === 'medusa-only' + ? '✅ Medusa 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS' + : '✅ 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS\n2. 设置 ProductRecommendations\n3. 配置 PreorderProducts 的预购设置' ) } catch (error) { console.error('数据重置失败:', error) setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error')) } finally { - setLoading(false) + setLoading(null) } } + const handleResetData = () => handleReset('full') + const handleResetMedusaOnly = () => handleReset('medusa-only') + return (
-
+
+
@@ -108,8 +113,8 @@ export function ResetDataButton({ className }: Props) { [{step.step}/3] {step.name}:{' '} - - {step.success ? '✅ 成功' : '❌ 失败'} + + {step.skipped ? '⏭️ 跳过' : step.success ? '✅ 成功' : '❌ 失败'} {step.deleted !== undefined && ( diff --git a/src/lib/medusa.ts b/src/lib/medusa.ts index 6faa11d..a7473a2 100644 --- a/src/lib/medusa.ts +++ b/src/lib/medusa.ts @@ -297,9 +297,9 @@ export function transformMedusaProductToPayload(product: MedusaProduct) { ).filter(price => typeof price === 'number' && price > 0) if (allPrices.length > 0) { - // 将美分转换为美元(保留两位小数) - const minPriceInCents = Math.min(...allPrices) - startPrice = Math.round(minPriceInCents) / 100 + // 价格以美元存储(项目约定),直接取最小值,保留两位小数 + const minPrice = Math.min(...allPrices) + startPrice = Math.round(minPrice * 100) / 100 } }