同步优化
This commit is contained in:
parent
4def0e2c0b
commit
14a2aaced0
|
|
@ -3,36 +3,37 @@ import { ThumbnailField as ThumbnailField_0d2fbe11370060d58b3925e5dbbb79d6 } fro
|
|||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
||||
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
|
||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
||||
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
||||
import { PreorderHealthCheckButton as PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892 } from '../../../components/views/PreorderHealthCheckButton'
|
||||
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||
import { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton'
|
||||
|
|
@ -45,36 +46,37 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
||||
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
|
||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
||||
"/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton": PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892,
|
||||
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||
"/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# 数据重置功能说明
|
||||
|
||||
## 概述
|
||||
|
||||
通过 Admin Settings 中的"数据重置"按钮,一键完成完整的数据重置流程。
|
||||
|
||||
## 功能
|
||||
|
||||
**一键重置所有数据**,包括:
|
||||
1. 清理 Payload CMS 数据(保留用户)
|
||||
2. 清理 Medusa 数据
|
||||
3. 重新导入 Medusa seed 数据
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 登录 Payload CMS: `http://localhost:1145/admin`
|
||||
2. 进入 **系统 → Admin Settings**
|
||||
3. 在"数据管理"区域找到"🔄 数据重置(Payload + Medusa)"
|
||||
4. 点击"🗑️ 重置所有数据"按钮
|
||||
5. 确认操作后等待完成
|
||||
|
||||
## 技术实现
|
||||
|
||||
### Payload CMS 端
|
||||
|
||||
**API 端点:**
|
||||
- `POST /api/admin/reset-data` - 数据重置主控端点
|
||||
|
||||
**UI 组件:**
|
||||
- `ResetDataButton` - 重置按钮组件
|
||||
- `AdminPanel` - 管理面板(包含重置按钮)
|
||||
|
||||
### Medusa 端
|
||||
|
||||
**API 端点:**
|
||||
- `POST /admin/custom/clean` - 清理 Medusa 数据
|
||||
- `POST /admin/custom/seed-pro` - 导入 seed 数据
|
||||
|
||||
## 执行流程
|
||||
|
||||
```
|
||||
用户点击按钮
|
||||
↓
|
||||
调用 /api/admin/reset-data
|
||||
↓
|
||||
步骤 1: 清理 Payload 数据(Products, PreorderProducts, Media, Announcements, Articles, Logs)
|
||||
↓
|
||||
步骤 2: 调用 Medusa /admin/custom/clean
|
||||
↓
|
||||
步骤 3: 调用 Medusa /admin/custom/seed-pro
|
||||
↓
|
||||
返回结果和详细信息
|
||||
```
|
||||
|
||||
## 后续操作
|
||||
|
||||
数据重置完成后需要:
|
||||
1. 同步 Medusa 商品到 Payload CMS
|
||||
2. 设置 ProductRecommendations 商品推荐
|
||||
3. 配置 PreorderProducts 的预购设置(fundingGoal, preorderEndDate 等)
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **危险操作** - 此操作不可撤销!
|
||||
- 会删除所有商品、媒体、公告、文章和日志数据
|
||||
- 保留用户账户和系统配置
|
||||
- 整个过程可能需要 2-3 分钟
|
||||
|
||||
## 已移除的文件
|
||||
|
||||
精简脚本后移除了:
|
||||
- `reset-data.bat` - 批处理脚本
|
||||
- `gb-payload/src/scripts/clean-payload.ts` - 清理脚本
|
||||
- `gb-payload/package.json` 中的 `clean` 命令
|
||||
|
||||
现在所有操作通过 Web UI 完成,无需命令行。
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
/**
|
||||
* API Route: Reset All Data
|
||||
* POST /api/admin/reset-data
|
||||
*
|
||||
* 执行完整的数据重置流程:
|
||||
* 1. 清理 Payload CMS 数据
|
||||
* 2. 清理 Medusa 数据
|
||||
* 3. 导入 Medusa seed 数据
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||
const results: any = {
|
||||
steps: [],
|
||||
success: true,
|
||||
}
|
||||
|
||||
// ==================== 步骤 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,
|
||||
})
|
||||
|
||||
if (!payloadResult.success) {
|
||||
results.success = false
|
||||
return NextResponse.json(results, { status: 500 })
|
||||
}
|
||||
|
||||
// ==================== 步骤 2: 清理 Medusa 数据 ====================
|
||||
console.log('🧹 [2/3] 开始清理 Medusa 数据...')
|
||||
try {
|
||||
const cleanResponse = await fetch(`${MEDUSA_BACKEND_URL}/admin/custom/clean`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!cleanResponse.ok) {
|
||||
throw new Error(`Medusa clean failed: ${cleanResponse.statusText}`)
|
||||
}
|
||||
|
||||
const cleanData = await cleanResponse.json()
|
||||
results.steps.push({
|
||||
step: 2,
|
||||
name: 'Clean Medusa',
|
||||
success: true,
|
||||
details: cleanData,
|
||||
})
|
||||
console.log('✅ Medusa 数据清理完成')
|
||||
} catch (error) {
|
||||
console.error('❌ Medusa 清理失败:', error)
|
||||
results.steps.push({
|
||||
step: 2,
|
||||
name: 'Clean Medusa',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
results.success = false
|
||||
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`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!seedResponse.ok) {
|
||||
throw new Error(`Medusa seed failed: ${seedResponse.statusText}`)
|
||||
}
|
||||
|
||||
const seedData = await seedResponse.json()
|
||||
results.steps.push({
|
||||
step: 3,
|
||||
name: 'Seed Medusa',
|
||||
success: true,
|
||||
details: seedData,
|
||||
})
|
||||
console.log('✅ Medusa 数据导入完成')
|
||||
} catch (error) {
|
||||
console.error('❌ Medusa seed 失败:', error)
|
||||
results.steps.push({
|
||||
step: 3,
|
||||
name: 'Seed Medusa',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
results.success = false
|
||||
return NextResponse.json(results, { status: 500 })
|
||||
}
|
||||
|
||||
// ==================== 完成 ====================
|
||||
console.log('✨ 数据重置完成!')
|
||||
results.message = '数据重置完成!现在可以同步 Medusa 商品到 Payload CMS。'
|
||||
|
||||
return NextResponse.json(results, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('❌ 数据重置失败:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 Payload CMS 数据
|
||||
*/
|
||||
async function cleanPayloadData() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const collections = [
|
||||
'media',
|
||||
'products',
|
||||
'preorder-products',
|
||||
'announcements',
|
||||
'articles',
|
||||
'logs'
|
||||
]
|
||||
|
||||
const details: any = {}
|
||||
let totalDeleted = 0
|
||||
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
const result = await payload.find({
|
||||
collection: collection as any,
|
||||
limit: 1000,
|
||||
})
|
||||
|
||||
if (result.totalDocs > 0) {
|
||||
for (const doc of result.docs) {
|
||||
await payload.delete({
|
||||
collection: collection as any,
|
||||
id: doc.id,
|
||||
})
|
||||
}
|
||||
|
||||
details[collection] = result.totalDocs
|
||||
totalDeleted += result.totalDocs
|
||||
console.log(` ✅ ${collection}: 已删除 ${result.totalDocs} 条记录`)
|
||||
} else {
|
||||
details[collection] = 0
|
||||
console.log(` ℹ️ ${collection}: 集合为空`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(` ❌ ${collection}: 清理失败`, error)
|
||||
details[collection] = { error: error instanceof Error ? error.message : 'Unknown error' }
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Payload 数据清理完成,共删除 ${totalDeleted} 条记录`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
totalDeleted,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
/**
|
||||
* GET /api/debug/preorder-products
|
||||
* Debug endpoint to check PreorderProducts data
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 直接查询 PreorderProducts 集合
|
||||
const preorderProducts = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: 5,
|
||||
depth: 0, // 不查询关联数据
|
||||
})
|
||||
|
||||
console.log('=== PreorderProducts Debug ===')
|
||||
console.log('Total docs:', preorderProducts.totalDocs)
|
||||
console.log('First doc:', JSON.stringify(preorderProducts.docs[0], null, 2))
|
||||
console.log('===============================')
|
||||
|
||||
return NextResponse.json({
|
||||
total: preorderProducts.totalDocs,
|
||||
products: preorderProducts.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
medusaId: doc.medusaId,
|
||||
thumbnail: doc.thumbnail,
|
||||
description: doc.description,
|
||||
preorderType: doc.preorderType,
|
||||
fundingGoal: doc.fundingGoal,
|
||||
orderCount: doc.orderCount,
|
||||
preorderStartDate: doc.preorderStartDate,
|
||||
preorderEndDate: doc.preorderEndDate,
|
||||
allFields: Object.keys(doc),
|
||||
})),
|
||||
}, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching preorder products:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch preorder products',
|
||||
message: error.message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import config from '@payload-config'
|
|||
|
||||
/**
|
||||
* GET /api/home
|
||||
* 获取首页所有数据:公告 + Hero Slider + 产品推荐
|
||||
* 获取首页所有数据:公告 + Hero Slider + 产品推荐(含完整产品信息)
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
|
|
@ -36,11 +36,14 @@ export async function GET(req: NextRequest) {
|
|||
slug: 'hero-slider',
|
||||
})
|
||||
|
||||
// 获取产品推荐
|
||||
// 获取产品推荐(包含深度查询的产品信息)
|
||||
const productRecommendations = await payload.findGlobal({
|
||||
slug: 'product-recommendations',
|
||||
depth: 3, // 增加深度以确保完全获取嵌套数据
|
||||
})
|
||||
|
||||
console.log('Raw productRecommendations:', JSON.stringify(productRecommendations, null, 2))
|
||||
|
||||
// 构建响应数据
|
||||
const response = {
|
||||
announcements: announcements.docs.map((announcement) => ({
|
||||
|
|
@ -56,7 +59,74 @@ export async function GET(req: NextRequest) {
|
|||
},
|
||||
productRecommendations: {
|
||||
enabled: productRecommendations.enabled || false,
|
||||
lists: productRecommendations.lists || [],
|
||||
lists: (productRecommendations.lists || []).map((list: any) => ({
|
||||
title: list.title,
|
||||
subtitle: list.subtitle,
|
||||
preorder: list.preorder || false,
|
||||
products: (list.products || []).map((productRef: any) => {
|
||||
const product = productRef.value
|
||||
|
||||
// 调试日志:查看实际接收到的产品数据
|
||||
console.log('=== Product Debug Info ===')
|
||||
console.log('relationTo:', productRef.relationTo)
|
||||
console.log('product object:', product)
|
||||
console.log('Available fields:', Object.keys(product || {}))
|
||||
console.log('=========================')
|
||||
|
||||
// 处理 description 字段
|
||||
// Products 使用 textarea (字符串),PreorderProducts 使用 richText (对象)
|
||||
let description = ''
|
||||
if (typeof product.description === 'string') {
|
||||
description = product.description
|
||||
} else if (product.description && typeof product.description === 'object') {
|
||||
// richText 字段,提取纯文本(简单处理)
|
||||
description = JSON.stringify(product.description)
|
||||
}
|
||||
|
||||
// 基础产品信息
|
||||
const baseInfo = {
|
||||
id: product.id,
|
||||
medusaId: product.medusaId,
|
||||
seedId: product.seedId,
|
||||
title: product.title,
|
||||
thumbnail: product.thumbnail,
|
||||
status: product.status,
|
||||
description,
|
||||
minPrice: product.minPrice,
|
||||
}
|
||||
|
||||
// 如果是预购产品,添加预购特有字段
|
||||
if (productRef.relationTo === 'preorder-products') {
|
||||
return {
|
||||
...baseInfo,
|
||||
relationTo: 'preorder-products',
|
||||
preorder: {
|
||||
type: product.preorderType || 'standard',
|
||||
fundingGoal: product.fundingGoal || 0,
|
||||
orderCount: product.orderCount || 0,
|
||||
startDate: product.preorderStartDate,
|
||||
endDate: product.preorderEndDate,
|
||||
// 计算进度百分比
|
||||
progress: product.fundingGoal > 0
|
||||
? Math.min(Math.round((product.orderCount / product.fundingGoal) * 100), 100)
|
||||
: 0,
|
||||
// 计算剩余天数
|
||||
daysLeft: product.preorderEndDate
|
||||
? Math.max(0, Math.ceil((new Date(product.preorderEndDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
|
||||
: null,
|
||||
// 支持者数量(使用 orderCount)
|
||||
backers: product.orderCount || 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 普通产品
|
||||
return {
|
||||
...baseInfo,
|
||||
relationTo: 'products',
|
||||
}
|
||||
}),
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
/**
|
||||
* GET /api/preorders/health-check
|
||||
*
|
||||
* 检查所有预购产品的健康状况
|
||||
* 返回详细的预购产品信息和潜在问题
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 获取所有预购产品
|
||||
const { docs: products } = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
limit: 1000,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
total: 0,
|
||||
healthy: 0,
|
||||
warnings: 0,
|
||||
errors: 0,
|
||||
},
|
||||
products: [],
|
||||
issues: [],
|
||||
})
|
||||
}
|
||||
|
||||
// 检查每个产品
|
||||
const productChecks: any[] = []
|
||||
const allIssues: string[] = []
|
||||
|
||||
let healthyCount = 0
|
||||
let warningCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
for (const product of products) {
|
||||
const issues: string[] = []
|
||||
let severity: 'healthy' | 'warning' | 'error' = 'healthy'
|
||||
|
||||
// 检查必要字段
|
||||
if (!product.medusaId) {
|
||||
issues.push('缺少 Medusa ID')
|
||||
severity = 'error'
|
||||
}
|
||||
|
||||
if (!product.title) {
|
||||
issues.push('缺少产品标题')
|
||||
severity = 'error'
|
||||
}
|
||||
|
||||
// 检查预购设置
|
||||
if (product.fundingGoal === undefined || product.fundingGoal === null) {
|
||||
issues.push('未设置众筹目标')
|
||||
severity = severity === 'error' ? 'error' : 'warning'
|
||||
} else if (product.fundingGoal === 0) {
|
||||
issues.push('众筹目标为 0(将使用变体总和)')
|
||||
}
|
||||
|
||||
// 检查日期
|
||||
if (!product.preorderStartDate) {
|
||||
issues.push('未设置预购开始日期')
|
||||
severity = severity === 'error' ? 'error' : 'warning'
|
||||
}
|
||||
|
||||
if (!product.preorderEndDate) {
|
||||
issues.push('未设置预购结束日期')
|
||||
severity = severity === 'error' ? 'error' : 'warning'
|
||||
}
|
||||
|
||||
// 检查日期逻辑
|
||||
if (product.preorderStartDate && product.preorderEndDate) {
|
||||
const startDate = new Date(product.preorderStartDate)
|
||||
const endDate = new Date(product.preorderEndDate)
|
||||
|
||||
if (startDate >= endDate) {
|
||||
issues.push('预购开始日期晚于或等于结束日期')
|
||||
severity = 'error'
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
if (endDate < now) {
|
||||
issues.push('预购已结束')
|
||||
} else if (startDate > now) {
|
||||
issues.push('预购尚未开始')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查订单计数
|
||||
const orderCount = parseInt(String(product.orderCount || 0), 10)
|
||||
const fakeOrderCount = parseInt(String(product.fakeOrderCount || 0), 10)
|
||||
const totalDisplayCount = orderCount + fakeOrderCount
|
||||
const fundingGoal = parseInt(String(product.fundingGoal || 0), 10)
|
||||
|
||||
if (fundingGoal > 0) {
|
||||
const completionPercentage = Math.round((totalDisplayCount / fundingGoal) * 100)
|
||||
|
||||
if (completionPercentage >= 100) {
|
||||
issues.push(`已达成目标 (${completionPercentage}%)`)
|
||||
} else if (completionPercentage < 10) {
|
||||
issues.push(`完成度较低 (${completionPercentage}%)`)
|
||||
severity = severity === 'error' ? 'error' : 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (product.status !== 'published') {
|
||||
issues.push(`产品状态为: ${product.status}`)
|
||||
severity = severity === 'error' ? 'error' : 'warning'
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
if (severity === 'error') {
|
||||
errorCount++
|
||||
} else if (severity === 'warning' || issues.length > 0) {
|
||||
warningCount++
|
||||
} else {
|
||||
healthyCount++
|
||||
}
|
||||
|
||||
// 记录产品检查结果
|
||||
productChecks.push({
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
medusaId: product.medusaId,
|
||||
seedId: product.seedId,
|
||||
status: product.status,
|
||||
severity,
|
||||
issues,
|
||||
stats: {
|
||||
orderCount,
|
||||
fakeOrderCount,
|
||||
totalDisplayCount,
|
||||
fundingGoal,
|
||||
completionPercentage: fundingGoal > 0
|
||||
? Math.round((totalDisplayCount / fundingGoal) * 100)
|
||||
: 0,
|
||||
},
|
||||
dates: {
|
||||
preorderStartDate: product.preorderStartDate,
|
||||
preorderEndDate: product.preorderEndDate,
|
||||
},
|
||||
})
|
||||
|
||||
// 添加到全局问题列表
|
||||
if (issues.length > 0) {
|
||||
allIssues.push(`${product.title}: ${issues.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 按严重程度排序
|
||||
productChecks.sort((a, b) => {
|
||||
const severityOrder: { [key: string]: number } = { error: 0, warning: 1, healthy: 2 }
|
||||
return severityOrder[a.severity] - severityOrder[b.severity]
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
total: products.length,
|
||||
healthy: healthyCount,
|
||||
warnings: warningCount,
|
||||
errors: errorCount,
|
||||
},
|
||||
products: productChecks,
|
||||
issues: allIssues,
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[Health Check API] Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to check preorder products health',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
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/sync/batch-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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,526 +0,0 @@
|
|||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
getAllMedusaProducts,
|
||||
transformMedusaProductToPayload,
|
||||
getMedusaProductsPaginated,
|
||||
getProductCollection,
|
||||
} from '@/lib/medusa'
|
||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||
|
||||
/**
|
||||
* 处理 CORS 预检请求
|
||||
*/
|
||||
export async function OPTIONS(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
return handleCorsOptions(origin)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 seedId 或 medusaId 查找产品(优先使用 seedId)
|
||||
* 会在两个 collection 中查找
|
||||
* @returns { product, collection } 或 null
|
||||
*/
|
||||
async function findProductBySeedIdOrMedusaId(
|
||||
payload: any,
|
||||
seedId: string | null,
|
||||
medusaId: string,
|
||||
): Promise<{ product: any; collection: 'products' | 'preorder-products' } | null> {
|
||||
const collections: Array<'products' | 'preorder-products'> = ['products', 'preorder-products']
|
||||
|
||||
// 优先通过 seedId 查找
|
||||
if (seedId) {
|
||||
for (const collection of collections) {
|
||||
const result = await payload.find({
|
||||
collection,
|
||||
where: {
|
||||
seedId: { equals: seedId },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
if (result.docs[0]) {
|
||||
return { product: result.docs[0], collection }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过 seedId 没找到,使用 medusaId 查找
|
||||
for (const collection of collections) {
|
||||
const result = await payload.find({
|
||||
collection,
|
||||
where: {
|
||||
medusaId: { equals: medusaId },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
if (result.docs[0]) {
|
||||
return { product: result.docs[0], collection }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并产品数据 - 只更新 Payload 中为空的字段
|
||||
* @param existingProduct Payload 中现有的产品数据
|
||||
* @param newData 从 Medusa 转换来的新数据
|
||||
* @param forceUpdate 是否强制更新所有字段
|
||||
* @returns 合并后的数据
|
||||
*/
|
||||
function mergeProductData(existingProduct: any, newData: any, forceUpdate: boolean): any {
|
||||
if (forceUpdate) {
|
||||
// 强制更新模式:使用所有新数据
|
||||
return { ...newData }
|
||||
}
|
||||
|
||||
// 只填充空值模式:只更新空字段
|
||||
const mergedData: any = {}
|
||||
|
||||
// 总是更新这些字段
|
||||
mergedData.lastSyncedAt = newData.lastSyncedAt
|
||||
mergedData.medusaId = newData.medusaId
|
||||
|
||||
// 如果 seedId 为空,更新它
|
||||
if (!existingProduct.seedId && newData.seedId) {
|
||||
mergedData.seedId = newData.seedId
|
||||
}
|
||||
|
||||
// 只在字段为空时更新基础字段
|
||||
if (!existingProduct.title) {
|
||||
mergedData.title = newData.title
|
||||
}
|
||||
if (!existingProduct.handle) {
|
||||
mergedData.handle = newData.handle
|
||||
}
|
||||
if (!existingProduct.thumbnail) {
|
||||
mergedData.thumbnail = newData.thumbnail
|
||||
}
|
||||
if (!existingProduct.status) {
|
||||
mergedData.status = newData.status
|
||||
}
|
||||
|
||||
// Medusa 属性字段:总是更新(以 Medusa 为准)
|
||||
mergedData.tags = newData.tags
|
||||
mergedData.type = newData.type
|
||||
mergedData.collection = newData.collection
|
||||
mergedData.category = newData.category
|
||||
|
||||
// 物理属性:总是更新
|
||||
mergedData.height = newData.height
|
||||
mergedData.width = newData.width
|
||||
mergedData.length = newData.length
|
||||
mergedData.weight = newData.weight
|
||||
|
||||
// 海关与物流:总是更新
|
||||
mergedData.midCode = newData.midCode
|
||||
mergedData.hsCode = newData.hsCode
|
||||
mergedData.countryOfOrigin = newData.countryOfOrigin
|
||||
|
||||
return mergedData
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 Medusa 商品到 Payload CMS
|
||||
* GET /api/sync/medusa - 同步所有商品
|
||||
* GET /api/sync/medusa?medusaId=prod_xxx - 同步单个商品
|
||||
* GET /api/sync/medusa?medusaId=prod_xxx&collection=preorder-products - 指定目标 collection
|
||||
* GET /api/sync/medusa?forceUpdate=true - 强制更新所有字段
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
||||
try {
|
||||
// 可选的 API Key 验证
|
||||
const authHeader = request.headers.get('authorization')
|
||||
const payloadApiKey = process.env.PAYLOAD_API_KEY
|
||||
|
||||
// 如果配置了 PAYLOAD_API_KEY,则验证请求
|
||||
if (payloadApiKey && authHeader) {
|
||||
const token = authHeader.replace('Bearer ', '')
|
||||
if (token !== payloadApiKey) {
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid API key',
|
||||
},
|
||||
{ status: 401 },
|
||||
)
|
||||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const medusaId = searchParams.get('medusaId')
|
||||
const collection = searchParams.get('collection') as 'products' | 'preorder-products' | null
|
||||
const forceUpdate = searchParams.get('forceUpdate') === 'true'
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 同步单个商品
|
||||
if (medusaId) {
|
||||
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection || undefined)
|
||||
const response = NextResponse.json(result)
|
||||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
|
||||
// 同步所有商品
|
||||
const result = await syncAllProducts(payload, forceUpdate)
|
||||
const response = NextResponse.json(result)
|
||||
return addCorsHeaders(response, origin)
|
||||
} catch (error) {
|
||||
console.error('[Sync API] ❌ 请求处理失败:', error)
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Medusa ID 同步单个商品
|
||||
*/
|
||||
async function syncSingleProductByMedusaId(
|
||||
payload: any,
|
||||
medusaId: string,
|
||||
forceUpdate: boolean,
|
||||
preferredCollection?: 'products' | 'preorder-products'
|
||||
) {
|
||||
console.log(`[Sync API] 🔄 开始同步产品: ${medusaId}`)
|
||||
console.log(`[Sync API] ⚙️ forceUpdate: ${forceUpdate}, preferredCollection: ${preferredCollection || 'auto'}`)
|
||||
|
||||
try {
|
||||
// 从 Medusa 获取商品数据
|
||||
const medusaProducts = await getAllMedusaProducts()
|
||||
const medusaProduct = medusaProducts.find((p) => p.id === medusaId)
|
||||
|
||||
if (!medusaProduct) {
|
||||
console.error(`[Sync API] ❌ Medusa 中未找到商品: ${medusaId}`)
|
||||
return {
|
||||
success: false,
|
||||
action: 'not_found',
|
||||
message: `Medusa 中未找到商品 ${medusaId}`,
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Sync API] ✅ 找到 Medusa 产品: ${medusaProduct.title}`)
|
||||
|
||||
// 确定应该同步到哪个 collection(优先使用传入的 collection,否则自动判断)
|
||||
const targetCollection = preferredCollection || getProductCollection(medusaProduct)
|
||||
console.log(`[Sync API] 🎯 目标 collection: ${targetCollection}${preferredCollection ? ' (指定)' : ' (自动判断)'}`)
|
||||
|
||||
const otherCollection =
|
||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
||||
|
||||
// 转换数据
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
const seedId = productData.seedId
|
||||
console.log(`[Sync API] 📦 产品数据: title=${productData.title}, seedId=${seedId}`)
|
||||
|
||||
// 使用新的查找函数(优先 seedId)
|
||||
const found = await findProductBySeedIdOrMedusaId(payload, seedId, medusaId)
|
||||
|
||||
// 如果在另一个 collection 中找到,需要移动
|
||||
if (found && found.collection !== targetCollection) {
|
||||
console.log(`[Sync API] 🚚 需要移动: ${found.collection} -> ${targetCollection}`)
|
||||
|
||||
await payload.delete({
|
||||
collection: found.collection,
|
||||
id: found.product.id,
|
||||
})
|
||||
|
||||
const created = await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
console.log(`[Sync API] ✅ 移动成功, 新 ID: ${created.id}`)
|
||||
return {
|
||||
success: true,
|
||||
action: 'moved',
|
||||
message: `商品 ${medusaId} 已从 ${found.collection} 移动到 ${targetCollection}`,
|
||||
productId: created.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果在目标 collection 中找到
|
||||
if (found) {
|
||||
console.log(`[Sync API] 🔎 在 ${found.collection} 中找到现有产品, ID: ${found.product.id}`)
|
||||
const existingProduct = found.product
|
||||
|
||||
// 如果存在且不强制更新,只更新空字段
|
||||
if (!forceUpdate) {
|
||||
console.log(`[Sync API] 🔄 模式: 只填充空字段,但 Medusa 属性总是更新`)
|
||||
// 合并数据(只更新空字段,但 Medusa 属性总是更新)
|
||||
const mergedData = mergeProductData(existingProduct, productData, false)
|
||||
|
||||
console.log(`[Sync API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
|
||||
// 更新(只更新空字段 + Medusa 属性)
|
||||
const updated = await payload.update({
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: mergedData,
|
||||
})
|
||||
|
||||
console.log(`[Sync API] ✅ 部分更新成功`)
|
||||
return {
|
||||
success: true,
|
||||
action: 'updated_partial',
|
||||
message: `商品 ${medusaId} 已部分更新(仅空字段)于 ${targetCollection}`,
|
||||
productId: updated.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Sync API] ⚡ 模式: 强制更新所有字段`)
|
||||
// 强制更新所有字段
|
||||
const updated = await payload.update({
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
console.log(`[Sync API] ✅ 强制更新成功`)
|
||||
return {
|
||||
success: true,
|
||||
action: 'updated',
|
||||
message: `商品 ${medusaId} 已更新于 ${targetCollection}`,
|
||||
productId: updated.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Sync API] ✨ 创建新产品`)
|
||||
// 不存在,创建新商品
|
||||
const created = await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
console.log(`[Sync API] ✅ 创建成功, ID: ${created.id}`)
|
||||
return {
|
||||
success: true,
|
||||
action: 'created',
|
||||
message: `商品 ${medusaId} 已创建于 ${targetCollection}`,
|
||||
productId: created.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Sync API] ❌ 同步失败:`, error)
|
||||
return {
|
||||
success: false,
|
||||
action: 'error',
|
||||
message: `同步商品 ${medusaId} 失败`,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步所有商品
|
||||
*/
|
||||
async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
||||
try {
|
||||
let offset = 0
|
||||
const limit = 100
|
||||
let hasMore = true
|
||||
const results = {
|
||||
total: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
updated_partial: 0,
|
||||
moved: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
details: [] as any[],
|
||||
}
|
||||
|
||||
while (hasMore) {
|
||||
// 分页获取 Medusa 商品
|
||||
const { products: medusaProducts, count } = await getMedusaProductsPaginated(offset, limit)
|
||||
|
||||
if (medusaProducts.length === 0) {
|
||||
hasMore = false
|
||||
break
|
||||
}
|
||||
|
||||
results.total += medusaProducts.length
|
||||
|
||||
// 处理每个商品
|
||||
for (const medusaProduct of medusaProducts) {
|
||||
try {
|
||||
// 确定应该同步到哪个 collection
|
||||
const targetCollection = getProductCollection(medusaProduct)
|
||||
|
||||
// 转换数据
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
const seedId = productData.seedId
|
||||
|
||||
// 使用新的查找函数(优先 seedId)
|
||||
const found = await findProductBySeedIdOrMedusaId(
|
||||
payload,
|
||||
seedId,
|
||||
medusaProduct.id,
|
||||
)
|
||||
|
||||
// 如果在错误的 collection 中,移动它
|
||||
if (found && found.collection !== targetCollection) {
|
||||
await payload.delete({
|
||||
collection: found.collection,
|
||||
id: found.product.id,
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
results.moved++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
seedId: seedId,
|
||||
title: medusaProduct.title,
|
||||
action: 'moved',
|
||||
from: found.collection,
|
||||
to: targetCollection,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果在目标 collection 中找到
|
||||
if (found) {
|
||||
const existingProduct = found.product
|
||||
|
||||
// 如果不强制更新,只更新空字段,但 Medusa 属性总是更新
|
||||
if (!forceUpdate) {
|
||||
const mergedData = mergeProductData(existingProduct, productData, false)
|
||||
|
||||
// 更新(只更新空字段 + Medusa 属性)
|
||||
await payload.update({
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: mergedData,
|
||||
})
|
||||
|
||||
results.updated_partial++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
seedId: seedId,
|
||||
title: medusaProduct.title,
|
||||
action: 'updated_partial',
|
||||
collection: targetCollection,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 强制更新
|
||||
await payload.update({
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: productData,
|
||||
})
|
||||
results.updated++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
seedId: seedId,
|
||||
title: medusaProduct.title,
|
||||
action: 'updated',
|
||||
collection: targetCollection,
|
||||
})
|
||||
} else {
|
||||
// 创建新商品
|
||||
await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
results.created++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
seedId: seedId,
|
||||
title: medusaProduct.title,
|
||||
action: 'created',
|
||||
collection: targetCollection,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing product ${medusaProduct.id}:`, error)
|
||||
results.errors++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
title: medusaProduct.title,
|
||||
action: 'error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新偏移量
|
||||
offset += limit
|
||||
if (offset >= count) {
|
||||
hasMore = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `同步完成: ${results.created} 个创建, ${results.updated} 个更新, ${results.updated_partial} 个部分更新, ${results.moved} 个移动, ${results.skipped} 个跳过, ${results.errors} 个错误`,
|
||||
results,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing all products:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sync/medusa
|
||||
* 手动触发同步(与 GET 参数相同)
|
||||
* Body: { medusaId?, collection?, forceUpdate? }
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const origin = request.headers.get('origin')
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// 可以在这里添加认证检查
|
||||
// const { user } = await payload.auth({ headers: request.headers })
|
||||
// if (!user) {
|
||||
// const response = NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
// return addCorsHeaders(response, origin)
|
||||
// }
|
||||
|
||||
const body = await request.json()
|
||||
const { medusaId, collection, forceUpdate = true } = body
|
||||
|
||||
// 同步单个商品
|
||||
if (medusaId) {
|
||||
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate, collection)
|
||||
const response = NextResponse.json(result)
|
||||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
|
||||
// 同步所有商品
|
||||
const result = await syncAllProducts(payload, forceUpdate)
|
||||
const response = NextResponse.json(result)
|
||||
return addCorsHeaders(response, origin)
|
||||
} catch (error) {
|
||||
console.error('[Sync API] ❌ POST 请求处理失败:', error)
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getAllMedusaProducts,
|
||||
transformMedusaProductToPayload,
|
||||
getProductCollection,
|
||||
convertTextToLexical,
|
||||
} from '@/lib/medusa'
|
||||
import { addCorsHeaders, handleCorsOptions } from '@/lib/cors'
|
||||
|
||||
|
|
@ -30,7 +31,15 @@ export async function POST(request: Request) {
|
|||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { medusaId, collection: preferredCollection, forceUpdate = false } = body
|
||||
const {
|
||||
medusaId,
|
||||
collection: preferredCollection,
|
||||
forceUpdate = false,
|
||||
// 预购相关字段(从 Medusa subscriber 传递)
|
||||
fundingGoal,
|
||||
preorderStartDate,
|
||||
preorderEndDate,
|
||||
} = body
|
||||
|
||||
if (!medusaId) {
|
||||
const response = NextResponse.json(
|
||||
|
|
@ -43,7 +52,12 @@ export async function POST(request: Request) {
|
|||
return addCorsHeaders(response, origin)
|
||||
}
|
||||
|
||||
console.log('[Sync Product API] 🎯 参数:', { medusaId, preferredCollection, forceUpdate })
|
||||
console.log('[Sync Product API] 🎯 参数:', {
|
||||
medusaId,
|
||||
preferredCollection,
|
||||
forceUpdate,
|
||||
preorderData: { fundingGoal, preorderStartDate, preorderEndDate }
|
||||
})
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
|
|
@ -123,9 +137,15 @@ export async function POST(request: Request) {
|
|||
id: existingProduct.id,
|
||||
})
|
||||
|
||||
// 准备移动数据,包括描述转换
|
||||
const moveData: any = { ...productData }
|
||||
if (medusaProduct.description) {
|
||||
moveData.description = convertTextToLexical(medusaProduct.description)
|
||||
}
|
||||
|
||||
finalProduct = await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
data: moveData,
|
||||
})
|
||||
|
||||
action = 'moved'
|
||||
|
|
@ -148,6 +168,19 @@ export async function POST(request: Request) {
|
|||
if (!existingProduct.handle) mergedData.handle = productData.handle
|
||||
if (!existingProduct.thumbnail) mergedData.thumbnail = productData.thumbnail
|
||||
if (!existingProduct.status) mergedData.status = productData.status
|
||||
// 描述为空时也从 Medusa 导入(转换为富文本格式)
|
||||
if (!existingProduct.description && medusaProduct.description) {
|
||||
(mergedData as any).description = convertTextToLexical(medusaProduct.description)
|
||||
}
|
||||
|
||||
// 最低价格和 seedId:总是更新
|
||||
mergedData.seedId = productData.seedId
|
||||
mergedData.startPrice = productData.startPrice
|
||||
|
||||
// 如果是预购产品,fundingGoal 也总是更新
|
||||
if (targetCollection === 'preorder-products' && fundingGoal !== undefined) {
|
||||
(mergedData as any).fundingGoal = fundingGoal
|
||||
}
|
||||
|
||||
// Medusa 属性字段:总是更新(以 Medusa 为准)
|
||||
mergedData.tags = productData.tags
|
||||
|
|
@ -166,6 +199,18 @@ export async function POST(request: Request) {
|
|||
mergedData.hsCode = productData.hsCode
|
||||
mergedData.countryOfOrigin = productData.countryOfOrigin
|
||||
|
||||
// 如果是预购产品,添加预购日期字段(只在为空时更新)
|
||||
if (targetCollection === 'preorder-products') {
|
||||
// Preorder Start Date - 只在为空时更新
|
||||
if (!existingProduct.preorderStartDate && preorderStartDate) {
|
||||
(mergedData as any).preorderStartDate = preorderStartDate
|
||||
}
|
||||
// Preorder End Date - 只在为空时更新
|
||||
if (!existingProduct.preorderEndDate && preorderEndDate) {
|
||||
(mergedData as any).preorderEndDate = preorderEndDate
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Sync Product API] 📝 更新字段(含 Medusa 属性): ${Object.keys(mergedData).join(', ')}`)
|
||||
finalProduct = await payload.update({
|
||||
collection: targetCollection,
|
||||
|
|
@ -174,12 +219,16 @@ export async function POST(request: Request) {
|
|||
})
|
||||
action = 'updated_partial'
|
||||
} else {
|
||||
// 强制更新所有字段
|
||||
// 强制更新所有字段(包括描述转换为富文本)
|
||||
console.log(`[Sync Product API] ⚡ 强制更新所有字段`)
|
||||
const forceUpdateData: any = { ...productData }
|
||||
if (medusaProduct.description) {
|
||||
forceUpdateData.description = convertTextToLexical(medusaProduct.description)
|
||||
}
|
||||
finalProduct = await payload.update({
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: productData,
|
||||
data: forceUpdateData,
|
||||
})
|
||||
action = 'updated'
|
||||
}
|
||||
|
|
@ -187,9 +236,35 @@ export async function POST(request: Request) {
|
|||
// 不存在,创建新产品
|
||||
else {
|
||||
console.log(`[Sync Product API] ✨ 创建新产品`)
|
||||
|
||||
// 如果是预购产品,添加预购相关字段
|
||||
const createData: any = { ...productData }
|
||||
|
||||
// 添加描述(转换为富文本格式)
|
||||
if (medusaProduct.description) {
|
||||
createData.description = convertTextToLexical(medusaProduct.description)
|
||||
}
|
||||
|
||||
if (targetCollection === 'preorder-products') {
|
||||
if (fundingGoal !== undefined) {
|
||||
createData.fundingGoal = fundingGoal
|
||||
}
|
||||
if (preorderStartDate) {
|
||||
createData.preorderStartDate = preorderStartDate
|
||||
}
|
||||
if (preorderEndDate) {
|
||||
createData.preorderEndDate = preorderEndDate
|
||||
}
|
||||
console.log(`[Sync Product API] 📋 添加预购字段:`, {
|
||||
fundingGoal: createData.fundingGoal,
|
||||
preorderStartDate: createData.preorderStartDate,
|
||||
preorderEndDate: createData.preorderEndDate,
|
||||
})
|
||||
}
|
||||
|
||||
finalProduct = await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
data: createData,
|
||||
})
|
||||
action = 'created'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const PreorderProducts: CollectionConfig = {
|
|||
components: {
|
||||
beforeListTable: [
|
||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||
'/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton',
|
||||
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,13 +3,25 @@ import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
|||
import { cacheAfterChange, cacheAfterDelete } from '../hooks/cacheInvalidation'
|
||||
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab } from './base/ProductBase'
|
||||
import {
|
||||
AlignFeature,
|
||||
BlocksFeature,
|
||||
BoldFeature,
|
||||
ChecklistFeature,
|
||||
HeadingFeature,
|
||||
IndentFeature,
|
||||
InlineCodeFeature,
|
||||
ItalicFeature,
|
||||
lexicalEditor,
|
||||
LinkFeature,
|
||||
OrderedListFeature,
|
||||
ParagraphFeature,
|
||||
RelationshipFeature,
|
||||
UnorderedListFeature,
|
||||
UploadFeature,
|
||||
FixedToolbarFeature,
|
||||
InlineToolbarFeature,
|
||||
HorizontalRuleFeature,
|
||||
BlockquoteFeature,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Products: CollectionConfig = {
|
||||
|
|
@ -44,29 +56,37 @@ export const Products: CollectionConfig = {
|
|||
label: '📄 商品详情',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
name: 'description',
|
||||
type: 'richText',
|
||||
admin: {
|
||||
description: '商品详细内容(支持图文混排)',
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
LinkFeature({
|
||||
enabledCollections: ['products'],
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
features: [
|
||||
ParagraphFeature(),
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||
BoldFeature(),
|
||||
ItalicFeature(),
|
||||
UnorderedListFeature(),
|
||||
OrderedListFeature(),
|
||||
LinkFeature(),
|
||||
AlignFeature(),
|
||||
BlockquoteFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
InlineCodeFeature(),
|
||||
IndentFeature(),
|
||||
ChecklistFeature(),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
slug: 'image',
|
||||
imageURL: '/api/media',
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'text',
|
||||
label: '图片说明',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
|
@ -76,19 +96,19 @@ export const Products: CollectionConfig = {
|
|||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
type: 'text',
|
||||
label: '图片说明',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
RelationshipFeature(),
|
||||
],
|
||||
}),
|
||||
admin: {
|
||||
description: '商品详细描述(支持富文本编辑)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -76,6 +76,14 @@ export const ProductBaseFields: Field[] = [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'startPrice',
|
||||
type: 'number',
|
||||
admin: {
|
||||
description: '起始价格(从 Medusa 同步,单位:美分)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lastSyncedAt',
|
||||
type: 'date',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export const ThumbnailCell = (props: any) => {
|
|||
const value = props.value || props.cellData || props.data
|
||||
const rowData = props.rowData || props.row
|
||||
|
||||
// 获取起始价格(已经是美元)
|
||||
const startPrice = rowData?.startPrice
|
||||
const formattedPrice = startPrice ? `$${startPrice.toFixed(2)}` : ''
|
||||
|
||||
// 优先从 props 中获取 collection 信息(Payload Cell API)
|
||||
let collectionSlug = props.collectionConfig?.slug || props.field?.relationTo || props.collection
|
||||
|
||||
|
|
@ -22,13 +26,30 @@ export const ThumbnailCell = (props: any) => {
|
|||
return (
|
||||
<Link
|
||||
href={editUrl}
|
||||
style={{ display: 'block', width: '100%', height: '200px', textDecoration: 'none' }}
|
||||
style={{ display: 'block', width: '100%', textDecoration: 'none', position: 'relative' }}
|
||||
>
|
||||
<div style={{ position: 'relative', width: '100%', height: '200px' }}>
|
||||
{isImage ? (
|
||||
<img src={value} alt="商品缩略图" className="thumbnail-img" />
|
||||
) : (
|
||||
<div className="no-image">{value || '无图片'}</div>
|
||||
)}
|
||||
{formattedPrice && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
right: '8px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{formattedPrice}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Data Button
|
||||
* 一键重置所有数据:清理 Payload + 清理 Medusa + Seed Medusa
|
||||
*/
|
||||
export function ResetDataButton({ className }: Props) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [details, setDetails] = useState<any>(null)
|
||||
|
||||
const handleResetData = async () => {
|
||||
if (!confirm(
|
||||
'⚠️ 危险操作:重置所有数据\n\n' +
|
||||
'此操作将:\n' +
|
||||
'1. 清理所有 Payload CMS 数据(保留用户)\n' +
|
||||
'2. 清理所有 Medusa 数据\n' +
|
||||
'3. 重新导入 Medusa seed 数据\n\n' +
|
||||
'⚠️ 此操作不可撤销!\n\n' +
|
||||
'确认要继续吗?'
|
||||
)) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setMessage('🔄 开始数据重置流程...')
|
||||
setDetails(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/reset-data', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Reset failed')
|
||||
}
|
||||
|
||||
setDetails(result)
|
||||
setMessage(
|
||||
'✅ 数据重置完成!\n\n' +
|
||||
'下一步:\n' +
|
||||
'1. 同步 Medusa 商品到 Payload CMS\n' +
|
||||
'2. 设置 ProductRecommendations\n' +
|
||||
'3. 配置 PreorderProducts 的预购设置'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('数据重置失败:', error)
|
||||
setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<Button
|
||||
onClick={handleResetData}
|
||||
buttonStyle="error"
|
||||
disabled={loading}
|
||||
size="medium"
|
||||
>
|
||||
{loading ? '🔄 重置中...' : '🗑️ 重置所有数据'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: message.includes('✅') ? '#d4edda' : message.includes('❌') ? '#f8d7da' : '#d1ecf1',
|
||||
border: `1px solid ${message.includes('✅') ? '#c3e6cb' : message.includes('❌') ? '#f5c6cb' : '#bee5eb'}`,
|
||||
borderRadius: '4px',
|
||||
marginTop: '1rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details && details.steps && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#f8f9fa',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<h4 style={{ marginTop: 0, marginBottom: '0.5rem' }}>详细信息:</h4>
|
||||
{details.steps.map((step: any, index: number) => (
|
||||
<div key={index} style={{ marginBottom: '0.5rem' }}>
|
||||
<strong>
|
||||
[{step.step}/3] {step.name}:{' '}
|
||||
</strong>
|
||||
<span style={{ color: step.success ? 'green' : 'red' }}>
|
||||
{step.success ? '✅ 成功' : '❌ 失败'}
|
||||
</span>
|
||||
{step.deleted !== undefined && (
|
||||
<span style={{ marginLeft: '0.5rem' }}>
|
||||
(删除 {step.deleted} 条记录)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
import { ResetDataButton } from '../sync/ResetDataButton'
|
||||
|
||||
/**
|
||||
* 管理员控制面板
|
||||
|
|
@ -144,6 +145,33 @@ export default function AdminPanel() {
|
|||
📦 数据管理
|
||||
</h2>
|
||||
|
||||
{/* 数据重置 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--theme-elevation-0)',
|
||||
borderRadius: '6px',
|
||||
padding: '1.5rem',
|
||||
marginBottom: '1rem',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem', fontWeight: '600' }}>
|
||||
🔄 数据重置(Payload + Medusa)
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--theme-elevation-600)',
|
||||
}}
|
||||
>
|
||||
一键重置所有数据:清理 Payload CMS → 清理 Medusa → 重新导入 Medusa seed 数据
|
||||
</p>
|
||||
|
||||
<ResetDataButton />
|
||||
</div>
|
||||
|
||||
{/* 清理数据库 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--theme-elevation-0)',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
|
||||
interface HealthCheckResult {
|
||||
success: boolean
|
||||
timestamp: string
|
||||
summary: {
|
||||
total: number
|
||||
healthy: number
|
||||
warnings: number
|
||||
errors: number
|
||||
}
|
||||
products: Array<{
|
||||
id: string
|
||||
title: string
|
||||
medusaId: string
|
||||
seedId: string
|
||||
status: string
|
||||
severity: 'healthy' | 'warning' | 'error'
|
||||
issues: string[]
|
||||
stats: {
|
||||
orderCount: number
|
||||
fakeOrderCount: number
|
||||
totalDisplayCount: number
|
||||
fundingGoal: number
|
||||
completionPercentage: number
|
||||
}
|
||||
dates: {
|
||||
preorderStartDate: string | null
|
||||
preorderEndDate: string | null
|
||||
}
|
||||
}>
|
||||
issues: string[]
|
||||
}
|
||||
|
||||
export const PreorderHealthCheck: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<HealthCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const runHealthCheck = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/preorders/health-check')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
setResult(data)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to run health check')
|
||||
console.error('Health check error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 自动运行一次健康检查
|
||||
runHealthCheck()
|
||||
}, [])
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
return 'text-red-600 bg-red-50'
|
||||
case 'warning':
|
||||
return 'text-yellow-600 bg-yellow-50'
|
||||
case 'healthy':
|
||||
return 'text-green-600 bg-green-50'
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50'
|
||||
}
|
||||
}
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
return '❌'
|
||||
case 'warning':
|
||||
return '⚠️'
|
||||
case 'healthy':
|
||||
return '✅'
|
||||
default:
|
||||
return 'ℹ️'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'N/A'
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">预购产品健康检查</h1>
|
||||
<p className="text-gray-600">
|
||||
检查所有预购产品的配置状态和潜在问题
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={runHealthCheck}
|
||||
disabled={loading}
|
||||
buttonStyle="primary"
|
||||
>
|
||||
{loading ? '检查中...' : '刷新检查'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-800">
|
||||
<strong>错误:</strong> {error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<>
|
||||
{/* 概览统计 */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<p className="text-sm text-blue-600 font-medium mb-1">总数</p>
|
||||
<p className="text-3xl font-bold text-blue-700">{result.summary.total}</p>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<p className="text-sm text-green-600 font-medium mb-1">健康</p>
|
||||
<p className="text-3xl font-bold text-green-700">{result.summary.healthy}</p>
|
||||
</div>
|
||||
<div className="bg-yellow-50 p-4 rounded-lg border border-yellow-200">
|
||||
<p className="text-sm text-yellow-600 font-medium mb-1">警告</p>
|
||||
<p className="text-3xl font-bold text-yellow-700">{result.summary.warnings}</p>
|
||||
</div>
|
||||
<div className="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||
<p className="text-sm text-red-600 font-medium mb-1">错误</p>
|
||||
<p className="text-3xl font-bold text-red-700">{result.summary.errors}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 检查时间 */}
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
最后检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
|
||||
{/* 产品列表 */}
|
||||
<div className="space-y-4">
|
||||
{result.products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className={`border rounded-lg p-4 ${
|
||||
product.severity === 'error'
|
||||
? 'border-red-300 bg-red-50'
|
||||
: product.severity === 'warning'
|
||||
? 'border-yellow-300 bg-yellow-50'
|
||||
: 'border-green-300 bg-green-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-2xl">{getSeverityIcon(product.severity)}</span>
|
||||
<h3 className="text-lg font-semibold">{product.title}</h3>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded-full ${
|
||||
product.status === 'published'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p>Medusa ID: {product.medusaId}</p>
|
||||
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>进度: {product.stats.completionPercentage}%</p>
|
||||
<p>
|
||||
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日期信息 */}
|
||||
<div className="flex gap-4 text-sm text-gray-600 mb-3">
|
||||
<div>
|
||||
<span className="font-medium">开始:</span>{' '}
|
||||
{formatDate(product.dates.preorderStartDate)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">结束:</span>{' '}
|
||||
{formatDate(product.dates.preorderEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 问题列表 */}
|
||||
{product.issues.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-300">
|
||||
<p className="text-sm font-medium mb-2">问题:</p>
|
||||
<ul className="text-sm space-y-1">
|
||||
{product.issues.map((issue, idx) => (
|
||||
<li key={idx} className="flex items-start">
|
||||
<span className="mr-2">•</span>
|
||||
<span>{issue}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{result.products.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<p className="text-lg">没有找到预购产品</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!result && !error && loading && (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">正在检查预购产品...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Modal } from '@payloadcms/ui'
|
||||
|
||||
interface HealthCheckResult {
|
||||
success: boolean
|
||||
timestamp: string
|
||||
summary: {
|
||||
total: number
|
||||
healthy: number
|
||||
warnings: number
|
||||
errors: number
|
||||
}
|
||||
products: Array<{
|
||||
id: string
|
||||
title: string
|
||||
medusaId: string
|
||||
seedId: string
|
||||
status: string
|
||||
severity: 'healthy' | 'warning' | 'error'
|
||||
issues: string[]
|
||||
stats: {
|
||||
orderCount: number
|
||||
fakeOrderCount: number
|
||||
totalDisplayCount: number
|
||||
fundingGoal: number
|
||||
completionPercentage: number
|
||||
}
|
||||
dates: {
|
||||
preorderStartDate: string | null
|
||||
preorderEndDate: string | null
|
||||
}
|
||||
}>
|
||||
issues: string[]
|
||||
}
|
||||
|
||||
export const PreorderHealthCheckButton: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<HealthCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const runHealthCheck = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/preorders/health-check')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
setResult(data)
|
||||
setIsOpen(true)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to run health check')
|
||||
console.error('Health check error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
return '❌'
|
||||
case 'warning':
|
||||
return '⚠️'
|
||||
case 'healthy':
|
||||
return '✅'
|
||||
default:
|
||||
return 'ℹ️'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'N/A'
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={runHealthCheck}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
icon="health"
|
||||
>
|
||||
{loading ? '检查中...' : '🏥 健康检查'}
|
||||
</Button>
|
||||
{error && (
|
||||
<span style={{ color: 'red', marginLeft: '1rem', alignSelf: 'center' }}>
|
||||
错误: {error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && result && (
|
||||
<Modal slug="health-check-modal" onClose={() => setIsOpen(false)}>
|
||||
<div style={{ padding: '2rem', maxWidth: '900px' }}>
|
||||
<h2 style={{ marginBottom: '1.5rem', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
预购产品健康检查
|
||||
</h2>
|
||||
|
||||
{/* 概览统计 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.5rem'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#EFF6FF',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #BFDBFE'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#2563EB', fontWeight: '500', marginBottom: '0.25rem' }}>总数</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1E40AF' }}>{result.summary.total}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#F0FDF4',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #BBF7D0'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#16A34A', fontWeight: '500', marginBottom: '0.25rem' }}>健康</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#15803D' }}>{result.summary.healthy}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FEFCE8',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #FDE047'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#CA8A04', fontWeight: '500', marginBottom: '0.25rem' }}>警告</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#A16207' }}>{result.summary.warnings}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FEF2F2',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #FECACA'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#DC2626', fontWeight: '500', marginBottom: '0.25rem' }}>错误</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#B91C1C' }}>{result.summary.errors}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.875rem', color: '#6B7280', marginBottom: '1.5rem' }}>
|
||||
检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
|
||||
{/* 产品列表 */}
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{result.products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
style={{
|
||||
border: `1px solid ${
|
||||
product.severity === 'error'
|
||||
? '#FCA5A5'
|
||||
: product.severity === 'warning'
|
||||
? '#FCD34D'
|
||||
: '#86EFAC'
|
||||
}`,
|
||||
backgroundColor: product.severity === 'error'
|
||||
? '#FEF2F2'
|
||||
: product.severity === 'warning'
|
||||
? '#FEFCE8'
|
||||
: '#F0FDF4',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '1rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem' }}>{getSeverityIcon(product.severity)}</span>
|
||||
<h3 style={{ fontSize: '1.125rem', fontWeight: '600' }}>{product.title}</h3>
|
||||
<span
|
||||
style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
borderRadius: '9999px',
|
||||
backgroundColor: product.status === 'published' ? '#D1FAE5' : '#F3F4F6',
|
||||
color: product.status === 'published' ? '#065F46' : '#374151',
|
||||
}}
|
||||
>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>Medusa ID: {product.medusaId}</p>
|
||||
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>进度: {product.stats.completionPercentage}%</p>
|
||||
<p>
|
||||
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.875rem', color: '#4B5563', marginBottom: '0.75rem' }}>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>开始:</span>{' '}
|
||||
{formatDate(product.dates.preorderStartDate)}
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>结束:</span>{' '}
|
||||
{formatDate(product.dates.preorderEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.issues.length > 0 && (
|
||||
<div style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid #D1D5DB' }}>
|
||||
<p style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>问题:</p>
|
||||
<ul style={{ fontSize: '0.875rem', paddingLeft: '1rem' }}>
|
||||
{product.issues.map((issue, idx) => (
|
||||
<li key={idx} style={{ marginBottom: '0.25rem' }}>
|
||||
{issue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{result.products.length === 0 && (
|
||||
<p style={{ textAlign: 'center', padding: '3rem', color: '#6B7280' }}>
|
||||
没有找到预购产品
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', textAlign: 'right' }}>
|
||||
<Button onClick={() => setIsOpen(false)} buttonStyle="primary">
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -217,6 +217,47 @@ export function isPreorderProduct(product: MedusaProduct): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 将纯文本转换为 Lexical 富文本格式
|
||||
* 自动处理换行符,将其转换为段落
|
||||
*/
|
||||
export function convertTextToLexical(text: string | null | undefined): any {
|
||||
if (!text) return null
|
||||
|
||||
// 按换行符分割文本,创建多个段落
|
||||
const paragraphs = text.split(/\r?\n/).filter(line => line.trim().length > 0)
|
||||
|
||||
if (paragraphs.length === 0) return null
|
||||
|
||||
return {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: paragraphs.map(paragraph => ({
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: paragraph.trim(),
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
})),
|
||||
direction: 'ltr',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品应该同步到的 collection
|
||||
*/
|
||||
|
|
@ -248,6 +289,20 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
|||
// 提取 category(从 categories 数组或 metadata)
|
||||
const category = product.categories?.[0]?.name || product.metadata?.category || null
|
||||
|
||||
// 计算起始价格(从所有 variants 的 prices 中找最低价格,并转换为美元)
|
||||
let startPrice: number | null = null
|
||||
if (product.variants && product.variants.length > 0) {
|
||||
const allPrices = product.variants.flatMap(variant =>
|
||||
variant.prices?.map(price => price.amount) || []
|
||||
).filter(price => typeof price === 'number' && price > 0)
|
||||
|
||||
if (allPrices.length > 0) {
|
||||
// 将美分转换为美元(保留两位小数)
|
||||
const minPriceInCents = Math.min(...allPrices)
|
||||
startPrice = Math.round(minPriceInCents) / 100
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
medusaId: product.id,
|
||||
seedId: product.metadata?.seed_id || null,
|
||||
|
|
@ -256,6 +311,7 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
|
|||
thumbnail: thumbnailUrl || null,
|
||||
status: 'draft',
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
startPrice: startPrice,
|
||||
|
||||
// Medusa 默认属性
|
||||
tags: tags,
|
||||
|
|
|
|||
|
|
@ -217,14 +217,18 @@ export interface Product {
|
|||
* 商品封面 URL(支持上传或输入 URL)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* 起始价格(从 Medusa 同步,单位:美分)
|
||||
*/
|
||||
startPrice?: number | null;
|
||||
/**
|
||||
* 最后同步时间
|
||||
*/
|
||||
lastSyncedAt?: string | null;
|
||||
/**
|
||||
* 商品详细内容(支持图文混排)
|
||||
* 商品详细描述(支持富文本编辑)
|
||||
*/
|
||||
content?: {
|
||||
description?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
|
|
@ -344,6 +348,10 @@ export interface PreorderProduct {
|
|||
* 商品封面 URL(支持上传或输入 URL)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* 起始价格(从 Medusa 同步,单位:美分)
|
||||
*/
|
||||
startPrice?: number | null;
|
||||
/**
|
||||
* 最后同步时间
|
||||
*/
|
||||
|
|
@ -910,8 +918,9 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||
seedId?: T;
|
||||
title?: T;
|
||||
thumbnail?: T;
|
||||
startPrice?: T;
|
||||
lastSyncedAt?: T;
|
||||
content?: T;
|
||||
description?: T;
|
||||
relatedProducts?: T;
|
||||
tags?: T;
|
||||
type?: T;
|
||||
|
|
@ -946,6 +955,7 @@ export interface PreorderProductsSelect<T extends boolean = true> {
|
|||
seedId?: T;
|
||||
title?: T;
|
||||
thumbnail?: T;
|
||||
startPrice?: T;
|
||||
lastSyncedAt?: T;
|
||||
preorderType?: T;
|
||||
fundingGoal?: T;
|
||||
|
|
|
|||
Loading…
Reference in New Issue