OAFLOW1 支持负责人本人报销兜底审批

这个提交包含在:
2026-05-24 21:12:18 +08:00
父节点 d6ae1cff89
当前提交 2e778d3c9f
共修改 23 个文件,包含 1324 行新增和 86 行删除
+196
查看文件
@@ -9,6 +9,14 @@ const BLOCKING_ASSIGNEE_SKIP_REASONS = new Set([
"cycle_detected",
"max_depth_exceeded",
]);
const SUPPORTED_OWNER_SELF_FALLBACK_ROLES = new Set(["finance_mgr"]);
const OWNER_SELF_FALLBACK_REASONS = new Set([
"no_manager_found",
"chain_broken",
"self_loop",
"cycle_detected",
"max_depth_exceeded",
]);
function parseWorkflowSteps(definitionJson) {
const steps = definitionJson?.steps;
@@ -60,6 +68,193 @@ function buildWorkflowStepsFromApprovalRule(approvalRule) {
}));
}
function isDepartmentManagerRule(rule) {
return rule?.type === "department_manager";
}
function getRoleCodeFromStep(step) {
const rule = step?.assignee_rule;
return rule?.type === "role" && typeof rule.value === "string" ? rule.value : null;
}
function buildRoleApprovalStep(roleCode, index, sourceStep, reason) {
const suffix = reason === "owner_self_fallback" ? "_owner_self_fallback" : "";
return {
id: `approval_${index + 1}_${roleCode}${suffix}`,
name:
reason === "owner_self_fallback"
? `负责人本人申请兜底:${roleCode}`
: sourceStep?.name ?? `审批节点${index + 1}`,
type: sourceStep?.type ?? "approval",
assignee_rule: { type: "role", value: roleCode },
};
}
async function loadOwnerRoleCodes(tx, ownerId) {
const owner = await tx.employee.findUnique({
where: { id: ownerId },
select: {
id: true,
roles: {
select: {
role_code: true,
},
},
},
});
if (!owner) {
return null;
}
return new Set(owner.roles.map((item) => item.role_code));
}
async function hasActiveNonSelfRoleHolder(tx, roleCode, ownerId) {
const holder = await tx.employeeRole.findFirst({
where: {
role_code: roleCode,
employee_id: { not: ownerId },
employee: {
status: "active",
archived_at: null,
},
},
select: {
employee_id: true,
},
});
return Boolean(holder);
}
function assertOwnerSelfFallbackRole(roleCode, ownerRoleCodes) {
if (!roleCode) {
return null;
}
if (!SUPPORTED_OWNER_SELF_FALLBACK_ROLES.has(roleCode)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { ownerSelfFallbackRole: "unsupported_business_approval_role" },
});
}
if (ownerRoleCodes?.has(roleCode)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: {
workflow: "no_actionable_approver",
reason: "owner_self_fallback_self_role",
},
});
}
return roleCode;
}
async function buildApprovalWorkflowSnapshot({ tx, approvalRule, ownerId, logger }) {
const rawSteps = buildWorkflowStepsFromApprovalRule(approvalRule);
const hasManagerStep = rawSteps.some((step) => isDepartmentManagerRule(step.assignee_rule));
if (!hasManagerStep || !approvalRule.owner_self_fallback_role) {
return rawSteps;
}
const ownerRoleCodes = await loadOwnerRoleCodes(tx, ownerId);
if (!ownerRoleCodes?.has("ceo")) {
return rawSteps;
}
const fallbackRole = assertOwnerSelfFallbackRole(
approvalRule.owner_self_fallback_role,
ownerRoleCodes,
);
const normalizedSteps = [];
const seenRoleCodes = new Set();
for (const rawStep of rawSteps) {
const roleCode = getRoleCodeFromStep(rawStep);
if (roleCode) {
if (roleCode === "ceo") {
continue;
}
if (seenRoleCodes.has(roleCode)) {
continue;
}
normalizedSteps.push(rawStep);
seenRoleCodes.add(roleCode);
continue;
}
if (!isDepartmentManagerRule(rawStep.assignee_rule)) {
normalizedSteps.push(rawStep);
continue;
}
const managerResult = await resolveDepartmentManagerAssignee({
tx,
ownerId,
logger,
});
if (managerResult.assigneeId) {
normalizedSteps.push(rawStep);
continue;
}
if (!fallbackRole || !OWNER_SELF_FALLBACK_REASONS.has(managerResult.reason)) {
normalizedSteps.push(rawStep);
continue;
}
const hasHolder = await hasActiveNonSelfRoleHolder(tx, fallbackRole, ownerId);
if (!hasHolder) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: {
workflow: "no_actionable_approver",
reason: "owner_self_fallback_role_unavailable",
fallbackRole,
},
});
}
if (!seenRoleCodes.has(fallbackRole)) {
normalizedSteps.push(
buildRoleApprovalStep(
fallbackRole,
normalizedSteps.length,
rawStep,
"owner_self_fallback",
),
);
seenRoleCodes.add(fallbackRole);
}
}
if (normalizedSteps.length === 0) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: {
workflow: "no_actionable_approver",
reason: "owner_self_fallback_empty_chain",
},
});
}
return normalizedSteps;
}
async function resolveDepartmentManagerAssignee({ tx, ownerId, logger }) {
let currentId = ownerId;
const visited = new Set();
@@ -322,6 +517,7 @@ function toMoneyString(value) {
}
module.exports = {
buildApprovalWorkflowSnapshot,
buildWorkflowStepsFromApprovalRule,
getWorkflowStepsSnapshot,
parseWorkflowSteps,