const crypto = require("node:crypto"); const fs = require("node:fs/promises"); require("dotenv/config"); const fastify = require("fastify"); const cors = require("@fastify/cors"); const cookie = require("@fastify/cookie"); const multipart = require("@fastify/multipart"); const { getConfig } = require("./config"); const authPlugin = require("./plugins/auth"); const prismaPlugin = require("./plugins/prisma"); const authRoutes = require("./routes/auth"); const meRoutes = require("./routes/me"); const reimbursementRoutes = require("./routes/reimbursements"); const invoicePoolRoutes = require("./routes/invoicePool"); const workflowRoutes = require("./routes/workflow"); const delegationRoutes = require("./routes/delegations"); const auditRoutes = require("./routes/audit"); const { startDelegationRevokeJob } = require("./jobs/delegationRevokeJob"); const { startOcrJob } = require("./jobs/ocrJob"); const { AppError } = require("./utils/AppError"); async function buildApp() { const config = getConfig(); const app = fastify({ logger: true, trustProxy: true, genReqId: () => crypto.randomUUID(), }); app.decorate("config", config); await fs.mkdir(config.storageDir, { recursive: true }); await app.register(cookie); await app.register(multipart, { attachFieldsToBody: false, limits: { fileSize: config.maxUploadSizeBytes, files: 1, }, }); const allowedOrigins = config.appOrigin ? config.appOrigin .split(",") .map((s) => s.trim()) .filter(Boolean) : null; await app.register(cors, { credentials: true, origin: (origin, cb) => { if (!origin) return cb(null, true); if (!allowedOrigins) return cb(null, true); if (allowedOrigins.includes(origin)) return cb(null, true); return cb(new Error("CORS origin not allowed"), false); }, }); app.addHook("onRequest", async (request, reply) => { reply.header("x-request-id", request.id); }); app.setNotFoundHandler(async () => { throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404, }); }); app.setErrorHandler((error, request, reply) => { if (error.code === "FST_REQ_FILE_TOO_LARGE") { return reply.status(400).send({ requestId: request.id, error: { code: "VALID_001", message: "参数校验失败", details: { file: "file_too_large" }, }, }); } if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? {}, }, }); } app.log.error({ err: error, requestId: request.id }, "Unhandled error"); return reply.status(500).send({ requestId: request.id, error: { code: "SYS_001", message: "服务器内部错误", details: {}, }, }); }); await app.register(prismaPlugin); await app.register(authPlugin); app.get("/health", async (request, reply) => { return reply.send({ requestId: request.id, data: { ok: true } }); }); await app.register(authRoutes, { prefix: "/api/auth" }); await app.register(meRoutes, { prefix: "/api" }); await app.register(reimbursementRoutes, { prefix: "/api" }); await app.register(invoicePoolRoutes, { prefix: "/api" }); await app.register(workflowRoutes, { prefix: "/api" }); await app.register(delegationRoutes, { prefix: "/api" }); await app.register(auditRoutes, { prefix: "/api" }); startDelegationRevokeJob(app); startOcrJob(app); return app; } module.exports = { buildApp };