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

13 KiB

内部 OA 运行与恢复底线设计

总体边界

本任务将现有四个底层脚本保留为单一职责工具,在其上增加一个成对备份入口和一个隔离演练入口。所有默认值面向本地/影子环境;真实环境只能沿 runbook 的人工门禁执行。

backup pair entry
  ├─ backup-db.sh      → <run-id>.db.dump
  ├─ backup-storage.sh → <run-id>.storage.tar.gz
  └─ integrity checks  → <run-id>.manifest

isolated drill
  ├─ disposable PostgreSQL/database
  ├─ disposable source/restore directories
  ├─ synthetic marker + synthetic attachment
  └─ restore → compare → cleanup exact targets

备份合同

  • 一个批次只使用一个 run_id,由上层入口传递明确的 OUT_FILE 给现有底层脚本。
  • 先把产物写入批次临时目录;DB 和附件均成功并通过完整性检查后,再写 manifest 并把批次标记为完整。
  • manifest 使用稳定、脱敏的键:schemaVersion、runId、createdAt、buildSha(真实可知时)、sourceImageDigest(实测可知时)、两份相对文件名、字节数和 SHA-256;两类来源标识至少有一项。
  • 不把连接串、容器密码、存储源路径、真实业务文件清单或主机绝对路径写入 manifest。
  • 本任务不负责异机传输实现;runbook 要求操作者把完整批次复制到异机/异盘,并记录保留周期。

DB 恢复安全合同

  • 恢复前通过数据库连接读取 current_database(),不得只相信 URL 字符串或调用者描述。
  • 调用者必须提供与实际数据库名一致的确认值,并显式选择 shadow。
  • 实际目标库名还必须包含独立的 shadow、drill 或 restore_test 标记,不能仅靠调用者把模式写成 shadow。
  • 确认缺失、连接失败、数据库名不匹配或模式不是 shadow 时,在 pg_restore --clean 之前退出。
  • Docker fallback 也必须执行同样的数据库名探测和确认,不得成为绕过路径。
  • 真实/生产恢复不在本任务脚本默认路径内;未来如确需开放,应另建任务定义双人确认和事故编号。

附件恢复安全合同

  • 必须显式传入恢复目标,不再默认落入试运行数据目录。
  • 明确拒绝仓库的默认试运行附件目录,即使该目录碰巧为空。
  • 影子恢复目标必须为空;非空时退出,不执行覆盖或合并。
  • 解压前列出归档成员并拒绝绝对路径、空成员和包含 .. 路径段的成员。
  • 只对已验证的归档执行解压;演练通过内容 SHA-256 比较证明往返一致。

隔离演练

  • 数据库只使用一次性本地容器或明确命名的 disposable database,插入不含业务信息的 marker 表/行。
  • 附件只使用临时目录和合成文本/二进制样件。
  • 演练记录开始/结束时间、各阶段通过状态和相对产物名;不记录连接串、容器密码或临时绝对路径。
  • 清理只针对演练创建的明确容器、数据库和临时目录;不得使用宽泛路径或工作区根目录。

兼容与回滚

  • backup-db.sh 和 backup-storage.sh 的现有单独调用继续可用;推荐运维入口改为成对备份命令。
  • restore-db.sh/restore-storage.sh 的安全收紧属于有意的命令合同变化,runbook 和 checklist 必须同步更新。
  • 回滚代码可恢复旧脚本,但已经生成的 dump/tar 仍保持 PostgreSQL/tar 标准格式,不被新 manifest 锁定。
  • 如演练环境缺少 PostgreSQL 客户端和 Docker,停止 DB 动态演练;仍可完成 shell 静态检查和附件演练,不得转向真实环境补测。

外部门禁

以下事项不因本地演练通过而自动获批:读取真实运行环境、复制真实备份、建立异机定时任务、停机、恢复真实库/附件、部署和公网 /ready。进入这些步骤前再次列出目标、只读/写入范围和回滚方式并取得明确批准。

企微 HR 双 Secret 数据流

通讯录 Secret -> user/list_id -> userid 集合
自建应用 Secret -> user/get + department/list + hr/get_staff_info
                 -> normalize -> 脱敏 dry-run -> 既有 HR 同步写入
  • 保留 WECOM_CONTACT_SECRET 与 WECOM_AGENT_SECRET 的现有配置名和 token 来源审计,不增加第三套 Secret。
  • listContactMembers 分离列表 token 与详情 token;默认详情 token 等于列表 token 以保持内部调用兼容,但正式聚合入口明确传入自建应用 token。
  • 指定 userid 与全量 userid 两条路径使用同一详情 token 合同;部门列表也使用自建应用 token。
  • /user/get 失败仍沿用最小 userid 回退,HR 信息可补足资料;真实 dry-run 必须以归一化后的必填字段和冲突检查作为写入门,不因 API errcode=0 自动放行。

真实职责写入门

  • 先读取全量候选并在内存中按目标姓名匹配,只输出两个匹配计数、身份哈希是否不同、缺失字段数和计划动作数量。
  • 只有两个匹配计数均为 1、企微身份不同、均为 active、必填资料完整且手机号/邮箱不冲突时,才进入 apply。
  • 员工和企微绑定通过既有 HR master sync 服务创建或更新;随后通过既有角色写入语义分别设置 employee + finance 和 employee + ceo。
  • 角色变更递增 token_version、撤销活动会话并写 SYSTEM_USER_ROLES_SET 审计;最终只核对数量、角色集合、不同 employee id、审计存在和付款业务零写入。
  • 任一门禁失败即停止,不进行部分角色写入;如员工同步已经成功而角色阶段失败,保留同步审计并立即报告,不用删除真实员工记录进行破坏性回滚。

企微本人敏感信息授权边界

  • /user/get 和人事助手批量读取继续只负责 userid、姓名、部门等可读字段;不得把批量缺字段误判为企微完全不支持手机号和邮箱。
  • 2022-06-20 后新建自建应用的敏感字段走独立 OAuth 数据流:管理员在应用详情选择字段,成员以 snsapi_privateinfo 手工确认,getuserinfo 返回短时 user_ticket,服务端立即调用 /cgi-bin/auth/getuserdetail。
  • user_ticket、授权 code 和完整敏感响应不得写日志、任务证据或审计 extra;只保存业务所需的规范化手机号/邮箱及字段存在性审计。
  • 普通 snsapi_base 登录与敏感资料授权分开,避免每次登录引入手工授权;用户已批准未绑定的 active 可见成员通过独立入口自助创建仅授予 employee 的 OA 账号。

OAuth 开通数据流

GET /auth/wecom/login?mode=onboard
  -> feature flag + state(mode=onboard)
  -> authorize(scope=snsapi_privateinfo)
  -> getuserinfo(code) -> userid + user_ticket
  -> getuserdetail(user_ticket) -> mobile + email|biz_mail
  -> user/get + department/list -> name + active + department
  -> unique precheck -> transaction(employee + employee role + binding + audit)
  -> authenticated session
  • mode=login|bind 保持既有 snsapi_base 和行为;新增 mode=onboard 才使用 snsapi_privateinfo。
  • getuserinfo 只把 user_ticket 传给同一请求中的详情交换函数,诊断信息只记录字段是否存在。
  • 详情中的 userid 必须与身份接口 userid 一致;邮箱优先使用成员授权的个人邮箱,无个人邮箱时使用企业邮箱,二者都缺失则停止。
  • 联系人详情必须证明成员 active,并提供姓名、部门;手机号/邮箱与不同 OA 员工冲突时停止,不自动合并。
  • 已有绑定员工只补齐经授权的规范化资料且不改角色;新员工在单事务内创建 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 数据和附件挂载为唯一数据事实源。
  • 在停机切换前固定记录活动容器、数据挂载、环境文件摘要、端口、Caddy 路由和回滚镜像;不得打印环境变量内容。
  • 现场通过非默认宿主端口提供 OA;部署脚本必须从已验证的 Compose/env 配置解析实际端口,不能回退到硬编码(本地已实现,待获批部署)。
  • 两套目录的 Compose 容器名和环境文件不同。切换前必须先选择唯一目录并证明其 DB/附件目标就是当前活动数据;不得让 Compose 创建第二套空数据库后误判部署成功。
  • 统一后应由同一目录负责 source sync、Compose、环境文件和运维 timer,并让 /ready、前端 marker 和容器环境同时暴露同一短构建标识。
  • 任一迁移状态、数据挂载、端口或公网构建标识不匹配时,立即切回旧容器和 Caddy 目标;不执行数据库 reset 或数据目录覆盖。
  • Compose 不再固定 container_name;deploy 与 timer 通过 -p 显式选择同一项目,PostgreSQL 与附件宿主路径分别由 POSTGRES_DATA_DIR、STORAGE_HOST_DIR 参数化。
  • 项目名隔离不是独立部署步骤。项目名、容器身份和数据目录必须在同一次停机切换中启用;旧实例未停止时不得让新 PostgreSQL 挂载同一 PGDATA。
  • DB 不发布宿主端口;应用仅通过 Compose 内部网络访问 db。环境契约必须拒绝 DB ports、固定容器名或未参数化数据目录回退。
  • Docker 备份/恢复必须显式传入由 compose ... ps -q db 得到的已核验容器 ID;附件备份必须显式传入活动挂载源,两个脚本都不猜默认值。
  • 远端 /ready 构建标识在 server 容器内用 Node 校验,宿主机只负责 curl 和 Compose,不要求安装 Node。
  • deploy.sh 不解析或 source 远端 .env 来猜宿主路径;调用者必须在发布命令中显式传入受限字符集的绝对 POSTGRES_DATA_DIR、STORAGE_HOST_DIR 和数据模式。existing 要求目录已存在且 PGDATA 含 PG_VERSION;initialize 才允许创建空目标。
  • 本地直接 Compose 可继续使用示例相对默认值,但正式发布入口拒绝相对路径,从而避免项目名变化时 Docker 以 root 静默创建第二套空目录。
  • manifest 的 buildSha 优先使用调用者显式值,否则仅在本地 Git 工作树存在时推导;另以可选 sourceImageDigest 记录活动镜像不可变标识。旧实例 build SHA 不可知时只记录实测 digest,不填写目标提交。

生产统一顺序

  1. 另行获批后,从活动 DB 和活动附件挂载源重新生成成对异机备份作为灾难回滚点;旧批次因混入非活动附件目录只保留审计,并记录活动数据挂载、环境摘要、容器镜像和 Caddy 端口。
  2. 本地先修复 Compose 可配置回环端口及部署脚本动态端口探测,通过全部门禁并提交、发布批准提交。
  3. 停机后把活动 PGDATA 和附件复制到新的显式绝对目录,复制前目标必须为空;保留旧目录原位且不再挂载写入,直到观察期结束。复制仅在旧 PostgreSQL 完全停止后执行,并保留所有权、权限、链接和扩展属性。
  4. 用新 Compose 重建 db/init/server,应用已提交 migrations;旧容器和旧目录在验收前保留,不清理。
  5. 公网构建标识、数据计数和关键部署只读检查通过后更新运维 timer;角色与企微入口仍按各自真实写入门单独验收,不阻塞部署运维基线收口。部署检查失败则停止新容器并恢复旧目录/容器和原 Caddy 目标。