素材库与考核调研此前各有一套人事数据,彼此没有外键,靠姓名、邮箱在运行时 逐层软匹配。本次把身份判定收敛到钉钉唯一值,并让组织架构与人员状态跟随 上游 HR 系统自动更新。 人员身份 - 新增账号与权威人员档案的正式绑定表,活跃绑定双向唯一 - users.dingtalkUserId 补上唯一约束,数据库层面杜绝一人多号 - 移除钉钉登录的同名自动合并,同名冲突一律转人工核对 - 看板删除特判由比对用户名改为比对钉钉唯一值,避免同名越权 组织架构 - 新增 OrganizationUnit,直接镜像上游 HR 部门树 - 不再从员工部门路径倒推,同名部门不会再被合并 - 上游已删除的部门下线而非物理删除 人员状态 - 新增规范化的 employmentState,取代对混杂原始值做关键词匹配 - 接入上游离职表,离职判定不再依赖中文关键词 - 离职联动:归档账号并把素材托管给所在部门,默认只处理离职满 30 天者 素材归属 - 归属变更纳入正式转移,记录最初上传者、转移流水与理由 - 新增素材托管部门,归属与署名分离:托管只改维护责任,不动署名 - 编辑素材的权限判定统一到 canManageMaterial - 修复任务-详情上传把邮箱误存为作者名的问题 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
/**
|
||
* 离职人员素材部门托管
|
||
*
|
||
* 把已归档(离职)账号名下的素材,交由各自所属部门接管。
|
||
*
|
||
* 托管只改变"谁负责维护":userId 与 authorName 保持不变,
|
||
* 首页素材卡片显示的作者仍然是本人——素材是谁做的,永远记在谁名下。
|
||
*
|
||
* 默认预演,不写任何数据。加 --apply 才实际执行。
|
||
*
|
||
* 用法:
|
||
* npx tsx scripts/custody-archived-user-materials.ts # 预演
|
||
* npx tsx scripts/custody-archived-user-materials.ts --apply # 实际执行
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client'
|
||
import { custodyMaterialsOfArchivedUsers } from '../lib/material-transfer'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
async function main() {
|
||
const apply = process.argv.includes('--apply')
|
||
|
||
console.log('\n离职人员素材部门托管')
|
||
console.log('模式:' + (apply ? '⚠️ 实际执行' : '预演,不写入'))
|
||
|
||
const result = await custodyMaterialsOfArchivedUsers({ dryRun: !apply })
|
||
|
||
console.log('\n===== 汇总 =====')
|
||
console.log(' 涉及离职账号 ' + result.accounts + ' 个')
|
||
console.log(' 待托管素材 ' + result.totalMaterials + ' 条')
|
||
if (apply) console.log(' 已完成托管 ' + result.custodied + ' 条')
|
||
|
||
if (result.details.length > 0) {
|
||
console.log('\n===== 明细(按素材数排序)=====')
|
||
console.log(' 离职账号 接管部门 素材数')
|
||
console.log(' ' + '-'.repeat(52))
|
||
result.details
|
||
.sort((a, b) => b.count - a.count)
|
||
.forEach((d) => {
|
||
console.log(
|
||
' ' + d.username.padEnd(18) + (d.department ?? '未知').padEnd(20) + String(d.count).padStart(6)
|
||
)
|
||
})
|
||
}
|
||
|
||
if (result.skipped.length > 0) {
|
||
console.log('\n===== 无法自动托管(需人工指定部门)=====')
|
||
result.skipped
|
||
.sort((a, b) => b.count - a.count)
|
||
.forEach((s) => {
|
||
console.log(' ' + s.username.padEnd(18) + String(s.count).padStart(5) + ' 条 ← ' + s.reason)
|
||
})
|
||
}
|
||
|
||
if (!apply) {
|
||
console.log('\n(预演结束,未写入任何数据。加 --apply 实际执行)')
|
||
}
|
||
|
||
await prisma.$disconnect()
|
||
}
|
||
|
||
main().catch(async (e) => {
|
||
console.error('\n执行失败:', e)
|
||
await prisma.$disconnect()
|
||
process.exitCode = 1
|
||
})
|