feat(catalog): add product catalog APIs

This commit is contained in:
lai_hong
2026-09-13 11:17:53 +08:00
parent 63c0013076
commit 59e398fcf9
15 changed files with 655 additions and 68 deletions
@@ -0,0 +1,22 @@
-- R1 商品目录:补齐小程序展示与下单所需的稳定商品字段。
ALTER TABLE "Product"
ADD COLUMN "originalPrice" DECIMAL(10,2),
ADD COLUMN "leadTime" TEXT NOT NULL DEFAULT '3-5个工作日',
ADD COLUMN "subtitle" TEXT,
ADD COLUMN "iconImg" TEXT,
ADD COLUMN "story" TEXT,
ADD COLUMN "scene" TEXT,
ADD COLUMN "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "specs" JSONB NOT NULL DEFAULT '[]',
ADD COLUMN "tone" INTEGER[] NOT NULL DEFAULT ARRAY[]::INTEGER[],
ADD COLUMN "mask" JSONB,
ADD COLUMN "sort" INTEGER NOT NULL DEFAULT 0;
-- 兼容已有开发数据;R1 seed 会为每个正式商品写入真实 mask。
UPDATE "Product"
SET "mask" = '{"shape":"rect","width":300,"height":300}'::jsonb
WHERE "mask" IS NULL;
ALTER TABLE "Product" ALTER COLUMN "mask" SET NOT NULL;
CREATE INDEX "Product_status_sort_idx" ON "Product"("status", "sort");
+20 -9
View File
@@ -52,15 +52,26 @@ model Category {
// ── 商品 ──────────────────────────────────────────────
model Product {
id String @id @default(cuid())
name String
categoryId String?
price Decimal @db.Decimal(10, 2)
images String[]
description String?
status ProductStatus @default(DRAFT)
category Category? @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
id String @id @default(cuid())
name String
categoryId String?
price Decimal @db.Decimal(10, 2)
originalPrice Decimal? @db.Decimal(10, 2)
leadTime String @default("3-5个工作日")
subtitle String?
images String[]
iconImg String?
description String?
story String?
scene String?
tags String[] @default([])
specs Json @default("[]")
tone Int[] @default([])
mask Json
sort Int @default(0)
status ProductStatus @default(DRAFT)
category Category? @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+32 -39
View File
@@ -1,46 +1,39 @@
import { PrismaClient } from '@prisma/client';
import { Prisma, PrismaClient, ProductStatus } from '@prisma/client';
const prisma = new PrismaClient();
const oss = 'https://wordcloudwechat.oss-cn-hangzhou.aliyuncs.com';
const catalog = [
{ id: 'notebook-small', categoryId: 'cat-notebook-small', categoryName: '微雕笔记本(小)', sort: 1, name: '微雕笔记本(小)', price: 12, originalPrice: 18, leadTime: '3-5个工作日', subtitle: '掌心间的专属印记 · 激光微雕', iconImg: '/icon/书本.png', description: '便携 A6 笔记本,PU 封面可激光微雕名字或图案。', story: '让纸笔的温度留下专属印记。', scene: '课堂笔记、旅行手账、日常备忘。', tags: ['伴手礼', '学生', '手账'], specs: [['尺寸', '105 × 148mmA6'], ['材质', '高档PU皮革']], tone: [28, 22, 18], mask: { shape: 'rect', width: 300, height: 420, borderRadius: 8 }, images: [`${oss}/img/book_small/The1.jpg`] },
{ id: 'notebook-large', categoryId: 'cat-notebook-large', categoryName: '微雕笔记本(大)', sort: 2, name: '微雕笔记本(大)', price: 45, originalPrice: 58, leadTime: '3-5个工作日', subtitle: '大篇幅定制首选 · 180°平摊', iconImg: '/icon/书本_大.png', description: 'A4 大开本,适合呈现班级名单、团队口号或公司 Logo。', story: '为一群人的记忆留下郑重的书写空间。', scene: '毕业纪念、企业礼品、团队伴手礼。', tags: ['毕业礼', '团建', '企业定制'], specs: [['尺寸', '210 × 297mmA4'], ['开合', '180°平摊']], tone: [35, 28, 22], mask: { shape: 'rect', width: 340, height: 480, borderRadius: 8 }, images: [`${oss}/img/book_big/The1.jpg`, `${oss}/img/book_big/The2.jpg`] },
{ id: 'coaster', categoryId: 'cat-coaster', categoryName: '铜质杯垫', sort: 3, name: '铜质杯垫', price: 25, originalPrice: 35, leadTime: '5-7个工作日', subtitle: '黄铜质感 · 拉丝哑光', iconImg: '/icon/杯子.png', description: '拉丝黄铜与软木防滑垫组合,适合刻印名字、日期或祝福。', story: '让日常器物留下温柔的氧化与记忆。', scene: '乔迁礼、新婚回礼、办公桌布置。', tags: ['乔迁礼', '新婚贺礼', '桌面美学'], specs: [['直径', '100mm'], ['材质', '黄铜 + 软木']], tone: [42, 30, 18], mask: { shape: 'circle', width: 280, height: 280 }, images: [`${oss}/img/cup/The1.jpg`] },
{ id: 'penbox', categoryId: 'cat-penbox', categoryName: '竹制笔盒', sort: 4, name: '竹制笔盒', price: 75, originalPrice: 98, leadTime: '7-10个工作日', subtitle: '天然楠竹 · 磁吸开合', iconImg: '/icon/笔盒.png', description: '天然楠竹打磨抛光,磁吸盒盖与分层隔板兼具实用和雅致。', story: '每一道竹纹都不重复,也值得留下不同的名字。', scene: '书房案头、教师节礼物、书法爱好者。', tags: ['文房', '书法', '教师节'], specs: [['尺寸', '200 × 65 × 30mm'], ['材质', '天然楠竹']], tone: [30, 24, 16], mask: { shape: 'rect', width: 320, height: 160, borderRadius: 12 }, images: [`${oss}/img/penbox/The1.jpg`, `${oss}/img/penbox/The2.jpg`] },
{ id: 'booklamp', categoryId: 'cat-booklamp', categoryName: '书本型灯', sort: 5, name: '书本型灯', price: 45, originalPrice: 68, leadTime: '5-7个工作日', subtitle: '开合即亮 · LED暖光', iconImg: '/icon/书灯.png', description: '打开即亮的暖光书灯,封面可定制文字、图案或照片。', story: '打开一本书,也打开一束为你而亮的光。', scene: '情侣礼物、毕业纪念、床头夜灯。', tags: ['情侣礼', '毕业纪念', '夜灯'], specs: [['光源', 'LED 暖白光(3000K'], ['续航', '6-8小时']], tone: [25, 20, 15], mask: { shape: 'rect', width: 320, height: 240, borderRadius: 4 }, images: [`${oss}/img/booklight/The1.jpg`, `${oss}/img/booklight/The2.jpg`] },
];
async function main() {
// 分类
const apparel = await prisma.category.upsert({
where: { id: 'cat-apparel' },
update: {},
create: { id: 'cat-apparel', name: '服饰', sort: 1 },
});
const tshirt = await prisma.category.upsert({
where: { id: 'cat-tshirt' },
update: {},
create: { id: 'cat-tshirt', name: 'T恤', parentId: apparel.id, sort: 1 },
});
// 示例商品
await prisma.product.upsert({
where: { id: 'prod-demo-tshirt' },
update: {},
create: {
id: 'prod-demo-tshirt',
name: '示例定制 T恤',
categoryId: tshirt.id,
price: 99.0,
images: [],
description: '用于本地开发联调的示例商品,可在此设计清单中上传自定义图案。',
status: 'ON_SALE',
},
});
for (const product of catalog) {
await prisma.category.upsert({
where: { id: product.categoryId },
update: { name: product.categoryName, sort: product.sort },
create: { id: product.categoryId, name: product.categoryName, sort: product.sort },
});
const { categoryName, ...data } = product;
if (!categoryName) throw new Error('分类名称不能为空');
const productData: Prisma.ProductUncheckedCreateInput = {
...data,
specs: data.specs as Prisma.InputJsonValue,
mask: data.mask as Prisma.InputJsonValue,
status: ProductStatus.ON_SALE,
};
await prisma.product.upsert({ where: { id: product.id }, update: productData, create: productData });
}
// eslint-disable-next-line no-console
console.log('Seed 完成:分类 + 示例商品已写入');
console.log('Seed 完成:5 个在售商品与稳定分类已写入');
}
main()
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
}).finally(async () => prisma.$disconnect());