gongxiangp2/scripts/sync-organization.ts

125 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 组织架构与在职状态同步
*
* 从上游 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
})