Compare commits

..

2 Commits

Author SHA1 Message Date
龟男日记\www c6771a7098 计数更新 2026-02-23 00:22:12 +08:00
龟男日记\www 63620571a2 重新导入按钮 2026-02-22 22:28:21 +08:00
5 changed files with 179 additions and 69 deletions

View File

@ -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) {

View File

@ -0,0 +1,72 @@
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,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 移动
@ -128,7 +141,10 @@ export async function GET(request: NextRequest) {
// 新建 // 新建
await payload.create({ await payload.create({
collection: targetCollection, collection: targetCollection,
data: { ...productData }, data: {
...productData,
status: (productData.status as 'draft' | 'published') ?? 'draft',
},
}) })
results.created++ results.created++
} }

View File

@ -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' }}>

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 minPriceInCents = Math.min(...allPrices) const minPrice = Math.min(...allPrices)
startPrice = Math.round(minPriceInCents) / 100 startPrice = Math.round(minPrice * 100) / 100
} }
} }