数据优化

This commit is contained in:
龟男日记\www 2026-02-13 04:08:43 +08:00
parent fa30986946
commit af1023c3d7
12 changed files with 4921 additions and 3 deletions

View File

@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { getCache, setCache } from '@/lib/redis'
/**
* GET /api/public/hero-slider
*
*/
export async function GET(req: NextRequest) {
try {
// 生成缓存 key
const cacheKey = 'hero-slider:data'
// 尝试从缓存获取
const cached = await getCache(cacheKey)
if (cached) {
return NextResponse.json({
success: true,
data: cached,
cached: true,
})
}
// 从数据库获取
const payload = await getPayload({ config })
const result = await payload.findGlobal({
slug: 'hero-slider' as any,
depth: 2, // 填充图片关联数据
})
// 获取所有幻灯片
const slides = (result as any).slides || []
const responseData = {
slides,
totalSlides: slides.length,
}
// 缓存结果1 小时)
await setCache(cacheKey, responseData, 3600)
return NextResponse.json({
success: true,
data: responseData,
cached: false,
})
} catch (error) {
console.error('Hero slider API error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch hero slider',
},
{ status: 500 },
)
}
}

View File

@ -0,0 +1,131 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { getCache, setCache } from '@/lib/redis'
/**
* GET /api/public/homepage
* +
*
* API
*/
export async function GET(req: NextRequest) {
try {
// 生成缓存 key
const cacheKey = 'homepage:data'
// 尝试从缓存获取
const cached = await getCache(cacheKey)
if (cached) {
return NextResponse.json({
success: true,
data: cached,
cached: true,
})
}
// 从数据库获取数据
const payload = await getPayload({ config })
// 并行获取所有数据
const [heroSliderResult, productRecommendationsResult] = await Promise.all([
// 获取幻灯片数据
payload
.findGlobal({
slug: 'hero-slider' as any,
depth: 2,
})
.catch((error) => {
console.error('Failed to fetch hero slider:', error)
return null
}),
// 获取商品推荐数据
payload
.findGlobal({
slug: 'product-recommendations' as any,
depth: 3,
})
.catch((error) => {
console.error('Failed to fetch product recommendations:', error)
return null
}),
])
// 处理幻灯片数据
let heroSlider = {
slides: [],
totalSlides: 0,
}
if (heroSliderResult) {
const slides = (heroSliderResult as any).slides || []
heroSlider = {
slides,
totalSlides: slides.length,
}
}
// 处理商品推荐数据
let productRecommendations = {
enabled: false,
lists: [],
totalLists: 0,
}
if (productRecommendationsResult && (productRecommendationsResult as any).enabled) {
// 获取所有列表
const lists = (productRecommendationsResult as any).lists || []
// 处理每个列表
const processedLists = lists.map((list: any) => {
const products = Array.isArray(list.products)
? list.products.filter((product: any) => product && product.status === 'published')
: []
return {
id: list.id,
title: list.title,
subtitle: list.subtitle,
products,
totalProducts: products.length,
}
})
productRecommendations = {
enabled: (productRecommendationsResult as any).enabled,
lists: processedLists,
totalLists: processedLists.length,
}
}
// 组合响应数据
const responseData = {
heroSlider,
productRecommendations,
meta: {
timestamp: new Date().toISOString(),
version: '1.0',
},
}
// 缓存结果1 小时)
await setCache(cacheKey, responseData, 3600)
return NextResponse.json({
success: true,
data: responseData,
cached: false,
})
} catch (error) {
console.error('Homepage API error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch homepage data',
},
{ status: 500 },
)
}
}

View File

