# 杨俊报销就绪:技术设计 ## 1. 设计目标 以最小生产变更打通“杨俊企微登录 → 杨俊提交真实报销 → 马建勇审批 → 王芝玲财务处理”,并在真实员工使用前由两个既有企微测试成员完成一次端到端体验验收。本任务不新增产品能力,复用现有通讯录导入、用户/角色管理、报销工作流和异机备份能力。 ## 2. 已确认边界 - 企业微信成员与 OA 员工是两个独立事实:杨俊已在企微通讯录,但生产尚无 OA 员工记录和企微绑定。 - 单人通讯录输入可唯一筛出杨俊;手机号为空符合现有 nullable 合同,企业邮箱存在,`incomplete=0`。 - 生产已有 37 个 active 费用类型和 1 个 active 报销工作流定义。 - active 法体为 0,但当前报销创建、提交和费用控制不以法体为硬门禁,本任务不创建法体。 - 杨俊的真实直属主管由用户确认是马建勇。 - 自动备份 daily/watchdog/drill-reminder 与 WARCH3 service 的 `BUILD_SHA` 均已漂移,必须先修复。 - 通讯录中已有两个状态为已激活的专用企微测试成员,分别命名为“测试员工”和“测试主管”;生产 OA 尚未导入它们,因此不存在可直接登录的测试 OA 账号。 ## 3. 数据流与执行门禁 ```text 现场只读复核活动构建与 4 个 unit → 重装 automatic-backup 三项 timer/service 与 WARCH3 timer → 验证 unit BUILD_SHA、enabled、active → 手工触发一份真实自动异机备份 → 验证源端成功与 ARM 恢复点完整/新鲜 → 从管理员导出通讯录在内存中仅筛选杨俊 → production dry-run(create=1、conflict/skip/fail=0) → production apply(初始角色仅 employee) → 验证 OA 员工 active、企微绑定唯一、其他成员未导入 → 通过现有用户管理接口设置 direct_manager_id=马建勇 → 验证主管 active、不同人、具备 expense.approve,并核对审计 → 从同一通讯录仅筛选两个既有测试成员并 dry-run/apply → 赋予最小角色并设置“测试员工 → 测试主管”的 OA 主管关系 → 两个测试成员通过企微分别登录 → 创建一条受控测试报销并由测试主管退回 → 删除测试报销、归档测试票据、停用测试 OA 账号并撤销会话 → 杨俊本人企微登录并提交真实报销 → 马建勇审批,随后进入王芝玲财务处理链路 ``` 任一门禁失败即停止后续步骤,不用后续成功掩盖前序失败。 ## 4. 复用合同 ### 4.1 通讯录导入 - 复用 `server/src/scripts/import-wecom-directory-export.js` 的 stdin 输入,不把筛选后的 XLSX 或成员原始 payload 写入磁盘、日志、Trellis 或聊天。 - dry-run 只记录人数与结果计数。 - apply 必须同时满足脚本 `--apply` 参数和容器内 `WECOM_DIRECTORY_EXPORT_APPLY=1`。当前 Compose v1 调用必须显式使用 `docker-compose exec -T -e WECOM_DIRECTORY_EXPORT_APPLY ...`,不能只在宿主 shell export。 - 输入严格只有杨俊一人;成功创建后的初始角色集合严格为 `employee`,不导入其他通讯录成员。 - 此路径是“管理员导出企微通讯录 → OA 员工/身份绑定”的单向导入。OA 侧主管字段仅服务 OA 工作流;不调用企微成员 create/update/disable,也不把 OA 主管、姓名、部门、手机或邮箱写回企微。 - 企微成员资料如需变化,由成员本人在企微允许的入口修改,或由企微管理员在管理后台修改;OA 后续只通过重新导出/受控同步读取变化。 ### 4.2 直属主管 - 复用 `POST /api/users/batch/set-manager`,不直写数据库。 - 写入目标仅杨俊,主管仅马建勇;接口负责 active、自指等现有校验。 - 写后核对 `SYSTEM_USERS_BATCH_SET_MANAGER` 审计增量,不记录完整账号或原始请求体。 - 马建勇必须是 active、与杨俊不同且按有效角色解析出 `expense.approve`。 ### 4.3 报销业务 - 杨俊无需先存在差旅申请;`linked_travel_request_id` 可空。 - 提交仍须满足既有硬门禁:至少一条费用明细、至少一张已绑定且状态为 `bound` 的真实票据、可匹配工作流并能解析审批人。 - 第一笔真实报销必须由杨俊本人操作;Codex 不代建生产报销、发票、付款或测试业务数据。 ### 4.4 timer 与备份 - 使用现有 `install-automatic-backup-timer.sh` 和 `install-warch3-ops-timer.sh`,从活动部署现场解析并显式传入合同参数;不在任务文档中固化 Secret、宿主路径或完整账号。 - 重装后核对 4 个 service 的 `BUILD_SHA` 与活动 server 一致,并确认对应 timers 为 enabled/active。 - 随后手工触发 daily 自动备份路径,验证源端 service 成功、ARM 批次完成且 dead-man/新鲜度检查通过。 - 新鲜异机恢复点是人员数据写入的前置条件,但不会用于回滚单个人员变更。 ### 4.5 两个既有测试成员 - 不创建新的 OA-only 测试身份,也不调用企微成员创建接口;复用管理员导出表中的两个专用企微测试成员,通过通讯录导入建立 OA 员工与企微身份绑定。 - 导入输入只含“测试员工”和“测试主管”,手机号、邮箱、企微 userid 等原始字段不进入任务证据、聊天或日志。 - “测试员工”角色集合严格为 `employee`;“测试主管”角色集合严格为 `employee + manager`。后者只用于验证 `expense.approve` 和主管待办,不授予财务、薪资、银行资料或系统管理权限。 - 通过现有主管接口把测试员工的 `direct_manager_id` 指向测试主管;所有 OA 主管与角色写入均保留现有审计,不回写企微。 - 两个账号必须分别从企微入口登录,以同时验证通讯录导入后的身份绑定和实际页面权限;无需生成或传递临时密码。 ### 4.6 受控测试报销生命周期 ```text 测试员工创建报销 → 使用 0.01 元最小测试值和“系统测试、非真实业务、禁止付款”描述 → 上传不含真实票据/个人资料的安全 PDF → OCR 成功则继续;OCR 失败则在票据池人工填入唯一测试字段并确认可用 → 绑定测试票据并提交 → 测试主管确认待办可见并执行“退回” → 报销恢复 draft → 测试员工删除 draft(票据释放为 available) → 测试员工归档已释放票据 → 管理员停用两个测试 OA 账号并核对会话撤销 ``` - 测试主管不得点击“通过”;这保证测试单不会进入真实财务列表或付款批次。 - 删除仅允许发生在退回后的 `draft`;`POST /api/workflow/tasks/:taskId/return` 会在同一事务中把报销明确改回 `draft` 并终止 workflow instance,因此无需再执行申请人撤回。现有草稿删除接口会把已绑定票据释放为 `available`。 - 票据仅在解绑且为 `available`/`ocr_failed` 时手工归档;归档后保留最小审计痕迹但不再是可用业务票据。 - 账号收口使用现有 OA 停用接口,递增 token version 并撤销活动会话;企微侧两个测试成员保持原状。 ### 4.7 受控测试发现与真实使用门禁 - 测试实际确认:生产当前使用 `OCR_PROVIDER=mock`,未配置腾讯 OCR 凭证;上传的合成 PDF 被写入明显的 Mock 发票号码、销售方、日期和金额并标记为可用。 - mock 只能作为合成测试依赖,不能让真实员工把其结果误认为真实识别。杨俊真实使用前必须二选一并再次复验:配置腾讯 OCR;或关闭 mock,明确采用人工录入/核对流程。 - 用户选择并批准关闭 mock。现场拓扑没有独立 worker,`JOBS_ENABLED=true`,因此只重建实际承载 Web 与后台任务的 server;不得为了“同时重启 worker”而凭空创建第二个作业执行器。 - `OCR_PROVIDER=off` 复验确认:新 PDF 直接进入 `available`,发票代码、号码、日期、购销方和金额字段均为空,OCR 尝试为 0、请求/完成时间为空;票据可以绑定到单明细 0.01 元测试报销并正常提交、退回和清理。 - `InvoicePool.ocr_provider` 是非空枚举且 schema 默认 `tencent`;off 路径传入 `undefined` 后数据库仍写入该默认值。该字段只是历史 schema 默认,不代表发生过 OCR 调用;当前 UI 不展示 provider。是否在后续迁移中改为 nullable 应与腾讯 OCR/人工录入产品决策一起规划,不阻断本次真实报销。 - off 模式下发票号为空,基于 `invoice_code + invoice_number` 的查重不可用,但文件 SHA-256 查重仍生效;当前小团队短期由财务查看 PDF 原件并人工核对,不在本任务增加强制补录。 - 草稿删除成功后,详情页在导航完成前仍短暂请求已删除的详情与 precheck,浏览器产生预期 404 控制台噪声,但用户已回到空草稿列表。这是非阻断前端时序 finding,不影响数据清理结果。 ## 5. 失败与回退 - timer 重装或异机备份失败:不写杨俊数据,保留本地诊断并走既有脱敏群告警。 - dry-run 不满足 `target=1 / active=1 / incomplete=0 / planned create=1 / conflicts=0 / skipped=0 / failed=0`:不 apply,重新核对输入或生产现状。 - apply 失败:停止;按脚本结果诊断,不手工拼接残缺账号。 - apply 成功但主管写入失败:保留已创建的 OA 员工和企微绑定,不删除、不恢复整库;主管仍为空时不能提交报销,可安全重试同一主管接口。 - 主管写错:通过同一接口恢复写入前的 `direct_manager_id`,不恢复整库。 - 真实报销提交失败:保留草稿和真实票据状态,按返回的业务错误定位;不得通过扩权、代批或伪造数据绕过。 - 测试导入或角色/主管设置失败:停止测试,不借用真实员工账号;按单步接口恢复两名测试成员原 OA 状态。 - 测试提交失败:保留 draft 和测试票据定位问题;不得改成“审批通过”绕过。修复后从同一测试草稿重试或删除后重建。 - 测试退回后删除失败:禁止停用账号,先保留两名测试账号以完成清理;不得直写数据库删除业务记录。 - 测试账号误被赋予超范围角色:立即通过现有角色接口恢复最小角色集合并撤销会话,不恢复整库。 - OCR 仍为 mock:保持测试账号停用,不让杨俊上传真实票据;不得用“人工记住忽略 Mock 字段”作为默认生产口径。当前已切换为 off,此回退项不再触发。 ## 6. 隐私与审计 - 终端与任务证据只保留布尔值、计数、角色 slug、审计 action 和脱敏状态。 - 不输出手机号、企业邮箱、企微 userid、完整账号、通讯录原始行、票据内容、银行资料、Secret 或完整宿主路径。 - 账号导入与主管写入分别保留现有系统审计,确保每一步可独立追踪和撤销。 ## 7. 不做的设计扩展 - 不新增导入 API、主管兜底规则、CEO 自审批能力或报销状态。 - 不启用 OA → 企微通讯录写回,不修改任何企微成员资料。 - 不创建法体、不补银行账号、不导入其余成员。 - 不把首次真实业务扩成管理员代办的生产自动化测试。 - 不创建新的企微测试成员,不给测试主管财务或管理员权限,不让测试单通过审批或进入付款链路。