feat: 初始化微信小程序后端骨架
- NestJS + TypeScript + Prisma + PostgreSQL 工程骨架 - 微信登录安全流程:服务端 code2Session 换 openid 后签发 JWT, session_key 缓存于 Redis,不信任前端 openid - 统一响应/异常处理、JWT 全局鉴权(@Public 豁免)、Swagger 文档 - Prisma 全量核心 schema(用户/分类/商品/设计清单/地址/订单/支付/上传/定制任务)+ seed - 业务模块空壳(商品/分类/设计清单/地址/订单/支付/上传/BullMQ 队列) - Docker 多阶段镜像 + 本地/生产 docker-compose - docs:密钥获取指南、COS SDK 移除记录 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProductStatus" AS ENUM ('DRAFT', 'ON_SALE', 'OFF_SHELF');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DesignListStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'PROCESSING', 'DONE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PAID', 'PROCESSING', 'SHIPPED', 'COMPLETED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentProvider" AS ENUM ('WECHAT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'SUCCESS', 'REFUNDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CustomizationTaskStatus" AS ENUM ('PENDING', 'RUNNING', 'SUCCESS', 'FAILED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"openid" TEXT NOT NULL,
|
||||
"unionid" TEXT,
|
||||
"nickname" TEXT,
|
||||
"avatar" TEXT,
|
||||
"phone" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Category" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"parentId" TEXT,
|
||||
"sort" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Product" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"categoryId" TEXT,
|
||||
"price" DECIMAL(10,2) NOT NULL,
|
||||
"images" TEXT[],
|
||||
"description" TEXT,
|
||||
"status" "ProductStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DesignList" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"items" JSONB NOT NULL,
|
||||
"status" "DesignListStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DesignList_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Address" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"province" TEXT NOT NULL,
|
||||
"city" TEXT NOT NULL,
|
||||
"district" TEXT NOT NULL,
|
||||
"detail" TEXT NOT NULL,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Address_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Order" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderNo" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "OrderStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"totalAmount" DECIMAL(10,2) NOT NULL,
|
||||
"addressSnapshot" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"productId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"price" DECIMAL(10,2) NOT NULL,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Payment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"transactionId" TEXT,
|
||||
"provider" "PaymentProvider" NOT NULL DEFAULT 'WECHAT',
|
||||
"status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Upload" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"cosKey" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"mimetype" TEXT,
|
||||
"size" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Upload_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CustomizationTask" (
|
||||
"id" TEXT NOT NULL,
|
||||
"designListId" TEXT,
|
||||
"userId" TEXT,
|
||||
"status" "CustomizationTaskStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"resultUrl" TEXT,
|
||||
"queueJobId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CustomizationTask_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_openid_key" ON "User"("openid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_phone_idx" ON "User"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Category_parentId_idx" ON "Category"("parentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Product_categoryId_idx" ON "Product"("categoryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Product_status_idx" ON "Product"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DesignList_userId_idx" ON "DesignList"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Address_userId_idx" ON "Address"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Order_orderNo_key" ON "Order"("orderNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_userId_idx" ON "Order"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_status_idx" ON "Order"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OrderItem_orderId_idx" ON "OrderItem"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Payment_orderId_key" ON "Payment"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Upload_userId_idx" ON "Upload"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CustomizationTask_designListId_idx" ON "CustomizationTask"("designListId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CustomizationTask_status_idx" ON "CustomizationTask"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DesignList" ADD CONSTRAINT "DesignList_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Address" ADD CONSTRAINT "Address_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Upload" ADD CONSTRAINT "Upload_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CustomizationTask" ADD CONSTRAINT "CustomizationTask_designListId_fkey" FOREIGN KEY ("designListId") REFERENCES "DesignList"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CustomizationTask" ADD CONSTRAINT "CustomizationTask_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,220 @@
|
||||
// 微信小程序后端 — Prisma schema
|
||||
// 数据库:PostgreSQL
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ── 用户 ──────────────────────────────────────────────
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
openid String @unique
|
||||
unionid String?
|
||||
nickname String?
|
||||
avatar String?
|
||||
phone String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
orders Order[]
|
||||
addresses Address[]
|
||||
designLists DesignList[]
|
||||
uploads Upload[]
|
||||
customizations CustomizationTask[]
|
||||
|
||||
@@index([phone])
|
||||
}
|
||||
|
||||
// ── 商品分类 ──────────────────────────────────────────
|
||||
model Category {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
parentId String?
|
||||
sort Int @default(0)
|
||||
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
|
||||
children Category[] @relation("CategoryTree")
|
||||
products Product[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
// ── 商品 ──────────────────────────────────────────────
|
||||
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[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([categoryId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum ProductStatus {
|
||||
DRAFT
|
||||
ON_SALE
|
||||
OFF_SHELF
|
||||
}
|
||||
|
||||
// ── 设计清单 / 定制需求 ───────────────────────────────
|
||||
model DesignList {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
title String
|
||||
items Json
|
||||
status DesignListStatus @default(DRAFT)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
customizations CustomizationTask[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum DesignListStatus {
|
||||
DRAFT
|
||||
SUBMITTED
|
||||
PROCESSING
|
||||
DONE
|
||||
}
|
||||
|
||||
// ── 收货地址 ──────────────────────────────────────────
|
||||
model Address {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
name String
|
||||
phone String
|
||||
province String
|
||||
city String
|
||||
district String
|
||||
detail String
|
||||
isDefault Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// ── 订单 ──────────────────────────────────────────────
|
||||
model Order {
|
||||
id String @id @default(cuid())
|
||||
orderNo String @unique
|
||||
userId String
|
||||
status OrderStatus @default(PENDING)
|
||||
totalAmount Decimal @db.Decimal(10, 2)
|
||||
addressSnapshot Json
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
items OrderItem[]
|
||||
payment Payment?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
PAID
|
||||
PROCESSING
|
||||
SHIPPED
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
productId String?
|
||||
name String
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
quantity Int
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product Product? @relation(fields: [productId], references: [id])
|
||||
|
||||
@@index([orderId])
|
||||
}
|
||||
|
||||
// ── 支付 ──────────────────────────────────────────────
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
transactionId String?
|
||||
provider PaymentProvider @default(WECHAT)
|
||||
status PaymentStatus @default(PENDING)
|
||||
paidAt DateTime?
|
||||
order Order @relation(fields: [orderId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum PaymentProvider {
|
||||
WECHAT
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
SUCCESS
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
// ── 文件上传 ──────────────────────────────────────────
|
||||
model Upload {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
cosKey String
|
||||
url String
|
||||
mimetype String?
|
||||
size Int?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// ── 异步定制任务 ──────────────────────────────────────
|
||||
model CustomizationTask {
|
||||
id String @id @default(cuid())
|
||||
designListId String?
|
||||
userId String?
|
||||
status CustomizationTaskStatus @default(PENDING)
|
||||
resultUrl String?
|
||||
queueJobId String?
|
||||
designList DesignList? @relation(fields: [designListId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([designListId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum CustomizationTaskStatus {
|
||||
PENDING
|
||||
RUNNING
|
||||
SUCCESS
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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 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 完成:分类 + 示例商品已写入');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user