feat: add api http conventions and booking state rules

This commit is contained in:
2026-09-18 17:17:47 +08:00
parent 0d4caaba00
commit 0e8b8e1591
5 changed files with 61 additions and 0 deletions

View File

@@ -132,6 +132,15 @@ export class BookingService {
) { ) {
const booking = await this.prisma.booking.findFirst({ where: { id, brandId } }); const booking = await this.prisma.booking.findFirst({ where: { id, brandId } });
if (!booking) throw new NotFoundException("订单不存在"); if (!booking) throw new NotFoundException("订单不存在");
const allowed: Record<string, string[]> = {
received: ["contacting", "cancelled"],
contacting: ["confirmed", "cancelled"],
confirmed: ["completed", "cancelled"],
completed: [],
cancelled: [],
};
if (status !== booking.status && !allowed[booking.status]?.includes(status))
throw new BadRequestException("不允许的订单状态流转");
return this.prisma.booking.update({ return this.prisma.booking.update({
where: { id }, where: { id },
data: { status, ...(adminNote !== undefined ? { adminNote } : {}), isRead: true }, data: { status, ...(adminNote !== undefined ? { adminNote } : {}), isRead: true },

32
apps/api/src/http.ts Normal file
View File

@@ -0,0 +1,32 @@
import { Catch, HttpException, Injectable } from "@nestjs/common";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import type { FastifyReply, FastifyRequest } from "fastify";
import { randomUUID } from "node:crypto";
@Injectable()
export class RequestIdMiddleware {
use(request: FastifyRequest, reply: FastifyReply, next: () => void): void {
const requestId = request.headers["x-request-id"]?.toString() ?? randomUUID();
reply.header("x-request-id", requestId);
next();
}
}
@Catch()
export class ApiExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const context = host.switchToHttp();
const response = context.getResponse<FastifyReply>();
const request = context.getRequest<FastifyRequest>();
const status = exception instanceof HttpException ? exception.getStatus() : 500;
const detail = exception instanceof HttpException ? exception.getResponse() : "服务器内部错误";
response.status(status).send({
data: null,
error: {
code: `HTTP_${status}`,
message: typeof detail === "string" ? detail : "请求失败",
},
requestId: request.headers["x-request-id"] ?? null,
});
}
}

View File

@@ -23,6 +23,7 @@ import { AdminAuthService } from "./admin-auth.service.js";
import { AdminLoginDto } from "./admin-auth.dto.js"; import { AdminLoginDto } from "./admin-auth.dto.js";
import type { FastifyRequest } from "fastify"; import type { FastifyRequest } from "fastify";
import { readIdentity, signIdentity } from "./auth.js"; import { readIdentity, signIdentity } from "./auth.js";
import { ApiExceptionFilter, RequestIdMiddleware } from "./http.js";
@Controller("health") @Controller("health")
export class HealthController { export class HealthController {
@@ -250,6 +251,8 @@ export async function createApp(): Promise<NestFastifyApplication> {
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
); );
app.useGlobalFilters(new ApiExceptionFilter());
app.use(new RequestIdMiddleware().use.bind(new RequestIdMiddleware()));
app.enableCors(); app.enableCors();
return app; return app;
} }

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: personal_brand
ports:
- "5432:5432"
volumes:
- personal-brand-postgres:/var/lib/postgresql/data
volumes:
personal-brand-postgres:

View File

@@ -14,6 +14,8 @@
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"check": "pnpm format:check && pnpm lint && pnpm typecheck", "check": "pnpm format:check && pnpm lint && pnpm typecheck",
"db:migrate": "pnpm --filter @personal-brand/api db:migrate",
"db:seed": "pnpm --filter @personal-brand/api db:seed",
"clean": "turbo run clean && rimraf node_modules .turbo" "clean": "turbo run clean && rimraf node_modules .turbo"
}, },
"devDependencies": { "devDependencies": {