Compare commits

..

No commits in common. "c6771a70983dfa75fabd2150ba80c5e1d0f46ac4" and "c8de57af22041e67de661eeb5c082b62a333d786" have entirely different histories.

5 changed files with 69 additions and 179 deletions

View File

@ -6,24 +6,20 @@ import config from '@payload-config'
* API Route: Reset All Data * API Route: Reset All Data
* POST /api/admin/reset-data * POST /api/admin/reset-data
* *
* Body: { mode?: 'full' | 'medusa-only' } *
* * 1. Payload CMS
* full (): Payload + Medusa + Medusa seed * 2. Medusa
* medusa-only: 仅清理 Medusa + Medusa seed Payload * 3. Medusa seed
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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 MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
const results: any = { const results: any = {
steps: [], steps: [],
success: true, success: true,
mode,
} }
// ==================== 步骤 1: 清理 Payload 数据full 模式才执行)==================== // ==================== 步骤 1: 清理 Payload 数据 ====================
if (mode === 'full') {
console.log('🧹 [1/3] 开始清理 Payload CMS 数据...') console.log('🧹 [1/3] 开始清理 Payload CMS 数据...')
const payloadResult = await cleanPayloadData() const payloadResult = await cleanPayloadData()
results.steps.push({ results.steps.push({
@ -38,15 +34,11 @@ export async function POST(request: NextRequest) {
results.success = false results.success = false
return NextResponse.json(results, { status: 500 }) 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 数据 ==================== // ==================== 步骤 2: 清理 Medusa 数据 ====================
console.log('🧹 [2/3] 开始清理 Medusa 数据...') console.log('🧹 [2/3] 开始清理 Medusa 数据...')
try { try {
const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/clean`, { const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/clean`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -55,8 +47,7 @@ export async function POST(request: NextRequest) {
}) })
if (!cleanResponse.ok) { if (!cleanResponse.ok) {
const bodyText = await cleanResponse.text().catch(() => '') throw new Error(`Medusa clean failed: ${cleanResponse.statusText}`)
throw new Error(`Medusa clean failed (${cleanResponse.status}): ${bodyText || cleanResponse.statusText}`)
} }
const cleanData = await cleanResponse.json() const cleanData = await cleanResponse.json()
@ -68,34 +59,30 @@ export async function POST(request: NextRequest) {
}) })
console.log('✅ Medusa 数据清理完成') console.log('✅ Medusa 数据清理完成')
} catch (error) { } catch (error) {
const errMsg = error instanceof Error ? error.message : 'Unknown error' console.error('❌ Medusa 清理失败:', error)
console.error('❌ Medusa 清理失败:', errMsg)
results.steps.push({ results.steps.push({
step: 2, step: 2,
name: 'Clean Medusa', name: 'Clean Medusa',
success: false, success: false,
error: errMsg, error: error instanceof Error ? error.message : 'Unknown error',
}) })
results.success = false results.success = false
results.error = `[步骤2] ${errMsg}`
return NextResponse.json(results, { status: 500 }) return NextResponse.json(results, { status: 500 })
} }
// ==================== 步骤 3: Seed Medusa 数据 ==================== // ==================== 步骤 3: Seed Medusa 数据 ====================
console.log('🌱 [3/3] 开始导入 Medusa 数据...') console.log('🌱 [3/3] 开始导入 Medusa 数据...')
try { try {
const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/seed-pro`, { const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/seed-pro`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-payload-api-key': process.env.PAYLOAD_API_KEY || '', 'x-payload-api-key': process.env.PAYLOAD_API_KEY || '',
}, },
// seed:pro 可能需要较长时间
}) })
if (!seedResponse.ok) { if (!seedResponse.ok) {
const bodyText = await seedResponse.text().catch(() => '') throw new Error(`Medusa seed failed: ${seedResponse.statusText}`)
throw new Error(`Medusa seed failed (${seedResponse.status}): ${bodyText || seedResponse.statusText}`)
} }
const seedData = await seedResponse.json() const seedData = await seedResponse.json()
@ -107,24 +94,20 @@ export async function POST(request: NextRequest) {
}) })
console.log('✅ Medusa 数据导入完成') console.log('✅ Medusa 数据导入完成')
} catch (error) { } catch (error) {
const errMsg = error instanceof Error ? error.message : 'Unknown error' console.error('❌ Medusa seed 失败:', error)
console.error('❌ Medusa seed 失败:', errMsg)
results.steps.push({ results.steps.push({
step: 3, step: 3,
name: 'Seed Medusa', name: 'Seed Medusa',
success: false, success: false,
error: errMsg, error: error instanceof Error ? error.message : 'Unknown error',
}) })
results.success = false results.success = false
results.error = `[步骤3] ${errMsg}`
return NextResponse.json(results, { status: 500 }) return NextResponse.json(results, { status: 500 })
} }
// ==================== 完成 ==================== // ==================== 完成 ====================
console.log('✨ 数据重置完成!') console.log('✨ 数据重置完成!')
results.message = mode === 'medusa-only' results.message = '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
? 'Medusa 数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
: '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
return NextResponse.json(results, { status: 200 }) return NextResponse.json(results, { status: 200 })
} catch (error) { } catch (error) {

View File

@ -1,72 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
/**
* POST /api/preorders/increment-count
* orderCount Medusa subscriber
*
* Body: { medusaId: string; increment: number }
* Auth: x-payload-api-key header
*/
export async function POST(req: NextRequest) {
const apiKey = req.headers.get('x-payload-api-key')
if (!apiKey || apiKey !== process.env.PAYLOAD_API_KEY) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const { medusaId, increment } = await req.json()
if (!medusaId || typeof increment !== 'number') {
return NextResponse.json(
{ success: false, error: 'medusaId 和 increment (number) 为必填' },
{ status: 400 },
)
}
const payload = await getPayload({ config })
// 查找对应的预购商品
const result = await payload.find({
collection: 'preorder-products',
where: { medusaId: { equals: medusaId } },
limit: 1,
})
const product = result.docs[0]
if (!product) {
return NextResponse.json(
{ success: false, error: `未找到 medusaId=${medusaId} 的预购商品` },
{ status: 404 },
)
}
const currentCount = typeof product.orderCount === 'number' ? product.orderCount : 0
const newCount = Math.max(0, currentCount + increment)
await payload.update({
collection: 'preorder-products',
id: product.id,
data: { orderCount: newCount },
})
console.log(
`[increment-count] ✅ ${product.title}: ${currentCount}${newCount} (+${increment})`,
)
return NextResponse.json({
success: true,
title: product.title,
previousCount: currentCount,
newCount,
increment,
})
} catch (error: any) {
console.error('[increment-count] ❌', error?.message)
return NextResponse.json(
{ success: false, error: error?.message || 'Unknown error' },
{ status: 500 },
)
}
}

