OSMOKE1 修复 OA-only smoke 账号治理

这个提交包含在:
2026-05-23 14:46:14 +08:00
父节点 88b06346da
当前提交 5a64809004
共修改 15 个文件,包含 1209 行新增和 13 行删除
@@ -23,6 +23,8 @@ Options:
--dry-run 只生成计划,不写数据库,默认。
--apply 执行受控 OA 写入,需 WOUA_FIX_APPLY=1。
--allow-employee-no=<no[,no]> 限定允许处理的员工号;可重复。
--oa-only-smoke-employee-no=<no[,no]>
明确标记 OA-only smoke 员工号;这些员工缺企微账号时不归档。
--clear-inactive-bindings 显式清理 inactiveWithBinding 中的绑定;默认不清理。
--repair-manager-effects 将归档测试主管的 active 下属和 pending 待办转给 fallback active manager。
--manager-fallback-employee-no=<no>
@@ -45,6 +47,7 @@ function parseArgs(argv) {
reportDir: process.env.WOUA_FIX_REPORT_DIR
|| path.join(DEFAULT_REPORT_ROOT, `wecom-oa-user-consistency-fix-${timestamp()}`),
allowEmployeeNos: [],
oaOnlySmokeEmployeeNos: splitList(process.env.OA_ONLY_SMOKE_EMPLOYEE_NOS || ""),
clearInactiveBindings: false,
repairManagerEffects: false,
managerFallbackEmployeeNo: "",
@@ -79,6 +82,11 @@ function parseArgs(argv) {
index += 1;
} else if (arg.startsWith("--allow-employee-no=")) {
options.allowEmployeeNos.push(...splitList(arg.slice("--allow-employee-no=".length)));
} else if (arg === "--oa-only-smoke-employee-no") {
options.oaOnlySmokeEmployeeNos.push(...splitList(argv[index + 1] || ""));
index += 1;
} else if (arg.startsWith("--oa-only-smoke-employee-no=")) {
options.oaOnlySmokeEmployeeNos.push(...splitList(arg.slice("--oa-only-smoke-employee-no=".length)));
} else if (arg === "--clear-inactive-bindings") {
options.clearInactiveBindings = true;
} else if (arg === "--repair-manager-effects") {
@@ -97,6 +105,7 @@ function parseArgs(argv) {
throw new Error(`Unsupported --format: ${options.format}`);
}
options.allowEmployeeNos = normalizeEmployeeNoList(options.allowEmployeeNos);
options.oaOnlySmokeEmployeeNos = normalizeEmployeeNoList(options.oaOnlySmokeEmployeeNos);
return options;
}
@@ -170,18 +179,40 @@ function buildAcceptedMissingRequiredFields(items) {
function buildFixPlan(report, options = {}) {
const findings = report?.findings ?? {};
const allowSet = new Set(normalizeEmployeeNoList(options.allowEmployeeNos));
const oaOnlySmokeSet = new Set(normalizeEmployeeNoList(options.oaOnlySmokeEmployeeNos));
const shouldInclude = (item) => !allowSet.size || allowSet.has(item.employeeNo);
const archiveCandidates = buildEmployeeNoActions(
const archiveSource = buildEmployeeNoActions(
findings.oaActiveMissingInWecom,
"archive_oa_test_employee",
"user_confirmed_oa_active_missing_in_wecom_are_test_data",
).filter(shouldInclude);
);
const oaOnlySmokeExempted = [
...buildEmployeeNoActions(
findings.oaOnlySmokeExempted,
"oa_only_smoke_exempted",
"source_report_classified_oa_only_smoke",
),
...archiveSource
.filter((item) => oaOnlySmokeSet.has(item.employeeNo))
.map((item) => ({
employeeNo: item.employeeNo,
action: "oa_only_smoke_exempted",
reason: "explicit_oa_only_smoke_employee_no",
})),
];
const exemptSet = new Set(oaOnlySmokeExempted.map((item) => item.employeeNo));
const archiveCandidates = archiveSource
.filter((item) => !exemptSet.has(item.employeeNo))
.filter(shouldInclude);
const excludedByAllowlist = allowSet.size
? buildEmployeeNoActions(
findings.oaActiveMissingInWecom,
"excluded_by_allowlist",
"allowlist_did_not_include_employee_no",
).filter((item) => !allowSet.has(item.employeeNo))
? archiveSource
.filter((item) => !exemptSet.has(item.employeeNo))
.map((item) => ({
employeeNo: item.employeeNo,
action: "excluded_by_allowlist",
reason: "allowlist_did_not_include_employee_no",
}))
.filter((item) => !allowSet.has(item.employeeNo))
: [];
const inactiveBindingAction = options.clearInactiveBindings
@@ -194,12 +225,14 @@ function buildFixPlan(report, options = {}) {
sourceGeneratedAt: report?.generatedAt ?? null,
decisions: {
oaActiveMissingInWecom: "archive_oa_test_employees",
oaOnlySmokeMissingInWecom: "exempt_no_archive",
missingRequiredFields: "accepted_no_write",
inactiveWithBinding: inactiveBindingAction,
managerSideEffects: options.repairManagerEffects ? "repair_to_active_manager" : "report_only",
wecomWriteBehavior: "no_wecom_writes",
},
archiveOaTestEmployees: archiveCandidates,
oaOnlySmokeExempted,
excludedByAllowlist,
acceptedMissingRequiredFields: buildAcceptedMissingRequiredFields(findings.missingRequiredFields),
inactiveWithBinding: buildEmployeeNoActions(
@@ -234,6 +267,7 @@ function buildDryRunResult(plan, options = {}) {
decisions: plan.decisions,
totals: {
archiveOaTestEmployees: plan.archiveOaTestEmployees.length,
oaOnlySmokeExempted: plan.oaOnlySmokeExempted.length,
excludedByAllowlist: plan.excludedByAllowlist.length,
acceptedMissingRequiredFields: plan.acceptedMissingRequiredFields.length,
inactiveWithBinding: plan.inactiveWithBinding.length,
@@ -243,6 +277,7 @@ function buildDryRunResult(plan, options = {}) {
},
actions: {
archiveOaTestEmployees: sample(plan.archiveOaTestEmployees),
oaOnlySmokeExempted: sample(plan.oaOnlySmokeExempted),
excludedByAllowlist: sample(plan.excludedByAllowlist),
acceptedMissingRequiredFields: sample(plan.acceptedMissingRequiredFields),
inactiveWithBinding: sample(plan.inactiveWithBinding),
@@ -610,6 +645,7 @@ function buildDatabaseResult({ plan, archived, inactiveBindings, managerRepairs
decisions: plan.decisions,
totals: {
archiveOaTestEmployees: archived.length,
oaOnlySmokeExempted: plan.oaOnlySmokeExempted.length,
archivedApplied: countByResult(archived, "applied"),
archivedPlanned: countByResult(archived, "planned"),
archivedNoop: countByResult(archived, "noop"),
@@ -629,6 +665,7 @@ function buildDatabaseResult({ plan, archived, inactiveBindings, managerRepairs
},
actions: {
archiveOaTestEmployees: sample(archived),
oaOnlySmokeExempted: sample(plan.oaOnlySmokeExempted),
excludedByAllowlist: sample(plan.excludedByAllowlist),
acceptedMissingRequiredFields: sample(plan.acceptedMissingRequiredFields),
inactiveWithBinding: sample(inactiveBindings),