122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
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;
|
||
}
|