feat(catalog): add product catalog APIs
This commit is contained in:
@@ -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
@@ -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
@@ -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 × 148mm(A6)'], ['材质', '高档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 × 297mm(A4)'], ['开合', '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());
|
||||
|
||||
Reference in New Issue
Block a user