支持企微通讯录缺失手机号(07-26-internal-ops-recovery-baseline)
这个提交包含在:
@@ -168,9 +168,10 @@ WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
|
||||
- `login|bind` always keeps `snsapi_base`; only explicit `onboard` uses
|
||||
`snsapi_privateinfo`.
|
||||
- The member must be visible to the app, active, return the same userid from
|
||||
both identity calls, and authorize a valid phone plus personal email or
|
||||
business email. A blank/invalid personal email may fall back to a valid
|
||||
business email.
|
||||
both identity calls, and authorize a valid personal email or business email.
|
||||
Phone may be absent; when returned it must be normalized and checked for
|
||||
uniqueness. A blank/invalid personal email may fall back to a valid business
|
||||
email.
|
||||
- `user_ticket`, OAuth code, access token and raw provider responses exist only
|
||||
in request memory. Do not persist them or include them in Cookie, audit,
|
||||
response details or logs; provider/network failures use fixed safe codes.
|
||||
@@ -190,8 +191,9 @@ WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
|
||||
- Provider/network failure -> `wecom_api_failed` or safe auth error; no raw
|
||||
payload, token or ticket in output/logs.
|
||||
- Userid mismatch -> `identity_mismatch`; inactive member -> `member_inactive`.
|
||||
- Missing/invalid phone, both emails invalid, or unresolved department ->
|
||||
`missing_required_field`.
|
||||
- Both emails invalid or unresolved department -> `missing_required_field`.
|
||||
- Phone absent -> continue with `NULL`; phone present but conflicting ->
|
||||
`employee_conflict`.
|
||||
- Multiple employees, unique-field race or cross-binding -> `employee_conflict`.
|
||||
- Department binding points to missing/archived data -> `department_invalid`;
|
||||
multiple same-name OA departments -> `department_ambiguous`.
|
||||
@@ -232,3 +234,92 @@ ordinary login -> snsapi_base
|
||||
one-time onboard -> explicit private consent -> safe unique employee-only write
|
||||
business role assignment -> separate administrator-audited operation
|
||||
```
|
||||
|
||||
## Scenario: WeCom Directory Export With Optional Phone
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: importing an administrator-exported WeCom directory XLSX, or
|
||||
changing `Employee.phone` across database, API, HR sync, OAuth and UI layers.
|
||||
- Purpose: allow a real member to remain temporarily without a phone while
|
||||
preserving stable identity, uniqueness and privacy controls.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- DB: `Employee.phone String? @unique`; migration drops only the `NOT NULL`
|
||||
constraint and keeps the unique index.
|
||||
- CLI: `npm run import:wecom-directory-export -- --input=- --dry-run`.
|
||||
- Apply: `WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply
|
||||
--actor-employee-no=<employee-no>`.
|
||||
- User API and frontend employee/user types expose `phone: string | null`;
|
||||
create/update accepts an omitted or explicit `null` phone.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Missing phone is stored as SQL `NULL`, never `""`, a repeated placeholder or
|
||||
a synthesized fake number. Multiple `NULL` rows are valid; non-null phones
|
||||
remain unique.
|
||||
- WeCom account and valid enterprise email remain required for directory-export
|
||||
onboarding. Account, non-null phone and enterprise email duplicates block
|
||||
the input.
|
||||
- An empty source phone never clears an existing OA phone. Only an explicitly
|
||||
supplied valid phone participates in update and unique-conflict checks.
|
||||
- The XLSX is accepted only from stdin and defaults to dry-run. Apply requires
|
||||
both `--apply` and `WECOM_DIRECTORY_EXPORT_APPLY=1`, plus an active actor.
|
||||
- CLI output contains counts and categories only. The workbook, full local path,
|
||||
names, accounts, phones, emails, departments and raw rows must not enter Git,
|
||||
Trellis artifacts or reports.
|
||||
- New accounts receive only `employee`; finance, CEO and admin roles remain a
|
||||
separate administrator-audited operation.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Missing phone cell -> valid profile with `phone=null`.
|
||||
- Supplied malformed phone -> incomplete/blocked; do not downgrade it to an
|
||||
absent phone and do not write it.
|
||||
- Missing/invalid enterprise email, account, name or department -> skipped or
|
||||
blocked as `missing_required_field`.
|
||||
- Duplicate account/non-null phone/enterprise email in the workbook ->
|
||||
`duplicate_values`; error output contains counts only.
|
||||
- Existing OA unique-field or binding conflict -> conflict; no automatic merge.
|
||||
- `--apply` without confirmation env or active actor -> refuse before sync.
|
||||
- Migration rollback with any `phone IS NULL` row -> stop; do not fill fake
|
||||
values merely to restore `NOT NULL`.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: five members have unique phones, one member has no phone, all six have
|
||||
unique accounts and enterprise emails; dry-run plans six records and reports
|
||||
only aggregate counts.
|
||||
- Base: a later export still omits a member phone; the member's existing OA
|
||||
phone is preserved.
|
||||
- Bad: write an empty string or shared placeholder, use personal email in place
|
||||
of the required enterprise email for bulk identity, or auto-grant a business
|
||||
role during import.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Prisma validation plus a migration review proving only `DROP NOT NULL`.
|
||||
- Unit tests for instruction rows/Chinese headers, five-present/one-missing,
|
||||
malformed phone, missing enterprise email and duplicate account/phone/email.
|
||||
- Route/service tests for create/update/import `null`, non-empty unique checks,
|
||||
OAuth missing phone and empty-source preservation of an existing phone.
|
||||
- Frontend typecheck/build and displays that render a clear empty-phone label.
|
||||
- Full backend regression, Trellis governance, sensitive-content scan and
|
||||
`git diff --check`; a real workbook may be parsed only into aggregate counts
|
||||
and remains Git-ignored.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
phone: source.phone || "00000000000"
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
phone: source.phone || null
|
||||
// Existing employee updates omit `phone` entirely when the source is empty.
|
||||
```
|
||||
|
||||
@@ -108,6 +108,15 @@ GET /auth/wecom/login?mode=onboard
|
||||
- 已有绑定员工只补齐经授权的规范化资料且不改角色;新员工在单事务内创建 employee、`employee` 角色、企微绑定和脱敏审计。任何一步失败均不留下部分账号。
|
||||
- 自助开通不替代管理员职责分配;`finance`、`ceo` 仍沿用角色接口的 token version、会话撤销和审计合同。
|
||||
|
||||
## 企微通讯录导出与手机号可空
|
||||
|
||||
- 管理员导出的 XLSX 只作为一次性受控输入,不进入 Git、Trellis、报告或生产文件系统;CLI 支持从 stdin 读取,默认仅 dry-run。
|
||||
- 入口识别企微官方说明行和中文表头,将姓名、账号、职务、部门、手机、邮箱/企业邮箱、激活/禁用状态归一化为既有 HR master profile,再复用部门解析、员工匹配、企微绑定、审计和 `employee` 初始角色语义。
|
||||
- `Employee.phone` 改为 nullable unique:缺失值统一保存为 `NULL`,非空值继续唯一。企业邮箱仍必填且唯一,企微账号仍是绑定主键;不生成占位手机号。
|
||||
- 来源未提供手机号时,不得把既有 OA 手机号清空;来源提供手机号时才进入变更和唯一性检查。本人授权同样允许手机号缺失,但邮箱或企业邮箱仍必须有效。
|
||||
- apply 需要命令参数和独立环境变量双门,且真实运行另行批准;dry-run 与 apply 的报告都只含计数、动作、冲突类别和脱敏摘要。
|
||||
- 生产迁移回滚前必须确认不存在 `phone IS NULL`;否则停止回滚,不用假数据填充。
|
||||
|
||||
## 部署目录统一设计约束
|
||||
|
||||
- 现场是混合部署,不能把“统一目录”处理成普通 rsync:必须以当前活动 PostgreSQL 数据和附件挂载为唯一数据事实源。
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
- [x] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
|
||||
- [ ] 用户在企微应用详情选择手机、邮箱、企业邮箱敏感字段;部署开关开启后,两名目标分别通过一次性链接本人确认授权。
|
||||
- [ ] 两人自助开通成功后写入获批 `finance` / `ceo` 职责,复核不同身份、maker-checker、会话撤销、审计和付款零写入。
|
||||
- [x] 将 `Employee.phone` 改为 nullable unique,并同步员工创建/批量导入、企微 HR master、本人授权、登录查询和前端空值合同;不执行生产 migration。
|
||||
- [x] 增加企微通讯录 XLSX/stdin 默认 dry-run 入口,复用 HR master 写入语义,apply 使用独立双门且本阶段不执行。
|
||||
- [x] 用完全合成的通讯录覆盖“5 个手机号 + 1 个缺失”、重复账号/手机号/邮箱、说明行/中文表头、既有手机号不被空来源清除和报告隐私门禁。
|
||||
|
||||
## 推荐文件边界
|
||||
|
||||
@@ -52,6 +55,7 @@
|
||||
- `cd server && npm run prisma:validate`
|
||||
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js`
|
||||
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js tests/unit/wecomOAuthOnboarding.service.test.js`
|
||||
- `cd server && npm test -- --run tests/unit/routes/users.import.test.js tests/unit/wecomHrMaster.service.test.js tests/unit/wecomOAuthOnboarding.service.test.js tests/unit/scripts/import-wecom-directory-export.test.js`
|
||||
- `bash -n deploy/trialrun/deploy.sh`
|
||||
- `bash -n deploy/trialrun/install-warch3-ops-timer.sh`
|
||||
|
||||
@@ -71,6 +75,13 @@
|
||||
- 已从活动 DB 容器与活动 server 附件挂载生成正确批次并复制到 ARM 备份机;DB catalog、manifest、源/目标产物 SHA-256、附件 tar 与 0 个普通文件计数均通过,且未伪造旧实例 build SHA。ARM 只承担异机备份与恢复演练职责,不运行 OA 服务。
|
||||
- 已在 ARM 备份机用不发布端口的一次性 PostgreSQL 16 容器恢复正确批次:58 项成功迁移、12 张关键表计数和 0 个附件普通文件均与冻结基线一致,包含镜像拉取的实测 RTO 为 33 秒。演练后容器、临时附件目录和本轮新增镜像均已删除,未运行 OA server;应用 `/ready` 留待生产部署环境验证。
|
||||
|
||||
## 手机号可空与通讯录导入本地证据(2026-07-27)
|
||||
|
||||
- 后端全量 Vitest:163 个测试文件、1016 个测试通过;补充双 apply 门断言后,通讯录导入聚焦 8 项再次通过。新增覆盖普通用户创建/显式清空、CSV 空手机号、HR 空来源保留既有值、OAuth 无手机号建档、通讯录说明行/中文表头、企业邮箱门禁、非法手机号、重复字段、apply 双门及脱敏状态。
|
||||
- `prisma validate`、四个后端改动文件语法、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。
|
||||
- 管理员提供的真实通讯录仅执行解析级只读检查:6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库、未执行 apply、未分配角色、未发送企微消息,文件保持 Git 排除。
|
||||
- 最终只读复核确认 migration 仅删除手机号 `NOT NULL`、唯一索引保留;导入及本人开通只创建 `employee`,不自动授予 `finance`、`ceo`、`admin` 或 `system_admin`;空来源更新省略 phone 字段,真实文件名、完整路径和个人字段未进入差异。
|
||||
|
||||
## 生产部署与 timer 收口证据(2026-07-27)
|
||||
|
||||
- 已在获批停机窗口把生产切换到唯一 Compose project;活动 DB 与附件从停机冻结副本进入新的独立目录,旧容器保持停止,旧数据目录原位保留作回滚点。
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- 经用户逐项授权后,修正企微通讯录与人事助手的双 Secret 读取分工,并用脱敏 dry-run 唯一确认两名既有企微成员。
|
||||
- 唯一匹配且必填资料、冲突检查通过后,将两名成员同步为 OA 员工,并分别配置 `employee + finance` 与 `employee + ceo`,形成不同自然人的付款制单/复核职责。
|
||||
- 增加独立的一次性企微敏感资料授权入口:普通登录继续使用 `snsapi_base`,首次开通使用 `snsapi_privateinfo`;未绑定的在职可见成员授权成功后只自动获得 `employee` 角色。
|
||||
- 支持管理员提供的企微通讯录 XLSX 通过 stdin 做脱敏 dry-run,并允许个别成员暂缺手机号;企业邮箱、企微账号、姓名和部门仍作为建档门禁。
|
||||
- 修复部署脚本与现场端口、目录、容器和构建标识契约,统一真实运行来源;切换前保留活动数据、环境文件和旧容器回滚点。
|
||||
|
||||
## Requirements
|
||||
@@ -37,10 +38,11 @@
|
||||
- 将真实内部实例核查列为后续同任务外部门禁:运行 commit/访问方式/TLS、真实职责分配、异机副本和恢复后 `/ready` 均需另行批准和脱敏留证。
|
||||
- 企微通讯录 Secret 只用于 `/user/list_id`;自建应用 Secret 用于 `/user/get`、`/department/list` 和获授权的人事助手 `/hr/get_staff_info`,不得回退到已废弃的人事助手 Secret。
|
||||
- 真实同步前只输出匹配数量、身份是否不同、缺失字段数、冲突数和计划创建/更新数,不输出姓名、userid、手机号、邮箱、部门原文或原始 payload。
|
||||
- `Employee.phone` 允许为 `NULL`,但不得写空字符串或假手机号;非空手机号继续保持唯一。企微账号和企业邮箱必须非空且唯一,缺手机号成员使用企微或邮箱登录,后续可由本人授权或管理员补齐。
|
||||
- 真实写入必须复用现有员工同步、角色、会话撤销和审计语义;财务处理人与 CEO 复核人必须是两个不同企微身份,且不得把 `admin` 当作普通业务复核角色。
|
||||
- 敏感资料授权必须显式受 `WECOM_OAUTH_ONBOARDING_ENABLED` 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并实际同意手机号及邮箱或企业邮箱授权。
|
||||
- 敏感资料授权必须显式受 `WECOM_OAUTH_ONBOARDING_ENABLED` 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并至少实际同意邮箱或企业邮箱授权;手机号可暂缺,但返回时必须规范化并参与唯一性检查。
|
||||
- `user_ticket` 只在服务端内存中用于一次 `getuserdetail` 调用,不写日志、Cookie、数据库、审计或错误详情;授权 code 和原始敏感响应同样不得持久化。
|
||||
- 自助开通按企微 userid、手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
|
||||
- 自助开通按企微 userid、非空手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
|
||||
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
|
||||
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
|
||||
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
|
||||
@@ -66,14 +68,15 @@
|
||||
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
|
||||
- [x] 部署入口拒绝隐式/相对数据目录及缺少数据模式,成对 manifest 可记录显式 build SHA 或活动镜像 digest,并有 Node/Python 等价测试。
|
||||
- [ ] 两名目标成员分别完成一次本人授权后,账号唯一、企微绑定不同,再由管理员写入 `employee + finance` 与 `employee + ceo`;maker-checker、会话撤销和角色审计通过。
|
||||
- [x] 本地合成企微导出证明 5 人有手机号、1 人手机号为空时 dry-run 可通过;空值保存为 `NULL`,非空手机号/邮箱/企微账号重复均被拒绝,报告不含个人字段原文。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不读取真实附件正文,不执行生产恢复、数据库 reset、迁移历史改写或付款/报销业务写入。
|
||||
- 不修改 Prisma schema、公开业务 API、权限模型、审批链或付款状态机;仅修复既有 HR 同步服务的凭证分工,并通过既有角色模型写入获批职责。
|
||||
- 除已批准的 `Employee.phone` nullable 兼容外,不扩大修改其他 Prisma schema、权限模型、审批链或付款状态机;公开用户 API 仅同步手机号可空合同,并通过既有角色模型写入获批职责。
|
||||
- 不引入 MinIO、云备份 SDK、全量灾备平台、自动跨地域复制或 24×7 值守。
|
||||
- 不在仓库提交备份文件、恢复数据、环境变量、凭证、真实账号或完整本地路径。
|
||||
- 不发送企微消息、不创建企微审批模板、不把手机号/邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
|
||||
- 不发送企微消息、不创建企微审批模板、不把邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
|
||||
- 不在本任务完成企微调休、导航、报销或模块退役;它们属于后续批次。
|
||||
|
||||
## Test Plan
|
||||
@@ -97,13 +100,16 @@
|
||||
|
||||
先改进可恢复性和防误操作,再接触真实环境。默认工作流只支持影子恢复;真实恢复不是普通脚本路径,而是需要停机、负责人确认、成对备份、回滚点和独立授权的事故操作。
|
||||
|
||||
企微敏感字段采用“日常静默登录 + 一次性本人授权开通”的双入口。批量同步不猜测敏感资料,不修改全局员工字段可空性;自助开通只授予 `employee`,业务角色继续由管理员审计写入。
|
||||
企微敏感字段采用“日常静默登录 + 一次性本人授权开通”的双入口。批量同步不猜测敏感资料;手机号按已批准范围改为全局 nullable,空值统一保存为 `NULL`。自助开通只授予 `employee`,业务角色继续由管理员审计写入。
|
||||
|
||||
## Local Evidence
|
||||
|
||||
- 已使用一次性 PostgreSQL 16 容器和合成附件完成多轮往返演练,最近一次本地参考 RTO 为 6 秒。
|
||||
- 演练覆盖 manifest/校验和、错误确认、正式风格库名、损坏 DB、损坏 tar、非空附件目录、默认试运行附件目录和危险 tar 路径拒绝。
|
||||
- 该证据不包含真实业务数据、真实运行实例、异机副本或部署后 `/ready`,因此最后一项验收保持未完成。
|
||||
- 2026-07-27 完成手机号 nullable 本地切片:Prisma 仅新增 `phone DROP NOT NULL` migration,用户创建/更新/批量导入、HR master、本人授权和前端空值合同已同步;本轮没有执行 migration 或写入真实员工。
|
||||
- 合成通讯录覆盖说明行/中文表头、5 个手机号加 1 个缺失、非法手机号、缺企业邮箱、重复账号/非空手机号/企业邮箱、双重 apply 门和脱敏报告。管理员提供的真实 XLSX 仅在内存中只读解析为 6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库,文件保持 Git 排除。
|
||||
- 最终本地门禁:后端全量 163 个测试文件、1016 个测试通过;补充双 apply 门测试后,通讯录导入聚焦 8 项再次通过。Prisma validate、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。文档审计保留仓库既有 22 个未注册文件 warning,无新增 error。
|
||||
|
||||
## Live Read-Only Evidence
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户