预购精简
This commit is contained in:
parent
af1023c3d7
commit
397dcb93ae
|
|
@ -28,6 +28,7 @@ import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932
|
|||
import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton'
|
||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_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 { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
|
|
@ -64,6 +65,7 @@ export const importMap = {
|
|||
"/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08,
|
||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ import { getCache, setCache } from '@/lib/redis'
|
|||
/**
|
||||
* GET /api/public/products/[id]
|
||||
* 获取单个产品详情(带缓存)
|
||||
* 支持参数:
|
||||
* - collection: 'preorder-products' | 'products' (可选,如不指定则自动搜索)
|
||||
*/
|
||||
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const { id } = params
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const collection = searchParams.get('collection')
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = `products:detail:${id}`
|
||||
const cacheKey = `products:detail:${id}:collection=${collection || 'auto'}`
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
|
|
@ -24,13 +28,50 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
|
|||
})
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
const payload = await getPayload({ config })
|
||||
const result = await payload.findByID({
|
||||
collection: 'products',
|
||||
let result: any = null
|
||||
let foundCollection: string = ''
|
||||
|
||||
if (collection) {
|
||||
// 如果指定了 collection,直接查询
|
||||
try {
|
||||
result = await payload.findByID({
|
||||
collection: collection as any,
|
||||
id,
|
||||
depth: 2,
|
||||
})
|
||||
foundCollection = collection
|
||||
} catch {
|
||||
// 找不到
|
||||
}
|
||||
} else {
|
||||
// 自动搜索各个 collection
|
||||
const collections = ['preorder-products', 'products']
|
||||
|
||||
for (const col of collections) {
|
||||
try {
|
||||
result = await payload.findByID({
|
||||
collection: col as any,
|
||||
id,
|
||||
depth: 2,
|
||||
})
|
||||
foundCollection = col
|
||||
break
|
||||
} catch {
|
||||
// 继续尝试下一个
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// 只有已发布的产品才返回
|
||||
if (result.status !== 'published') {
|
||||
|
|
@ -43,12 +84,18 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
|
|||
)
|
||||
}
|
||||
|
||||
// 添加 collection 信息
|
||||
const resultWithMeta = {
|
||||
...result,
|
||||
_collection: foundCollection,
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, result, 3600)
|
||||
await setCache(cacheKey, resultWithMeta, 3600)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result,
|
||||
data: resultWithMeta,
|
||||
cached: false,
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import { getCache, setCache } from '@/lib/redis'
|
|||
/**
|
||||
* GET /api/public/products
|
||||
* 获取产品列表(带缓存)
|
||||
* 支持参数:
|
||||
* - type: 'preorder' | 'order' | 'all' (默认 'all')
|
||||
* - page: 页码
|
||||
* - limit: 每页数量
|
||||
* - status: 'draft' | 'published' (默认 'published')
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
|
|
@ -13,9 +18,10 @@ export async function GET(req: NextRequest) {
|
|||
const page = parseInt(searchParams.get('page') || '1', 10)
|
||||
const limit = parseInt(searchParams.get('limit') || '10', 10)
|
||||
const status = searchParams.get('status') || 'published'
|
||||
const type = searchParams.get('type') || 'all'
|
||||
|
||||
// 生成缓存 key
|
||||
const cacheKey = `products:list:page=${page}:limit=${limit}:status=${status}`
|
||||
const cacheKey = `products:list:type=${type}:page=${page}:limit=${limit}:status=${status}`
|
||||
|
||||
// 尝试从缓存获取
|
||||
const cached = await getCache(cacheKey)
|
||||
|
|
@ -27,17 +33,74 @@ export async function GET(req: NextRequest) {
|
|||
})
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
const payload = await getPayload({ config })
|
||||
const result = await payload.find({
|
||||
const where = { status: { equals: status } }
|
||||
|
||||
let result
|
||||
|
||||
if (type === 'all') {
|
||||
// 查询所有类型
|
||||
const [preorders, products] = await Promise.all([
|
||||
payload.find({
|
||||
collection: 'preorder-products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
}),
|
||||
payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
status: { equals: status },
|
||||
},
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
}),
|
||||
])
|
||||
|
||||
// 合并结果
|
||||
result = {
|
||||
docs: [
|
||||
...preorders.docs.map((doc) => ({ ...doc, _type: 'preorder-products' })),
|
||||
...products.docs.map((doc) => ({ ...doc, _type: 'products' })),
|
||||
],
|
||||
totalDocs: preorders.totalDocs + products.totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages: Math.ceil(
|
||||
(preorders.totalDocs + products.totalDocs) / limit,
|
||||
),
|
||||
hasNextPage:
|
||||
page < Math.ceil((preorders.totalDocs + products.totalDocs) / limit),
|
||||
hasPrevPage: page > 1,
|
||||
}
|
||||
} else if (type === 'preorder') {
|
||||
// 只查询预售商品
|
||||
result = await payload.find({
|
||||
collection: 'preorder-products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
} else if (type === 'order') {
|
||||
// 只查询现货商品
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
} else {
|
||||
// 旧的 products collection
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
depth: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 缓存结果(1 小时)
|
||||
await setCache(cacheKey, result, 3600)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getAllMedusaProducts,
|
||||
transformMedusaProductToPayload,
|
||||
getMedusaProductsPaginated,
|
||||
getProductCollection,
|
||||
} from '@/lib/medusa'
|
||||
|
||||
/**
|
||||
|
|
@ -18,6 +19,7 @@ export async function GET(request: Request) {
|
|||
const { searchParams } = new URL(request.url)
|
||||
const medusaId = searchParams.get('medusaId')
|
||||
const payloadId = searchParams.get('payloadId')
|
||||
const collection = searchParams.get('collection')
|
||||
const forceUpdate = searchParams.get('forceUpdate') === 'true'
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
|
@ -30,7 +32,12 @@ export async function GET(request: Request) {
|
|||
|
||||
// 同步单个商品(通过 Payload ID)
|
||||
if (payloadId) {
|
||||
const result = await syncSingleProductByPayloadId(payload, payloadId, forceUpdate)
|
||||
const result = await syncSingleProductByPayloadId(
|
||||
payload,
|
||||
payloadId,
|
||||
collection || '',
|
||||
forceUpdate,
|
||||
)
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
|
||||
|
|
@ -54,27 +61,6 @@ export async function GET(request: Request) {
|
|||
*/
|
||||
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)
|
||||
|
|
@ -87,12 +73,69 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
|||
}
|
||||
}
|
||||
|
||||
// 确定应该同步到哪个 collection
|
||||
const targetCollection = getProductCollection(medusaProduct)
|
||||
const otherCollection =
|
||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
||||
|
||||
// 在目标 collection 中检查是否已存在
|
||||
const existingInTarget = await payload.find({
|
||||
collection: targetCollection,
|
||||
where: {
|
||||
medusaId: { equals: medusaId },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
// 在另一个 collection 中检查是否存在(产品类型可能改变)
|
||||
const existingInOther = await payload.find({
|
||||
collection: otherCollection,
|
||||
where: {
|
||||
medusaId: { equals: medusaId },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
|
||||
// 如果在另一个 collection 中存在,需要删除并在正确的 collection 中创建
|
||||
if (existingInOther.docs[0]) {
|
||||
await payload.delete({
|
||||
collection: otherCollection,
|
||||
id: existingInOther.docs[0].id,
|
||||
})
|
||||
|
||||
const created = await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'moved',
|
||||
message: `商品 ${medusaId} 已从 ${otherCollection} 移动到 ${targetCollection}`,
|
||||
productId: created.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
|
||||
const existingProduct = existingInTarget.docs[0]
|
||||
|
||||
// 如果存在且不强制更新,跳过
|
||||
if (existingProduct && !forceUpdate) {
|
||||
return {
|
||||
success: true,
|
||||
action: 'skipped',
|
||||
message: `商品 ${medusaId} 已存在于 ${targetCollection}`,
|
||||
productId: existingProduct.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
|
||||
if (existingProduct) {
|
||||
// 更新现有商品
|
||||
const updated = await payload.update({
|
||||
collection: 'products',
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: productData,
|
||||
})
|
||||
|
|
@ -100,21 +143,23 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
|||
return {
|
||||
success: true,
|
||||
action: 'updated',
|
||||
message: `商品 ${medusaId} 已更新`,
|
||||
message: `商品 ${medusaId} 已更新于 ${targetCollection}`,
|
||||
productId: updated.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
} else {
|
||||
// 创建新商品
|
||||
const created = await payload.create({
|
||||
collection: 'products',
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'created',
|
||||
message: `商品 ${medusaId} 已创建`,
|
||||
message: `商品 ${medusaId} 已创建于 ${targetCollection}`,
|
||||
productId: created.id,
|
||||
collection: targetCollection,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -131,19 +176,45 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
|
|||
/**
|
||||
* 通过 Payload ID 同步单个商品
|
||||
*/
|
||||
async function syncSingleProductByPayloadId(payload: any, payloadId: string, forceUpdate: boolean) {
|
||||
async function syncSingleProductByPayloadId(
|
||||
payload: any,
|
||||
payloadId: string,
|
||||
collection: string,
|
||||
forceUpdate: boolean,
|
||||
) {
|
||||
try {
|
||||
// 获取 Payload 商品
|
||||
const product = await payload.findByID({
|
||||
// 如果未指定 collection,尝试在两个 collections 中查找
|
||||
let product: any = null
|
||||
let foundCollection: string = collection
|
||||
|
||||
if (collection) {
|
||||
product = await payload.findByID({
|
||||
collection,
|
||||
id: payloadId,
|
||||
})
|
||||
} else {
|
||||
// 尝试 preorder-products
|
||||
try {
|
||||
product = await payload.findByID({
|
||||
collection: 'preorder-products',
|
||||
id: payloadId,
|
||||
})
|
||||
foundCollection = 'preorder-products'
|
||||
} catch {
|
||||
// 最后尝试旧的 products
|
||||
product = await payload.findByID({
|
||||
collection: 'products',
|
||||
id: payloadId,
|
||||
})
|
||||
foundCollection = 'products'
|
||||
}
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
success: false,
|
||||
action: 'not_found',
|
||||
message: `Payload 中未找到商品 ID: ${payloadId}`,
|
||||
message: `未找到商品 ID: ${payloadId}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,16 +271,55 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|||
// 处理每个商品
|
||||
for (const medusaProduct of medusaProducts) {
|
||||
try {
|
||||
// 检查是否已存在
|
||||
const existing = await payload.find({
|
||||
collection: 'products',
|
||||
// 确定应该同步到哪个 collection
|
||||
const targetCollection = getProductCollection(medusaProduct)
|
||||
const otherCollection =
|
||||
targetCollection === 'preorder-products' ? 'products' : 'preorder-products'
|
||||
|
||||
// 在目标 collection 中检查是否已存在
|
||||
const existingInTarget = await payload.find({
|
||||
collection: targetCollection,
|
||||
where: {
|
||||
medusaId: { equals: medusaProduct.id },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
const existingProduct = existing.docs[0]
|
||||
// 在另一个 collection 中检查是否存在(产品类型可能改变)
|
||||
const existingInOther = await payload.find({
|
||||
collection: otherCollection,
|
||||
where: {
|
||||
medusaId: { equals: medusaProduct.id },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
|
||||
// 如果在错误的 collection 中,移动它
|
||||
if (existingInOther.docs[0]) {
|
||||
await payload.delete({
|
||||
collection: otherCollection,
|
||||
id: existingInOther.docs[0].id,
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
|
||||
results.updated++
|
||||
results.details.push({
|
||||
medusaId: medusaProduct.id,
|
||||
title: medusaProduct.title,
|
||||
action: 'moved',
|
||||
from: otherCollection,
|
||||
to: targetCollection,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const existingProduct = existingInTarget.docs[0]
|
||||
|
||||
// 如果存在且不强制更新,跳过
|
||||
if (existingProduct && !forceUpdate) {
|
||||
|
|
@ -218,16 +328,15 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|||
medusaId: medusaProduct.id,
|
||||
title: medusaProduct.title,
|
||||
action: 'skipped',
|
||||
collection: targetCollection,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const productData = transformMedusaProductToPayload(medusaProduct)
|
||||
|
||||
if (existingProduct) {
|
||||
// 更新
|
||||
await payload.update({
|
||||
collection: 'products',
|
||||
collection: targetCollection,
|
||||
id: existingProduct.id,
|
||||
data: productData,
|
||||
})
|
||||
|
|
@ -236,11 +345,12 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|||
medusaId: medusaProduct.id,
|
||||
title: medusaProduct.title,
|
||||
action: 'updated',
|
||||
collection: targetCollection,
|
||||
})
|
||||
} else {
|
||||
// 创建
|
||||
await payload.create({
|
||||
collection: 'products',
|
||||
collection: targetCollection,
|
||||
data: productData,
|
||||
})
|
||||
results.created++
|
||||
|
|
@ -248,6 +358,7 @@ async function syncAllProducts(payload: any, forceUpdate: boolean) {
|
|||
medusaId: medusaProduct.id,
|
||||
title: medusaProduct.title,
|
||||
action: 'created',
|
||||
collection: targetCollection,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -295,7 +406,7 @@ export async function POST(request: Request) {
|
|||
// }
|
||||
|
||||
const body = await request.json()
|
||||
const { medusaId, payloadId, forceUpdate = true } = body
|
||||
const { medusaId, payloadId, collection = '', forceUpdate = true } = body
|
||||
|
||||
if (medusaId) {
|
||||
const result = await syncSingleProductByMedusaId(payload, medusaId, forceUpdate)
|
||||
|
|
@ -303,7 +414,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
if (payloadId) {
|
||||
const result = await syncSingleProductByPayloadId(payload, payloadId, forceUpdate)
|
||||
const result = await syncSingleProductByPayloadId(payload, payloadId, collection, forceUpdate)
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
import type { CollectionConfig } from 'payload'
|
||||
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||
import { cacheAfterChange, cacheAfterDelete } from '../hooks/cacheInvalidation'
|
||||
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 PreorderProducts: CollectionConfig = {
|
||||
slug: 'preorder-products',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
||||
description: '管理预售商品的详细内容和描述',
|
||||
listSearchableFields: ['title', 'medusaId', 'handle'],
|
||||
pagination: {
|
||||
defaultLimit: 25,
|
||||
},
|
||||
components: {
|
||||
edit: {
|
||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||
},
|
||||
beforeListTable: [
|
||||
'/components/sync/SyncMedusaButton#SyncMedusaButton',
|
||||
'/components/list/ProductGridStyler',
|
||||
],
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: '基本信息',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'medusaId',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
description: 'Medusa 商品 ID',
|
||||
readOnly: true,
|
||||
width: '60%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'draft',
|
||||
options: [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已发布', value: 'published' },
|
||||
],
|
||||
admin: {
|
||||
description: '商品详情状态',
|
||||
width: '40%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
description: '商品标题(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'handle',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '商品 URL handle(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thumbnail',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '商品缩略图 URL(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lastSyncedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
description: '上次同步时间',
|
||||
readOnly: true,
|
||||
date: {
|
||||
displayFormat: 'yyyy-MM-dd HH:mm:ss',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '商品描述',
|
||||
fields: [
|
||||
{
|
||||
name: 'description',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: [
|
||||
ParagraphFeature(),
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||
BoldFeature(),
|
||||
ItalicFeature(),
|
||||
UnorderedListFeature(),
|
||||
OrderedListFeature(),
|
||||
LinkFeature(),
|
||||
AlignFeature(),
|
||||
BlockquoteFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
InlineCodeFeature(),
|
||||
IndentFeature(),
|
||||
ChecklistFeature(),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'image',
|
||||
imageURL: '/api/media',
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'text',
|
||||
label: '图片说明',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
media: {
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'text',
|
||||
label: '图片说明',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
RelationshipFeature(),
|
||||
],
|
||||
}),
|
||||
admin: {
|
||||
description: '预售商品的详细描述(支持富文本编辑)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '相关商品',
|
||||
fields: [
|
||||
{
|
||||
name: 'relatedProducts',
|
||||
type: 'relationship',
|
||||
relationTo: ['preorder-products', 'products'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: '推荐的相关商品',
|
||||
components: {
|
||||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [cacheAfterChange, logAfterChange],
|
||||
afterDelete: [cacheAfterDelete, logAfterDelete],
|
||||
},
|
||||
timestamps: true,
|
||||
}
|
||||
|
|
@ -188,7 +188,7 @@ export const Products: CollectionConfig = {
|
|||
{
|
||||
name: 'relatedProducts',
|
||||
type: 'relationship',
|
||||
relationTo: 'products',
|
||||
relationTo: ['products', 'preorder-products'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: '相关商品,支持搜索联想',
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const ProductRecommendations: GlobalConfig = {
|
|||
en: 'Products',
|
||||
zh: '商品列表',
|
||||
},
|
||||
relationTo: 'products',
|
||||
relationTo: ['products', 'preorder-products'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ interface MedusaProduct {
|
|||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, any> & {
|
||||
is_preorder?: boolean
|
||||
}
|
||||
images?: Array<{
|
||||
id: string
|
||||
url: string
|
||||
|
|
@ -173,6 +175,21 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断产品是否为预售商品
|
||||
* 检查 metadata.is_preorder
|
||||
*/
|
||||
export function isPreorderProduct(product: MedusaProduct): boolean {
|
||||
return product.metadata?.is_preorder === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品应该同步到的 collection
|
||||
*/
|
||||
export function getProductCollection(product: MedusaProduct): 'preorder-products' | 'products' {
|
||||
return isPreorderProduct(product) ? 'preorder-products' : 'products'
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 Medusa 商品数据到 Payload 格式
|
||||
* 直接保存 Medusa 的图片 URL,不上传到 Media 集合
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
|||
import { MigrateUpArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_preorder_products_status" AS ENUM('draft', 'published');
|
||||
CREATE TABLE "preorder_products" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"medusa_id" varchar NOT NULL,
|
||||
"status" "enum_preorder_products_status" DEFAULT 'draft' NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"handle" varchar,
|
||||
"thumbnail" varchar,
|
||||
"last_synced_at" timestamp(3) with time zone,
|
||||
"description" 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 "preorder_products_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"preorder_products_id" integer,
|
||||
"products_id" integer
|
||||
);
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "preorder_products_id" integer;
|
||||
ALTER TABLE "product_recommendations_rels" ADD COLUMN "preorder_products_id" integer;
|
||||
ALTER TABLE "preorder_products_rels" ADD CONSTRAINT "preorder_products_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."preorder_products"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "preorder_products_rels" ADD CONSTRAINT "preorder_products_rels_preorder_products_fk" FOREIGN KEY ("preorder_products_id") REFERENCES "public"."preorder_products"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "preorder_products_rels" ADD CONSTRAINT "preorder_products_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE UNIQUE INDEX "preorder_products_medusa_id_idx" ON "preorder_products" USING btree ("medusa_id");
|
||||
CREATE INDEX "preorder_products_updated_at_idx" ON "preorder_products" USING btree ("updated_at");
|
||||
CREATE INDEX "preorder_products_created_at_idx" ON "preorder_products" USING btree ("created_at");
|
||||
CREATE INDEX "preorder_products_rels_order_idx" ON "preorder_products_rels" USING btree ("order");
|
||||
CREATE INDEX "preorder_products_rels_parent_idx" ON "preorder_products_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "preorder_products_rels_path_idx" ON "preorder_products_rels" USING btree ("path");
|
||||
CREATE INDEX "preorder_products_rels_preorder_products_id_idx" ON "preorder_products_rels" USING btree ("preorder_products_id");
|
||||
CREATE INDEX "preorder_products_rels_products_id_idx" ON "preorder_products_rels" USING btree ("products_id");
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_preorder_products_fk" FOREIGN KEY ("preorder_products_id") REFERENCES "public"."preorder_products"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "product_recommendations_rels" ADD CONSTRAINT "product_recommendations_rels_preorder_products_fk" FOREIGN KEY ("preorder_products_id") REFERENCES "public"."preorder_products"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "payload_locked_documents_rels_preorder_products_id_idx" ON "payload_locked_documents_rels" USING btree ("preorder_products_id");
|
||||
CREATE INDEX "product_recommendations_rels_preorder_products_id_idx" ON "product_recommendations_rels" USING btree ("preorder_products_id");`)
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import { MigrateUpArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(
|
||||
sql`ALTER TABLE hero_slider_slides ADD COLUMN IF NOT EXISTS link TEXT;`
|
||||
)
|
||||
await db.execute(sql`ALTER TABLE hero_slider_slides ADD COLUMN IF NOT EXISTS link TEXT;`)
|
||||
|
||||
await db.execute(
|
||||
sql`UPDATE hero_slider_slides SET link = cta_link WHERE cta_link IS NOT NULL AND cta_enabled = true;`
|
||||
sql`UPDATE hero_slider_slides SET link = cta_link WHERE cta_link IS NOT NULL AND cta_enabled = true;`,
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
|
|
@ -19,6 +17,6 @@ export async function up({ db }: MigrateUpArgs): Promise<void> {
|
|||
DROP COLUMN IF EXISTS text_position,
|
||||
DROP COLUMN IF EXISTS text_color,
|
||||
DROP COLUMN IF EXISTS overlay,
|
||||
DROP COLUMN IF EXISTS status;`
|
||||
DROP COLUMN IF EXISTS status;`,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as migration_20260208_171142 from './20260208_171142';
|
||||
import * as migration_20260212_193303 from './20260212_193303';
|
||||
import * as migration_hero_slider_simplify from './hero_slider_simplify';
|
||||
import * as migration_product_recommendations_simplify from './product_recommendations_simplify';
|
||||
import * as migration_20260208_171142 from './20260208_171142'
|
||||
import * as migration_20260212_193303 from './20260212_193303'
|
||||
import * as migration_20260212_202303 from './20260212_202303'
|
||||
import * as migration_hero_slider_simplify from './hero_slider_simplify'
|
||||
import * as migration_product_recommendations_simplify from './product_recommendations_simplify'
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -12,14 +13,18 @@ export const migrations = [
|
|||
{
|
||||
up: migration_20260212_193303.up,
|
||||
down: migration_20260212_193303.down,
|
||||
name: '20260212_193303'
|
||||
name: '20260212_193303',
|
||||
},
|
||||
{
|
||||
up: migration_20260212_202303.up,
|
||||
name: '20260212_202303',
|
||||
},
|
||||
{
|
||||
up: migration_hero_slider_simplify.up,
|
||||
name: 'hero_slider_simplify'
|
||||
name: 'hero_slider_simplify',
|
||||
},
|
||||
{
|
||||
up: migration_product_recommendations_simplify.up,
|
||||
name: 'product_recommendations_simplify'
|
||||
name: 'product_recommendations_simplify',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ export async function up({ db }: MigrateUpArgs): Promise<void> {
|
|||
DROP COLUMN IF EXISTS show_price,
|
||||
DROP COLUMN IF EXISTS show_rating,
|
||||
DROP COLUMN IF EXISTS show_quick_view,
|
||||
DROP COLUMN IF EXISTS status;`
|
||||
DROP COLUMN IF EXISTS status;`,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export interface Config {
|
|||
users: User;
|
||||
media: Media;
|
||||
products: Product;
|
||||
'preorder-products': PreorderProduct;
|
||||
announcements: Announcement;
|
||||
articles: Article;
|
||||
logs: Log;
|
||||
|
|
@ -84,6 +85,7 @@ export interface Config {
|
|||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
'preorder-products': PreorderProductsSelect<false> | PreorderProductsSelect<true>;
|
||||
announcements: AnnouncementsSelect<false> | AnnouncementsSelect<true>;
|
||||
articles: ArticlesSelect<false> | ArticlesSelect<true>;
|
||||
logs: LogsSelect<false> | LogsSelect<true>;
|
||||
|
|
@ -235,7 +237,86 @@ export interface Product {
|
|||
/**
|
||||
* 相关商品,支持搜索联想
|
||||
*/
|
||||
relatedProducts?: (number | Product)[] | null;
|
||||
relatedProducts?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'products';
|
||||
value: number | Product;
|
||||
}
|
||||
| {
|
||||
relationTo: 'preorder-products';
|
||||
value: number | PreorderProduct;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* 管理预售商品的详细内容和描述
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "preorder-products".
|
||||
*/
|
||||
export interface PreorderProduct {
|
||||
id: number;
|
||||
/**
|
||||
* Medusa 商品 ID
|
||||
*/
|
||||
medusaId: string;
|
||||
/**
|
||||
* 商品详情状态
|
||||
*/
|
||||
status: 'draft' | 'published';
|
||||
/**
|
||||
* 商品标题(从 Medusa 同步)
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 商品 URL handle(从 Medusa 同步)
|
||||
*/
|
||||
handle?: string | null;
|
||||
/**
|
||||
* 商品缩略图 URL(从 Medusa 同步)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* 上次同步时间
|
||||
*/
|
||||
lastSyncedAt?: string | null;
|
||||
/**
|
||||
* 预售商品的详细描述(支持富文本编辑)
|
||||
*/
|
||||
description?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
/**
|
||||
* 推荐的相关商品
|
||||
*/
|
||||
relatedProducts?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'preorder-products';
|
||||
value: number | PreorderProduct;
|
||||
}
|
||||
| {
|
||||
relationTo: 'products';
|
||||
value: number | Product;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
|
@ -573,6 +654,10 @@ export interface PayloadLockedDocument {
|
|||
relationTo: 'products';
|
||||
value: number | Product;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'preorder-products';
|
||||
value: number | PreorderProduct;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'announcements';
|
||||
value: number | Announcement;
|
||||
|
|
@ -684,6 +769,22 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "preorder-products_select".
|
||||
*/
|
||||
export interface PreorderProductsSelect<T extends boolean = true> {
|
||||
medusaId?: T;
|
||||
status?: T;
|
||||
title?: T;
|
||||
handle?: T;
|
||||
thumbnail?: T;
|
||||
lastSyncedAt?: T;
|
||||
description?: T;
|
||||
relatedProducts?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "announcements_select".
|
||||
|
|
@ -893,7 +994,18 @@ export interface ProductRecommendation {
|
|||
/**
|
||||
* Select and drag to reorder products
|
||||
*/
|
||||
products?: (number | Product)[] | null;
|
||||
products?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'products';
|
||||
value: number | Product;
|
||||
}
|
||||
| {
|
||||
relationTo: 'preorder-products';
|
||||
value: number | PreorderProduct;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import sharp from 'sharp'
|
|||
import { Users } from './collections/Users'
|
||||
import { Media } from './collections/Media'
|
||||
import { Products } from './collections/Products'
|
||||
import { PreorderProducts } from './collections/PreorderProducts'
|
||||
import { Announcements } from './collections/Announcements'
|
||||
import { Articles } from './collections/Articles'
|
||||
import { Logs } from './collections/Logs'
|
||||
|
|
@ -46,7 +47,15 @@ export default buildConfig({
|
|||
},
|
||||
fallbackLanguage: 'zh',
|
||||
},
|
||||
collections: [Users, Media, Products, Announcements, Articles, Logs],
|
||||
collections: [
|
||||
Users,
|
||||
Media,
|
||||
Products,
|
||||
PreorderProducts,
|
||||
Announcements,
|
||||
Articles,
|
||||
Logs,
|
||||
],
|
||||
globals: [AdminSettings, LogsManager, HeroSlider, ProductRecommendations],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
|
|
|
|||
Loading…
Reference in New Issue