文件
hyb-oa/server/src/utils/workflow.js
T

333 行
7.7 KiB
JavaScript

const { AppError } = require("./AppError");
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const MAX_MANAGER_DEPTH = 10;
const BLOCKING_ASSIGNEE_SKIP_REASONS = new Set([
"no_manager_found",
"chain_broken",
"self_loop",
"cycle_detected",
"max_depth_exceeded",
]);
function parseWorkflowSteps(definitionJson) {
const steps = definitionJson?.steps;
if (!Array.isArray(steps) || steps.length === 0) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_steps" },
});
}
return steps;
}
function buildWorkflowStepsFromApprovalRule(approvalRule) {
if (!approvalRule || typeof approvalRule !== "object") {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_approval_rule" },
});
}
const roleCodes = [
approvalRule.level1_role,
approvalRule.level2_role,
approvalRule.level3_role,
].filter(Boolean);
if (roleCodes.length === 0) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "empty_approval_chain" },
});
}
return roleCodes.map((roleCode, index) => ({
id: `approval_${index + 1}_${roleCode}`,
name: `审批节点${index + 1}`,
type: "approval",
assignee_rule:
roleCode === "manager"
? { type: "department_manager" }
: { type: "role", value: roleCode },
}));
}
async function resolveDepartmentManagerAssignee({ tx, ownerId, logger }) {
let currentId = ownerId;
const visited = new Set();
for (let depth = 1; depth <= MAX_MANAGER_DEPTH; depth += 1) {
if (visited.has(currentId)) {
logger?.warn?.(
{ ownerId, currentId, reason: "cycle_detected" },
"Circular manager reference detected",
);
return {
assigneeId: null,
reason: "cycle_detected",
depthChecked: depth - 1,
};
}
visited.add(currentId);
const employee = await tx.employee.findUnique({
where: { id: currentId },
select: {
id: true,
direct_manager_id: true,
},
});
if (!employee) {
return {
assigneeId: null,
reason: "chain_broken",
depthChecked: depth,
};
}
const managerId = employee.direct_manager_id;
if (!managerId) {
return {
assigneeId: null,
reason: "no_manager_found",
depthChecked: depth,
};
}
if (managerId === currentId) {
logger?.warn?.(
{ ownerId, currentId, reason: "self_loop" },
"Self-loop manager reference detected",
);
return {
assigneeId: null,
reason: "self_loop",
depthChecked: depth,
};
}
if (managerId === ownerId) {
currentId = managerId;
continue;
}
if (visited.has(managerId)) {
logger?.warn?.(
{ ownerId, currentId: managerId, reason: "cycle_detected" },
"Circular manager reference detected",
);
return {
assigneeId: null,
reason: "cycle_detected",
depthChecked: depth,
};
}
return {
assigneeId: managerId,
reason: null,
depthChecked: depth,
};
}
logger?.warn?.(
{ ownerId, currentId, reason: "max_depth_exceeded" },
"Manager resolution reached max depth",
);
return {
assigneeId: null,
reason: "max_depth_exceeded",
depthChecked: MAX_MANAGER_DEPTH,
};
}
async function resolveAssigneeFromRule(rule, options = {}) {
if (!rule || typeof rule !== "object") {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_assignee_rule" },
});
}
if (rule.type === "role") {
if (typeof rule.value !== "string" || !rule.value) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_role_assignee" },
});
}
return {
assigneeId: null,
assigneeRole: rule.value,
skipReason: null,
depthChecked: 0,
};
}
if (rule.type === "specific_user") {
if (typeof rule.value !== "string" || !UUID_REGEX.test(rule.value)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_specific_user_assignee" },
});
}
return {
assigneeId: rule.value,
assigneeRole: null,
skipReason: null,
depthChecked: 0,
};
}
if (rule.type === "department_manager") {
const ownerId = options.ownerId;
const tx = options.tx;
if (!ownerId || !UUID_REGEX.test(ownerId) || !tx) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_department_manager_context" },
});
}
const managerResult = await resolveDepartmentManagerAssignee({
tx,
ownerId,
logger: options.logger,
});
if (!managerResult.assigneeId) {
return {
assigneeId: null,
assigneeRole: null,
skipReason: managerResult.reason,
depthChecked: managerResult.depthChecked,
};
}
return {
assigneeId: managerResult.assigneeId,
assigneeRole: null,
skipReason: null,
depthChecked: managerResult.depthChecked,
};
}
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "unsupported_assignee_rule_type" },
});
}
async function resolveActionableStep({
tx,
steps,
startIndex,
ownerId,
logger,
blockOnUnresolvedAssignee = false,
}) {
const skipped = [];
for (let index = startIndex; index < steps.length; index += 1) {
const step = steps[index];
const resolved = await resolveAssigneeFromRule(step.assignee_rule, {
tx,
ownerId,
logger,
});
if (resolved.assigneeId || resolved.assigneeRole) {
return {
actionable: {
index,
step,
assigneeId: resolved.assigneeId,
assigneeRole: resolved.assigneeRole,
},
skipped,
};
}
if (resolved.skipReason) {
if (blockOnUnresolvedAssignee && BLOCKING_ASSIGNEE_SKIP_REASONS.has(resolved.skipReason)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: {
workflow: "no_actionable_approver",
reason: resolved.skipReason,
stepId: step.id,
},
});
}
skipped.push({
index,
step,
reason: resolved.skipReason,
depthChecked: resolved.depthChecked,
});
continue;
}
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { workflow: "invalid_resolved_assignee" },
});
}
return {
actionable: null,
skipped,
};
}
function getWorkflowStepsSnapshot(workflowInstance) {
const snapshot = workflowInstance?.context_data?.workflow_steps_snapshot;
if (Array.isArray(snapshot) && snapshot.length > 0) {
return parseWorkflowSteps({ steps: snapshot });
}
return parseWorkflowSteps(workflowInstance?.workflow_definition?.definition_json);
}
function toMoneyString(value) {
if (value == null) return null;
return typeof value === "string" ? value : value.toString();
}
module.exports = {
buildWorkflowStepsFromApprovalRule,
getWorkflowStepsSnapshot,
parseWorkflowSteps,
resolveActionableStep,
resolveAssigneeFromRule,
resolveDepartmentManagerAssignee,
toMoneyString,
};