import Fastify from "fastify"; import fastifyJwt from "@fastify/jwt"; import { ZodError } from "zod"; import { env } from "./config/env"; import { pingDatabase } from "./db/pool"; import { authRoutes } from "./modules/auth/auth.controller"; import { managementGuard } from "./modules/auth/auth.guard"; import { catalogRoutes } from "./modules/catalog/catalog.controller"; import { employeeRoutes } from "./modules/employees/employee.controller"; import { permissionRoutes } from "./modules/permissions/permission.controller"; import { HttpError } from "./shared/http-error"; import { ok } from "./shared/response"; // createApp 只创建并配置 Fastify 实例,不直接监听端口。 // 这样 server.ts 可以负责启动服务,测试代码也可以单独创建 app 实例。 export function createApp() { const app = Fastify({ logger: true, }); // 前端 Axios 默认会给 DELETE 带上 application/json;空 body 不应被当作服务端异常。 app.addContentTypeParser( "application/json", { parseAs: "string" }, (_request, body, done) => { if (body === "") { done(null, undefined); return; } try { done(null, JSON.parse(body as string)); } catch (error) { done(error as Error, undefined); } }, ); // 注册 JWT 能力。登录接口负责签发 token,受保护接口通过 authGuard 校验 token。 app.register(fastifyJwt, { secret: env.JWT_SECRET, sign: { expiresIn: env.JWT_EXPIRES_IN, }, }); // 健康检查接口,供负载均衡器和监控系统使用。 app.get("/health", async () => { await pingDatabase(); return ok({ status: "ok", database: "up", now: new Date().toISOString(), }); }); // 登录接口不需要 token;/auth/me 在 authRoutes 内部单独加了 authGuard。 app.register(authRoutes, { prefix: "/api" }); app.register(permissionRoutes, { prefix: "/api" }); // 业务管理接口统一要求后台权限:超级管理员或拥有 admin 角色的员工。 app.register( async (protectedApp) => { protectedApp.addHook("preHandler", managementGuard); protectedApp.register(catalogRoutes); protectedApp.register(employeeRoutes); }, { prefix: "/api" }, ); // 全局错误处理器,捕获所有未处理的异常,并根据错误类型返回合适的 HTTP 状态码和错误信息。 app.setErrorHandler((error, request, reply) => { if (error instanceof ZodError) { return reply.code(400).send({ success: false, error: { code: "VALIDATION_ERROR", message: "请求参数不合法", details: error.issues, }, }); } if (error instanceof HttpError) { return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, details: error.details, }, }); } const mysqlCode = (error as { code?: string }).code; // 数据库唯一索引冲突也转成统一的业务错误响应,避免把 MySQL 原始错误直接暴露给调用方。 if (mysqlCode === "ER_DUP_ENTRY") { return reply.code(409).send({ success: false, error: { code: "CONFLICT", message: "数据已存在,请检查唯一字段", }, }); } request.log.error({ error }, "未处理的服务异常"); return reply.code(500).send({ success: false, error: { code: "INTERNAL_SERVER_ERROR", message: "服务器内部错误", }, }); }); return app; }