diff --git a/package.json b/package.json
index ff35980..d9843eb 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@payloadcms/plugin-cloud-storage": "^3.75.0",
"@payloadcms/richtext-lexical": "3.75.0",
"@payloadcms/storage-s3": "^3.75.0",
+ "@payloadcms/translations": "3.75.0",
"@payloadcms/ui": "3.75.0",
"cross-env": "^7.0.3",
"dotenv": "16.4.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e8ef892..7442b25 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
'@payloadcms/storage-s3':
specifier: ^3.75.0
version: 3.75.0(@types/react@19.2.9)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.75.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
+ '@payloadcms/translations':
+ specifier: 3.75.0
+ version: 3.75.0
'@payloadcms/ui':
specifier: 3.75.0
version: 3.75.0(@types/react@19.2.9)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.75.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
@@ -8139,8 +8142,8 @@ snapshots:
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.7.3)
eslint: 9.39.2
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2)
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2)
eslint-plugin-react: 7.37.5(eslint@9.39.2)
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2)
@@ -8159,7 +8162,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -8170,22 +8173,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.7.3)
eslint: 9.39.2
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -8196,7 +8199,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
index ea1a336..ad824b6 100644
--- a/src/app/(payload)/admin/importMap.js
+++ b/src/app/(payload)/admin/importMap.js
@@ -1,7 +1,9 @@
+import { ForceSyncButton as ForceSyncButton_86f9d5df4f20495427521354d06db618 } from '../../../components/products/ForceSyncButton'
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
export const importMap = {
+ "/components/products/ForceSyncButton#ForceSyncButton": ForceSyncButton_86f9d5df4f20495427521354d06db618,
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
}
diff --git a/src/app/api/sync-medusa/route.ts b/src/app/api/sync-medusa/route.ts
new file mode 100644
index 0000000..3d15f51
--- /dev/null
+++ b/src/app/api/sync-medusa/route.ts
@@ -0,0 +1,312 @@
+import { getPayload } from 'payload'
+import config from '@payload-config'
+import { NextResponse } from 'next/server'
+import {
+ getAllMedusaProducts,
+ transformMedusaProductToPayload,
+ getMedusaProductsPaginated,
+} from '@/lib/medusa'
+
+/**
+ * 同步 Medusa 商品到 Payload CMS
+ * GET /api/sync-medusa
+ * GET /api/sync-medusa?medusaId=prod_xxx (通过 Medusa ID 同步单个商品)
+ * GET /api/sync-medusa?payloadId=123 (通过 Payload ID 同步单个商品)
+ */
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const medusaId = searchParams.get('medusaId')
+ const payloadId = searchParams.get('payloadId')
+ const forceUpdate = searchParams.get('forceUpdate') === 'true'
+
+ const payload = await getPayload({ config })
+
+ // 同步单个商品(通过 Medusa ID)
+ if (medusaId) {
+ const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate)
+ return NextResponse.json(result)
+ }
+
+ // 同步单个商品(通过 Payload ID)
+ if (payloadId) {
+ const result = await syncSingleProductByPayloadId(payload, payloadId, forceUpdate)
+ return NextResponse.json(result)
+ }
+
+ // 同步所有商品
+ const result = await syncAllProducts(payload, forceUpdate)
+ return NextResponse.json(result)
+ } catch (error) {
+ console.error('Sync error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 },
+ )
+ }
+}
+
+/**
+ * 通过 Medusa ID 同步单个商品
+ */
+async function syncSingleProductByMedusaId(payload: any, medusaId: string, forceUpdate: boolean) {
+ try {
+ // 检查商品是否已存在
+ const existing = await payload.find({
+ collection: 'products',
+ where: {
+ medusaId: { equals: medusaId },
+ },
+ limit: 1,
+ })
+
+ const existingProduct = existing.docs[0]
+
+ // 如果存在且不强制更新,跳过
+ if (existingProduct && !forceUpdate) {
+ return {
+ success: true,
+ action: 'skipped',
+ message: `商品 ${medusaId} 已存在`,
+ productId: existingProduct.id,
+ }
+ }
+
+ // 从 Medusa 获取商品数据
+ const medusaProducts = await getAllMedusaProducts()
+ const medusaProduct = medusaProducts.find((p) => p.id === medusaId)
+
+ if (!medusaProduct) {
+ return {
+ success: false,
+ action: 'not_found',
+ message: `Medusa 中未找到商品 ${medusaId}`,
+ }
+ }
+
+ const productData = transformMedusaProductToPayload(medusaProduct)
+
+ if (existingProduct) {
+ // 更新现有商品
+ const updated = await payload.update({
+ collection: 'products',
+ id: existingProduct.id,
+ data: productData,
+ })
+
+ return {
+ success: true,
+ action: 'updated',
+ message: `商品 ${medusaId} 已更新`,
+ productId: updated.id,
+ }
+ } else {
+ // 创建新商品
+ const created = await payload.create({
+ collection: 'products',
+ data: productData,
+ })
+
+ return {
+ success: true,
+ action: 'created',
+ message: `商品 ${medusaId} 已创建`,
+ productId: created.id,
+ }
+ }
+ } catch (error) {
+ console.error(`Error syncing product ${medusaId}:`, error)
+ throw error
+ }
+}
+
+/**
+ * 通过 Payload ID 同步单个商品
+ */
+async function syncSingleProductByPayloadId(payload: any, payloadId: string, forceUpdate: boolean) {
+ try {
+ // 获取 Payload 商品
+ const product = await payload.findByID({
+ collection: 'products',
+ id: payloadId,
+ })
+
+ if (!product) {
+ return {
+ success: false,
+ action: 'not_found',
+ message: `Payload 中未找到商品 ID: ${payloadId}`,
+ }
+ }
+
+ // 如果没有 medusaId,无法同步
+ if (!product.medusaId) {
+ return {
+ success: false,
+ action: 'no_medusa_id',
+ message: `商品 ${product.title} 没有关联的 Medusa ID,无法同步`,
+ }
+ }
+
+ // 使用 medusaId 同步
+ return await syncSingleProductByMedusaId(payload, product.medusaId, forceUpdate)
+ } catch (error) {
+ console.error(`Error syncing product by Payload ID ${payloadId}:`, error)
+ throw 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,
+ 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 {
+ // 检查是否已存在
+ const existing = await payload.find({
+ collection: 'products',
+ where: {
+ medusaId: { equals: medusaProduct.id },
+ },
+ limit: 1,
+ })
+
+ const existingProduct = existing.docs[0]
+
+ // 如果存在且不强制更新,跳过
+ if (existingProduct && !forceUpdate) {
+ results.skipped++
+ results.details.push({
+ medusaId: medusaProduct.id,
+ title: medusaProduct.title,
+ action: 'skipped',
+ })
+ continue
+ }
+
+ const productData = transformMedusaProductToPayload(medusaProduct)
+
+ if (existingProduct) {
+ // 更新
+ await payload.update({
+ collection: 'products',
+ id: existingProduct.id,
+ data: productData,
+ })
+ results.updated++
+ results.details.push({
+ medusaId: medusaProduct.id,
+ title: medusaProduct.title,
+ action: 'updated',
+ })
+ } else {
+ // 创建
+ await payload.create({
+ collection: 'products',
+ data: productData,
+ })
+ results.created++
+ results.details.push({
+ medusaId: medusaProduct.id,
+ title: medusaProduct.title,
+ action: 'created',
+ })
+ }
+ } 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.skipped} 个跳过, ${results.errors} 个错误`,
+ results,
+ }
+ } catch (error) {
+ console.error('Error syncing all products:', error)
+ throw error
+ }
+}
+
+/**
+ * POST /api/sync-medusa
+ * 触发手动同步(需要认证)
+ */
+export async function POST(request: Request) {
+ try {
+ const payload = await getPayload({ config })
+
+ // 可以在这里添加认证检查
+ // const { user } = await payload.auth({ headers: request.headers })
+ // if (!user) {
+ // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ // }
+
+ const body = await request.json()
+ const { medusaId, payloadId, forceUpdate = true } = body
+
+ if (medusaId) {
+ const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate)
+ return NextResponse.json(result)
+ }
+
+ if (payloadId) {
+ const result = await syncSingleProductByPayloadId(payload, payloadId, forceUpdate)
+ return NextResponse.json(result)
+ }
+
+ const result = await syncAllProducts(payload, forceUpdate)
+ return NextResponse.json(result)
+ } catch (error) {
+ console.error('Sync error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 },
+ )
+ }
+}
diff --git a/src/collections/Products.ts b/src/collections/Products.ts
new file mode 100644
index 0000000..8823f05
--- /dev/null
+++ b/src/collections/Products.ts
@@ -0,0 +1,96 @@
+import type { CollectionConfig } from 'payload'
+
+export const Products: CollectionConfig = {
+ slug: 'products',
+ admin: {
+ useAsTitle: 'title',
+ defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
+ description: '管理 Medusa 商品的详细内容和描述',
+ listSearchableFields: ['title', 'medusaId', 'handle'],
+ pagination: {
+ defaultLimit: 20,
+ },
+ components: {
+ edit: {
+ PreviewButton: '/components/products/ForceSyncButton#ForceSyncButton',
+ },
+ },
+ },
+ access: {
+ read: () => true, // 公开可读
+ },
+ fields: [
+ {
+ name: 'medusaId',
+ type: 'text',
+ required: true,
+ unique: true,
+ index: true,
+ admin: {
+ description: 'Medusa 商品 ID',
+ readOnly: true,
+ },
+ },
+ {
+ name: 'title',
+ type: 'text',
+ required: true,
+ admin: {
+ description: '商品标题(从 Medusa 同步)',
+ },
+ },
+ {
+ name: 'handle',
+ type: 'text',
+ admin: {
+ description: '商品 URL slug(从 Medusa 同步)',
+ readOnly: true,
+ },
+ },
+ {
+ name: 'thumbnail',
+ type: 'text',
+ admin: {
+ description: '商品缩略图 URL(从 Medusa 同步)',
+ readOnly: true,
+ },
+ },
+ {
+ name: 'status',
+ type: 'select',
+ required: true,
+ defaultValue: 'draft',
+ options: [
+ {
+ label: '草稿',
+ value: 'draft',
+ },
+ {
+ label: '已发布',
+ value: 'published',
+ },
+ ],
+ admin: {
+ description: '商品详情状态',
+ },
+ },
+ {
+ name: 'relatedProducts',
+ type: 'relationship',
+ relationTo: 'products',
+ hasMany: true,
+ admin: {
+ description: '相关商品',
+ },
+ },
+ {
+ name: 'lastSyncedAt',
+ type: 'date',
+ admin: {
+ description: '最后同步时间',
+ readOnly: true,
+ },
+ },
+ ],
+ timestamps: true,
+}
diff --git a/src/components/products/ForceSyncButton.tsx b/src/components/products/ForceSyncButton.tsx
new file mode 100644
index 0000000..4514668
--- /dev/null
+++ b/src/components/products/ForceSyncButton.tsx
@@ -0,0 +1,72 @@
+'use client'
+import { useState } from 'react'
+import { Button } from '@payloadcms/ui'
+import { useDocumentInfo } from '@payloadcms/ui'
+
+/**
+ * 单个商品强制同步按钮
+ * 用于在商品编辑页面强制更新该商品的信息
+ */
+export function ForceSyncButton() {
+ const { id } = useDocumentInfo()
+ const [loading, setLoading] = useState(false)
+ const [message, setMessage] = useState('')
+
+ const handleForceSync = async () => {
+ if (!id) {
+ setMessage('无法获取商品 ID')
+ return
+ }
+
+ if (!confirm('确定要从 Medusa 强制更新此商品吗?这将覆盖当前的商品信息。')) {
+ return
+ }
+
+ setLoading(true)
+ setMessage('')
+
+ try {
+ const response = await fetch(`/api/sync-medusa?payloadId=${id}&forceUpdate=true`, {
+ method: 'GET',
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ setMessage(data.message || '强制同步成功!')
+ // 刷新页面显示更新后的数据
+ setTimeout(() => window.location.reload(), 1500)
+ } else {
+ setMessage(`同步失败: ${data.error || data.message}`)
+ }
+ } catch (error) {
+ setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+ )
+}
diff --git a/src/components/products/SyncMedusaButton.tsx b/src/components/products/SyncMedusaButton.tsx
new file mode 100644
index 0000000..51f41ca
--- /dev/null
+++ b/src/components/products/SyncMedusaButton.tsx
@@ -0,0 +1,187 @@
+'use client'
+import { useState } from 'react'
+import { Button } from '@payloadcms/ui'
+
+export function SyncMedusaButton() {
+ const [loading, setLoading] = useState(false)
+ const [message, setMessage] = useState('')
+ const [showConfirmInput, setShowConfirmInput] = useState(false)
+ const [confirmText, setConfirmText] = useState('')
+
+ const handleSync = async () => {
+ setLoading(true)
+ setMessage('')
+
+ try {
+ const response = await fetch('/api/sync-medusa?forceUpdate=false', {
+ method: 'GET',
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ setMessage(data.message || '同步成功!')
+ // 刷新页面显示新商品
+ setTimeout(() => window.location.reload(), 1500)
+ } else {
+ setMessage(`同步失败: ${data.error}`)
+ }
+ } catch (error) {
+ setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleForceUpdateAll = () => {
+ setShowConfirmInput(true)
+ setMessage('')
+ setConfirmText('')
+ }
+
+ const handleConfirmForceUpdate = async () => {
+ if (confirmText !== 'FORCE_UPDATE_ALL') {
+ setMessage('确认字符不正确,请输入: FORCE_UPDATE_ALL')
+ return
+ }
+
+ setLoading(true)
+ setMessage('')
+ setShowConfirmInput(false)
+
+ try {
+ const response = await fetch('/api/sync-medusa?forceUpdate=true', {
+ method: 'GET',
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ setMessage(data.message || '强制更新成功!')
+ setTimeout(() => window.location.reload(), 1500)
+ } else {
+ setMessage(`同步失败: ${data.error}`)
+ }
+ } catch (error) {
+ setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
+ } finally {
+ setLoading(false)
+ setConfirmText('')
+ }
+ }
+
+ const handleCancelForceUpdate = () => {
+ setShowConfirmInput(false)
+ setConfirmText('')
+ setMessage('')
+ }
+
+ return (
+
+
Medusa 商品同步
+
+ {showConfirmInput ? (
+
+
+
+ ⚠️ 危险操作
+
+
+ 这将强制更新所有已存在的商品,覆盖所有本地修改。
+
+
+ 请输入{' '}
+
+ FORCE_UPDATE_ALL
+ {' '}
+ 确认:
+
+
+
setConfirmText(e.target.value)}
+ placeholder="输入 FORCE_UPDATE_ALL"
+ style={{
+ width: '100%',
+ padding: '0.5rem',
+ marginBottom: '0.75rem',
+ border: '1px solid var(--theme-elevation-400)',
+ borderRadius: '4px',
+ fontSize: '0.875rem',
+ }}
+ disabled={loading}
+ />
+
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ {message && (
+
+ {message}
+
+ )}
+
+ {!showConfirmInput && (
+
+
+ • 同步新商品: 从 Medusa 导入尚未同步的商品,不会更新已存在的商品。
+
+
+ • 强制更新全部: 更新所有商品,覆盖本地修改(需要确认)。
+
+
+ )}
+
+ )
+}
diff --git a/src/lib/medusa.ts b/src/lib/medusa.ts
new file mode 100644
index 0000000..3867399
--- /dev/null
+++ b/src/lib/medusa.ts
@@ -0,0 +1,196 @@
+/**
+ * Medusa API 客户端
+ */
+
+const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
+const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
+
+interface MedusaProduct {
+ id: string
+ title: string
+ handle: string
+ description: string
+ thumbnail: string
+ status: string
+ created_at: string
+ updated_at: string
+ metadata?: Record
+ images?: Array<{
+ id: string
+ url: string
+ }>
+ variants?: Array<{
+ id: string
+ title: string
+ prices: Array<{
+ amount: number
+ currency_code: string
+ }>
+ }>
+ tags?: Array<{
+ id: string
+ value: string
+ }>
+ collection_id?: string
+ type_id?: string
+}
+
+interface MedusaResponse {
+ products?: T[]
+ product?: T
+ count?: number
+ offset?: number
+ limit?: number
+}
+
+/**
+ * 获取所有 Medusa 商品
+ */
+export async function getAllMedusaProducts(): Promise {
+ try {
+ const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
+ },
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch products: ${response.statusText}`)
+ }
+
+ const data: MedusaResponse = await response.json()
+ return data.products || []
+ } catch (error) {
+ console.error('Error fetching Medusa products:', error)
+ throw error
+ }
+}
+
+/**
+ * 获取单个 Medusa 商品
+ */
+export async function getMedusaProduct(productId: string): Promise {
+ try {
+ const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products/${productId}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
+ },
+ })
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return null
+ }
+ throw new Error(`Failed to fetch product: ${response.statusText}`)
+ }
+
+ const data: MedusaResponse = await response.json()
+ return data.product || null
+ } catch (error) {
+ console.error(`Error fetching Medusa product ${productId}:`, error)
+ throw error
+ }
+}
+
+/**
+ * 分页获取 Medusa 商品
+ */
+export async function getMedusaProductsPaginated(
+ offset: number = 0,
+ limit: number = 100,
+): Promise<{ products: MedusaProduct[]; count: number }> {
+ try {
+ const response = await fetch(
+ `${MEDUSA_BACKEND_URL}/store/products?offset=${offset}&limit=${limit}`,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
+ },
+ },
+ )
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch products: ${response.statusText}`)
+ }
+
+ const data: MedusaResponse = await response.json()
+ return {
+ products: data.products || [],
+ count: data.count || 0,
+ }
+ } catch (error) {
+ console.error('Error fetching Medusa products (paginated):', error)
+ throw error
+ }
+}
+
+/**
+ * 转换 Medusa 商品为 Payload 格式
+ */
+/**
+ * 从 URL 下载并上传图片到 Payload Media 集合
+ */
+export async function uploadImageFromUrl(imageUrl: string, payload: any): Promise {
+ if (!imageUrl) return null
+
+ try {
+ // 下载图片
+ const response = await fetch(imageUrl)
+ if (!response.ok) {
+ console.error(`Failed to download image: ${imageUrl}`)
+ return null
+ }
+
+ const blob = await response.blob()
+ const arrayBuffer = await blob.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+
+ // 从 URL 提取文件名
+ const urlParts = imageUrl.split('/')
+ const filename = urlParts[urlParts.length - 1] || 'product-image.jpg'
+
+ // 上传到 Media 集合
+ const media = await payload.create({
+ collection: 'media',
+ data: {
+ alt: filename,
+ },
+ file: {
+ data: buffer,
+ mimetype: blob.type || 'image/jpeg',
+ name: filename,
+ size: buffer.length,
+ },
+ })
+
+ return media.id
+ } catch (error) {
+ console.error(`Error uploading image from URL ${imageUrl}:`, error)
+ return null
+ }
+}
+
+/**
+ * 转换 Medusa 商品数据到 Payload 格式
+ * 直接保存 Medusa 的图片 URL,不上传到 Media 集合
+ */
+export function transformMedusaProductToPayload(product: MedusaProduct) {
+ // 优先使用 thumbnail,如果没有则使用第一张图片的 URL
+ let thumbnailUrl = product.thumbnail
+
+ if (!thumbnailUrl && product.images && product.images.length > 0) {
+ thumbnailUrl = product.images[0].url
+ }
+
+ return {
+ medusaId: product.id,
+ title: product.title,
+ handle: product.handle,
+ thumbnail: thumbnailUrl || null,
+ status: 'draft',
+ lastSyncedAt: new Date().toISOString(),
+ }
+}
diff --git a/src/migrations/20260208_171142.json b/src/migrations/20260208_171142.json
new file mode 100644
index 0000000..e9e6a93
--- /dev/null
+++ b/src/migrations/20260208_171142.json
@@ -0,0 +1,1254 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.users_sessions": {
+ "name": "users_sessions",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "users_sessions_order_idx": {
+ "name": "users_sessions_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_sessions_parent_id_idx": {
+ "name": "users_sessions_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "users_sessions_parent_id_fk": {
+ "name": "users_sessions_parent_id_fk",
+ "tableFrom": "users_sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "users_updated_at_idx": {
+ "name": "users_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_created_at_idx": {
+ "name": "users_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.media": {
+ "name": "media",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "alt": {
+ "name": "alt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "thumbnail_u_r_l": {
+ "name": "thumbnail_u_r_l",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filesize": {
+ "name": "filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_x": {
+ "name": "focal_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_y": {
+ "name": "focal_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "media_updated_at_idx": {
+ "name": "media_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_created_at_idx": {
+ "name": "media_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_filename_idx": {
+ "name": "media_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.products": {
+ "name": "products",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "medusa_id": {
+ "name": "medusa_id",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "handle": {
+ "name": "handle",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "thumbnail": {
+ "name": "thumbnail",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "enum_products_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "products_medusa_id_idx": {
+ "name": "products_medusa_id_idx",
+ "columns": [
+ {
+ "expression": "medusa_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "products_updated_at_idx": {
+ "name": "products_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "products_created_at_idx": {
+ "name": "products_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.products_rels": {
+ "name": "products_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "products_id": {
+ "name": "products_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "products_rels_order_idx": {
+ "name": "products_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "products_rels_parent_idx": {
+ "name": "products_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "products_rels_path_idx": {
+ "name": "products_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "products_rels_products_id_idx": {
+ "name": "products_rels_products_id_idx",
+ "columns": [
+ {
+ "expression": "products_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "products_rels_parent_fk": {
+ "name": "products_rels_parent_fk",
+ "tableFrom": "products_rels",
+ "tableTo": "products",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "products_rels_products_fk": {
+ "name": "products_rels_products_fk",
+ "tableFrom": "products_rels",
+ "tableTo": "products",
+ "columnsFrom": [
+ "products_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_kv": {
+ "name": "payload_kv",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "payload_kv_key_idx": {
+ "name": "payload_kv_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_locked_documents": {
+ "name": "payload_locked_documents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "global_slug": {
+ "name": "global_slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_global_slug_idx": {
+ "name": "payload_locked_documents_global_slug_idx",
+ "columns": [
+ {
+ "expression": "global_slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_updated_at_idx": {
+ "name": "payload_locked_documents_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_created_at_idx": {
+ "name": "payload_locked_documents_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_locked_documents_rels": {
+ "name": "payload_locked_documents_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "products_id": {
+ "name": "products_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_rels_order_idx": {
+ "name": "payload_locked_documents_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_parent_idx": {
+ "name": "payload_locked_documents_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_path_idx": {
+ "name": "payload_locked_documents_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_users_id_idx": {
+ "name": "payload_locked_documents_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_media_id_idx": {
+ "name": "payload_locked_documents_rels_media_id_idx",
+ "columns": [
+ {
+ "expression": "media_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_products_id_idx": {
+ "name": "payload_locked_documents_rels_products_id_idx",
+ "columns": [
+ {
+ "expression": "products_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_locked_documents_rels_parent_fk": {
+ "name": "payload_locked_documents_rels_parent_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "payload_locked_documents",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_users_fk": {
+ "name": "payload_locked_documents_rels_users_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "users",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_media_fk": {
+ "name": "payload_locked_documents_rels_media_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_products_fk": {
+ "name": "payload_locked_documents_rels_products_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "products",
+ "columnsFrom": [
+ "products_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_preferences": {
+ "name": "payload_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_preferences_key_idx": {
+ "name": "payload_preferences_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_updated_at_idx": {
+ "name": "payload_preferences_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_created_at_idx": {
+ "name": "payload_preferences_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_preferences_rels": {
+ "name": "payload_preferences_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_preferences_rels_order_idx": {
+ "name": "payload_preferences_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_parent_idx": {
+ "name": "payload_preferences_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_path_idx": {
+ "name": "payload_preferences_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_users_id_idx": {
+ "name": "payload_preferences_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_preferences_rels_parent_fk": {
+ "name": "payload_preferences_rels_parent_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "payload_preferences",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_users_fk": {
+ "name": "payload_preferences_rels_users_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "users",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.payload_migrations": {
+ "name": "payload_migrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch": {
+ "name": "batch",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_migrations_updated_at_idx": {
+ "name": "payload_migrations_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_migrations_created_at_idx": {
+ "name": "payload_migrations_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.enum_products_status": {
+ "name": "enum_products_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "published"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "id": "14812669-da54-4cd0-abf6-680b50e27c2f",
+ "prevId": "00000000-0000-0000-0000-000000000000"
+}
\ No newline at end of file
diff --git a/src/migrations/20260208_171142.ts b/src/migrations/20260208_171142.ts
new file mode 100644
index 0000000..45f5654
--- /dev/null
+++ b/src/migrations/20260208_171142.ts
@@ -0,0 +1,169 @@
+import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
+
+export async function up({ db, payload, req }: MigrateUpArgs): Promise {
+ await db.execute(sql`
+ CREATE TYPE "public"."enum_products_status" AS ENUM('draft', 'published');
+ CREATE TABLE "users_sessions" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL,
+ "created_at" timestamp(3) with time zone,
+ "expires_at" timestamp(3) with time zone NOT NULL
+ );
+
+ CREATE TABLE "users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "email" varchar NOT NULL,
+ "reset_password_token" varchar,
+ "reset_password_expiration" timestamp(3) with time zone,
+ "salt" varchar,
+ "hash" varchar,
+ "login_attempts" numeric DEFAULT 0,
+ "lock_until" timestamp(3) with time zone
+ );
+
+ CREATE TABLE "media" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "alt" varchar NOT NULL,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "url" varchar,
+ "thumbnail_u_r_l" varchar,
+ "filename" varchar,
+ "mime_type" varchar,
+ "filesize" numeric,
+ "width" numeric,
+ "height" numeric,
+ "focal_x" numeric,
+ "focal_y" numeric
+ );
+
+ CREATE TABLE "products" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "medusa_id" varchar NOT NULL,
+ "title" varchar NOT NULL,
+ "handle" varchar,
+ "thumbnail" varchar,
+ "status" "enum_products_status" DEFAULT 'draft' NOT NULL,
+ "last_synced_at" timestamp(3) with time zone,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "products_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "products_id" integer
+ );
+
+ CREATE TABLE "payload_kv" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar NOT NULL,
+ "data" jsonb NOT NULL
+ );
+
+ CREATE TABLE "payload_locked_documents" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "global_slug" varchar,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload_locked_documents_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer,
+ "media_id" integer,
+ "products_id" integer
+ );
+
+ CREATE TABLE "payload_preferences" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar,
+ "value" jsonb,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload_preferences_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer
+ );
+
+ CREATE TABLE "payload_migrations" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar,
+ "batch" numeric,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "products_rels" ADD CONSTRAINT "products_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "products_rels" ADD CONSTRAINT "products_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+ CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
+ CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
+ CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
+ CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
+ CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
+ CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
+ CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
+ CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
+ CREATE UNIQUE INDEX "products_medusa_id_idx" ON "products" USING btree ("medusa_id");
+ CREATE INDEX "products_updated_at_idx" ON "products" USING btree ("updated_at");
+ CREATE INDEX "products_created_at_idx" ON "products" USING btree ("created_at");
+ CREATE INDEX "products_rels_order_idx" ON "products_rels" USING btree ("order");
+ CREATE INDEX "products_rels_parent_idx" ON "products_rels" USING btree ("parent_id");
+ CREATE INDEX "products_rels_path_idx" ON "products_rels" USING btree ("path");
+ CREATE INDEX "products_rels_products_id_idx" ON "products_rels" USING btree ("products_id");
+ CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
+ CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
+ CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
+ CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
+ CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
+ CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
+ CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
+ CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id");
+ CREATE INDEX "payload_locked_documents_rels_products_id_idx" ON "payload_locked_documents_rels" USING btree ("products_id");
+ CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
+ CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
+ CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
+ CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
+ CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
+ CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
+ CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
+ CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");`)
+}
+
+export async function down({ db, payload, req }: MigrateDownArgs): Promise {
+ await db.execute(sql`
+ DROP TABLE "users_sessions" CASCADE;
+ DROP TABLE "users" CASCADE;
+ DROP TABLE "media" CASCADE;
+ DROP TABLE "products" CASCADE;
+ DROP TABLE "products_rels" CASCADE;
+ DROP TABLE "payload_kv" CASCADE;
+ DROP TABLE "payload_locked_documents" CASCADE;
+ DROP TABLE "payload_locked_documents_rels" CASCADE;
+ DROP TABLE "payload_preferences" CASCADE;
+ DROP TABLE "payload_preferences_rels" CASCADE;
+ DROP TABLE "payload_migrations" CASCADE;
+ DROP TYPE "public"."enum_products_status";`)
+}
diff --git a/src/migrations/index.ts b/src/migrations/index.ts
new file mode 100644
index 0000000..935e859
--- /dev/null
+++ b/src/migrations/index.ts
@@ -0,0 +1,9 @@
+import * as migration_20260208_171142 from './20260208_171142'
+
+export const migrations = [
+ {
+ up: migration_20260208_171142.up,
+ down: migration_20260208_171142.down,
+ name: '20260208_171142',
+ },
+]
diff --git a/src/payload-types.ts b/src/payload-types.ts
index cee4a2a..b0daac3 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -69,6 +69,7 @@ export interface Config {
collections: {
users: User;
media: Media;
+ products: Product;
'payload-kv': PayloadKv;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -78,6 +79,7 @@ export interface Config {
collectionsSelect: {
users: UsersSelect | UsersSelect;
media: MediaSelect | MediaSelect;
+ products: ProductsSelect | ProductsSelect;
'payload-kv': PayloadKvSelect | PayloadKvSelect;
'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect;
@@ -158,6 +160,45 @@ export interface Media {
focalX?: number | null;
focalY?: number | null;
}
+/**
+ * 管理 Medusa 商品的详细内容和描述
+ *
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "products".
+ */
+export interface Product {
+ id: number;
+ /**
+ * Medusa 商品 ID
+ */
+ medusaId: string;
+ /**
+ * 商品标题(从 Medusa 同步)
+ */
+ title: string;
+ /**
+ * 商品 URL slug(从 Medusa 同步)
+ */
+ handle?: string | null;
+ /**
+ * 商品缩略图 URL(从 Medusa 同步)
+ */
+ thumbnail?: string | null;
+ /**
+ * 商品详情状态
+ */
+ status: 'draft' | 'published';
+ /**
+ * 相关商品
+ */
+ relatedProducts?: (number | Product)[] | null;
+ /**
+ * 最后同步时间
+ */
+ lastSyncedAt?: string | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
@@ -189,6 +230,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'media';
value: number | Media;
+ } | null)
+ | ({
+ relationTo: 'products';
+ value: number | Product;
} | null);
globalSlug?: string | null;
user: {
@@ -272,6 +317,21 @@ export interface MediaSelect {
focalX?: T;
focalY?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "products_select".
+ */
+export interface ProductsSelect {
+ medusaId?: T;
+ title?: T;
+ handle?: T;
+ thumbnail?: T;
+ status?: T;
+ relatedProducts?: T;
+ lastSyncedAt?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".
diff --git a/src/payload.config.ts b/src/payload.config.ts
index f0e8aad..a7e4c3b 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -7,7 +7,14 @@ import sharp from 'sharp'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
+import { Products } from './collections/Products'
import { s3Storage } from '@payloadcms/storage-s3'
+import { en } from '@payloadcms/translations/languages/en'
+import { zh } from '@payloadcms/translations/languages/zh'
+
+// 导入自定义翻译
+import enProducts from './translations/en/products.json'
+import zhProducts from './translations/zh/products.json'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -19,7 +26,20 @@ export default buildConfig({
baseDir: path.resolve(dirname),
},
},
- collections: [Users, Media],
+ i18n: {
+ supportedLanguages: {
+ en: {
+ ...en,
+ ...enProducts,
+ },
+ zh: {
+ ...zh,
+ ...zhProducts,
+ },
+ },
+ fallbackLanguage: 'zh',
+ },
+ collections: [Users, Media, Products],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
@@ -29,6 +49,7 @@ export default buildConfig({
pool: {
connectionString: process.env.DATABASE_URL || '',
},
+ migrationDir: path.resolve(dirname, 'migrations'),
}),
sharp,
plugins: [
diff --git a/src/translations/en/products.json b/src/translations/en/products.json
new file mode 100644
index 0000000..7482d0f
--- /dev/null
+++ b/src/translations/en/products.json
@@ -0,0 +1,12 @@
+{
+ "products:gridView:loading": "Loading...",
+ "products:gridView:empty": "No Products",
+ "products:gridView:emptyDescription": "Click the \"Sync New Products\" button above to import products from Medusa",
+ "products:gridView:viewMode:grid": "Grid View",
+ "products:gridView:viewMode:table": "Table View",
+ "products:gridView:status:published": "Published",
+ "products:gridView:status:draft": "Draft",
+ "products:gridView:pagination:previous": "Previous",
+ "products:gridView:pagination:next": "Next",
+ "products:gridView:pagination:info": "Page {{page}} / {{totalPages}} ({{totalDocs}} total)"
+}
diff --git a/src/translations/zh/products.json b/src/translations/zh/products.json
new file mode 100644
index 0000000..2fe7c32
--- /dev/null
+++ b/src/translations/zh/products.json
@@ -0,0 +1,12 @@
+{
+ "products:gridView:loading": "加载中...",
+ "products:gridView:empty": "暂无商品",
+ "products:gridView:emptyDescription": "点击上方的「同步新商品」按钮从 Medusa 导入商品",
+ "products:gridView:viewMode:grid": "网格视图",
+ "products:gridView:viewMode:table": "表格视图",
+ "products:gridView:status:published": "已发布",
+ "products:gridView:status:draft": "草稿",
+ "products:gridView:pagination:previous": "上一页",
+ "products:gridView:pagination:next": "下一页",
+ "products:gridView:pagination:info": "第 {{page}} / {{totalPages}} 页(共 {{totalDocs}} 个商品)"
+}