View File

@ -98,31 +98,18 @@ export async function GET(request: NextRequest) {
} }
} }
// 在非 forceUpdate 模式下,跳过已存在的产品(只同步新产品)
if (!forceUpdate && existingProduct) {
results.skipped++
continue
}
if (existingProduct) { if (existingProduct) {
// 构建更新数据forceUpdate 时覆盖所有字段,否则只更新 Medusa 来源字段(保留 Payload 编辑内容) // 强制更新:更新所有 Medusa 同步字段
const updateData: any = { const updateData: any = {
lastSyncedAt: productData.lastSyncedAt, ...productData,
medusaId: productData.medusaId, // thumbnail 保留 Payload 已有值(除非 forceUpdate 或为空)
seedId: productData.seedId, thumbnail: existingProduct.thumbnail || productData.thumbnail,
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 移动 // 如果需要跨 collection 移动
@ -141,10 +128,7 @@ export async function GET(request: NextRequest) {
// 新建 // 新建
await payload.create({ await payload.create({
collection: targetCollection, collection: targetCollection,
data: { data: { ...productData },
...productData,
status: (productData.status as 'draft' | 'published') ?? 'draft',
},
}) })
results.created++ results.created++
} }

View File

@ -9,74 +9,69 @@ interface Props {
/** /**
* Reset Data Button * Reset Data Button
* Payload + Medusa + Seed Medusa * Payload + Medusa + Seed Medusa
* Medusa Medusa + Seed Medusa Payload
*/ */
export function ResetDataButton({ className }: Props) { export function ResetDataButton({ className }: Props) {
const [loading, setLoading] = useState<'full' | 'medusa-only' | null>(null) const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [details, setDetails] = useState<any>(null) const [details, setDetails] = useState<any>(null)
const handleReset = async (mode: 'full' | 'medusa-only') => { const handleResetData = async () => {
const confirmMsg = mode === 'medusa-only' if (!confirm(
? '⚠️ 重置 Medusa 数据\n\n此操作将\n1. 清理所有 Medusa 数据\n2. 重新导入 Medusa seed 数据\n\nPayload CMS 数据不受影响。\n\n⚠ 此操作不可撤销!确认继续吗?' '⚠️ 危险操作:重置所有数据\n\n' +
: '⚠️ 危险操作:重置所有数据\n\n此操作将\n1. 清理所有 Payload CMS 数据(保留用户)\n2. 清理所有 Medusa 数据\n3. 重新导入 Medusa seed 数据\n\n⚠ 此操作不可撤销!确认要继续吗?' '此操作将:\n' +
'1. 清理所有 Payload CMS 数据(保留用户)\n' +
'2. 清理所有 Medusa 数据\n' +
'3. 重新导入 Medusa seed 数据\n\n' +
'⚠️ 此操作不可撤销!\n\n' +
'确认要继续吗?'
)) {
return
}
if (!confirm(confirmMsg)) return setLoading(true)
setLoading(mode)
setMessage('🔄 开始数据重置流程...') setMessage('🔄 开始数据重置流程...')
setDetails(null) setDetails(null)
try { try {
const response = await fetch('/api/admin/reset-data', { const response = await fetch('/api/admin/reset-data', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
body: JSON.stringify({ mode }), 'Content-Type': 'application/json',
},
}) })
const result = await response.json() const result = await response.json()
if (!result.success) { if (!result.success) {
// 优先显示顶级 error否则找第一个失败步骤的错误 throw new Error(result.error || 'Reset failed')
const stepError = result.steps?.find((s: any) => !s.success && !s.skipped && s.error)?.error
throw new Error(result.error || stepError || 'Reset failed')
} }
setDetails(result) setDetails(result)
setMessage( setMessage(
mode === 'medusa-only' '✅ 数据重置完成!\n\n' +
? '✅ Medusa 数据重置完成!\n\n下一步\n1. 同步 Medusa 商品到 Payload CMS' '下一步:\n' +
: '✅ 数据重置完成!\n\n下一步\n1. 同步 Medusa 商品到 Payload CMS\n2. 设置 ProductRecommendations\n3. 配置 PreorderProducts 的预购设置' '1. 同步 Medusa 商品到 Payload CMS\n' +
'2. 设置 ProductRecommendations\n' +
'3. 配置 PreorderProducts 的预购设置'
) )
} catch (error) { } catch (error) {
console.error('数据重置失败:', error) console.error('数据重置失败:', error)
setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error')) setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
} finally { } finally {
setLoading(null) setLoading(false)
} }
} }
const handleResetData = () => handleReset('full')
const handleResetMedusaOnly = () => handleReset('medusa-only')
return ( return (
<div className={className}> <div className={className}>
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
<Button <Button
onClick={handleResetData} onClick={handleResetData}
buttonStyle="error" buttonStyle="error"
disabled={loading !== null} disabled={loading}
size="medium" size="medium"
> >
{loading === 'full' ? '🔄 重置中...' : '🗑️ 重置所有数据'} {loading ? '🔄 重置中...' : '🗑️ 重置所有数据'}
</Button>
<Button
onClick={handleResetMedusaOnly}
buttonStyle="secondary"
disabled={loading !== null}
size="medium"
>
{loading === 'medusa-only' ? '🔄 重置中...' : '🔄 仅重置 Medusa'}
</Button> </Button>
</div> </div>
@ -113,8 +108,8 @@ export function ResetDataButton({ className }: Props) {
<strong> <strong>
[{step.step}/3] {step.name}:{' '} [{step.step}/3] {step.name}:{' '}
</strong> </strong>
<span style={{ color: step.skipped ? '#888' : step.success ? 'green' : 'red' }}> <span style={{ color: step.success ? 'green' : 'red' }}>
{step.skipped ? '⏭️ 跳过' : step.success ? '✅ 成功' : '❌ 失败'} {step.success ? '✅ 成功' : '❌ 失败'}
</span> </span>
{step.deleted !== undefined && ( {step.deleted !== undefined && (
<span style={{ marginLeft: '0.5rem' }}> <span style={{ marginLeft: '0.5rem' }}>

View File

@ -297,9 +297,9 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
).filter(price => typeof price === 'number' && price > 0) ).filter(price => typeof price === 'number' && price > 0)
if (allPrices.length > 0) { if (allPrices.length > 0) {
// 价格以美元存储(项目约定),直接取最小值,保留两位小数 // 将美分转换为美元(保留两位小数)
const minPrice = Math.min(...allPrices) const minPriceInCents = Math.min(...allPrices)
startPrice = Math.round(minPrice * 100) / 100 startPrice = Math.round(minPriceInCents) / 100
} }
} }