feat(P25-1/P25-5/P26-1/P26-3/P24-1/P29-4): 落地内部OA够用版后端与试运行能力
关联 Issue: P25-1, P25-2, P25-3, P25-4, P25-5, P26-1, P26-3, P24-1, P24-2, P29-1, P29-2, P29-3, P29-4
这个提交包含在:
@@ -0,0 +1,117 @@
|
||||
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(),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return sendData(reply, request, { item: buildPersistedRiskFlag(updated) });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = riskFlagsRoutes;
|
||||
在新工单中引用
屏蔽一个用户