const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const { buildAuditLogData } = require("../utils/audit"); const { buildPersistedRiskFlag } = require("../services/feeControl"); const UUID_SCHEMA = z.string().uuid(); const QUERY_SCHEMA = z.object({ entityType: z.string().trim().max(32).optional(), entityId: UUID_SCHEMA.optional(), status: z.enum(["open", "resolved", "dismissed"]).optional(), source: z.string().trim().max(32).optional(), }); const ACTION_BODY_SCHEMA = z.object({ note: z.string().trim().max(500).nullable().optional(), }).strict(); function buildValidationError(details, statusCode = 400) { return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details, }); } function hasListPermission(fastify, request) { return ( fastify.hasPermission(request.employee, "budget.view") || fastify.hasPermission(request.employee, "payment.batch.manage") || fastify.hasPermission(request.employee, "risk.review.manage") ); } function ensureListPermission(fastify, request) { if (!hasListPermission(fastify, request)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } } function ensureManagePermission(fastify, request) { if (!fastify.hasPermission(request.employee, "risk.review.manage")) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } } function parseQuery(query) { const parsed = QUERY_SCHEMA.safeParse(query ?? {}); if (!parsed.success) { throw buildValidationError(parsed.error.flatten()); } return parsed.data; } function parseId(value) { const parsed = UUID_SCHEMA.safeParse(value); if (!parsed.success) { throw buildValidationError({ id: "invalid_uuid" }); } return parsed.data; } function parseBody(body) { const parsed = ACTION_BODY_SCHEMA.safeParse(body ?? {}); if (!parsed.success) { throw buildValidationError(parsed.error.flatten()); } return parsed.data; } function normalizeNote(value) { if (value == null) { return null; } const trimmed = String(value).trim(); return trimmed || null; } async function riskFlagsRoutes(fastify) { fastify.get("/risk-flags", { preHandler: fastify.authenticate }, async (request, reply) => { ensureListPermission(fastify, request); const query = parseQuery(request.query); const items = await fastify.prisma.entityRiskFlag.findMany({ where: { ...(query.entityType ? { entity_type: query.entityType } : {}), ...(query.entityId ? { entity_id: query.entityId } : {}), ...(query.status ? { status: query.status } : {}), ...(query.source ? { source: query.source } : {}), }, orderBy: [{ created_at: "desc" }, { id: "desc" }], }); return sendData(reply, request, { items: items.map(buildPersistedRiskFlag) }); }); for (const action of ["resolve", "dismiss"]) { fastify.post(`/risk-flags/:id/${action}`, { preHandler: fastify.authenticate }, async (request, reply) => { ensureManagePermission(fastify, request); const riskFlagId = parseId(request.params?.id); const body = parseBody(request.body); const note = normalizeNote(body.note); const current = await fastify.prisma.entityRiskFlag.findUnique({ where: { id: riskFlagId } }); if (!current) { throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 }); } if (current.status !== "open") { throw buildValidationError({ status: "risk_flag_not_open" }, 409); } const updated = await fastify.prisma.entityRiskFlag.update({ where: { id: riskFlagId }, data: { status: action === "resolve" ? "resolved" : "dismissed", resolved_at: new Date(), resolved_by: request.employee.id, }, }); await fastify.prisma.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action: action === "resolve" ? "ENTITY_RISK_FLAG_RESOLVED" : "ENTITY_RISK_FLAG_DISMISSED", targetType: "entity_risk_flag", targetId: updated.id, extra: { entityType: updated.entity_type, entityId: updated.entity_id, riskCode: updated.risk_code, note, }, }), }); return sendData(reply, request, { item: buildPersistedRiskFlag(updated) }); }); } } module.exports = riskFlagsRoutes;