diff --git a/CLAUDE.md b/CLAUDE.md index 95325b1..214a485 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,8 +44,9 @@ npm run lint # ESLint **物理服务器:** - 生产服务器:`192.168.58.20` -- 构建服务器:`192.168.42.134` +- 构建服务器:`192.168.40.206`(部署脚本内的固定值;旧文档中的 `192.168.42.134` 已不是部署入口) - 香港公网服务器:`103.118.245.6`(FRP 入口 + Nginx 反向代理) +- 上游报表库:`218.93.9.21`(SQL Server 2019,人事与组织架构数据源) **公网访问架构:** - 域名:`sc.jiansun.vip`(素材平台)、`s3.jiansun.vip`(MinIO) @@ -62,6 +63,102 @@ npm run lint # ESLint - `gongxiangpan-redis`(生产 Redis) - `youyidian-3d-shadow`(影子环境应用) +### 上游报表库(人事与组织架构数据源) + +⚠️ **重要认知**:花名册**不是**从钉钉通讯录来的。钉钉在本项目中只负责登录鉴权与消息推送,从未调用过通讯录接口。人事权威源是公司报表库。 + +**连接信息(只读账号,配置在 `.env`,勿写入仓库):** +- Server:`218.93.9.21` +- Database:`JSReportReplica` +- Schema:`rpt`(只读) +- 环境变量:`REPORT_DB_SERVER` / `REPORT_DB_DATABASE` / `REPORT_DB_USER` / `REPORT_DB_PASSWORD` + +⚠️ 密码含 `#` 等特殊字符时,在 `.env` 中**必须加引号**,否则 `#` 之后会被当作注释截断,表现为登录失败。 + +**关键表:** + +| 表 | 行数量级 | 用途 | +|---|---|---| +| `rpt.hr_groups` | 1075 | 组织架构树。仅 `Id` / `parent_id` / `name` 三列可用,无状态位、无排序、无编码。单一根节点(Id=1),零孤儿节点 | +| `rpt.hr_employee` | 1296 | 钉钉员工表,含 `manager_userid`、`dept_id_list`、`active` | +| `rpt.hr_employee_register` | 1296 | 入职登记,含 `sys01-employeeStatus` 状态码、`sys00-flowerName` 花名 | +| `rpt.hr_flowerName_book` | — | 完整员工档案(含社保、银行卡等敏感信息,谨慎查询) | +| `rpt.hr_employee_dimission` | 4711 | **离职表**,含 `last_work_day`、`handover_userid` 交接人 | + +**员工状态码:`2` 试用、`3` 正式、`5` 已离职。**注意上游还存在中文值(正式/离职/试用)与空值,口径混杂,不可用关键词匹配判断在职。 + +--- + +## 人员架构与组织同步(2026-08-13 重构) + +### 核心原则 + +**钉钉唯一值是全项目唯一的人员身份判定依据。** 姓名、花名、邮箱只能用于人工排错,不得自动建立绑定。 + +### 数据模型 + +| 模型 | 作用 | +|---|---| +| `UserEmployeeBinding` | 素材库账号 ↔ 权威人员档案的正式绑定。活跃绑定双向唯一(部分唯一索引,仅约束 `status='ACTIVE'`) | +| `OrganizationUnit` | 共享组织架构,直接镜像 `hr_groups`。上游删除的部门置 `isActive=false` 而非物理删除 | +| `PerformanceSurveyEmployeeProfile` | 事实上的平台级人员主档(表名仍带历史前缀)。新增 `employmentState`、`resignedAt`、`handoverDingtalkUserId` | + +`users.dingtalkUserId` 已加唯一约束,数据库层面杜绝一人多号。 + +### 关键文件 + +| 文件 | 用途 | +|---|---| +| `lib/employee-directory.ts` | **通用人员目录服务**。上层统一通过它访问人员数据,不直接读带 `PerformanceSurvey` 前缀的模型 | +| `lib/organization-sync.ts` | 组织架构同步、在职状态校正、离职联动、每日定时任务 | +| `lib/material-transfer.ts` | 素材正式转移与部门托管 | + +### 在职状态判定 + +**不要用关键词匹配 `employmentStatus`**(上游原样值,口径混杂)。使用规范化的 `employmentState`: + +- `ACTIVE` / `RESIGNED` / `UNKNOWN` +- `UNKNOWN` 视为在职——上游查不到明确状态的人宁可多留,误判导致"人凭空消失"比"多出几个人"难发现 +- 判定条件统一走 `getActivePerformanceSurveyEmployeeProfileWhere()` 或 `getActiveEmployeeWhere()` + +### 离职联动 + +上游标记离职后自动执行:归档素材库账号 + 把素材托管给所在部门。 + +- **默认只处理离职满 30 天者**(`RESIGNATION_MIN_DAYS`),避开交接期、返聘与上游数据延迟 +- 离职日期缺失者暂缓,交人工核实 +- 超级管理员账号不由自动流程处理 + +### 素材归属与署名分离 + +**这是刻意的设计,不是数据不一致:** + +- `Material.userId`(归属)+ `authorName`(署名):素材是谁做的,永远记在谁名下 +- `Material.custodyDepartmentId`(托管部门):人离职后由部门接手维护,**不改 userId 与 authorName** +- 首页卡片显示的作者读的是 `user.realName || username`,托管不影响显示 + +### 定时同步 + +`ensureDailyOrganizationSyncStarted()` 在考核调研上下文中懒启动,每 24 小时执行:组织架构 → 在职状态 → 离职联动。 + +⚠️ **生产容器必须配置 `REPORT_DB_*` 环境变量**,否则同步连不上上游,人员档案会退化成用素材库信息拼的兜底假档案(无工号、无职级、`isLeadership=false`)。 + +### 运维脚本 + +```bash +# 人员架构盘点(纯只读,可重复执行) +npx tsx scripts/diagnose-people-architecture.ts + +# 组织架构与在职状态同步(默认预演,--apply 才写入) +npx tsx --env-file=.env scripts/sync-organization.ts +npx tsx --env-file=.env scripts/sync-organization.ts --apply + +# 离职人员素材部门托管 +npx tsx scripts/custody-archived-user-materials.ts +``` + +--- + ### 数据库配置 **PostgreSQL (192.168.58.20:5432):** @@ -246,12 +343,32 @@ ssh root@192.168.58.20 "cat /root/youyidian-3d/.env 2>/dev/null || echo 'No .env **当前部署流程:** 1. 源码先提交并推送到 Git 仓库 -2. 构建服务器 (192.168.42.134) 增量拉取指定提交 -3. 构建服务器构建 Docker 镜像并推送到本地镜像仓库 (192.168.42.134:5000) +2. 构建服务器 (192.168.40.206) 增量拉取指定提交 +3. 构建服务器构建 Docker 镜像并推送到本地镜像仓库 (192.168.40.206:5000) 4. 生产服务器 (192.168.58.20) 拉取指定提交镜像 5. 替换正式容器或影子容器 6. 检查 `/api/health`,失败时默认尝试回滚旧镜像 +**⚠️ 部署前置条件(缺一不可):** + +脚本由维护人员执行,需要以下环境变量,且**必须在内网环境**(构建机 192.168.40.206 走公网穿透连不上,表现为 TCP 端口"可达"但 SSH 握手失败——穿透代理对任何连接都返回成功,端口连通性测试是假阳性): + +```powershell +$env:BUILD_SERVER_PASSWORD = '构建机密码' # 脚本用 plink -pw,不走 SSH 密钥 +$env:BUILD_SERVER_HOST_KEY = 'SHA256:...' # 已人工核对的构建机指纹 +$env:PROD_SERVER_HOST_KEY = 'SHA256:...' # 已人工核对的生产机指纹 +``` + +已记录的主机指纹(来自 `~/.ssh/known_hosts`,生产机已现场验证一致): +- 生产机 192.168.58.20:`SHA256:b0qukGVPuI8j+lykfCIplkcKWh7d4Pq9APzvr4FDnss` +- 构建机 192.168.40.206:`SHA256:MXW5w48SFSTUaqS+dS1G9hY228hI6SLepjhCmItHL04` + +正式部署必须显式指定 40 位提交 SHA,脚本不接受"分支最新提交": + +```powershell +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' +``` + `deploy-remote-build.ps1` 属于本地历史脚本名,仅作为识别应急资料使用,不是仓库交付入口,也不再作为默认部署入口。旧的“本机打包源码 -> 上传到构建机”链路不再作为当前标准口径。 详细部署规范见: @@ -566,6 +683,11 @@ catch (error) { 10. **构建缓存** - 默认保留构建缓存加快构建,只在必要时使用 `-AggressiveBuildCacheCleanup` 11. **Alpine 镜像源** - 使用清华源,避免官方源在国内网络环境下的连接问题 12. **音频/文件访问路由** - `.dockerignore` 和部署脚本中排除 `uploads` 时要精确,避免误伤 `app/api/uploads` 路由 +13. **人员身份判定** - 只认钉钉唯一值。钉钉登录遇到同名账号会**拒绝并转人工**,不再自动合并——这是有意的摩擦,防止重名变成越权 +14. **在职状态** - 不要用关键词匹配 `employmentStatus`,用 `employmentState`。上游有数字码、中文、空值三种口径并存 +15. **素材署名与归属分离** - 署名和归属对不上通常是**正确的**(素材转过手,署名保留原作者),不要"修正"成一致 +16. **`Material.user` 是级联删除** - 一旦有代码真的删除 User,会连带硬删其全部素材。目前全项目无硬删除调用,靠"没人碰"撑着而非数据库拦着;引入离职下线流程前必须先改掉 +17. **上游连接密码含特殊字符** - `.env` 中含 `#` 的值必须加引号,否则被当注释截断 ### 最近修复的问题(2026-04-30 至 2026-05-05) @@ -599,6 +721,22 @@ catch (error) { - 历史数据回填:2730 条 SKU 的 `white_background_uploaded_at` 字段(使用资产最后创建时间) - 历史数据默认值:`white_background_source_type` 统一回填为"渲染" +**2026-08-13:人员架构统一到钉钉唯一值** + +执行了三个迁移(均已在生产库 `lobechat` 应用): +- `20260813_add_user_employee_binding`:绑定表 + `users.dingtalkUserId` 唯一约束 +- `20260813_add_material_custody_department`:素材托管部门 +- `20260813_add_organization_units_and_employment_state`:组织架构表 + 规范化在职状态 + +数据操作结果: +- 建立 503 条账号↔人员绑定,覆盖活跃账号的 96.3%(余下 18 个无钉钉 ID,需人工确认) +- 同步 1075 个部门,单根树 5 层,零孤儿节点 +- 校正在职状态:1285 在职 / 219 离职 / 214 未知(此前 216 条已离职档案中有 215 条被误判为在职) +- 离职联动:归档 139 个账号(离职满 30 天者),39 人暂缓 +- 素材部门托管:3775 条,孤儿素材清零 +- 回填 1807 条被误存为邮箱的作者名(全部来自任务-详情入口的历史缺陷) +- 账号治理:处置 1 个"组织上查无此人"的 ADMIN 账号(素材移交后撤权归档) + --- ## 👤 开发者偏好