@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { getCache, setCache } from '@/lib/redis'
/**
* GET /api/public/product-recommendations
*
*/
export async function GET(req: NextRequest) {
try {
// 生成缓存 key
const cacheKey = 'product-recommendations:data'
// 尝试从缓存获取
const cached = await getCache(cacheKey)
if (cached) {
return NextResponse.json({
success: true,
data: cached,
cached: true,
})
}
// 从数据库获取
const payload = await getPayload({ config })
const result = await payload.findGlobal({
slug: 'product-recommendations' as any,
depth: 3, // 填充商品和图片关联数据
})
// 如果功能未启用,返回空数据
if (!(result as any).enabled) {
const emptyData = {
enabled: false,
lists: [],
}
// 缓存空数据(较短时间)
await setCache(cacheKey, emptyData, 600) // 10 分钟
return NextResponse.json({
success: true,
data: emptyData,
cached: false,
})
}
// 获取所有列表
const lists = (result as any).lists || []
// 处理每个列表
const processedLists = lists.map((list: any) => {
const products = Array.isArray(list.products)
? list.products.filter((product: any) => product && product.status === 'published')
: []
return {
id: list.id,
title: list.title,
subtitle: list.subtitle,
products,
totalProducts: products.length,
}
})
const responseData = {
enabled: (result as any).enabled,
lists: processedLists,
totalLists: processedLists.length,
}
// 缓存结果1 小时)
await setCache(cacheKey, responseData, 3600)
return NextResponse.json({
success: true,
data: responseData,
cached: false,
})
} catch (error) {
console.error('Product recommendations API error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch product recommendations',
},
{ status: 500 },
)
}
}

139
src/globals/HeroSlider.ts Normal file
View File

@ -0,0 +1,139 @@
import type { GlobalConfig } from 'payload'
import { deleteCachePattern } from '@/lib/redis'
export const HeroSlider: GlobalConfig = {
slug: 'hero-slider',
label: {
en: 'Hero Slider',
zh: '首页幻灯片',
},
access: {
read: () => true, // 公开可读
update: ({ req: { user } }) => {
// 只有 admin 和 editor 可以更新
if (!user) return false
return user.roles?.includes('admin') || user.roles?.includes('editor') || false
},
},
admin: {
group: {
en: 'Content',
zh: '内容管理',
},
description: {
en: 'Manage homepage hero slider/banner',
zh: '管理首页轮播图/横幅',
},
},
fields: [
{
name: 'slides',
type: 'array',
label: {
en: 'Slides',
zh: '幻灯片',
},
labels: {
singular: {
en: 'Slide',
zh: '幻灯片',
},
plural: {
en: 'Slides',
zh: '幻灯片列表',
},
},
minRows: 1,
maxRows: 10,
admin: {
description: {
en: 'Add slides to the hero slider (drag to reorder)',
zh: '添加幻灯片(拖动排序)',
},
initCollapsed: true,
},
fields: [
{
name: 'title',
type: 'text',
label: {
en: 'Title',
zh: '标题',
},
required: true,
},
{
name: 'subtitle',
type: 'textarea',
label: {
en: 'Subtitle',
zh: '副标题',
},
maxLength: 200,
admin: {
rows: 2,
},
},
{
name: 'image',
type: 'upload',
label: {
en: 'Image',
zh: '图片',
},
relationTo: 'media',
required: true,
admin: {
description: {
en: 'Recommended: 1920x800px',
zh: '推荐尺寸1920x800px',
},
},
},
{
name: 'imageMobile',
type: 'upload',
label: {
en: 'Mobile Image',
zh: '移动端图片',
},
relationTo: 'media',
admin: {
description: {
en: 'Optional. Recommended: 750x1000px',
zh: '可选。推荐尺寸750x1000px',
},
},
},
{
name: 'link',
type: 'text',
label: {
en: 'Link',
zh: '链接',
},
admin: {
description: {
en: 'Where to go when clicked (e.g., /products)',
zh: '点击后跳转的链接(如:/products',
},
},
},
],
},
],
hooks: {
afterChange: [
async ({ doc }) => {
try {
// 清除 hero-slider 的所有缓存
const deletedCount = await deleteCachePattern('hero-slider:*')
console.log(`[Cache] Cleared ${deletedCount} cache keys for hero-slider after change`)
} catch (error) {
console.error('[Cache] Failed to clear hero-slider cache:', error)
}
return doc
},
],
},
}

View File

