feat: add api http conventions and booking state rules
This commit is contained in:
@@ -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
32
apps/api/src/http.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
15
docker-compose.yml
Normal 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:
|
||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user