feat(r3): complete order and payment flow

This commit is contained in:
2026-09-13 13:56:59 +08:00
parent 63c0013076
commit 16e625aefb
20 changed files with 454 additions and 126 deletions
@@ -0,0 +1,19 @@
-- R1 product fields and R3 order idempotency fields.
ALTER TABLE "Product"
ADD COLUMN IF NOT EXISTS "originalPrice" DECIMAL(10,2),
ADD COLUMN IF NOT EXISTS "leadTime" TEXT NOT NULL DEFAULT '7-10 个工作日',
ADD COLUMN IF NOT EXISTS "subtitle" TEXT,
ADD COLUMN IF NOT EXISTS "tone" INTEGER[] NOT NULL DEFAULT ARRAY[]::INTEGER[],
ADD COLUMN IF NOT EXISTS "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN IF NOT EXISTS "specs" JSONB,
ADD COLUMN IF NOT EXISTS "mask" JSONB,
ADD COLUMN IF NOT EXISTS "story" TEXT,
ADD COLUMN IF NOT EXISTS "scene" TEXT,
ADD COLUMN IF NOT EXISTS "iconImg" TEXT,
ADD COLUMN IF NOT EXISTS "sort" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Order"
ADD COLUMN IF NOT EXISTS "requestId" TEXT,
ADD COLUMN IF NOT EXISTS "paidAt" TIMESTAMP(3);
CREATE UNIQUE INDEX IF NOT EXISTS "Order_userId_requestId_key" ON "Order"("userId", "requestId");
@@ -0,0 +1,11 @@
-- 待付款订单统一在服务端以创建后 30 分钟为截止时间,避免只由客户端倒计时控制。
ALTER TYPE "OrderStatus" ADD VALUE IF NOT EXISTS 'PAYMENT_EXPIRED';
ALTER TABLE "Order" ADD COLUMN IF NOT EXISTS "paymentExpiresAt" TIMESTAMP(3);
-- 为已有待付款订单补齐截止时间;历史订单会按创建时间计算并在下一次读取时自动过期。
UPDATE "Order"
SET "paymentExpiresAt" = "createdAt" + INTERVAL '30 minutes'
WHERE "paymentExpiresAt" IS NULL AND "status" = 'PENDING';
CREATE INDEX IF NOT EXISTS "Order_paymentExpiresAt_idx" ON "Order"("paymentExpiresAt");
+18
View File
@@ -56,8 +56,19 @@ model Product {
name String
categoryId String?
price Decimal @db.Decimal(10, 2)
originalPrice Decimal? @db.Decimal(10, 2)
leadTime String @default("7-10 个工作日")
subtitle String?
tone Int[]
tags String[]
specs Json?
mask Json?
story String?
scene String?
iconImg String?
images String[]
description String?
sort Int @default(0)
status ProductStatus @default(DRAFT)
category Category? @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
@@ -127,6 +138,10 @@ model Order {
status OrderStatus @default(PENDING)
totalAmount Decimal @db.Decimal(10, 2)
addressSnapshot Json
requestId String?
paidAt DateTime?
// 待付款订单的固定截止时刻;创建后 30 分钟不可再支付。
paymentExpiresAt DateTime?
// 关联的设计清单(R4 下单后 WCD 派单据此读取 designData
designListId String?
user User @relation(fields: [userId], references: [id])
@@ -139,7 +154,9 @@ model Order {
@@index([userId])
@@index([status])
@@index([paymentExpiresAt])
@@index([designListId])
@@unique([userId, requestId])
}
enum OrderStatus {
@@ -149,6 +166,7 @@ enum OrderStatus {
SHIPPED
COMPLETED
CANCELLED
PAYMENT_EXPIRED
}
model OrderItem {
+20
View File
@@ -0,0 +1,20 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const products = [
{ id: 'notebook-small', name: '微雕笔记本(小)', price: 12, originalPrice: 18, leadTime: '3-5个工作日', iconImg: '/icon/书本.png', images: ['/img/book_small/The1.jpg'], mask: { shape: 'rect', width: 300, height: 420, borderRadius: 8 }, tags: ['伴手礼', '学生', '手账'], sort: 1 },
{ id: 'notebook-large', name: '微雕笔记本(大)', price: 45, originalPrice: 58, leadTime: '3-5个工作日', iconImg: '/icon/书本_大.png', images: ['/img/book_big/The1.jpg', '/img/book_big/The2.jpg'], mask: { shape: 'rect', width: 340, height: 480, borderRadius: 8 }, tags: ['毕业礼', '团建', '企业定制'], sort: 2 },
{ id: 'coaster', name: '铜质杯垫', price: 25, originalPrice: 35, leadTime: '5-7个工作日', iconImg: '/icon/杯子.png', images: ['/img/cup/The1.jpg'], mask: { shape: 'circle', width: 280, height: 280 }, tags: ['乔迁礼', '黄铜', '桌面美学'], sort: 3 },
{ id: 'penbox', name: '竹制笔盒', price: 75, originalPrice: 98, leadTime: '7-10个工作日', iconImg: '/icon/笔盒.png', images: ['/img/penbox/The1.jpg', '/img/penbox/The2.jpg', '/img/penbox/The3.jpg'], mask: { shape: 'rect', width: 320, height: 160, borderRadius: 12 }, tags: ['文房', '书法', '天然材质'], sort: 4 },
{ id: 'booklamp', name: '书本型灯', price: 45, originalPrice: 68, leadTime: '5-7个工作日', iconImg: '/icon/书灯.png', images: ['/img/booklight/The1.jpg', '/img/booklight/The2.jpg'], mask: { shape: 'rect', width: 320, height: 240, borderRadius: 4 }, tags: ['情侣礼', '夜灯', '创意礼物'], sort: 5 },
];
async function main() {
const category = await prisma.category.upsert({ where: { id: 'cat-custom' }, update: { name: '定制商品', sort: 1 }, create: { id: 'cat-custom', name: '定制商品', sort: 1 } });
for (const product of products) {
await prisma.product.upsert({ where: { id: product.id }, update: { ...product, categoryId: category.id, status: 'ON_SALE', tone: [28, 22, 18] }, create: { ...product, categoryId: category.id, status: 'ON_SALE', tone: [28, 22, 18] } });
}
console.log(`Seed 完成:${products.length} 个在售商品已写入`);
}
main().catch((error) => { console.error(error); process.exitCode = 1; }).finally(() => prisma.$disconnect());
+19 -29
View File
@@ -3,36 +3,26 @@ import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// 分类
const apparel = await prisma.category.upsert({
where: { id: 'cat-apparel' },
update: {},
create: { id: 'cat-apparel', name: '服饰', sort: 1 },
const category = await prisma.category.upsert({
where: { id: 'cat-custom' },
update: { name: '定制商品', sort: 1 },
create: { id: 'cat-custom', 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',
},
});
// eslint-disable-next-line no-console
console.log('Seed 完成:分类 + 示例商品已写入');
const products = [
{ id: 'notebook-small', name: '微雕笔记本(小)', price: 12, originalPrice: 18, leadTime: '3-5个工作日', iconImg: '/icon/书本.png', images: ['/img/book_small/The1.jpg'], mask: { shape: 'rect', width: 300, height: 420, borderRadius: 8 }, tags: ['伴手礼', '学生', '手账'], sort: 1 },
{ id: 'notebook-large', name: '微雕笔记本(大)', price: 45, originalPrice: 58, leadTime: '3-5个工作日', iconImg: '/icon/书本_大.png', images: ['/img/book_big/The1.jpg', '/img/book_big/The2.jpg'], mask: { shape: 'rect', width: 340, height: 480, borderRadius: 8 }, tags: ['毕业礼', '团建', '企业定制'], sort: 2 },
{ id: 'coaster', name: '铜质杯垫', price: 25, originalPrice: 35, leadTime: '5-7个工作日', iconImg: '/icon/杯子.png', images: ['/img/cup/The1.jpg'], mask: { shape: 'circle', width: 280, height: 280 }, tags: ['乔迁礼', '黄铜', '桌面美学'], sort: 3 },
{ id: 'penbox', name: '竹制笔盒', price: 75, originalPrice: 98, leadTime: '7-10个工作日', iconImg: '/icon/笔盒.png', images: ['/img/penbox/The1.jpg', '/img/penbox/The2.jpg', '/img/penbox/The3.jpg'], mask: { shape: 'rect', width: 320, height: 160, borderRadius: 12 }, tags: ['文房', '书法', '天然材质'], sort: 4 },
{ id: 'booklamp', name: '书本型灯', price: 45, originalPrice: 68, leadTime: '5-7个工作日', iconImg: '/icon/书灯.png', images: ['/img/booklight/The1.jpg', '/img/booklight/The2.jpg'], mask: { shape: 'rect', width: 320, height: 240, borderRadius: 4 }, tags: ['情侣礼', '夜灯', '创意礼物'], sort: 5 },
];
for (const product of products) {
await prisma.product.upsert({
where: { id: product.id },
update: { ...product, categoryId: category.id, status: 'ON_SALE', tone: [28, 22, 18] },
create: { ...product, categoryId: category.id, status: 'ON_SALE', tone: [28, 22, 18] },
});
}
console.log(`Seed 完成:${products.length} 个在售商品已写入`);
}
main()