/** * 组织架构与在职状态同步 * * 从上游 HR 系统镜像部门树,并依据离职表校正本地档案的在职状态。 * * 默认预演,不写任何数据。加 --apply 才实际执行。 * * 用法: * npx tsx --env-file=.env scripts/sync-organization.ts # 预演 * npx tsx --env-file=.env scripts/sync-organization.ts --apply # 实际执行 * npx tsx --env-file=.env scripts/sync-organization.ts --org # 只同步组织架构 * npx tsx --env-file=.env scripts/sync-organization.ts --employment # 只同步在职状态 */ import { PrismaClient } from '@prisma/client' import { isReportDbConfigured, syncOrganizationUnits, syncEmploymentStates, applyResignationEffects, } from '../lib/organization-sync' const prisma = new PrismaClient() async function main() { const args = process.argv.slice(2) const apply = args.includes('--apply') const onlyOrg = args.includes('--org') const onlyEmployment = args.includes('--employment') const runOrg = !onlyEmployment const runEmployment = !onlyOrg if (!isReportDbConfigured()) { console.error('未配置上游报表库(REPORT_DB_SERVER / DATABASE / USER / PASSWORD)') process.exitCode = 1 return } console.log('\n组织架构与在职状态同步') console.log('模式:' + (apply ? '⚠️ 实际执行' : '预演,不写入')) if (runOrg) { console.log('\n===== 一、组织架构 =====') const r = await syncOrganizationUnits({ dryRun: !apply }) console.log(' 上游部门总数 ' + r.fetched) console.log(' 根节点 ' + (r.rootName ?? '未识别')) console.log(' 最大层级 ' + r.maxDepth) if (apply) { console.log(' 新建 ' + r.created) console.log(' 更新 ' + r.updated) console.log(' 下线(上游已删)' + r.deactivated) } } if (runEmployment) { console.log('\n===== 二、在职状态 =====') const r = await syncEmploymentStates({ dryRun: !apply }) console.log(' 上游离职表人数 ' + r.upstreamResigned) console.log(' 上游在册人数 ' + r.upstreamRegistered) console.log(' 本地档案总数 ' + r.localProfiles) console.log(' ├ 判定为在职 ' + r.markedActive) console.log(' ├ 判定为离职 ' + r.markedResigned) console.log(' └ 状态未知 ' + r.markedUnknown) console.log('\n===== 三、离职联动 =====') // 人事确认:花名册标记离职即为事实离职,立即托管+归档,无天数缓冲 const e = await applyResignationEffects({ dryRun: !apply }) console.log(' 人已离职、账号仍活跃 ' + e.candidates + ' 人') console.log(' └ 本次纳入处理 ' + e.pending + ' 人') if (apply) { console.log('\n 已归档账号 ' + e.archivedAccounts) console.log(' 已托管素材 ' + e.custodiedMaterials + ' 条') } if (e.details.length > 0) { console.log('\n 纳入处理的(按素材数排序,前 20):') e.details .sort((a, b) => b.materials - a.materials) .slice(0, 20) .forEach((d) => console.log( ' ' + d.displayName.padEnd(12) + (d.department ?? '无部门').padEnd(14) + String(d.materials).padStart(5) + ' 条素材 ' + (d.resignedAt ? '最后工作日 ' + d.resignedAt.toLocaleDateString('zh-CN') : '离职日期未知'), ), ) if (e.details.length > 20) console.log(' ...另有 ' + (e.details.length - 20) + ' 人') } if (e.deferredDetails.length > 0) { console.log('\n 暂缓的(前 10):') e.deferredDetails.slice(0, 10).forEach((d) => console.log(' ' + d.displayName.padEnd(12) + d.reason), ) if (e.deferredDetails.length > 10) { console.log(' ...另有 ' + (e.deferredDetails.length - 10) + ' 人') } } if (e.failures.length > 0) { console.log('\n 需人工处理:') e.failures.forEach((f) => console.log(' ' + f.username.padEnd(14) + f.reason)) } } if (apply && runOrg) { const total = await prisma.organizationUnit.count() const active = await prisma.organizationUnit.count({ where: { isActive: true } }) const roots = await prisma.organizationUnit.count({ where: { parentId: null, isActive: true } }) console.log('\n===== 落库校验 =====') console.log(' 组织节点总数 ' + total) console.log(' 其中有效 ' + active) console.log(' 根节点数量 ' + roots + (roots === 1 ? ' ✅ 单根树' : ' ⚠️ 预期为 1')) } if (!apply) { console.log('\n(预演结束,未写入任何数据。加 --apply 实际执行)') } await prisma.$disconnect() } main().catch(async (e) => { console.error('\n同步失败:', e instanceof Error ? e.message : e) await prisma.$disconnect() process.exitCode = 1 })