修复企微通讯录生产导入兼容性(07-26-internal-ops-recovery-baseline)
这个提交包含在:
@@ -251,6 +251,9 @@ business role assignment -> separate administrator-audited operation
|
|||||||
- CLI: `npm run import:wecom-directory-export -- --input=- --dry-run`.
|
- CLI: `npm run import:wecom-directory-export -- --input=- --dry-run`.
|
||||||
- Apply: `WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply
|
- Apply: `WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply
|
||||||
--actor-employee-no=<employee-no>`.
|
--actor-employee-no=<employee-no>`.
|
||||||
|
- Binary stdin reader: `fs.readFileSync(0)`; do not assume
|
||||||
|
`fs.promises.readFile(0)` accepts a numeric file descriptor on the production
|
||||||
|
Node runtime.
|
||||||
- User API and frontend employee/user types expose `phone: string | null`;
|
- User API and frontend employee/user types expose `phone: string | null`;
|
||||||
create/update accepts an omitted or explicit `null` phone.
|
create/update accepts an omitted or explicit `null` phone.
|
||||||
|
|
||||||
@@ -266,6 +269,9 @@ business role assignment -> separate administrator-audited operation
|
|||||||
supplied valid phone participates in update and unique-conflict checks.
|
supplied valid phone participates in update and unique-conflict checks.
|
||||||
- The XLSX is accepted only from stdin and defaults to dry-run. Apply requires
|
- 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.
|
both `--apply` and `WECOM_DIRECTORY_EXPORT_APPLY=1`, plus an active actor.
|
||||||
|
- Read the XLSX directly from fd 0 without a temporary file. A runtime that
|
||||||
|
rejects the stdin read must stop before Prisma sync; do not fall back to
|
||||||
|
persisting the workbook on the host or in the container.
|
||||||
- CLI output contains counts and categories only. The workbook, full local path,
|
- CLI output contains counts and categories only. The workbook, full local path,
|
||||||
names, accounts, phones, emails, departments and raw rows must not enter Git,
|
names, accounts, phones, emails, departments and raw rows must not enter Git,
|
||||||
Trellis artifacts or reports.
|
Trellis artifacts or reports.
|
||||||
@@ -283,6 +289,9 @@ business role assignment -> separate administrator-audited operation
|
|||||||
`duplicate_values`; error output contains counts only.
|
`duplicate_values`; error output contains counts only.
|
||||||
- Existing OA unique-field or binding conflict -> conflict; no automatic merge.
|
- Existing OA unique-field or binding conflict -> conflict; no automatic merge.
|
||||||
- `--apply` without confirmation env or active actor -> refuse before sync.
|
- `--apply` without confirmation env or active actor -> refuse before sync.
|
||||||
|
- Promise-based stdin fd read returns `ERR_INVALID_ARG_TYPE` on the deployed
|
||||||
|
Node runtime -> no sync/write; use the fd-compatible reader and rerun dry-run
|
||||||
|
before requesting apply approval.
|
||||||
- Migration rollback with any `phone IS NULL` row -> stop; do not fill fake
|
- Migration rollback with any `phone IS NULL` row -> stop; do not fill fake
|
||||||
values merely to restore `NOT NULL`.
|
values merely to restore `NOT NULL`.
|
||||||
|
|
||||||
@@ -293,6 +302,8 @@ business role assignment -> separate administrator-audited operation
|
|||||||
only aggregate counts.
|
only aggregate counts.
|
||||||
- Base: a later export still omits a member phone; the member's existing OA
|
- Base: a later export still omits a member phone; the member's existing OA
|
||||||
phone is preserved.
|
phone is preserved.
|
||||||
|
- Base: the local and production Node versions differ; the same binary stdin
|
||||||
|
workbook parses through `fs.readFileSync(0)` without a temporary file.
|
||||||
- Bad: write an empty string or shared placeholder, use personal email in place
|
- 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
|
of the required enterprise email for bulk identity, or auto-grant a business
|
||||||
role during import.
|
role during import.
|
||||||
@@ -302,6 +313,8 @@ business role assignment -> separate administrator-audited operation
|
|||||||
- Prisma validation plus a migration review proving only `DROP NOT NULL`.
|
- Prisma validation plus a migration review proving only `DROP NOT NULL`.
|
||||||
- Unit tests for instruction rows/Chinese headers, five-present/one-missing,
|
- Unit tests for instruction rows/Chinese headers, five-present/one-missing,
|
||||||
malformed phone, missing enterprise email and duplicate account/phone/email.
|
malformed phone, missing enterprise email and duplicate account/phone/email.
|
||||||
|
- Unit regression asserts the CLI stdin helper calls `fs.readFileSync(0)`;
|
||||||
|
production dry-run must prove aggregate parsing/sync counts before apply.
|
||||||
- Route/service tests for create/update/import `null`, non-empty unique checks,
|
- Route/service tests for create/update/import `null`, non-empty unique checks,
|
||||||
OAuth missing phone and empty-source preservation of an existing phone.
|
OAuth missing phone and empty-source preservation of an existing phone.
|
||||||
- Frontend typecheck/build and displays that render a clear empty-phone label.
|
- Frontend typecheck/build and displays that render a clear empty-phone label.
|
||||||
@@ -323,3 +336,15 @@ phone: source.phone || "00000000000"
|
|||||||
phone: source.phone || null
|
phone: source.phone || null
|
||||||
// Existing employee updates omit `phone` entirely when the source is empty.
|
// Existing employee updates omit `phone` entirely when the source is empty.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Wrong
|
||||||
|
|
||||||
|
```js
|
||||||
|
const workbook = await fs.promises.readFile(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Correct
|
||||||
|
|
||||||
|
```js
|
||||||
|
const workbook = fs.readFileSync(0);
|
||||||
|
```
|
||||||
|
|||||||
@@ -75,14 +75,20 @@ isolated drill
|
|||||||
|
|
||||||
## 真实职责写入门
|
## 真实职责写入门
|
||||||
|
|
||||||
- 先读取全量候选并在内存中按目标姓名匹配,只输出两个匹配计数、身份哈希是否不同、缺失字段数和计划动作数量。
|
- 从管理员通讯录文件在本地内存中筛选两名目标,只把两人子集通过 stdin 送入生产 dry-run/apply;其余四人不进入生产输入、不创建账号,留待后续独立批次。
|
||||||
|
- dry-run 和 apply 只输出两个匹配计数、身份是否不同、缺失字段数、冲突数和计划动作数量,不持久化筛选文件、完整路径或原始行。
|
||||||
- 只有两个匹配计数均为 1、企微身份不同、均为 active、必填资料完整且手机号/邮箱不冲突时,才进入 apply。
|
- 只有两个匹配计数均为 1、企微身份不同、均为 active、必填资料完整且手机号/邮箱不冲突时,才进入 apply。
|
||||||
- 员工和企微绑定通过既有 HR master sync 服务创建或更新;随后通过既有角色写入语义分别设置 `employee + finance` 和 `employee + ceo`。
|
- 员工和企微绑定通过既有 HR master sync 服务创建或更新;随后通过既有角色写入语义分别设置 `employee + finance` 和 `employee + ceo`。
|
||||||
- 角色变更递增 `token_version`、撤销活动会话并写 `SYSTEM_USER_ROLES_SET` 审计;最终只核对数量、角色集合、不同 employee id、审计存在和付款业务零写入。
|
- `finance` 的批准范围明确包含 `payroll.manage`、`employee.bank.read`、财务付款和付款批次管理,不包含 `asset.transfer`;不以 `finance_mgr` 解决 CEO 本人报销兜底。
|
||||||
|
- 角色变更递增 `token_version`、撤销活动会话并写 `SYSTEM_USER_ROLES_SET` 审计;最终只核对数量、角色集合、不同 employee id、角色待办计数变化、审计存在和付款业务零写入。
|
||||||
|
- maker-checker 只验证付款批次创建人不能复核同一批次,不宣称费用审批与财务处理完全分离;生产不创建测试付款批次,使用合成路由测试和生产只读身份/权限证据。
|
||||||
|
- 回滚是通过相同角色接口恢复两人的写入前角色集合,并再次撤销会话、保留审计;不得通过整库恢复回退角色。
|
||||||
- 任一门禁失败即停止,不进行部分角色写入;如员工同步已经成功而角色阶段失败,保留同步审计并立即报告,不用删除真实员工记录进行破坏性回滚。
|
- 任一门禁失败即停止,不进行部分角色写入;如员工同步已经成功而角色阶段失败,保留同步审计并立即报告,不用删除真实员工记录进行破坏性回滚。
|
||||||
|
|
||||||
## 企微本人敏感信息授权边界
|
## 企微本人敏感信息授权边界
|
||||||
|
|
||||||
|
- 本轮生产身份建档不走本人授权,而走管理员通讯录两人子集。以下 OAuth 合同继续保留为已实现、默认关闭的未来补资料能力,本轮验收标记为不适用,不要求成员点击授权链接。
|
||||||
|
|
||||||
- `/user/get` 和人事助手批量读取继续只负责 userid、姓名、部门等可读字段;不得把批量缺字段误判为企微完全不支持手机号和邮箱。
|
- `/user/get` 和人事助手批量读取继续只负责 userid、姓名、部门等可读字段;不得把批量缺字段误判为企微完全不支持手机号和邮箱。
|
||||||
- 2022-06-20 后新建自建应用的敏感字段走独立 OAuth 数据流:管理员在应用详情选择字段,成员以 `snsapi_privateinfo` 手工确认,`getuserinfo` 返回短时 `user_ticket`,服务端立即调用 `/cgi-bin/auth/getuserdetail`。
|
- 2022-06-20 后新建自建应用的敏感字段走独立 OAuth 数据流:管理员在应用详情选择字段,成员以 `snsapi_privateinfo` 手工确认,`getuserinfo` 返回短时 `user_ticket`,服务端立即调用 `/cgi-bin/auth/getuserdetail`。
|
||||||
- `user_ticket`、授权 code 和完整敏感响应不得写日志、任务证据或审计 extra;只保存业务所需的规范化手机号/邮箱及字段存在性审计。
|
- `user_ticket`、授权 code 和完整敏感响应不得写日志、任务证据或审计 extra;只保存业务所需的规范化手机号/邮箱及字段存在性审计。
|
||||||
@@ -115,6 +121,7 @@ GET /auth/wecom/login?mode=onboard
|
|||||||
- `Employee.phone` 改为 nullable unique:缺失值统一保存为 `NULL`,非空值继续唯一。企业邮箱仍必填且唯一,企微账号仍是绑定主键;不生成占位手机号。
|
- `Employee.phone` 改为 nullable unique:缺失值统一保存为 `NULL`,非空值继续唯一。企业邮箱仍必填且唯一,企微账号仍是绑定主键;不生成占位手机号。
|
||||||
- 来源未提供手机号时,不得把既有 OA 手机号清空;来源提供手机号时才进入变更和唯一性检查。本人授权同样允许手机号缺失,但邮箱或企业邮箱仍必须有效。
|
- 来源未提供手机号时,不得把既有 OA 手机号清空;来源提供手机号时才进入变更和唯一性检查。本人授权同样允许手机号缺失,但邮箱或企业邮箱仍必须有效。
|
||||||
- apply 需要命令参数和独立环境变量双门,且真实运行另行批准;dry-run 与 apply 的报告都只含计数、动作、冲突类别和脱敏摘要。
|
- apply 需要命令参数和独立环境变量双门,且真实运行另行批准;dry-run 与 apply 的报告都只含计数、动作、冲突类别和脱敏摘要。
|
||||||
|
- 本轮在内存中筛选两名目标后再送入 stdin;筛选结果不落盘,目标外四人不进入 dry-run/apply,也不因出现在原始导出文件中而获得 OA 活跃账号。
|
||||||
- 生产迁移回滚前必须确认不存在 `phone IS NULL`;否则停止回滚,不用假数据填充。
|
- 生产迁移回滚前必须确认不存在 `phone IS NULL`;否则停止回滚,不用假数据填充。
|
||||||
|
|
||||||
## 部署目录统一设计约束
|
## 部署目录统一设计约束
|
||||||
|
|||||||
@@ -27,8 +27,12 @@
|
|||||||
- [x] 让成对 manifest 尊重显式 `BUILD_SHA` 并支持活动镜像 digest;补 Node/Python 等价与非法来源标识负向测试。
|
- [x] 让成对 manifest 尊重显式 `BUILD_SHA` 并支持活动镜像 digest;补 Node/Python 等价与非法来源标识负向测试。
|
||||||
- [x] 把旧 Compose project 实测、镜像 digest、58 项迁移与关键表计数、影子逐表/附件计数验收、停机后复制并保留旧目录写入 runbook/checklist/spec。
|
- [x] 把旧 Compose project 实测、镜像 digest、58 项迁移与关键表计数、影子逐表/附件计数验收、停机后复制并保留旧目录写入 runbook/checklist/spec。
|
||||||
- [x] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
|
- [x] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
|
||||||
- [ ] 用户在企微应用详情选择手机、邮箱、企业邮箱敏感字段;部署开关开启后,两名目标分别通过一次性链接本人确认授权。
|
- [x] 本轮不适用/延后:不启用企微敏感资料 OAuth 开关,不要求两名目标点击一次性授权;入口保持默认关闭,未来仅用于本人补资料。
|
||||||
- [ ] 两人自助开通成功后写入获批 `finance` / `ceo` 职责,复核不同身份、maker-checker、会话撤销、审计和付款零写入。
|
- [x] 从管理员通讯录在内存中筛选两名目标,先执行生产脱敏 dry-run;其余四人不进入输入、不创建账号,另轮处理。
|
||||||
|
- [ ] dry-run 零冲突后另行批准 apply,确认两人账号/企微绑定唯一且初始角色严格为 `employee`,不持久化通讯录文件。
|
||||||
|
- [ ] 记录两人原角色、活跃会话、角色待办和审计基线,再分别写入 `employee + finance` / `employee + ceo`;明确 `finance` 含薪资/银行资料权限且不含 `asset.transfer`,不授予 `finance_mgr`。
|
||||||
|
- [ ] 复核角色接口递增 token version、撤销会话和 `SYSTEM_USER_ROLES_SET` 审计;以合成测试证明付款批次 maker-checker,不在生产创建测试付款数据,也不把结论扩大为费用审批完全分离。
|
||||||
|
- [ ] 如需回滚,只通过相同角色接口恢复两人原角色集合并复核会话/审计,不恢复整库;记录 CEO 既有角色待办可见性变化及本人无主管报销继续阻断的已知限制。
|
||||||
- [x] 将 `Employee.phone` 改为 nullable unique,并同步员工创建/批量导入、企微 HR master、本人授权、登录查询和前端空值合同;不执行生产 migration。
|
- [x] 将 `Employee.phone` 改为 nullable unique,并同步员工创建/批量导入、企微 HR master、本人授权、登录查询和前端空值合同;不执行生产 migration。
|
||||||
- [x] 增加企微通讯录 XLSX/stdin 默认 dry-run 入口,复用 HR master 写入语义,apply 使用独立双门且本阶段不执行。
|
- [x] 增加企微通讯录 XLSX/stdin 默认 dry-run 入口,复用 HR master 写入语义,apply 使用独立双门且本阶段不执行。
|
||||||
- [x] 用完全合成的通讯录覆盖“5 个手机号 + 1 个缺失”、重复账号/手机号/邮箱、说明行/中文表头、既有手机号不被空来源清除和报告隐私门禁。
|
- [x] 用完全合成的通讯录覆盖“5 个手机号 + 1 个缺失”、重复账号/手机号/邮箱、说明行/中文表头、既有手机号不被空来源清除和报告隐私门禁。
|
||||||
@@ -81,6 +85,8 @@
|
|||||||
- `prisma validate`、四个后端改动文件语法、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。
|
- `prisma validate`、四个后端改动文件语法、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。
|
||||||
- 管理员提供的真实通讯录仅执行解析级只读检查:6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库、未执行 apply、未分配角色、未发送企微消息,文件保持 Git 排除。
|
- 管理员提供的真实通讯录仅执行解析级只读检查:6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库、未执行 apply、未分配角色、未发送企微消息,文件保持 Git 排除。
|
||||||
- 最终只读复核确认 migration 仅删除手机号 `NOT NULL`、唯一索引保留;导入及本人开通只创建 `employee`,不自动授予 `finance`、`ceo`、`admin` 或 `system_admin`;空来源更新省略 phone 字段,真实文件名、完整路径和个人字段未进入差异。
|
- 最终只读复核确认 migration 仅删除手机号 `NOT NULL`、唯一索引保留;导入及本人开通只创建 `employee`,不自动授予 `finance`、`ceo`、`admin` 或 `system_admin`;空来源更新省略 phone 字段,真实文件名、完整路径和个人字段未进入差异。
|
||||||
|
- 获批生产脱敏 dry-run 只接收内存筛选的两人子集,结果为输入 2、active 2、不完整 0、计划创建 2、更新 0、冲突/跳过/失败均为 0;两名目标均有手机号和企业邮箱。dry-run 未写库、未创建账号、未分配角色,目标外四人未进入输入。
|
||||||
|
- 生产 Node 对 Promise 版 `readFile(0)` 返回 `ERR_INVALID_ARG_TYPE`,正式 CLI 在解析前安全失败;本地改为 `fs.readFileSync(0)` 并增加 fd 读取回归。生产证据使用同一解析、同步和报告逻辑的 fd 兼容只读入口取得,正式 CLI 修复部署前不用于 apply。
|
||||||
|
|
||||||
## 生产部署与 timer 收口证据(2026-07-27)
|
## 生产部署与 timer 收口证据(2026-07-27)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
- 唯一匹配且必填资料、冲突检查通过后,将两名成员同步为 OA 员工,并分别配置 `employee + finance` 与 `employee + ceo`,形成不同自然人的付款制单/复核职责。
|
- 唯一匹配且必填资料、冲突检查通过后,将两名成员同步为 OA 员工,并分别配置 `employee + finance` 与 `employee + ceo`,形成不同自然人的付款制单/复核职责。
|
||||||
- 增加独立的一次性企微敏感资料授权入口:普通登录继续使用 `snsapi_base`,首次开通使用 `snsapi_privateinfo`;未绑定的在职可见成员授权成功后只自动获得 `employee` 角色。
|
- 增加独立的一次性企微敏感资料授权入口:普通登录继续使用 `snsapi_base`,首次开通使用 `snsapi_privateinfo`;未绑定的在职可见成员授权成功后只自动获得 `employee` 角色。
|
||||||
- 支持管理员提供的企微通讯录 XLSX 通过 stdin 做脱敏 dry-run,并允许个别成员暂缺手机号;企业邮箱、企微账号、姓名和部门仍作为建档门禁。
|
- 支持管理员提供的企微通讯录 XLSX 通过 stdin 做脱敏 dry-run,并允许个别成员暂缺手机号;企业邮箱、企微账号、姓名和部门仍作为建档门禁。
|
||||||
|
- 本轮通讯录导入只筛选两名已具备职责验收计划的目标成员;其余四名成员不进入 dry-run apply 输入、不创建活跃账号,明确留待后续独立批次。
|
||||||
- 修复部署脚本与现场端口、目录、容器和构建标识契约,统一真实运行来源;切换前保留活动数据、环境文件和旧容器回滚点。
|
- 修复部署脚本与现场端口、目录、容器和构建标识契约,统一真实运行来源;切换前保留活动数据、环境文件和旧容器回滚点。
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -40,9 +41,14 @@
|
|||||||
- 真实同步前只输出匹配数量、身份是否不同、缺失字段数、冲突数和计划创建/更新数,不输出姓名、userid、手机号、邮箱、部门原文或原始 payload。
|
- 真实同步前只输出匹配数量、身份是否不同、缺失字段数、冲突数和计划创建/更新数,不输出姓名、userid、手机号、邮箱、部门原文或原始 payload。
|
||||||
- `Employee.phone` 允许为 `NULL`,但不得写空字符串或假手机号;非空手机号继续保持唯一。企微账号和企业邮箱必须非空且唯一,缺手机号成员使用企微或邮箱登录,后续可由本人授权或管理员补齐。
|
- `Employee.phone` 允许为 `NULL`,但不得写空字符串或假手机号;非空手机号继续保持唯一。企微账号和企业邮箱必须非空且唯一,缺手机号成员使用企微或邮箱登录,后续可由本人授权或管理员补齐。
|
||||||
- 真实写入必须复用现有员工同步、角色、会话撤销和审计语义;财务处理人与 CEO 复核人必须是两个不同企微身份,且不得把 `admin` 当作普通业务复核角色。
|
- 真实写入必须复用现有员工同步、角色、会话撤销和审计语义;财务处理人与 CEO 复核人必须是两个不同企微身份,且不得把 `admin` 当作普通业务复核角色。
|
||||||
|
- 目标角色固定为 `employee + finance` 与 `employee + ceo`,不授予 `finance_mgr`。接受 `finance` 同时具备 `payroll.manage` 和 `employee.bank.read`,意味着财务处理人可访问薪资和员工银行账号;`finance`、`finance_mgr` 均不得拥有 `asset.transfer`。
|
||||||
|
- 本轮 maker-checker 验收只针对付款批次复核:创建人复核同一批次必须被 `maker_checker_conflict` 拒绝;不得把结论表述为费用审批、财务付款或全部财务操作已完全按自然人分离。
|
||||||
- 敏感资料授权必须显式受 `WECOM_OAUTH_ONBOARDING_ENABLED` 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并至少实际同意邮箱或企业邮箱授权;手机号可暂缺,但返回时必须规范化并参与唯一性检查。
|
- 敏感资料授权必须显式受 `WECOM_OAUTH_ONBOARDING_ENABLED` 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并至少实际同意邮箱或企业邮箱授权;手机号可暂缺,但返回时必须规范化并参与唯一性检查。
|
||||||
- `user_ticket` 只在服务端内存中用于一次 `getuserdetail` 调用,不写日志、Cookie、数据库、审计或错误详情;授权 code 和原始敏感响应同样不得持久化。
|
- `user_ticket` 只在服务端内存中用于一次 `getuserdetail` 调用,不写日志、Cookie、数据库、审计或错误详情;授权 code 和原始敏感响应同样不得持久化。
|
||||||
- 自助开通按企微 userid、非空手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
|
- 自助开通按企微 userid、非空手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
|
||||||
|
- 本轮真实建档改用管理员导出的通讯录两人子集:先以 stdin dry-run/apply 创建或更新 employee 与企微绑定,且新账号严格只有 `employee`;再由管理员单独写入附加角色。一次性 `snsapi_privateinfo/getuserdetail` 入口保持默认关闭,仅作为未来本人补资料能力,不作为本轮生产验收门禁。
|
||||||
|
- 角色写入前后记录目标原角色集合、不同 employee 身份、对应角色待办计数、活跃会话数量和审计计数;回滚只把两人恢复为写入前角色集合,复用 token version、会话撤销和审计,不恢复整库。
|
||||||
|
- CEO 角色会让既有 `assignee_role=ceo` 待办立即可见,并影响新提交生成的角色节点,但不重写既有审批快照,也不恢复过去失败的提交。CEO 本人无主管且没有 active 非本人 `finance_mgr` 时,报销保持阻断,本轮不通过扩权绕过。
|
||||||
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
|
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
|
||||||
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
|
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
|
||||||
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
|
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
|
||||||
@@ -60,14 +66,14 @@
|
|||||||
- [x] 相关 shell 语法、QA 脚本、Trellis 治理、敏感扫描和 `git diff --check` 通过。
|
- [x] 相关 shell 语法、QA 脚本、Trellis 治理、敏感扫描和 `git diff --check` 通过。
|
||||||
- [ ] 在获得单独授权后,确认真实运行 commit、访问边界、职责分离、异机备份及恢复后 `/ready`;未授权前保持未勾选且不宣称正式可用。
|
- [ ] 在获得单独授权后,确认真实运行 commit、访问边界、职责分离、异机备份及恢复后 `/ready`;未授权前保持未勾选且不宣称正式可用。
|
||||||
- [x] 双 Secret 调用测试证明成员 ID 由通讯录 token 读取,成员详情、部门和 HR 资料由自建应用 token 读取。
|
- [x] 双 Secret 调用测试证明成员 ID 由通讯录 token 读取,成员详情、部门和 HR 资料由自建应用 token 读取。
|
||||||
- [ ] 真实脱敏 dry-run 证明两个目标各唯一匹配、身份不同、必填资料和唯一约束无阻塞。
|
- [x] 真实脱敏 dry-run 证明两个目标各唯一匹配、身份不同、必填资料和唯一约束无阻塞。
|
||||||
- [ ] 获批写入后,两名活跃员工分别具有 `employee + finance` 与 `employee + ceo`,企微绑定不同,角色审计存在且未产生付款业务写入。
|
- [ ] 获批写入后,两名活跃员工分别具有 `employee + finance` 与 `employee + ceo`,企微绑定不同,角色审计存在且未产生付款业务写入。
|
||||||
- [x] 普通企微登录仍生成 `snsapi_base`,一次性开通入口生成 `snsapi_privateinfo`;缺少开关、`user_ticket`、成员授权字段、active 状态或唯一性时均拒绝创建(本地 synthetic 验证)。
|
- [x] 普通企微登录仍生成 `snsapi_base`,一次性开通入口生成 `snsapi_privateinfo`;缺少开关、`user_ticket`、成员授权字段、active 状态或唯一性时均拒绝创建(本地 synthetic 验证)。
|
||||||
- [ ] 一次性授权通过 `getuserdetail` 获取成员同意的手机和邮箱,自动创建或补齐 OA 员工与企微绑定;新建账号角色严格等于 `employee`,日志和审计不含票据或完整敏感响应。
|
- [x] 本轮不适用:生产不启用 `snsapi_privateinfo/getuserdetail` 本人授权,OAuth 入口保持默认关闭并延后为补资料能力;真实建档改用企微通讯录两人子集,仍要求新账号角色严格等于 `employee`,且不持久化通讯录文件或原始行。
|
||||||
- [x] 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 `/ready` 和前端 marker 返回同一短构建标识。
|
- [x] 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 `/ready` 和前端 marker 返回同一短构建标识。
|
||||||
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
|
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
|
||||||
- [x] 部署入口拒绝隐式/相对数据目录及缺少数据模式,成对 manifest 可记录显式 build SHA 或活动镜像 digest,并有 Node/Python 等价测试。
|
- [x] 部署入口拒绝隐式/相对数据目录及缺少数据模式,成对 manifest 可记录显式 build SHA 或活动镜像 digest,并有 Node/Python 等价测试。
|
||||||
- [ ] 两名目标成员分别完成一次本人授权后,账号唯一、企微绑定不同,再由管理员写入 `employee + finance` 与 `employee + ceo`;maker-checker、会话撤销和角色审计通过。
|
- [ ] 两名目标成员经企微通讯录两人子集 dry-run/apply 后账号唯一、企微绑定不同且初始角色仅为 `employee`,再由管理员写入 `employee + finance` 与 `employee + ceo`;其余四人保持未导入,付款批次 maker-checker、会话撤销和角色审计通过。
|
||||||
- [x] 本地合成企微导出证明 5 人有手机号、1 人手机号为空时 dry-run 可通过;空值保存为 `NULL`,非空手机号/邮箱/企微账号重复均被拒绝,报告不含个人字段原文。
|
- [x] 本地合成企微导出证明 5 人有手机号、1 人手机号为空时 dry-run 可通过;空值保存为 `NULL`,非空手机号/邮箱/企微账号重复均被拒绝,报告不含个人字段原文。
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
@@ -77,6 +83,7 @@
|
|||||||
- 不引入 MinIO、云备份 SDK、全量灾备平台、自动跨地域复制或 24×7 值守。
|
- 不引入 MinIO、云备份 SDK、全量灾备平台、自动跨地域复制或 24×7 值守。
|
||||||
- 不在仓库提交备份文件、恢复数据、环境变量、凭证、真实账号或完整本地路径。
|
- 不在仓库提交备份文件、恢复数据、环境变量、凭证、真实账号或完整本地路径。
|
||||||
- 不发送企微消息、不创建企微审批模板、不把邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
|
- 不发送企微消息、不创建企微审批模板、不把邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
|
||||||
|
- 不导入通讯录中的其余四名成员,不授予 `finance_mgr`,不修改审批规则来放行 CEO 本人无主管报销。
|
||||||
- 不在本任务完成企微调休、导航、报销或模块退役;它们属于后续批次。
|
- 不在本任务完成企微调休、导航、报销或模块退役;它们属于后续批次。
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
@@ -124,3 +131,5 @@
|
|||||||
- 企微结构化探测证明 6 名成员的批量详情接口均成功并返回部门,但手机号、邮箱和企业邮箱可读数量均为 0;HR 接口均成功但没有 `staff_info`。官方文档确认这不代表字段不可获取:2022-06-20 后新建自建应用应由管理员在应用详情选择敏感字段,再由成员通过 `snsapi_privateinfo` 手工授权,并用 `user_ticket` 调 `/cgi-bin/auth/getuserdetail` 获取本人手机号、邮箱或企业邮箱。
|
- 企微结构化探测证明 6 名成员的批量详情接口均成功并返回部门,但手机号、邮箱和企业邮箱可读数量均为 0;HR 接口均成功但没有 `staff_info`。官方文档确认这不代表字段不可获取:2022-06-20 后新建自建应用应由管理员在应用详情选择敏感字段,再由成员通过 `snsapi_privateinfo` 手工授权,并用 `user_ticket` 调 `/cgi-bin/auth/getuserdetail` 获取本人手机号、邮箱或企业邮箱。
|
||||||
- 当前 OA OAuth 只使用 `snsapi_base`,且没有调用 `/cgi-bin/auth/getuserdetail`,因此现有登录链路不能采集敏感字段。两名目标各唯一匹配且身份不同,但批量 dry-run 均缺 `phone`、`email`,真实写入已按门禁停止。
|
- 当前 OA OAuth 只使用 `snsapi_base`,且没有调用 `/cgi-bin/auth/getuserdetail`,因此现有登录链路不能采集敏感字段。两名目标各唯一匹配且身份不同,但批量 dry-run 均缺 `phone`、`email`,真实写入已按门禁停止。
|
||||||
- 本轮已在本地实现默认关闭的一次性 `onboard` 入口;普通登录继续静默。全量后端门禁与安全补修后的企微聚焦回归结果记录在 `implement.md`。该证据不代表功能已部署,也不代表企微后台敏感字段已由管理员选择。
|
- 本轮已在本地实现默认关闭的一次性 `onboard` 入口;普通登录继续静默。全量后端门禁与安全补修后的企微聚焦回归结果记录在 `implement.md`。该证据不代表功能已部署,也不代表企微后台敏感字段已由管理员选择。
|
||||||
|
- 2026-07-27 经明确授权,仅把管理员通讯录的两人内存子集通过 stdin 送入活动 server 做生产 dry-run:输入 2、active 2、手机号存在 2、企业邮箱存在 2、不完整 0,计划创建 2、更新 0、冲突 0、跳过 0、失败 0;未执行 apply、未写数据库、未持久化筛选文件,其余四人未进入输入。
|
||||||
|
- 首次正式 CLI dry-run 暴露生产 Node 对 `fs.promises.readFile(0)` 不兼容并在解析前停止;随后用同一解析器、同一 HR master 服务和同一脱敏报告构造器,仅将 stdin 读取替换为 fd 兼容方式完成上述 dry-run。本地已修复 CLI 读取方式并补回归,修复部署前不得直接使用正式 CLI 执行 apply。
|
||||||
|
|||||||
+11
@@ -152,3 +152,14 @@
|
|||||||
### R-3 资产转移权限
|
### R-3 资产转移权限
|
||||||
- `asset.transfer` 只给 `administration / 资产管理员`
|
- `asset.transfer` 只给 `administration / 资产管理员`
|
||||||
- `finance_mgr` 有 `asset.manage` 但不拥有实物调拨权限,这是有意设计
|
- `finance_mgr` 有 `asset.manage` 但不拥有实物调拨权限,这是有意设计
|
||||||
|
|
||||||
|
### R-4 小团队财务处理与付款复核边界
|
||||||
|
- 日常财务处理角色使用 `finance`,付款复核角色使用 `ceo`;不得为了负责人本人报销兜底而顺带授予权限更宽的 `finance_mgr`。
|
||||||
|
- `finance` 除费用审批、财务付款和付款批次管理外,还包含 `payroll.manage` 与 `employee.bank.read`,因此会接触薪资数据和员工银行账号;这属于明确接受的财务职责范围,不是隐含副作用。
|
||||||
|
- `ceo` 通过 `payment.batch.review` 承担付款批次复核。maker-checker 只保证付款批次创建人与复核人不同,不代表费用审批、财务付款和全部财务操作已经按自然人完全分离。
|
||||||
|
- `finance`、`finance_mgr` 均不拥有 `asset.transfer`;实物调拨继续只属于 `administration`。
|
||||||
|
|
||||||
|
### R-5 CEO 本人无主管报销限制
|
||||||
|
- 现有负责人本人报销规则只允许 active、非本人的 `finance_mgr` 作为 `ownerSelfFallbackRole`;普通 `finance` 不满足该兜底角色。
|
||||||
|
- 当 CEO 本人没有可解析直属主管且公司没有合适的非本人 `finance_mgr` 时,报销提交保持阻断。不得通过给财务人员顺带增加 `finance_mgr`、使用 `admin` 代批或恢复整库来绕过。
|
||||||
|
- 授予 `ceo` 后,既有 `assignee_role=ceo` 待办会对该角色持有人可见,新提交单据按当前规则生成 CEO 节点;既有审批快照不重路由,过去提交失败的单据也不会自动恢复。若未来要放开 CEO 本人报销,应另开产品任务调整兜底规则和最小权限,而不是在账号配置阶段扩权。
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ Reports contain counts and categories only; personal field values are never prin
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStdinBuffer() {
|
||||||
|
return fs.readFileSync(0);
|
||||||
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const options = {
|
const options = {
|
||||||
input: "-",
|
input: "-",
|
||||||
@@ -258,7 +262,7 @@ async function main() {
|
|||||||
printHelp();
|
printHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsed = await parseDirectoryExport(await fs.promises.readFile(0));
|
const parsed = await parseDirectoryExport(readStdinBuffer());
|
||||||
resources = createPrismaClient();
|
resources = createPrismaClient();
|
||||||
const actorId = await resolveActorId(resources.prisma, options.actorEmployeeNo);
|
const actorId = await resolveActorId(resources.prisma, options.actorEmployeeNo);
|
||||||
const syncRun = await runWecomHrMasterSync({
|
const syncRun = await runWecomHrMasterSync({
|
||||||
@@ -293,4 +297,5 @@ module.exports = {
|
|||||||
findHeaderRow,
|
findHeaderRow,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
parseDirectoryExport,
|
parseDirectoryExport,
|
||||||
|
readStdinBuffer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
const ExcelJS = require("exceljs");
|
const ExcelJS = require("exceljs");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -5,6 +6,7 @@ const {
|
|||||||
buildSafeReport,
|
buildSafeReport,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
parseDirectoryExport,
|
parseDirectoryExport,
|
||||||
|
readStdinBuffer,
|
||||||
} = require("../../../src/scripts/import-wecom-directory-export");
|
} = require("../../../src/scripts/import-wecom-directory-export");
|
||||||
|
|
||||||
async function buildDirectoryWorkbook(rows) {
|
async function buildDirectoryWorkbook(rows) {
|
||||||
@@ -39,6 +41,17 @@ async function buildDirectoryWorkbook(rows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("WeCom directory export importer", () => {
|
describe("WeCom directory export importer", () => {
|
||||||
|
it("reads stdin through the fd-compatible synchronous API", () => {
|
||||||
|
const expected = Buffer.from("synthetic-xlsx");
|
||||||
|
const readFileSync = vi.spyOn(fs, "readFileSync").mockReturnValue(expected);
|
||||||
|
try {
|
||||||
|
expect(readStdinBuffer()).toBe(expected);
|
||||||
|
expect(readFileSync).toHaveBeenCalledWith(0);
|
||||||
|
} finally {
|
||||||
|
readFileSync.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("requires stdin, the apply environment gate, and an actor for apply", () => {
|
it("requires stdin, the apply environment gate, and an actor for apply", () => {
|
||||||
const previous = process.env.WECOM_DIRECTORY_EXPORT_APPLY;
|
const previous = process.env.WECOM_DIRECTORY_EXPORT_APPLY;
|
||||||
delete process.env.WECOM_DIRECTORY_EXPORT_APPLY;
|
delete process.env.WECOM_DIRECTORY_EXPORT_APPLY;
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户