feat(server): P5 基础能力补齐(组织/报销/代办/认证安全)
- 组织/人员/委派对齐:费用类型、部门、员工别名、配置只读与回归脚本(F2/F3/F26/F27/F29/F31/F40) - 报销单 PATCH/DELETE 与 items CRUD(事务内重算 total_amount)(F4/F5) - 行政代提交/代上传后端校验与审计(F7/F8) - 认证安全基线:首次登录改密、失败锁定、弱密码拦截与回归脚本(F36/F37/F39/F41)
这个提交包含在:
@@ -0,0 +1,147 @@
|
||||
const { z } = require("zod");
|
||||
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { sendData } = require("../utils/reply");
|
||||
|
||||
const ENABLED_QUERY_SCHEMA = z.object({
|
||||
enabled: z.enum(["true", "false", "1", "0"]).optional(),
|
||||
});
|
||||
|
||||
function parseEnabledQuery(query) {
|
||||
const parsed = ENABLED_QUERY_SCHEMA.safeParse(query ?? {});
|
||||
if (!parsed.success) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: parsed.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
const enabledRaw = parsed.data.enabled;
|
||||
const enabled =
|
||||
enabledRaw == null
|
||||
? null
|
||||
: ["true", "1"].includes(enabledRaw)
|
||||
? true
|
||||
: ["false", "0"].includes(enabledRaw)
|
||||
? false
|
||||
: null;
|
||||
|
||||
return { enabled };
|
||||
}
|
||||
|
||||
function mapApprovalRule(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
minAmount: item.min_amount?.toString?.() ?? String(item.min_amount ?? "0"),
|
||||
maxAmount: item.max_amount == null ? null : item.max_amount?.toString?.() ?? String(item.max_amount),
|
||||
level1Role: item.level1_role,
|
||||
level2Role: item.level2_role ?? null,
|
||||
level3Role: item.level3_role ?? null,
|
||||
enabled: item.enabled,
|
||||
priority: item.priority,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDocumentSequence(item, exists) {
|
||||
const fallback = {
|
||||
documentType: "reimbursement",
|
||||
prefix: "BX",
|
||||
dateFormat: "YYYYMMDD",
|
||||
separator: "-",
|
||||
numberLength: 4,
|
||||
resetPeriod: "none",
|
||||
currentNumber: 0,
|
||||
lastResetDate: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
};
|
||||
|
||||
if (!item) {
|
||||
return { exists, ...fallback };
|
||||
}
|
||||
|
||||
return {
|
||||
exists,
|
||||
documentType: item.document_type,
|
||||
prefix: item.prefix,
|
||||
dateFormat: item.date_format,
|
||||
separator: item.separator,
|
||||
numberLength: item.number_length,
|
||||
resetPeriod: item.reset_period,
|
||||
currentNumber: item.current_number,
|
||||
lastResetDate: item.last_reset_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||
updatedBy: item.updated_by ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function configReadRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/approval-rules",
|
||||
{
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
const query = parseEnabledQuery(request.query);
|
||||
|
||||
const items = await fastify.prisma.approvalRule.findMany({
|
||||
where: {
|
||||
...(query.enabled == null ? {} : { enabled: query.enabled }),
|
||||
},
|
||||
orderBy: [{ priority: "asc" }, { min_amount: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
min_amount: true,
|
||||
max_amount: true,
|
||||
level1_role: true,
|
||||
level2_role: true,
|
||||
level3_role: true,
|
||||
enabled: true,
|
||||
priority: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
});
|
||||
|
||||
return sendData(reply, request, {
|
||||
enabled: query.enabled,
|
||||
items: items.map(mapApprovalRule),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
"/document-sequences/reimbursement",
|
||||
{
|
||||
preHandler: fastify.requirePermission("system.settings"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
const sequence = await fastify.prisma.documentSequence.findUnique({
|
||||
where: { document_type: "reimbursement" },
|
||||
select: {
|
||||
document_type: true,
|
||||
prefix: true,
|
||||
date_format: true,
|
||||
separator: true,
|
||||
number_length: true,
|
||||
reset_period: true,
|
||||
current_number: true,
|
||||
last_reset_date: true,
|
||||
updated_at: true,
|
||||
updated_by: true,
|
||||
},
|
||||
});
|
||||
|
||||
return sendData(reply, request, mapDocumentSequence(sequence, Boolean(sequence)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = configReadRoutes;
|
||||
|
||||
在新工单中引用
屏蔽一个用户