feat: 完成P6-P8后端能力与P9路由重构(V1-V2,D1,D3,B1-B2,B4,I1-I4,P9A2-P9A3)

这个提交包含在:
2026-02-10 11:28:28 +08:00
父节点 bec21e1b38
当前提交 6bc09f557b
共修改 33 个文件,包含 3467 行新增和 13 行删除
+106
查看文件
@@ -0,0 +1,106 @@
const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const { buildAuditLogData } = require("../utils/audit");
const { enqueueIntegrationEvent } = require("../services/integrations/outbox");
const { buildErpPayload } = require("../services/integrations/erp");
const { runIntegrationOutboxOnce } = require("../jobs/integrationOutboxJob");
async function integrationsRoutes(fastify) {
fastify.post(
"/integrations/erp/sync/:reimbursementId",
{
preHandler: fastify.requirePermission("integration.manage"),
},
async (request, reply) => {
if (!fastify.config.erpSyncEnabled) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { integration: "erp_sync_disabled" },
});
}
const parsedId = z.string().uuid().safeParse(request.params?.reimbursementId);
if (!parsedId.success) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { reimbursementId: "invalid_uuid" },
});
}
const reimbursementId = parsedId.data;
const reimbursement = await fastify.prisma.reimbursementForm.findUnique({
where: { id: reimbursementId },
select: {
id: true,
form_number: true,
owner_id: true,
submitter_id: true,
status: true,
total_amount: true,
payment_voucher_no: true,
payment_date: true,
paid_at: true,
},
});
if (!reimbursement) {
throw new AppError({
code: "SYS_404",
message: "Not Found",
statusCode: 404,
});
}
if (reimbursement.status !== "paid") {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { reimbursement: "erp_sync_requires_paid" },
});
}
const payload = buildErpPayload({ reimbursement });
const enqueued = await enqueueIntegrationEvent(fastify.prisma, {
eventType: "erp.reimbursement.paid",
payload,
dedupeKey: reimbursement.id,
maxAttempts: fastify.config.integrationOutboxMaxAttempts,
actorId: request.employee.id,
});
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: "ERP_SYNC_ENQUEUED",
targetType: "reimbursement_form",
targetId: reimbursementId,
extra: {
outboxId: enqueued.id,
duplicated: enqueued.duplicated,
},
}),
});
const runNow = request.query?.runNow === "1" || request.query?.runNow === 1 || request.query?.runNow === true || request.query?.runNow === "true";
if (runNow) {
await runIntegrationOutboxOnce(fastify.prisma, fastify.config, fastify.log);
}
return sendData(reply, request, {
outboxId: enqueued.id,
status: enqueued.status,
duplicated: enqueued.duplicated,
});
},
);
}
module.exports = integrationsRoutes;