diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
index 2bfdfcc..495552f 100644
--- a/src/app/(payload)/admin/importMap.js
+++ b/src/app/(payload)/admin/importMap.js
@@ -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,
diff --git a/src/app/api/admin/reset-data/README.md b/src/app/api/admin/reset-data/README.md
new file mode 100644
index 0000000..a847104
--- /dev/null
+++ b/src/app/api/admin/reset-data/README.md
@@ -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 完成,无需命令行。
diff --git a/src/app/api/admin/reset-data/route.ts b/src/app/api/admin/reset-data/route.ts
new file mode 100644
index 0000000..68479af
--- /dev/null
+++ b/src/app/api/admin/reset-data/route.ts
@@ -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,
+ }
+}
diff --git a/src/app/api/debug/preorder-products/route.ts b/src/app/api/debug/preorder-products/route.ts
new file mode 100644
index 0000000..cbe693b
--- /dev/null
+++ b/src/app/api/debug/preorder-products/route.ts
@@ -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 }
+ )
+ }
+}
diff --git a/src/app/api/home/route.ts b/src/app/api/home/route.ts
index 1512d92..6fb7eb4 100644
--- a/src/app/api/home/route.ts
+++ b/src/app/api/home/route.ts
@@ -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',
+ }
+ }),
+ })),
},
}
diff --git a/src/app/api/preorders/health-check/route.ts b/src/app/api/preorders/health-check/route.ts
new file mode 100644
index 0000000..6c818c0
--- /dev/null
+++ b/src/app/api/preorders/health-check/route.ts
@@ -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 }
+ )
+ }
+}
diff --git a/src/app/api/sync/batch-medusa/route.ts b/src/app/api/sync/batch-medusa/route.ts
deleted file mode 100644
index 47e5f94..0000000
--- a/src/app/api/sync/batch-medusa/route.ts
+++ /dev/null
@@ -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 },
- )
- }
-}
diff --git a/src/app/api/sync/medusa/route.ts b/src/app/api/sync/medusa/route.ts
deleted file mode 100644
index 4e6d72e..0000000
--- a/src/app/api/sync/medusa/route.ts
+++ /dev/null
@@ -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)
- }
-}
diff --git a/src/app/api/sync/product/route.ts b/src/app/api/sync/product/route.ts
index 3228c68..28f352c 100644
--- a/src/app/api/sync/product/route.ts
+++ b/src/app/api/sync/product/route.ts
@@ -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'
}
diff --git a/src/collections/PreorderProducts.ts b/src/collections/PreorderProducts.ts
index e90e48c..f3ff3e5 100644
--- a/src/collections/PreorderProducts.ts
+++ b/src/collections/PreorderProducts.ts
@@ -37,6 +37,7 @@ export const PreorderProducts: CollectionConfig = {
components: {
beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
+ '/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton',
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
],
},
diff --git a/src/collections/Products.ts b/src/collections/Products.ts
index 32056ca..ec7b972 100644
--- a/src/collections/Products.ts
+++ b/src/collections/Products.ts
@@ -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: '商品详细描述(支持富文本编辑)',
+ },
},
],
},
diff --git a/src/collections/base/ProductBase.ts b/src/collections/base/ProductBase.ts
index c9b7c40..2b5eaf0 100644
--- a/src/collections/base/ProductBase.ts
+++ b/src/collections/base/ProductBase.ts
@@ -76,6 +76,14 @@ export const ProductBaseFields: Field[] = [
},
},
},
+ {
+ name: 'startPrice',
+ type: 'number',
+ admin: {
+ description: '起始价格(从 Medusa 同步,单位:美分)',
+ readOnly: true,
+ },
+ },
{
name: 'lastSyncedAt',
type: 'date',
diff --git a/src/components/cells/ThumbnailCell.tsx b/src/components/cells/ThumbnailCell.tsx
index 47b05d9..7e0fab7 100644
--- a/src/components/cells/ThumbnailCell.tsx
+++ b/src/components/cells/ThumbnailCell.tsx
@@ -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 (
- {isImage ? (
-
- ) : (
-
+ 一键重置所有数据:清理 Payload CMS → 清理 Medusa → 重新导入 Medusa seed 数据 +
+ ++ 检查所有预购产品的配置状态和潜在问题 +
++ 错误: {error} +
+总数
+{result.summary.total}
+健康
+{result.summary.healthy}
+警告
+{result.summary.warnings}
+错误
+{result.summary.errors}
++ 最后检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} +
+ + {/* 产品列表 */} +Medusa ID: {product.medusaId}
+ {product.seedId &&Seed ID: {product.seedId}
} +进度: {product.stats.completionPercentage}%
++ {product.stats.totalDisplayCount} / {product.stats.fundingGoal} +
+问题:
+没有找到预购产品
+正在检查预购产品...
+总数
+{result.summary.total}
+健康
+{result.summary.healthy}
+警告
+{result.summary.warnings}
+错误
+{result.summary.errors}
++ 检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} +
+ + {/* 产品列表 */} +Medusa ID: {product.medusaId}
+ {product.seedId &&Seed ID: {product.seedId}
} +进度: {product.stats.completionPercentage}%
++ {product.stats.totalDisplayCount} / {product.stats.fundingGoal} +
+问题:
++ 没有找到预购产品 +
+ )} + +