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 ? ( - 商品缩略图 - ) : ( -
{value || '无图片'}
- )} +
+ {isImage ? ( + 商品缩略图 + ) : ( +
{value || '无图片'}
+ )} + {formattedPrice && ( +
+ {formattedPrice} +
+ )} +
) } diff --git a/src/components/sync/ResetDataButton.tsx b/src/components/sync/ResetDataButton.tsx new file mode 100644 index 0000000..0e30a8b --- /dev/null +++ b/src/components/sync/ResetDataButton.tsx @@ -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(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 ( +
+
+ +
+ + {message && ( +
+ {message} +
+ )} + + {details && details.steps && ( +
+

详细信息:

+ {details.steps.map((step: any, index: number) => ( +
+ + [{step.step}/3] {step.name}:{' '} + + + {step.success ? '✅ 成功' : '❌ 失败'} + + {step.deleted !== undefined && ( + + (删除 {step.deleted} 条记录) + + )} +
+ ))} +
+ )} +
+ ) +} diff --git a/src/components/views/AdminPanel.tsx b/src/components/views/AdminPanel.tsx index 0b5749f..5f333b6 100644 --- a/src/components/views/AdminPanel.tsx +++ b/src/components/views/AdminPanel.tsx @@ -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() { 📦 数据管理 + {/* 数据重置 */} +
+

+ 🔄 数据重置(Payload + Medusa) +

+

+ 一键重置所有数据:清理 Payload CMS → 清理 Medusa → 重新导入 Medusa seed 数据 +

+ + +
+ + {/* 清理数据库 */}
+ issues: string[] +} + +export const PreorderHealthCheck: React.FC = () => { + const [loading, setLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState(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 ( +
+
+
+

预购产品健康检查

+

+ 检查所有预购产品的配置状态和潜在问题 +

+
+ +
+ + {error && ( +
+

+ 错误: {error} +

+
+ )} + + {result && ( + <> + {/* 概览统计 */} +
+
+

总数

+

{result.summary.total}

+
+
+

健康

+

{result.summary.healthy}

+
+
+

警告

+

{result.summary.warnings}

+
+
+

错误

+

{result.summary.errors}

+
+
+ + {/* 检查时间 */} +

+ 最后检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} +

+ + {/* 产品列表 */} +
+ {result.products.map((product) => ( +
+
+
+
+ {getSeverityIcon(product.severity)} +

{product.title}

+ + {product.status} + +
+
+

Medusa ID: {product.medusaId}

+ {product.seedId &&

Seed ID: {product.seedId}

} +
+
+
+
+

进度: {product.stats.completionPercentage}%

+

+ {product.stats.totalDisplayCount} / {product.stats.fundingGoal} +

+
+
+
+ + {/* 日期信息 */} +
+
+ 开始:{' '} + {formatDate(product.dates.preorderStartDate)} +
+
+ 结束:{' '} + {formatDate(product.dates.preorderEndDate)} +
+
+ + {/* 问题列表 */} + {product.issues.length > 0 && ( +
+

问题:

+
    + {product.issues.map((issue, idx) => ( +
  • + + {issue} +
  • + ))} +
+
+ )} +
+ ))} +
+ + {result.products.length === 0 && ( +
+

没有找到预购产品

+
+ )} + + )} + + {!result && !error && loading && ( +
+
+

正在检查预购产品...

+
+ )} +
+ ) +} diff --git a/src/components/views/PreorderHealthCheckButton.tsx b/src/components/views/PreorderHealthCheckButton.tsx new file mode 100644 index 0000000..97368a8 --- /dev/null +++ b/src/components/views/PreorderHealthCheckButton.tsx @@ -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(null) + const [error, setError] = useState(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 ( + <> +
+ + {error && ( + + 错误: {error} + + )} +
+ + {isOpen && result && ( + setIsOpen(false)}> +
+

+ 预购产品健康检查 +

+ + {/* 概览统计 */} +
+
+

总数

+

{result.summary.total}

+
+
+

健康

+

{result.summary.healthy}

+
+
+

警告

+

{result.summary.warnings}

+
+
+

错误

+

{result.summary.errors}

+
+
+ +

+ 检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} +

+ + {/* 产品列表 */} +
+ {result.products.map((product) => ( +
+
+
+
+ {getSeverityIcon(product.severity)} +

{product.title}

+ + {product.status} + +
+
+

Medusa ID: {product.medusaId}

+ {product.seedId &&

Seed ID: {product.seedId}

} +
+
+
+

进度: {product.stats.completionPercentage}%

+

+ {product.stats.totalDisplayCount} / {product.stats.fundingGoal} +

+
+
+ +
+
+ 开始:{' '} + {formatDate(product.dates.preorderStartDate)} +
+
+ 结束:{' '} + {formatDate(product.dates.preorderEndDate)} +
+
+ + {product.issues.length > 0 && ( +
+

问题:

+
    + {product.issues.map((issue, idx) => ( +
  • + {issue} +
  • + ))} +
+
+ )} +
+ ))} +
+ + {result.products.length === 0 && ( +

+ 没有找到预购产品 +

+ )} + +
+ +
+
+
+ )} + + ) +} diff --git a/src/lib/medusa.ts b/src/lib/medusa.ts index 54f51d4..d39acfc 100644 --- a/src/lib/medusa.ts +++ b/src/lib/medusa.ts @@ -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, diff --git a/src/payload-types.ts b/src/payload-types.ts index b3bae83..f39923a 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -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 { 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 { seedId?: T; title?: T; thumbnail?: T; + startPrice?: T; lastSyncedAt?: T; preorderType?: T; fundingGoal?: T;