P25L-1~P25L-5 实现预算控制与AP最小可用运营

合同 CSV 已回填 reviewer/QA 结果;运行时回归受当前环境缺少 node/npm 阻塞。
这个提交包含在:
2026-03-29 09:21:17 +08:00
父节点 e8d16c47f2
当前提交 6dee2daf3e
共修改 19 个文件,包含 1394 行新增和 482 行删除
+10 -46
查看文件
@@ -20,8 +20,8 @@ const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureF
const { ensureCutoverAllowed } = require("../services/cutover");
const {
buildPersistedRiskFlag,
buildTravelRequestPrecheck,
createRiskFlagsFromPrecheck,
resolveEntityPrecheck,
} = require("../services/feeControl");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
@@ -300,20 +300,6 @@ function ensureDateRangeValid(startDate, endDate) {
}
}
function buildDisabledPrecheck(entityType, entityId) {
return {
entityType,
entityId,
decision: "pass",
matchedBudgetControlRule: null,
budgetHint: null,
warnings: [],
blockingReasons: [],
riskFlags: [],
nextActions: ["功能未开启,沿用当前提交流程"],
};
}
function buildPrecheckBlockedError(precheck) {
return new AppError({
code: "VALID_001",
@@ -922,29 +908,13 @@ async function createTravelRequestsRoutes(fastify) {
},
async (request, reply) => {
const travelRequestId = parseTravelRequestId(request.params?.id);
const travelRequest = await fastify.prisma.travelRequest.findUnique({
where: { id: travelRequestId },
select: { id: true, department_id: true },
});
if (!travelRequest) {
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
}
const gate = await evaluateFeatureFlag({
const precheck = await resolveEntityPrecheck({
prisma: fastify.prisma,
config: fastify.config,
flagKey: FEATURE_FLAG_KEYS.budgetPrecheck,
departmentId: travelRequest.department_id ?? request.employee.department_id ?? null,
strictDepartment: false,
});
if (!gate.enabled) {
return sendData(reply, request, buildDisabledPrecheck("travel_request", travelRequestId));
}
const precheck = await buildTravelRequestPrecheck(fastify.prisma, {
travelRequestId,
config: fastify.config,
logger: fastify.log,
entityType: "travel_request",
entityId: travelRequestId,
actorDepartmentId: request.employee.department_id ?? null,
});
return sendData(reply, request, precheck);
},
@@ -978,20 +948,14 @@ async function createTravelRequestsRoutes(fastify) {
throw buildValidationError({ segments: "required" }, 409);
}
const budgetPrecheckGate = await evaluateFeatureFlag({
const authoritativePrecheck = await resolveEntityPrecheck({
prisma: tx,
config: fastify.config,
flagKey: FEATURE_FLAG_KEYS.budgetPrecheck,
departmentId: travelRequest.department_id ?? request.employee.department_id ?? null,
strictDepartment: false,
logger: fastify.log,
entityType: "travel_request",
entityId: travelRequestId,
actorDepartmentId: request.employee.department_id ?? null,
});
const authoritativePrecheck = budgetPrecheckGate.enabled
? await buildTravelRequestPrecheck(tx, {
travelRequestId,
config: fastify.config,
logger: fastify.log,
})
: buildDisabledPrecheck("travel_request", travelRequestId);
if (authoritativePrecheck.decision === "block") {
throw buildPrecheckBlockedError(authoritativePrecheck);
}