重新导入按钮
This commit is contained in:
parent
c8de57af22
commit
63620571a2
|
|
@ -6,20 +6,24 @@ 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 数据
|
*
|
||||||
* 2. 清理 Medusa 数据
|
* full (默认): 清理 Payload + 清理 Medusa + 导入 Medusa seed 数据
|
||||||
* 3. 导入 Medusa seed 数据
|
* medusa-only: 仅清理 Medusa + 导入 Medusa seed 数据(不动 Payload)
|
||||||
*/
|
*/
|
||||||
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 数据 ====================
|
// ==================== 步骤 1: 清理 Payload 数据(full 模式才执行)====================
|
||||||
|
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({
|
||||||
|
|
@ -34,11 +38,15 @@ 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}/admin/custom/clean`, {
|
const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/clean`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -47,7 +55,8 @@ export async function POST(request: NextRequest) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!cleanResponse.ok) {
|
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()
|
const cleanData = await cleanResponse.json()
|
||||||
|
|
@ -59,30 +68,34 @@ export async function POST(request: NextRequest) {
|
||||||
})
|
})
|
||||||
console.log('✅ Medusa 数据清理完成')
|
console.log('✅ Medusa 数据清理完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Medusa 清理失败:', error)
|
const errMsg = error instanceof Error ? error.message : 'Unknown 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: error instanceof Error ? error.message : 'Unknown error',
|
error: errMsg,
|
||||||
})
|
})
|
||||||
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}/admin/custom/seed-pro`, {
|
const seedResponse = await fetch(`${MEDUSA_BACKEND_URL}/hooks/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) {
|
||||||
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()
|
const seedData = await seedResponse.json()
|
||||||
|
|
@ -94,20 +107,24 @@ export async function POST(request: NextRequest) {
|
||||||
})
|
})
|
||||||
console.log('✅ Medusa 数据导入完成')
|
console.log('✅ Medusa 数据导入完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Medusa seed 失败:', error)
|
const errMsg = error instanceof Error ? error.message : 'Unknown 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: error instanceof Error ? error.message : 'Unknown error',
|
error: errMsg,
|
||||||
})
|
})
|
||||||
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 = '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
|
results.message = mode === 'medusa-only'
|
||||||
|
? 'Medusa 数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
|
||||||
|
: '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
|
||||||
|
|
||||||
return NextResponse.json(results, { status: 200 })
|
return NextResponse.json(results, { status: 200 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -98,18 +98,31 @@ export async function GET(request: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在非 forceUpdate 模式下,跳过已存在的产品(只同步新产品)
|
|
||||||
if (!forceUpdate && existingProduct) {
|
|
||||||
results.skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingProduct) {
|
if (existingProduct) {
|
||||||
// 强制更新:更新所有 Medusa 同步字段
|
// 构建更新数据:forceUpdate 时覆盖所有字段,否则只更新 Medusa 来源字段(保留 Payload 编辑内容)
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
...productData,
|
lastSyncedAt: productData.lastSyncedAt,
|
||||||
// thumbnail 保留 Payload 已有值(除非 forceUpdate 或为空)
|
medusaId: productData.medusaId,
|
||||||
thumbnail: existingProduct.thumbnail || productData.thumbnail,
|
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 移动
|
// 如果需要跨 collection 移动
|
||||||
|
|
|
||||||
|
|
@ -9,69 +9,74 @@ 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(false)
|
const [loading, setLoading] = useState<'full' | 'medusa-only' | null>(null)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [details, setDetails] = useState<any>(null)
|
const [details, setDetails] = useState<any>(null)
|
||||||
|
|
||||||
const handleResetData = async () => {
|
const handleReset = async (mode: 'full' | 'medusa-only') => {
|
||||||
if (!confirm(
|
const confirmMsg = mode === 'medusa-only'
|
||||||
'⚠️ 危险操作:重置所有数据\n\n' +
|
? '⚠️ 重置 Medusa 数据\n\n此操作将:\n1. 清理所有 Medusa 数据\n2. 重新导入 Medusa seed 数据\n\nPayload CMS 数据不受影响。\n\n⚠️ 此操作不可撤销!确认继续吗?'
|
||||||
'此操作将:\n' +
|
: '⚠️ 危险操作:重置所有数据\n\n此操作将:\n1. 清理所有 Payload CMS 数据(保留用户)\n2. 清理所有 Medusa 数据\n3. 重新导入 Medusa seed 数据\n\n⚠️ 此操作不可撤销!确认要继续吗?'
|
||||||
'1. 清理所有 Payload CMS 数据(保留用户)\n' +
|
|
||||||
'2. 清理所有 Medusa 数据\n' +
|
|
||||||
'3. 重新导入 Medusa seed 数据\n\n' +
|
|
||||||
'⚠️ 此操作不可撤销!\n\n' +
|
|
||||||
'确认要继续吗?'
|
|
||||||
)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
if (!confirm(confirmMsg)) return
|
||||||
|
|
||||||
|
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: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ mode }),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
|
||||||
if (!result.success) {
|
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)
|
setDetails(result)
|
||||||
setMessage(
|
setMessage(
|
||||||
'✅ 数据重置完成!\n\n' +
|
mode === 'medusa-only'
|
||||||
'下一步:\n' +
|
? '✅ Medusa 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS'
|
||||||
'1. 同步 Medusa 商品到 Payload CMS\n' +
|
: '✅ 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS\n2. 设置 ProductRecommendations\n3. 配置 PreorderProducts 的预购设置'
|
||||||
'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(false)
|
setLoading(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleResetData = () => handleReset('full')
|
||||||
|
const handleResetMedusaOnly = () => handleReset('medusa-only')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleResetData}
|
onClick={handleResetData}
|
||||||
buttonStyle="error"
|
buttonStyle="error"
|
||||||
disabled={loading}
|
disabled={loading !== null}
|
||||||
size="medium"
|
size="medium"
|
||||||
>
|
>
|
||||||
{loading ? '🔄 重置中...' : '🗑️ 重置所有数据'}
|
{loading === 'full' ? '🔄 重置中...' : '🗑️ 重置所有数据'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleResetMedusaOnly}
|
||||||
|
buttonStyle="secondary"
|
||||||
|
disabled={loading !== null}
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{loading === 'medusa-only' ? '🔄 重置中...' : '🔄 仅重置 Medusa'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -108,8 +113,8 @@ export function ResetDataButton({ className }: Props) {
|
||||||
<strong>
|
<strong>
|
||||||
[{step.step}/3] {step.name}:{' '}
|
[{step.step}/3] {step.name}:{' '}
|
||||||
</strong>
|
</strong>
|
||||||
<span style={{ color: step.success ? 'green' : 'red' }}>
|
<span style={{ color: step.skipped ? '#888' : step.success ? 'green' : 'red' }}>
|
||||||
{step.success ? '✅ 成功' : '❌ 失败'}
|
{step.skipped ? '⏭️ 跳过' : step.success ? '✅ 成功' : '❌ 失败'}
|
||||||
</span>
|
</span>
|
||||||
{step.deleted !== undefined && (
|
{step.deleted !== undefined && (
|
||||||
<span style={{ marginLeft: '0.5rem' }}>
|
<span style={{ marginLeft: '0.5rem' }}>
|
||||||
|
|
|
||||||
|
|
@ -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 minPriceInCents = Math.min(...allPrices)
|
const minPrice = Math.min(...allPrices)
|
||||||
startPrice = Math.round(minPriceInCents) / 100
|
startPrice = Math.round(minPrice * 100) / 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue