文件
hyb-oa/.trellis/tasks/07-26-internal-ops-recovery-baseline/prd.md
T

13 KiB

内部OA运行与恢复底线

Goal

为 5–10 人内部 OA 建立一个主要开发者可执行的备份与恢复底线:数据库和附件始终成对备份、产物可校验、恢复默认只能进入明确的影子目标,并用合成数据证明可恢复;同时把真实内部实例的运行来源、角色职责、异机副本和 /ready 验收留在明确的外部授权门后。

Background

  • 当前可信状态仍是 LOCAL_CANDIDATE_PASS,不能据此宣称已部署或可以正式内用。
  • 已有 backup-db.sh、backup-storage.sh、restore-db.sh 和 restore-storage.sh,runbook 也描述了 DB + 文件恢复,但尚无当前恢复演练证据。
  • DB 恢复会执行 pg_restore --clean --if-exists;当前脚本没有影子目标确认,误指向真实库会产生破坏性后果。
  • 文件恢复会直接解压到目标目录;当前不会拒绝非空目录,可能把旧文件和恢复文件混合。
  • DB 与附件备份分别生成时间戳,没有共同批次清单、完整性校验和,无法稳定证明两份产物属于同一次恢复点。
  • 本任务以小团队低维护为目标,不引入备份平台、对象存储、Kubernetes、云厂商锁定或复杂灾备编排。

Scope

  • 本地备份/恢复 shell 脚本的安全合同、成对备份入口、完整性校验与脱敏 manifest。
  • 使用一次性 PostgreSQL 目标和临时附件目录完成合成数据恢复演练及负向测试。
  • 更新现有试运行 runbook、checklist 和部署规约,明确影子恢复与真实恢复边界。
  • 规划并保留真实运行 commit、访问边界、角色职责、异机副本及恢复后 /ready 的外部授权门;未授权前不执行这些动作。
  • 经用户逐项授权后,修正企微通讯录与人事助手的双 Secret 读取分工,并用脱敏 dry-run 唯一确认两名既有企微成员。
  • 唯一匹配且必填资料、冲突检查通过后,将两名成员同步为 OA 员工,并分别配置 employee + finance 与 employee + ceo,形成不同自然人的付款制单/复核职责。
  • 增加独立的一次性企微敏感资料授权入口:普通登录继续使用 snsapi_base,首次开通使用 snsapi_privateinfo;未绑定的在职可见成员授权成功后只自动获得 employee 角色。
  • 修复部署脚本与现场端口、目录、容器和构建标识契约,统一真实运行来源;切换前保留活动数据、环境文件和旧容器回滚点。

Requirements

  • 提供一个简单的成对备份入口,复用现有 DB/附件脚本,以同一批次标识产生两份产物和脱敏 manifest。
  • manifest 只包含批次号、相对文件名、大小、SHA-256、创建时间及安全的版本信息,不包含数据库 URL、Secret、真实附件名或完整绝对路径。
  • 备份完成前验证 DB dump 可列出、附件 tar 可读取;任一部分失败时不得产出“完整批次”结论。
  • DB 恢复必须先解析并显示目标数据库名,并要求调用者显式确认相同目标;默认仅允许 shadow 演练模式,且实际库名必须带 shadow、drill 或 restore_test 独立标记。本任务不开放生产恢复快捷开关。
  • 附件恢复必须要求显式目标目录;默认拒绝非空目录,并在解压前检查归档条目,禁止绝对路径和 .. 越界路径。
  • 用完全隔离的合成数据库和合成附件完成本地往返演练,不读取或复制真实发票、合同、银行资料或生产数据。
  • 恢复演练至少证明:DB 标记数据恢复、附件内容哈希一致、损坏备份被拒绝、错误确认值被拒绝、非空附件目标被拒绝。
  • 更新现有 runbook/checklist,使正常备份、影子演练、真实恢复三种场景不再共用含糊命令;保持现有本地存储默认,不启用 MinIO。
  • 将真实内部实例核查列为后续同任务外部门禁:运行 commit/访问方式/TLS、真实职责分配、异机副本和恢复后 /ready 均需另行批准和脱敏留证。
  • 企微通讯录 Secret 只用于 /user/list_id;自建应用 Secret 用于 /user/get、/department/list 和获授权的人事助手 /hr/get_staff_info,不得回退到已废弃的人事助手 Secret。
  • 真实同步前只输出匹配数量、身份是否不同、缺失字段数、冲突数和计划创建/更新数,不输出姓名、userid、手机号、邮箱、部门原文或原始 payload。
  • 真实写入必须复用现有员工同步、角色、会话撤销和审计语义;财务处理人与 CEO 复核人必须是两个不同企微身份,且不得把 admin 当作普通业务复核角色。
  • 敏感资料授权必须显式受 WECOM_OAUTH_ONBOARDING_ENABLED 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并实际同意手机号及邮箱或企业邮箱授权。
  • user_ticket 只在服务端内存中用于一次 getuserdetail 调用,不写日志、Cookie、数据库、审计或错误详情;授权 code 和原始敏感响应同样不得持久化。
  • 自助开通按企微 userid、手机号和邮箱做唯一性检查;新账号只创建 employee 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
  • 普通 snsapi_base 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
  • 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
  • 部署健康检查必须从 Compose 解析实际宿主端口,并校验后端 /ready、前端 marker 与批准提交一致;应用已提交的 Prisma migrations 但禁止 reset 或改写历史。

Acceptance Criteria

  • 一条命令生成同批 DB、附件和脱敏 manifest,且完整性校验通过。
  • DB 恢复在缺少或不匹配目标确认时失败,不会执行 pg_restore --clean。
  • 附件恢复拒绝隐式目标、非空目录和危险 tar 路径。
  • 隔离合成数据演练证明 DB 与附件可往返恢复,并记录实际耗时作为本地 RTO 参考。
  • runbook/checklist 明确影子演练命令、生产停止条件、DB + 附件配对要求和异机副本要求。
  • 相关 shell 语法、QA 脚本、Trellis 治理、敏感扫描和 git diff --check 通过。
  • 在获得单独授权后,确认真实运行 commit、访问边界、职责分离、异机备份及恢复后 /ready;未授权前保持未勾选且不宣称正式可用。
  • 双 Secret 调用测试证明成员 ID 由通讯录 token 读取,成员详情、部门和 HR 资料由自建应用 token 读取。
  • 真实脱敏 dry-run 证明两个目标各唯一匹配、身份不同、必填资料和唯一约束无阻塞。
  • 获批写入后,两名活跃员工分别具有 employee + finance 与 employee + ceo,企微绑定不同,角色审计存在且未产生付款业务写入。
  • 普通企微登录仍生成 snsapi_base,一次性开通入口生成 snsapi_privateinfo;缺少开关、user_ticket、成员授权字段、active 状态或唯一性时均拒绝创建(本地 synthetic 验证)。
  • 一次性授权通过 getuserdetail 获取成员同意的手机和邮箱,自动创建或补齐 OA 员工与企微绑定;新建账号角色严格等于 employee,日志和审计不含票据或完整敏感响应。
  • 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 /ready 和前端 marker 返回同一短构建标识。
  • 两名目标成员分别完成一次本人授权后,账号唯一、企微绑定不同,再由管理员写入 employee + finance 与 employee + ceo;maker-checker、会话撤销和角色审计通过。

Out of Scope

  • 不读取真实附件正文,不执行生产恢复、数据库 reset、迁移历史改写或付款/报销业务写入。
  • 不修改 Prisma schema、公开业务 API、权限模型、审批链或付款状态机;仅修复既有 HR 同步服务的凭证分工,并通过既有角色模型写入获批职责。
  • 不引入 MinIO、云备份 SDK、全量灾备平台、自动跨地域复制或 24×7 值守。
  • 不在仓库提交备份文件、恢复数据、环境变量、凭证、真实账号或完整本地路径。
  • 不发送企微消息、不创建企微审批模板、不把手机号/邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
  • 不在本任务完成企微调休、导航、报销或模块退役;它们属于后续批次。

