Files
access-manage/src/app.ts
T
2026-05-26 12:30:38 +08:00

122 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}