/** * 上游报表库探查 · 花名册与组织架构相关表 * * 纯只读:只查 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 })