素材库与考核调研此前各有一套人事数据,彼此没有外键,靠姓名、邮箱在运行时 逐层软匹配。本次把身份判定收敛到钉钉唯一值,并让组织架构与人员状态跟随 上游 HR 系统自动更新。 人员身份 - 新增账号与权威人员档案的正式绑定表,活跃绑定双向唯一 - users.dingtalkUserId 补上唯一约束,数据库层面杜绝一人多号 - 移除钉钉登录的同名自动合并,同名冲突一律转人工核对 - 看板删除特判由比对用户名改为比对钉钉唯一值,避免同名越权 组织架构 - 新增 OrganizationUnit,直接镜像上游 HR 部门树 - 不再从员工部门路径倒推,同名部门不会再被合并 - 上游已删除的部门下线而非物理删除 人员状态 - 新增规范化的 employmentState,取代对混杂原始值做关键词匹配 - 接入上游离职表,离职判定不再依赖中文关键词 - 离职联动:归档账号并把素材托管给所在部门,默认只处理离职满 30 天者 素材归属 - 归属变更纳入正式转移,记录最初上传者、转移流水与理由 - 新增素材托管部门,归属与署名分离:托管只改维护责任,不动署名 - 编辑素材的权限判定统一到 canManageMaterial - 修复任务-详情上传把邮箱误存为作者名的问题 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
/**
|
||
* 上游报表库探查 · 花名册与组织架构相关表
|
||
*
|
||
* 纯只读:只查 INFORMATION_SCHEMA 与少量样本行,不写入、不建表、不改结构。
|
||
*
|
||
* 凭据一律从环境变量读取,脚本内不含任何账号密码。
|
||
*
|
||
* 用法(凭据在命令行传入,不落盘):
|
||
* REPORT_DB_SERVER=... REPORT_DB_DATABASE=... REPORT_DB_USER=... REPORT_DB_PASSWORD=... \
|
||
* npx tsx scripts/probe-report-db.ts
|
||
*/
|
||
|
||
import mssql from 'mssql'
|
||
|
||
const required = ['REPORT_DB_SERVER', 'REPORT_DB_DATABASE', 'REPORT_DB_USER', 'REPORT_DB_PASSWORD']
|
||
const missing = required.filter((k) => !process.env[k])
|
||
if (missing.length > 0) {
|
||
console.error('缺少环境变量:' + missing.join(', '))
|
||
process.exit(1)
|
||
}
|
||
|
||
const config: mssql.config = {
|
||
server: process.env.REPORT_DB_SERVER as string,
|
||
database: process.env.REPORT_DB_DATABASE as string,
|
||
user: process.env.REPORT_DB_USER as string,
|
||
password: process.env.REPORT_DB_PASSWORD as string,
|
||
port: Number(process.env.REPORT_DB_PORT || 1433),
|
||
options: {
|
||
encrypt: process.env.REPORT_DB_ENCRYPT === 'true',
|
||
trustServerCertificate: process.env.REPORT_DB_TRUST_SERVER_CERTIFICATE !== 'false',
|
||
},
|
||
connectionTimeout: 20000,
|
||
requestTimeout: 60000,
|
||
pool: { max: 2, min: 0, idleTimeoutMillis: 10000 },
|
||
}
|
||
|
||
async function main() {
|
||
const pool = await mssql.connect(config)
|
||
try {
|
||
console.log('已连接:' + config.server + ' / ' + config.database)
|
||
|
||
// 一、rpt schema 下与人事、组织相关的表
|
||
const tables = await pool.request().query(`
|
||
SELECT TABLE_SCHEMA, TABLE_NAME
|
||
FROM INFORMATION_SCHEMA.TABLES
|
||
WHERE TABLE_SCHEMA = 'rpt'
|
||
AND (
|
||
TABLE_NAME LIKE '%hr[_]%' OR TABLE_NAME LIKE '%employee%' OR
|
||
TABLE_NAME LIKE '%flower%' OR TABLE_NAME LIKE '%group%' OR
|
||
TABLE_NAME LIKE '%dept%' OR TABLE_NAME LIKE '%staff%' OR
|
||
TABLE_NAME LIKE '%org%' OR TABLE_NAME LIKE '%user%'
|
||
)
|
||
ORDER BY TABLE_NAME
|
||
`)
|
||
console.log('\n===== 一、人事/组织相关表(rpt schema)=====')
|
||
tables.recordset.forEach((r: any) => console.log(' ' + r.TABLE_SCHEMA + '.' + r.TABLE_NAME))
|
||
console.log(' 共 ' + tables.recordset.length + ' 张')
|
||
|
||
// 二、rpt schema 表总数,用于判断整体规模
|
||
const total = await pool.request().query(`
|
||
SELECT COUNT(*) AS c FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'rpt'
|
||
`)
|
||
console.log('\n (rpt schema 共有 ' + total.recordset[0].c + ' 张表)')
|
||
|
||
// 三、重点表的列结构
|
||
const focus = [
|
||
'hr_groups',
|
||
'hr_employee_dimission',
|
||
'hr_flowerName_book',
|
||
'op_OperationGroup_Reflect',
|
||
]
|
||
for (const t of focus) {
|
||
const cols = await pool.request().input('t', t).query(`
|
||
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH AS LEN, IS_NULLABLE
|
||
FROM INFORMATION_SCHEMA.COLUMNS
|
||
WHERE TABLE_SCHEMA = 'rpt' AND TABLE_NAME = @t
|
||
ORDER BY ORDINAL_POSITION
|
||
`)
|
||
console.log('\n===== rpt.' + t + ' =====')
|
||
if (cols.recordset.length === 0) {
|
||
console.log(' ⚠️ 表不存在')
|
||
continue
|
||
}
|
||
const cnt = await pool.request().query('SELECT COUNT(*) AS c FROM rpt.' + t)
|
||
console.log(' 行数:' + cnt.recordset[0].c)
|
||
cols.recordset.forEach((c: any) => {
|
||
const len = c.LEN ? '(' + (c.LEN === -1 ? 'max' : c.LEN) + ')' : ''
|
||
console.log(' ' + String(c.COLUMN_NAME).padEnd(28) + (c.DATA_TYPE + len).padEnd(18) + (c.IS_NULLABLE === 'YES' ? 'NULL' : 'NOT NULL'))
|
||
})
|
||
}
|
||
|
||
// 四、组织树的根节点与层级结构
|
||
console.log('\n===== 组织树结构分析(hr_groups)=====')
|
||
const rootStats = await pool.request().query(`
|
||
SELECT
|
||
SUM(CASE WHEN parent_id IS NULL THEN 1 ELSE 0 END) AS null_parent,
|
||
SUM(CASE WHEN parent_id = 0 THEN 1 ELSE 0 END) AS zero_parent,
|
||
SUM(CASE WHEN parent_id = Id THEN 1 ELSE 0 END) AS self_parent,
|
||
COUNT(*) AS total
|
||
FROM rpt.hr_groups
|
||
`)
|
||
const rs = rootStats.recordset[0]
|
||
console.log(' 部门总数 ' + rs.total)
|
||
console.log(' parent_id 为 NULL ' + rs.null_parent)
|
||
console.log(' parent_id 为 0 ' + rs.zero_parent)
|
||
console.log(' parent_id 指向自己 ' + rs.self_parent)
|
||
|
||
const roots = await pool.request().query(`
|
||
SELECT TOP 10 Id, name, parent_id FROM rpt.hr_groups
|
||
WHERE parent_id IS NULL OR parent_id = 0 OR parent_id = Id
|
||
ORDER BY Id
|
||
`)
|
||
console.log('\n 根节点样本:')
|
||
roots.recordset.forEach((r: any) =>
|
||
console.log(' Id=' + String(r.Id).padEnd(12) + 'parent=' + String(r.parent_id ?? 'NULL').padEnd(10) + r.name)
|
||
)
|
||
|
||
// 孤儿节点:父部门在表中不存在
|
||
const orphans = await pool.request().query(`
|
||
SELECT COUNT(*) AS c FROM rpt.hr_groups g
|
||
WHERE g.parent_id IS NOT NULL AND g.parent_id <> 0 AND g.parent_id <> g.Id
|
||
AND NOT EXISTS (SELECT 1 FROM rpt.hr_groups p WHERE p.Id = g.parent_id)
|
||
`)
|
||
console.log('\n 孤儿节点(父部门不存在) ' + orphans.recordset[0].c)
|
||
|
||
// 五、在职状态口径
|
||
console.log('\n===== 在职状态口径 =====')
|
||
const active = await pool.request().query(`
|
||
SELECT active, COUNT(*) AS c FROM rpt.hr_employee GROUP BY active
|
||
`)
|
||
active.recordset.forEach((r: any) =>
|
||
console.log(' hr_employee.active = ' + String(r.active) + ' → ' + r.c + ' 人')
|
||
)
|
||
const status = await pool.request().query(`
|
||
SELECT [sys01-employeeStatus] AS s, COUNT(*) AS c
|
||
FROM rpt.hr_employee_register GROUP BY [sys01-employeeStatus] ORDER BY c DESC
|
||
`)
|
||
console.log('')
|
||
status.recordset.forEach((r: any) =>
|
||
console.log(' employeeStatus = ' + String(r.s ?? '(空)').padEnd(12) + '→ ' + r.c + ' 人')
|
||
)
|
||
} finally {
|
||
await pool.close()
|
||
}
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error('探查失败:', e instanceof Error ? e.message : e)
|
||
process.exitCode = 1
|
||
})
|