@ -0,0 +1,131 @@
import type { GlobalConfig } from 'payload'
import { deleteCachePattern } from '@/lib/redis'
export const ProductRecommendations: GlobalConfig = {
slug: 'product-recommendations',
label: {
en: 'Product Recommendations',
zh: '商品推荐',
},
access: {
read: () => true, // 公开可读
update: ({ req: { user } }) => {
// 只有 admin 和 editor 可以更新
if (!user) return false
return user.roles?.includes('admin') || user.roles?.includes('editor') || false
},
},
admin: {
group: {
en: 'Content',
zh: '内容管理',
},
description: {
en: 'Manage product recommendation lists for homepage and other pages',
zh: '管理首页及其他页面的商品推荐列表',
},
},
fields: [
{
name: 'enabled',
type: 'checkbox',
label: {
en: 'Enable Recommendations',
zh: '启用推荐',
},
defaultValue: true,
admin: {
description: {
en: 'Toggle to show/hide product recommendations',
zh: '切换显示/隐藏商品推荐',
},
},
},
{
name: 'lists',
type: 'array',
label: {
en: 'Recommendation Lists',
zh: '推荐列表',
},
labels: {
singular: {
en: 'List',
zh: '列表',
},
plural: {
en: 'Lists',
zh: '列表集合',
},
},
minRows: 0,
maxRows: 10,
admin: {
description: {
en: 'Create multiple product recommendation lists (e.g., Hot Items, New Arrivals, Limited Offers)',
zh: '创建多个商品推荐列表(如:热门商品、新品上架、限时优惠)',
},
initCollapsed: true,
},
fields: [
{
name: 'title',
type: 'text',
label: {
en: 'List Title',
zh: '列表标题',
},
required: true,
},
{
name: 'subtitle',
type: 'textarea',
label: {
en: 'Subtitle',
zh: '副标题',
},
maxLength: 200,
admin: {
rows: 2,
},
},
{
name: 'products',
type: 'relationship',
label: {
en: 'Products',
zh: '商品列表',
},
relationTo: 'products',
hasMany: true,
admin: {
description: {
en: 'Select and drag to reorder products',
zh: '相关商品,支持搜索联想',
},
components: {
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
},
},
},
],
},
],
hooks: {
afterChange: [
async ({ doc }) => {
try {
// 清除商品推荐和首页的所有缓存
const deletedCount = await deleteCachePattern('product-recommendations:*')
const deletedHomepage = await deleteCachePattern('homepage:*')
console.log(
`[Cache] Cleared ${deletedCount} product-recommendations cache keys and ${deletedHomepage} homepage cache keys`,
)
} catch (error) {
console.error('[Cache] Failed to clear product-recommendations cache:', error)
}
return doc
},
],
},
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,370 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_users_roles" AS ENUM('admin', 'editor', 'user');
CREATE TYPE "public"."enum_announcements_type" AS ENUM('info', 'warning', 'important', 'urgent');
CREATE TYPE "public"."enum_announcements_status" AS ENUM('draft', 'published', 'archived');
CREATE TYPE "public"."enum_articles_status" AS ENUM('draft', 'published');
CREATE TYPE "public"."enum_articles_category" AS ENUM('news', 'tutorial', 'tech', 'review', 'industry', 'other');
CREATE TYPE "public"."enum__articles_v_version_status" AS ENUM('draft', 'published');
CREATE TYPE "public"."enum__articles_v_version_category" AS ENUM('news', 'tutorial', 'tech', 'review', 'industry', 'other');
CREATE TYPE "public"."enum_logs_action" AS ENUM('create', 'update', 'delete', 'sync', 'login', 'logout');
CREATE TYPE "public"."enum_payload_jobs_log_task_slug" AS ENUM('inline', 'schedulePublish');
CREATE TYPE "public"."enum_payload_jobs_log_state" AS ENUM('failed', 'succeeded');
CREATE TYPE "public"."enum_payload_jobs_task_slug" AS ENUM('inline', 'schedulePublish');
CREATE TABLE "users_roles" (
"order" integer NOT NULL,
"parent_id" integer NOT NULL,
"value" "enum_users_roles",
"id" serial PRIMARY KEY NOT NULL
);
CREATE TABLE "announcements" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"type" "enum_announcements_type" DEFAULT 'info' NOT NULL,
"status" "enum_announcements_status" DEFAULT 'draft' NOT NULL,
"priority" numeric DEFAULT 0,
"summary" varchar,
"content" jsonb NOT NULL,
"published_at" timestamp(3) with time zone,
"expires_at" timestamp(3) with time zone,
"show_on_homepage" boolean DEFAULT false,
"author_id" integer,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "articles" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar,
"status" "enum_articles_status" DEFAULT 'draft',
"slug" varchar,
"featured_image_id" integer,
"excerpt" varchar,
"content" jsonb,
"category" "enum_articles_category",
"featured" boolean DEFAULT false,
"author_id" integer,
"published_at" timestamp(3) with time zone,
"meta_title" varchar,
"meta_description" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"_status" "enum_articles_status" DEFAULT 'draft'
);
CREATE TABLE "articles_texts" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer NOT NULL,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"text" varchar
);
CREATE TABLE "articles_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"articles_id" integer,
"products_id" integer
);
CREATE TABLE "_articles_v" (
"id" serial PRIMARY KEY NOT NULL,
"parent_id" integer,
"version_title" varchar,
"version_status" "enum__articles_v_version_status" DEFAULT 'draft',
"version_slug" varchar,
"version_featured_image_id" integer,
"version_excerpt" varchar,
"version_content" jsonb,
"version_category" "enum__articles_v_version_category",
"version_featured" boolean DEFAULT false,
"version_author_id" integer,
"version_published_at" timestamp(3) with time zone,
"version_meta_title" varchar,
"version_meta_description" varchar,
"version_updated_at" timestamp(3) with time zone,
"version_created_at" timestamp(3) with time zone,
"version__status" "enum__articles_v_version_status" DEFAULT 'draft',
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"latest" boolean,
"autosave" boolean
);
CREATE TABLE "_articles_v_texts" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer NOT NULL,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"text" varchar
);
CREATE TABLE "_articles_v_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"articles_id" integer,
"products_id" integer
);
CREATE TABLE "logs" (
"id" serial PRIMARY KEY NOT NULL,
"action" "enum_logs_action" NOT NULL,
"collection" varchar NOT NULL,
"document_id" varchar,
"document_title" varchar,
"user_id" integer NOT NULL,
"changes" jsonb,
"ip" varchar,
"user_agent" 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_jobs_log" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"executed_at" timestamp(3) with time zone NOT NULL,
"completed_at" timestamp(3) with time zone NOT NULL,
"task_slug" "enum_payload_jobs_log_task_slug" NOT NULL,
"task_i_d" varchar NOT NULL,
"input" jsonb,
"output" jsonb,
"state" "enum_payload_jobs_log_state" NOT NULL,
"error" jsonb
);
CREATE TABLE "payload_jobs" (
"id" serial PRIMARY KEY NOT NULL,
"input" jsonb,
"completed_at" timestamp(3) with time zone,
"total_tried" numeric DEFAULT 0,
"has_error" boolean DEFAULT false,
"error" jsonb,
"task_slug" "enum_payload_jobs_task_slug",
"queue" varchar DEFAULT 'default',
"wait_until" timestamp(3) with time zone,
"processing" boolean DEFAULT false,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "admin_settings" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar DEFAULT '管理员设置',
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
CREATE TABLE "logs_manager" (
"id" serial PRIMARY KEY NOT NULL,
"placeholder" varchar,
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
CREATE TABLE "hero_slider_slides" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"subtitle" varchar,
"image_id" integer NOT NULL,
"image_mobile_id" integer,
"link" varchar
);
CREATE TABLE "hero_slider" (
"id" serial PRIMARY KEY NOT NULL,
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
CREATE TABLE "product_recommendations_lists" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"subtitle" varchar
);
CREATE TABLE "product_recommendations" (
"id" serial PRIMARY KEY NOT NULL,
"enabled" boolean DEFAULT true,
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
CREATE TABLE "product_recommendations_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"products_id" integer
);
ALTER TABLE "products" ADD COLUMN "content" jsonb;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "announcements_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "articles_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "logs_id" integer;
ALTER TABLE "users_roles" ADD CONSTRAINT "users_roles_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "announcements" ADD CONSTRAINT "announcements_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "articles" ADD CONSTRAINT "articles_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "articles" ADD CONSTRAINT "articles_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "articles_texts" ADD CONSTRAINT "articles_texts_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "articles_rels" ADD CONSTRAINT "articles_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "articles_rels" ADD CONSTRAINT "articles_rels_articles_fk" FOREIGN KEY ("articles_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "articles_rels" ADD CONSTRAINT "articles_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_articles_v" ADD CONSTRAINT "_articles_v_parent_id_articles_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."articles"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_articles_v" ADD CONSTRAINT "_articles_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_articles_v" ADD CONSTRAINT "_articles_v_version_author_id_users_id_fk" FOREIGN KEY ("version_author_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_articles_v_texts" ADD CONSTRAINT "_articles_v_texts_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_articles_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_articles_v_rels" ADD CONSTRAINT "_articles_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_articles_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_articles_v_rels" ADD CONSTRAINT "_articles_v_rels_articles_fk" FOREIGN KEY ("articles_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_articles_v_rels" ADD CONSTRAINT "_articles_v_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "logs" ADD CONSTRAINT "logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "payload_jobs_log" ADD CONSTRAINT "payload_jobs_log_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "hero_slider_slides" ADD CONSTRAINT "hero_slider_slides_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "hero_slider_slides" ADD CONSTRAINT "hero_slider_slides_image_mobile_id_media_id_fk" FOREIGN KEY ("image_mobile_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "hero_slider_slides" ADD CONSTRAINT "hero_slider_slides_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."hero_slider"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "product_recommendations_lists" ADD CONSTRAINT "product_recommendations_lists_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."product_recommendations"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "product_recommendations_rels" ADD CONSTRAINT "product_recommendations_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."product_recommendations"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "product_recommendations_rels" ADD CONSTRAINT "product_recommendations_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "users_roles_order_idx" ON "users_roles" USING btree ("order");
CREATE INDEX "users_roles_parent_idx" ON "users_roles" USING btree ("parent_id");
CREATE INDEX "announcements_author_idx" ON "announcements" USING btree ("author_id");
CREATE INDEX "announcements_updated_at_idx" ON "announcements" USING btree ("updated_at");
CREATE INDEX "announcements_created_at_idx" ON "announcements" USING btree ("created_at");
CREATE UNIQUE INDEX "articles_slug_idx" ON "articles" USING btree ("slug");
CREATE INDEX "articles_featured_image_idx" ON "articles" USING btree ("featured_image_id");
CREATE INDEX "articles_author_idx" ON "articles" USING btree ("author_id");
CREATE INDEX "articles_updated_at_idx" ON "articles" USING btree ("updated_at");
CREATE INDEX "articles_created_at_idx" ON "articles" USING btree ("created_at");
CREATE INDEX "articles__status_idx" ON "articles" USING btree ("_status");
CREATE INDEX "articles_texts_order_parent" ON "articles_texts" USING btree ("order","parent_id");
CREATE INDEX "articles_rels_order_idx" ON "articles_rels" USING btree ("order");
CREATE INDEX "articles_rels_parent_idx" ON "articles_rels" USING btree ("parent_id");
CREATE INDEX "articles_rels_path_idx" ON "articles_rels" USING btree ("path");
CREATE INDEX "articles_rels_articles_id_idx" ON "articles_rels" USING btree ("articles_id");
CREATE INDEX "articles_rels_products_id_idx" ON "articles_rels" USING btree ("products_id");
CREATE INDEX "_articles_v_parent_idx" ON "_articles_v" USING btree ("parent_id");
CREATE INDEX "_articles_v_version_version_slug_idx" ON "_articles_v" USING btree ("version_slug");
CREATE INDEX "_articles_v_version_version_featured_image_idx" ON "_articles_v" USING btree ("version_featured_image_id");
CREATE INDEX "_articles_v_version_version_author_idx" ON "_articles_v" USING btree ("version_author_id");
CREATE INDEX "_articles_v_version_version_updated_at_idx" ON "_articles_v" USING btree ("version_updated_at");
CREATE INDEX "_articles_v_version_version_created_at_idx" ON "_articles_v" USING btree ("version_created_at");
CREATE INDEX "_articles_v_version_version__status_idx" ON "_articles_v" USING btree ("version__status");
CREATE INDEX "_articles_v_created_at_idx" ON "_articles_v" USING btree ("created_at");
CREATE INDEX "_articles_v_updated_at_idx" ON "_articles_v" USING btree ("updated_at");
CREATE INDEX "_articles_v_latest_idx" ON "_articles_v" USING btree ("latest");
CREATE INDEX "_articles_v_autosave_idx" ON "_articles_v" USING btree ("autosave");
CREATE INDEX "_articles_v_texts_order_parent" ON "_articles_v_texts" USING btree ("order","parent_id");
CREATE INDEX "_articles_v_rels_order_idx" ON "_articles_v_rels" USING btree ("order");
CREATE INDEX "_articles_v_rels_parent_idx" ON "_articles_v_rels" USING btree ("parent_id");
CREATE INDEX "_articles_v_rels_path_idx" ON "_articles_v_rels" USING btree ("path");
CREATE INDEX "_articles_v_rels_articles_id_idx" ON "_articles_v_rels" USING btree ("articles_id");
CREATE INDEX "_articles_v_rels_products_id_idx" ON "_articles_v_rels" USING btree ("products_id");
CREATE INDEX "logs_collection_idx" ON "logs" USING btree ("collection");
CREATE INDEX "logs_document_id_idx" ON "logs" USING btree ("document_id");
CREATE INDEX "logs_user_idx" ON "logs" USING btree ("user_id");
CREATE INDEX "logs_updated_at_idx" ON "logs" USING btree ("updated_at");
CREATE INDEX "logs_created_at_idx" ON "logs" USING btree ("created_at");
CREATE INDEX "payload_jobs_log_order_idx" ON "payload_jobs_log" USING btree ("_order");
CREATE INDEX "payload_jobs_log_parent_id_idx" ON "payload_jobs_log" USING btree ("_parent_id");
CREATE INDEX "payload_jobs_completed_at_idx" ON "payload_jobs" USING btree ("completed_at");
CREATE INDEX "payload_jobs_total_tried_idx" ON "payload_jobs" USING btree ("total_tried");
CREATE INDEX "payload_jobs_has_error_idx" ON "payload_jobs" USING btree ("has_error");
CREATE INDEX "payload_jobs_task_slug_idx" ON "payload_jobs" USING btree ("task_slug");
CREATE INDEX "payload_jobs_queue_idx" ON "payload_jobs" USING btree ("queue");
CREATE INDEX "payload_jobs_wait_until_idx" ON "payload_jobs" USING btree ("wait_until");
CREATE INDEX "payload_jobs_processing_idx" ON "payload_jobs" USING btree ("processing");
CREATE INDEX "payload_jobs_updated_at_idx" ON "payload_jobs" USING btree ("updated_at");
CREATE INDEX "payload_jobs_created_at_idx" ON "payload_jobs" USING btree ("created_at");
CREATE INDEX "hero_slider_slides_order_idx" ON "hero_slider_slides" USING btree ("_order");
CREATE INDEX "hero_slider_slides_parent_id_idx" ON "hero_slider_slides" USING btree ("_parent_id");
CREATE INDEX "hero_slider_slides_image_idx" ON "hero_slider_slides" USING btree ("image_id");
CREATE INDEX "hero_slider_slides_image_mobile_idx" ON "hero_slider_slides" USING btree ("image_mobile_id");
CREATE INDEX "product_recommendations_lists_order_idx" ON "product_recommendations_lists" USING btree ("_order");
CREATE INDEX "product_recommendations_lists_parent_id_idx" ON "product_recommendations_lists" USING btree ("_parent_id");
CREATE INDEX "product_recommendations_rels_order_idx" ON "product_recommendations_rels" USING btree ("order");
CREATE INDEX "product_recommendations_rels_parent_idx" ON "product_recommendations_rels" USING btree ("parent_id");
CREATE INDEX "product_recommendations_rels_path_idx" ON "product_recommendations_rels" USING btree ("path");
CREATE INDEX "product_recommendations_rels_products_id_idx" ON "product_recommendations_rels" USING btree ("products_id");
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_announcements_fk" FOREIGN KEY ("announcements_id") REFERENCES "public"."announcements"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_articles_fk" FOREIGN KEY ("articles_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_logs_fk" FOREIGN KEY ("logs_id") REFERENCES "public"."logs"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "payload_locked_documents_rels_announcements_id_idx" ON "payload_locked_documents_rels" USING btree ("announcements_id");
CREATE INDEX "payload_locked_documents_rels_articles_id_idx" ON "payload_locked_documents_rels" USING btree ("articles_id");
CREATE INDEX "payload_locked_documents_rels_logs_id_idx" ON "payload_locked_documents_rels" USING btree ("logs_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "users_roles" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "announcements" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "articles" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "articles_texts" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "articles_rels" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_articles_v" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_articles_v_texts" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_articles_v_rels" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "logs" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "payload_jobs_log" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "payload_jobs" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "admin_settings" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "logs_manager" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "hero_slider_slides" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "hero_slider" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "product_recommendations_lists" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "product_recommendations" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "product_recommendations_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "users_roles" CASCADE;
DROP TABLE "announcements" CASCADE;
DROP TABLE "articles" CASCADE;
DROP TABLE "articles_texts" CASCADE;
DROP TABLE "articles_rels" CASCADE;
DROP TABLE "_articles_v" CASCADE;
DROP TABLE "_articles_v_texts" CASCADE;
DROP TABLE "_articles_v_rels" CASCADE;
DROP TABLE "logs" CASCADE;
DROP TABLE "payload_jobs_log" CASCADE;
DROP TABLE "payload_jobs" CASCADE;
DROP TABLE "admin_settings" CASCADE;
DROP TABLE "logs_manager" CASCADE;
DROP TABLE "hero_slider_slides" CASCADE;
DROP TABLE "hero_slider" CASCADE;
DROP TABLE "product_recommendations_lists" CASCADE;
DROP TABLE "product_recommendations" CASCADE;
DROP TABLE "product_recommendations_rels" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_announcements_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_articles_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_logs_fk";
DROP INDEX "payload_locked_documents_rels_announcements_id_idx";
DROP INDEX "payload_locked_documents_rels_articles_id_idx";
DROP INDEX "payload_locked_documents_rels_logs_id_idx";
ALTER TABLE "products" DROP COLUMN "content";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "announcements_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "articles_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "logs_id";
DROP TYPE "public"."enum_users_roles";
DROP TYPE "public"."enum_announcements_type";
DROP TYPE "public"."enum_announcements_status";
DROP TYPE "public"."enum_articles_status";
DROP TYPE "public"."enum_articles_category";
DROP TYPE "public"."enum__articles_v_version_status";
DROP TYPE "public"."enum__articles_v_version_category";
DROP TYPE "public"."enum_logs_action";
DROP TYPE "public"."enum_payload_jobs_log_task_slug";
DROP TYPE "public"."enum_payload_jobs_log_state";
DROP TYPE "public"."enum_payload_jobs_task_slug";`)
}

View File

@ -0,0 +1,24 @@
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`UPDATE hero_slider_slides SET link = cta_link WHERE cta_link IS NOT NULL AND cta_enabled = true;`
)
await db.execute(
sql`ALTER TABLE hero_slider_slides
DROP COLUMN IF EXISTS cta_enabled,
DROP COLUMN IF EXISTS cta_text,
DROP COLUMN IF EXISTS cta_style,
DROP COLUMN IF EXISTS cta_link,
DROP COLUMN IF EXISTS cta_new_tab,
DROP COLUMN IF EXISTS text_position,
DROP COLUMN IF EXISTS text_color,
DROP COLUMN IF EXISTS overlay,
DROP COLUMN IF EXISTS status;`
)
}

View File

@ -1,4 +1,7 @@
import * as migration_20260208_171142 from './20260208_171142'
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';
export const migrations = [
{
@ -6,4 +9,17 @@ export const migrations = [
down: migration_20260208_171142.down,
name: '20260208_171142',
},
]
{
up: migration_20260212_193303.up,
down: migration_20260212_193303.down,
name: '20260212_193303'
},
{
up: migration_hero_slider_simplify.up,
name: 'hero_slider_simplify'
},
{
up: migration_product_recommendations_simplify.up,
name: 'product_recommendations_simplify'
},
];

View File

@ -0,0 +1,18 @@
import { MigrateUpArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(
sql`ALTER TABLE product_recommendations_lists
DROP COLUMN IF EXISTS slug,
DROP COLUMN IF EXISTS type,
DROP COLUMN IF EXISTS display_limit,
DROP COLUMN IF EXISTS show_view_all,
DROP COLUMN IF EXISTS view_all_link,
DROP COLUMN IF EXISTS layout,
DROP COLUMN IF EXISTS columns,
DROP COLUMN IF EXISTS show_price,
DROP COLUMN IF EXISTS show_rating,
DROP COLUMN IF EXISTS show_quick_view,
DROP COLUMN IF EXISTS status;`
)
}

View File

@ -100,10 +100,14 @@ export interface Config {
globals: {
'admin-settings': AdminSetting;
'logs-manager': LogsManager;
'hero-slider': HeroSlider;
'product-recommendations': ProductRecommendation;
};
globalsSelect: {
'admin-settings': AdminSettingsSelect<false> | AdminSettingsSelect<true>;
'logs-manager': LogsManagerSelect<false> | LogsManagerSelect<true>;
'hero-slider': HeroSliderSelect<false> | HeroSliderSelect<true>;
'product-recommendations': ProductRecommendationsSelect<false> | ProductRecommendationsSelect<true>;
};
locale: null;
user: User;
@ -834,6 +838,68 @@ export interface LogsManager {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* Manage homepage hero slider/banner
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-slider".
*/
export interface HeroSlider {
id: number;
/**
* Add slides to the hero slider (drag to reorder)
*/
slides?:
| {
title: string;
subtitle?: string | null;
/**
* Recommended: 1920x800px
*/
image: number | Media;
/**
* Optional. Recommended: 750x1000px
*/
imageMobile?: (number | null) | Media;
/**
* Where to go when clicked (e.g., /products)
*/
link?: string | null;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* Manage product recommendation lists for homepage and other pages
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "product-recommendations".
*/
export interface ProductRecommendation {
id: number;
/**
* Toggle to show/hide product recommendations
*/
enabled?: boolean | null;
/**
* Create multiple product recommendation lists (e.g., Hot Items, New Arrivals, Limited Offers)
*/
lists?:
| {
title: string;
subtitle?: string | null;
/**
* Select and drag to reorder products
*/
products?: (number | Product)[] | null;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "admin-settings_select".
@ -854,6 +920,43 @@ export interface LogsManagerSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-slider_select".
*/
export interface HeroSliderSelect<T extends boolean = true> {
slides?:
| T
| {
title?: T;
subtitle?: T;
image?: T;
imageMobile?: T;
link?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "product-recommendations_select".
*/
export interface ProductRecommendationsSelect<T extends boolean = true> {
enabled?: T;
lists?:
| T
| {
title?: T;
subtitle?: T;
products?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".

View File

@ -13,6 +13,8 @@ import { Articles } from './collections/Articles'
import { Logs } from './collections/Logs'
import { AdminSettings } from './globals/AdminSettings'
import { LogsManager } from './globals/LogsManager'
import { HeroSlider } from './globals/HeroSlider'
import { ProductRecommendations } from './globals/ProductRecommendations'
import { s3Storage } from '@payloadcms/storage-s3'
import { en } from '@payloadcms/translations/languages/en'
import { zh } from '@payloadcms/translations/languages/zh'
@ -45,7 +47,7 @@ export default buildConfig({
fallbackLanguage: 'zh',
},
collections: [Users, Media, Products, Announcements, Articles, Logs],
globals: [AdminSettings, LogsManager],
globals: [AdminSettings, LogsManager, HeroSlider, ProductRecommendations],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {