预购精简

This commit is contained in:
龟男日记\www 2026-02-13 05:10:26 +08:00
parent af1023c3d7
commit 397dcb93ae
15 changed files with 5265 additions and 83 deletions

View File

@ -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,

View File

@ -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',
id,
depth: 2,
})
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) {

View File

@ -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({
collection: 'products',
where: {
status: { equals: status },
},
page,
limit,
depth: 1,
})
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,
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)

View File

@ -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: 'products',
id: payloadId,
})
// 如果未指定 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)
}

View File

@ -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,
}

View File

@ -188,7 +188,7 @@ export const Products: CollectionConfig = {
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
relationTo: ['products', 'preorder-products'],
hasMany: true,
admin: {
description: '相关商品,支持搜索联想',

View File

@ -96,7 +96,7 @@ export const ProductRecommendations: GlobalConfig = {
en: 'Products',
zh: '商品列表',
},
relationTo: 'products',
relationTo: ['products', 'preorder-products'],
hasMany: true,
admin: {
description: {

View File

@ -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

View File

@ -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");`)
}

View File

@ -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;`,
)
}

View File

@ -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',
},
];
]

View File

@ -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;`,
)
}

View File

@ -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;

View File

@ -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 || '',