Test Plan

  • bash -n scripts/backup-db.sh scripts/backup-storage.sh scripts/restore-db.sh scripts/restore-storage.sh <新增脚本>
  • 使用临时目录和一次性本地 PostgreSQL 目标运行合成数据恢复演练;若本机缺少 PostgreSQL/Docker,则记录为环境阻塞,不改用真实库。
  • 覆盖损坏 dump/tar、危险 tar 路径、非空目标目录、缺少确认和确认不匹配等负向场景。
  • python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-26-internal-ops-recovery-baseline
  • node scripts/qa/check-trellis-governance.mjs
  • node scripts/qa/check-sensitive-content.mjs
  • git diff --check
  • cd server && node --check src/services/integrations/wecomHrMaster.js
  • cd server && npm test -- --run tests/unit/wecomHrMaster.service.test.js
  • cd server && npm run prisma:validate
  • cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js
  • bash -n deploy/trialrun/deploy.sh
  • node deploy/trialrun/validate-env-contract.js
  • 获批部署窗口内执行迁移状态、Compose 数据挂载、/ready、前端 marker、Caddy 公网路由和回滚容器只读复验。

Key Decision

先改进可恢复性和防误操作,再接触真实环境。默认工作流只支持影子恢复;真实恢复不是普通脚本路径,而是需要停机、负责人确认、成对备份、回滚点和独立授权的事故操作。

企微敏感字段采用“日常静默登录 + 一次性本人授权开通”的双入口。批量同步不猜测敏感资料,不修改全局员工字段可空性;自助开通只授予 employee,业务角色继续由管理员审计写入。

Local Evidence

  • 已使用一次性 PostgreSQL 16 容器和合成附件完成多轮往返演练,最近一次本地参考 RTO 为 6 秒。
  • 演练覆盖 manifest/校验和、错误确认、正式风格库名、损坏 DB、损坏 tar、非空附件目录、默认试运行附件目录和危险 tar 路径拒绝。
  • 该证据不包含真实业务数据、真实运行实例、异机副本或部署后 /ready,因此最后一项验收保持未完成。

Live Read-Only Evidence

  • 获批只读核查结果见 research/live-read-only-baseline.md,结论为 NO_GO_FOR_FORMAL_INTERNAL_USE。
  • 公开 HTTPS、TLS、HSTS 和 /ready 当前可用,但运行环境没有后端/前端构建标识,且远端迁移少于本地当前基线。
  • 真实角色只有一个活跃账号,财务和付款复核候选均为 0,不满足 maker-checker。
  • 只读核查时的历史 DB/附件备份约 55 天未更新、无 manifest,且与运行数据位于同一设备;获批后已生成新的 DB+附件+manifest 完整批次并复制到独立主机,两端校验一致。
  • 当前代码已在获批后以普通快进方式发布到 upstream,来源门禁为 PASS;尚未执行角色修改、部署、迁移或真实格式影子恢复,因此最后一项验收继续保持未勾选。
  • 当前 OA 是宿主机 Caddy、容器化 server/frontend、独立 PostgreSQL 容器和宿主附件挂载组成的混合部署;公网 OA 路由确实进入 server 容器。
  • 服务器存在两套无 Git 元数据的部署目录:运行容器来源与日常运维任务指向不同目录,Compose 和环境文件也不一致;运行代码与本机当前代码不一致且缺少构建标识。
  • 现场端口与当前部署脚本的固定健康检查端口不一致;未统一活动数据目录、容器名、端口和环境文件前,不得直接运行现有部署脚本。
  • 企微结构化探测证明 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,真实写入已按门禁停止。
  • 本轮已在本地实现默认关闭的一次性 onboard 入口;普通登录继续静默。全量后端门禁与安全补修后的企微聚焦回归结果记录在 implement.md。该证据不代表功能已部署,也不代表企微后台敏感字段已由管理员选择。