feat: add api foundation and booking schema
This commit is contained in:
@@ -4,9 +4,29 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"dev": "tsc -p tsconfig.json --watch",
|
"dev": "tsx watch src/main.ts",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"clean": "rimraf dist"
|
"clean": "rimraf dist",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"db:generate": "prisma generate",
|
||||||
|
"db:migrate": "prisma migrate dev",
|
||||||
|
"db:seed": "tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.11",
|
||||||
|
"@nestjs/core": "^11.0.11",
|
||||||
|
"@nestjs/platform-fastify": "^11.0.11",
|
||||||
|
"@prisma/client": "^6.2.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"fastify": "^5.2.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"prisma": "^6.2.1",
|
||||||
|
"tsx": "^4.19.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
apps/api/prisma/schema.prisma
Normal file
147
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BookingStatus {
|
||||||
|
received
|
||||||
|
contacting
|
||||||
|
confirmed
|
||||||
|
completed
|
||||||
|
cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BookingRequestType {
|
||||||
|
package
|
||||||
|
private_chef
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContactPreference {
|
||||||
|
wechat
|
||||||
|
phone
|
||||||
|
none
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FulfillmentType {
|
||||||
|
pickup
|
||||||
|
delivery
|
||||||
|
}
|
||||||
|
|
||||||
|
model BrandProfile {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
tagline String?
|
||||||
|
description String?
|
||||||
|
avatarUrl String?
|
||||||
|
coverUrl String?
|
||||||
|
hours String?
|
||||||
|
address String?
|
||||||
|
deliveryNote String?
|
||||||
|
contactPhone String?
|
||||||
|
contactWechat String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
services Service[]
|
||||||
|
bookings Booking[]
|
||||||
|
customerUsers CustomerUser[]
|
||||||
|
tags Tag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Service {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
brandId String
|
||||||
|
brand BrandProfile @relation(fields: [brandId], references: [id])
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
coverUrl String?
|
||||||
|
priceText String?
|
||||||
|
priceAmount Int?
|
||||||
|
bookingEnabled Boolean @default(true)
|
||||||
|
isEnabled Boolean @default(true)
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
bookings Booking[]
|
||||||
|
bookingItems BookingItem[]
|
||||||
|
@@index([brandId, isEnabled, sortOrder])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CustomerUser {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
brandId String
|
||||||
|
brand BrandProfile @relation(fields: [brandId], references: [id])
|
||||||
|
openid String
|
||||||
|
lastLoginAt DateTime @default(now())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
bookings Booking[]
|
||||||
|
@@unique([brandId, openid])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Booking {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
bookingNo String @unique
|
||||||
|
brandId String
|
||||||
|
brand BrandProfile @relation(fields: [brandId], references: [id])
|
||||||
|
customerUserId String
|
||||||
|
customerUser CustomerUser @relation(fields: [customerUserId], references: [id])
|
||||||
|
requestType BookingRequestType
|
||||||
|
customerName String
|
||||||
|
phone String?
|
||||||
|
wechatId String?
|
||||||
|
contactPreference ContactPreference
|
||||||
|
fulfillmentType FulfillmentType?
|
||||||
|
serviceId String?
|
||||||
|
service Service? @relation(fields: [serviceId], references: [id], onDelete: SetNull)
|
||||||
|
requestedDate DateTime?
|
||||||
|
requestedTime String?
|
||||||
|
deliveryAddress String?
|
||||||
|
estimatedAmount Int?
|
||||||
|
extraData Json?
|
||||||
|
note String?
|
||||||
|
adminNote String?
|
||||||
|
status BookingStatus @default(received)
|
||||||
|
isRead Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
items BookingItem[]
|
||||||
|
tags BookingTag[]
|
||||||
|
@@index([brandId, status, createdAt])
|
||||||
|
@@index([customerUserId, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model BookingItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
bookingId String
|
||||||
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||||
|
serviceId String?
|
||||||
|
service Service? @relation(fields: [serviceId], references: [id], onDelete: SetNull)
|
||||||
|
serviceNameSnapshot String
|
||||||
|
unitPriceSnapshot Int
|
||||||
|
quantity Int
|
||||||
|
subtotalSnapshot Int
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
brandId String
|
||||||
|
brand BrandProfile @relation(fields: [brandId], references: [id])
|
||||||
|
name String
|
||||||
|
isPreset Boolean @default(false)
|
||||||
|
isEnabled Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
bookings BookingTag[]
|
||||||
|
@@unique([brandId, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model BookingTag {
|
||||||
|
bookingId String
|
||||||
|
tagId String
|
||||||
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
@@id([bookingId, tagId])
|
||||||
|
}
|
||||||
59
apps/api/prisma/seed.ts
Normal file
59
apps/api/prisma/seed.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const brand = await prisma.brandProfile.upsert({
|
||||||
|
where: { id: "00000000-0000-0000-0000-000000000001" },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: "00000000-0000-0000-0000-000000000001",
|
||||||
|
name: "阿成家常菜",
|
||||||
|
tagline: "现做家常套餐,也为小型聚餐定制一桌热饭菜",
|
||||||
|
description: "每天认真做好一顿家常饭。",
|
||||||
|
hours: "周一至周六 10:30-20:00",
|
||||||
|
address: "大学城生活区东门附近",
|
||||||
|
deliveryNote: "目前支持部分学生宿舍楼配送,无法送达时会主动联系。",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.service.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
brandId: brand.id,
|
||||||
|
name: "红烧肉双拼套餐",
|
||||||
|
description: "红烧肉、时蔬、米饭",
|
||||||
|
priceText: "¥28 / 份",
|
||||||
|
priceAmount: 2800,
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brand.id,
|
||||||
|
name: "小炒鸡套餐",
|
||||||
|
description: "现炒鸡块、时蔬、米饭",
|
||||||
|
priceText: "¥25 / 份",
|
||||||
|
priceAmount: 2500,
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brand.id,
|
||||||
|
name: "家庭聚餐私厨",
|
||||||
|
description: "根据人数、地点和口味人工沟通报价",
|
||||||
|
priceText: "面议",
|
||||||
|
bookingEnabled: true,
|
||||||
|
sortOrder: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const name of ["普通套餐", "私厨", "配送", "自取", "熟客", "重点跟进"]) {
|
||||||
|
await prisma.tag.upsert({
|
||||||
|
where: { brandId_name: { brandId: brand.id, name } },
|
||||||
|
update: {},
|
||||||
|
create: { brandId: brand.id, name, isPreset: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().finally(() => prisma.$disconnect());
|
||||||
@@ -1,3 +1,35 @@
|
|||||||
export function healthMessage(): string {
|
import "reflect-metadata";
|
||||||
return "personal-brand-api";
|
import { Controller, Get, Injectable, Module } from "@nestjs/common";
|
||||||
|
import { NestFactory } from "@nestjs/core";
|
||||||
|
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
||||||
|
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient {
|
||||||
|
async onModuleDestroy(): Promise<void> {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller("health")
|
||||||
|
export class HealthController {
|
||||||
|
@Get()
|
||||||
|
check(): { status: string; service: string } {
|
||||||
|
return { status: "ok", service: "personal-brand-api" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module({ controllers: [HealthController], providers: [PrismaService], exports: [PrismaService] })
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
export async function createApp(): Promise<NestFastifyApplication> {
|
||||||
|
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
|
||||||
|
app.enableCors();
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "test") {
|
||||||
|
const app = await createApp();
|
||||||
|
await app.listen({ port: Number(process.env.API_PORT ?? 3001), host: "0.0.0.0" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import eslint from "@eslint/js";
|
|||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ["**/dist/**", "**/.next/**", "**/node_modules/**", "prototype/**"] },
|
{ ignores: ["**/dist/**", "**/.next/**", "**/node_modules/**", "prototype/**", "**/prisma/**"] },
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,3 +2,13 @@ export type BookingStatus = "received" | "contacting" | "confirmed" | "completed
|
|||||||
export type BookingRequestType = "package" | "private_chef";
|
export type BookingRequestType = "package" | "private_chef";
|
||||||
export type ContactPreference = "wechat" | "phone" | "none";
|
export type ContactPreference = "wechat" | "phone" | "none";
|
||||||
export type FulfillmentType = "pickup" | "delivery";
|
export type FulfillmentType = "pickup" | "delivery";
|
||||||
|
export type OrderStatus = BookingStatus;
|
||||||
|
|
||||||
|
export interface PublicService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
priceText: string | null;
|
||||||
|
priceAmount: number | null;
|
||||||
|
bookingEnabled: boolean;
|
||||||
|
}
|
||||||
|
|||||||
1302
pnpm-lock.yaml
generated
1302
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user