WRULE1 实现企微审批链校验器 POC

这个提交包含在:
2026-05-21 00:35:10 +08:00
父节点 1440cfe9a2
当前提交 c119183da6
共修改 14 个文件,包含 1225 行新增和 29 行删除
@@ -0,0 +1,331 @@
const { maskApprovalIdentifier } = require("./wecomApproval");
const APPROVAL_RULE_TARGETS = Object.freeze(["leave", "missed_punch", "outing", "overtime"]);
const MANUAL_GATE_VALUES = Object.freeze(["A", "B", "C", "D"]);
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function toArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function roleCodes(employee) {
return toArray(employee?.roles)
.map((item) => (typeof item === "string" ? item : item?.role_code))
.filter(Boolean);
}
function hasWecomBinding(employee) {
return Boolean(employee?.wecom_identity_binding?.external_user_id);
}
function isActiveEmployee(employee) {
return Boolean(employee && employee.status === "active");
}
function employeeSafeSummary(employee) {
if (!employee) return null;
return {
employeeNoPrefix: maskEmployeeIdentifier(employee.employee_no),
status: employee.status || "unknown",
roleCodes: roleCodes(employee),
hasWecomBinding: hasWecomBinding(employee),
};
}
function maskEmployeeIdentifier(value) {
const raw = toSafeString(value, 80);
if (!raw) return null;
if (raw.length <= 4) return `${raw.slice(0, 1)}***`;
return `${raw.slice(0, 3)}***${raw.slice(-2)}`;
}
function uniqueById(items) {
const seen = new Set();
const result = [];
for (const item of items || []) {
if (!item?.id || seen.has(item.id)) continue;
seen.add(item.id);
result.push(item);
}
return result;
}
function normalizeManualGates(rawManualGates = {}) {
const gates = {};
for (const [target, value] of Object.entries(rawManualGates || {})) {
const normalizedTarget = toSafeString(target, 40);
const gate = toSafeString(value, 4).toUpperCase();
if (!APPROVAL_RULE_TARGETS.includes(normalizedTarget) || !MANUAL_GATE_VALUES.includes(gate)) continue;
gates[normalizedTarget] = gate;
}
return gates;
}
function buildExpectedApprovers({ applicant, fallbackEmployee, approvalRoleEmployees = [] }) {
const findings = [];
const directManager = applicant?.direct_manager || null;
if (directManager && directManager.id === applicant?.id) {
findings.push({
type: "FAIL_SELF_APPROVAL",
severity: "blocker",
reason: "direct_manager_resolves_to_applicant",
});
}
if (
directManager
&& directManager.id !== applicant?.id
&& isActiveEmployee(directManager)
&& hasWecomBinding(directManager)
) {
return {
mode: "direct_manager",
expectedEmployees: [directManager],
findings,
};
}
const validRoleEmployees = uniqueById(approvalRoleEmployees.filter((employee) => (
employee.id !== applicant?.id
&& isActiveEmployee(employee)
&& hasWecomBinding(employee)
)));
if (validRoleEmployees.length) {
return {
mode: "approval_role",
expectedEmployees: validRoleEmployees,
findings,
};
}
if (
fallbackEmployee
&& fallbackEmployee.id !== applicant?.id
&& isActiveEmployee(fallbackEmployee)
&& hasWecomBinding(fallbackEmployee)
) {
return {
mode: "admin_fallback",
expectedEmployees: [fallbackEmployee],
findings: [
...findings,
{
type: "PASS_WITH_ADMIN_FALLBACK",
severity: "warning",
reason: "direct_manager_or_role_approver_missing_or_unavailable",
},
],
};
}
return {
mode: "no_candidate",
expectedEmployees: [],
findings: [
...findings,
{
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "no_valid_direct_manager_role_or_admin_fallback",
},
],
};
}
function actualApproverEmployees(archive) {
const fromParticipants = toArray(archive?.participants)
.filter((item) => item?.role === "approver" && item.employee)
.sort((a, b) => (a.node_index || 0) - (b.node_index || 0))
.map((item) => item.employee);
if (fromParticipants.length) return uniqueById(fromParticipants);
const fromActions = toArray(archive?.actions)
.filter((item) => item?.operator && ["approve", "reject", "pending"].includes(item.action_type))
.sort((a, b) => (a.node_index || 0) - (b.node_index || 0))
.map((item) => item.operator);
return uniqueById(fromActions);
}
function manualGateFinding(templateType, gate) {
if (!gate) {
return {
type: "PAUSED_MANUAL_CONFIRMATION",
severity: "warning",
reason: "manual_wappr1_gate_missing",
templateType,
};
}
if (gate === "A") return null;
if (gate === "D") {
return {
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "manual_wappr1_template_not_visible_or_not_configured",
templateType,
};
}
return {
type: "FAIL_APPLICANT_CAN_CHOOSE",
severity: "blocker",
reason: gate === "B" ? "manual_wappr1_approver_can_be_changed" : "manual_wappr1_approver_must_be_chosen_by_applicant",
templateType,
};
}
function validateApprovalRuleArchive(archive, context = {}) {
const templateType = archive?.template_type || "unknown";
const manualGates = normalizeManualGates(context.manualGates);
const applicant = archive?.applicant || null;
const findings = [];
const manualFinding = manualGateFinding(templateType, manualGates[templateType]);
if (manualFinding) findings.push(manualFinding);
if (!applicant) {
findings.push({
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "applicant_not_mapped_to_oa_employee",
templateType,
});
}
const expected = applicant
? buildExpectedApprovers({
applicant,
fallbackEmployee: context.fallbackEmployee || null,
approvalRoleEmployees: context.approvalRoleEmployees || [],
})
: { mode: "no_applicant", expectedEmployees: [], findings: [] };
findings.push(...expected.findings);
const actualEmployees = actualApproverEmployees(archive);
if (!actualEmployees.length) {
findings.push({
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "actual_approver_missing_or_unmapped",
templateType,
});
}
if (applicant && actualEmployees.some((employee) => employee.id === applicant.id)) {
findings.push({
type: "FAIL_SELF_APPROVAL",
severity: "blocker",
reason: "actual_approver_is_applicant",
templateType,
});
}
const expectedIds = new Set(expected.expectedEmployees.map((employee) => employee.id));
const actualIds = new Set(actualEmployees.map((employee) => employee.id));
const matched = [...actualIds].some((id) => expectedIds.has(id));
if (actualEmployees.length && expected.expectedEmployees.length && !matched) {
findings.push({
type: "FAIL_APPROVER_MISMATCH",
severity: "blocker",
reason: "actual_approver_not_in_oa_theoretical_approvers",
templateType,
});
}
const blockerTypes = new Set(findings.filter((item) => item.severity === "blocker").map((item) => item.type));
let status = "PASS_RULE_MATCH";
if (blockerTypes.has("FAIL_SELF_APPROVAL")) {
status = "FAIL_SELF_APPROVAL";
} else if (blockerTypes.has("FAIL_APPLICANT_CAN_CHOOSE")) {
status = "FAIL_APPLICANT_CAN_CHOOSE";
} else if (blockerTypes.has("FAIL_APPROVER_MISMATCH")) {
status = "FAIL_APPROVER_MISMATCH";
} else if (blockerTypes.has("SKIP/BLOCKED_BY_EXTERNAL_CONFIG")) {
status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
} else if (findings.some((item) => item.type === "PAUSED_MANUAL_CONFIRMATION")) {
status = "PAUSED_MANUAL_CONFIRMATION";
} else if (findings.some((item) => item.type === "PASS_WITH_ADMIN_FALLBACK")) {
status = "PASS_WITH_ADMIN_FALLBACK";
}
return {
archiveId: archive?.id || null,
spNoPrefix: maskApprovalIdentifier(archive?.sp_no),
templateIdPrefix: maskApprovalIdentifier(archive?.template_id),
templateType,
normalizedStatus: archive?.normalized_status || "unknown",
ruleMode: expected.mode,
status,
manualGate: manualGates[templateType] || null,
applicant: employeeSafeSummary(applicant),
expectedApprovers: expected.expectedEmployees.map(employeeSafeSummary),
actualApprovers: actualEmployees.map(employeeSafeSummary),
findings: findings.map((item) => ({
type: item.type,
severity: item.severity,
reason: item.reason,
templateType: item.templateType || templateType,
})),
};
}
function countBy(items, selector) {
const counts = {};
for (const item of items || []) {
const key = toSafeString(selector(item), 100) || "unknown";
counts[key] = (counts[key] || 0) + 1;
}
return counts;
}
function evaluateRuleReport(items, {
readError = null,
manualGateFindings = [],
manualGateGaps = [],
templateDataGaps = [],
noData = false,
} = {}) {
if (readError) return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
if (items.some((item) => item.status === "FAIL_SELF_APPROVAL")) return "FAIL_SELF_APPROVAL";
if (
items.some((item) => item.status === "FAIL_APPLICANT_CAN_CHOOSE")
|| manualGateFindings.some((item) => item.status === "FAIL_APPLICANT_CAN_CHOOSE")
) {
return "FAIL_APPLICANT_CAN_CHOOSE";
}
if (items.some((item) => item.status === "FAIL_APPROVER_MISMATCH")) return "FAIL_APPROVER_MISMATCH";
if (
items.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")
|| manualGateFindings.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")
) {
return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
}
if (
manualGateGaps.length
|| items.some((item) => item.status === "PAUSED_MANUAL_CONFIRMATION")
|| templateDataGaps.some((item) => item.status === "PAUSED_MANUAL_CONFIRMATION")
) {
return "PAUSED_MANUAL_CONFIRMATION";
}
if (templateDataGaps.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")) {
return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
}
if (noData) return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
if (items.some((item) => item.status === "PASS_WITH_ADMIN_FALLBACK")) return "PASS_WITH_ADMIN_FALLBACK";
return "PASS_RULE_MATCH";
}
module.exports = {
APPROVAL_RULE_TARGETS,
MANUAL_GATE_VALUES,
actualApproverEmployees,
buildExpectedApprovers,
countBy,
employeeSafeSummary,
evaluateRuleReport,
maskEmployeeIdentifier,
normalizeManualGates,
validateApprovalRuleArchive,
};