文件

9.2 KiB

审批退回结果通知缺失治理

Goal

修复请假、采购和差旅申请被审批人退回后申请人收不到结果通知的静默失败,使申请人能够及时知道单据已回到草稿、查看退回意见并继续修改;同时确保通知失败不改变已经完成的审批结果。

Background

审批退回路由已经对报销、请假、采购和差旅执行同一通知入口,但 returned 的通知类型映射只覆盖报销。请假、采购和差旅因此在创建站内通知前被跳过;问题不在 dispatcher,也不需要重写退回事务。

生产只读核验确认 reimbursement.returned 已产生真实站内通知,并至少有一条成功外部投递;当前生产没有请假、采购或差旅 return 历史,也没有符合“退回超过 7 天、仍为草稿且未重提”的存量单据。因此本任务不含历史补发。

生产唯一启用的 IM 渠道使用显式通知类型与领域白名单,目前未放行请假、采购和差旅域。代码修复与生产渠道 routing 必须分阶段执行;只部署代码不能宣称企业微信通知已修复。

详细证据见 research/return-notification-gap.md。

Scope

  • 新增请假、采购和差旅退回的运行时通知类型及领域映射。
  • 接通既有站内通知、邮件任务和 IM Outbox 流程,复用当前收件人、文案和 deeplink 规则。
  • 为三类实体增加精确的结果通知与路由回归测试,并保护现有审批动作和通知类型。
  • 更新稳定通知事实源和审批工作流规约。
  • 在代码发布并取得独立生产写入授权后,以受控方式扩充生产 IM 渠道 routing,并保留可执行回滚快照。
  • 无真实事件样本时,明确区分“代码/配置已就绪”和“真实企业微信送达已验证”。

Requirements

R1:补齐 returned 通知类型合同

  • 新增 leave.returned、purchase_request.returned、travel_request.returned。
  • 三种类型必须进入 IM 支持清单,并分别映射到 leave、procurement、travel domain scope。
  • 报销 reimbursement.returned 以及所有 approved/rejected 类型保持原值和原行为;补卡仍不提供 return。

R2:三类退回统一生成结果通知

  • 请假、采购和差旅 return 成功后必须为既有申请人/提交人规则解析出的目标用户创建站内通知。
  • 通知标题使用“已退回”,正文明确“已退回,可修改后重新提交”并包含审批意见;四类可退回实体共用同一能力口径,不沿用“驳回”措辞或新增实体文案分叉。
  • targetType、targetId 与 deeplink 必须沿用实体元数据:请假进入请假列表,采购进入采购列表,差旅进入带目标 ID 的详情入口。
  • 采购和差旅的申请人、提交人为同一人时只生成一份通知。

R3:渠道副作用与业务事务解耦

  • 站内通知、email task 与 IM dispatch task 必须由同一次 outcome 投影产生,不能三处分别判断 returned 类型。
  • 通知创建、邮件或 IM 派发失败不得回滚已完成的退回、草稿回写、假期释放、预算释放或审批历史。
  • 邮件只验证任务构造和既有安全发送路径;按 WF8 冻结口径,不启用 provider,也不把真实邮件送达作为本任务硬门禁。

R4:生产 IM routing 受控放行

  • 生产 routing 变更必须晚于支持新枚举的代码部署,并单独取得生产写入授权。
  • 变更只允许新增三种 returned 类型所需的 allowedTypes 与 domain scopes;mode、timeout、roleCodes、模板、Secret 状态和其他允许类型不得漂移。
  • 写入前必须保存脱敏 routing 快照并计算有效路由差异;若除目标三种通知外还有其他类型被新放行,必须中止。
  • 只能通过带权限校验和审计记录的通知渠道管理 API 更新,不得直接手工修改数据库。
  • 读回配置与审计记录后才能认定 routing 生效;回滚时先恢复原 routing,再回滚应用版本。

R5:无存量补发

  • 当前无请假、采购或差旅 return 历史,不创建补发脚本、不重放 workflow history、不伪造通知记录。
  • 若实施期间只读复核发现新的存量样本,只报告脱敏数量和状态;补发通知或联系员工必须另行授权。
  • 三张历史 rejected 报销不纳入本任务,它们不是 return 通知缺口的受影响对象。

R6:兼容与回归边界

  • 不修改 return/reject 的 allowedActions、endpoint、状态、outcome、history、释放逻辑或重新提交规则。
  • approved、rejected、报销 returned、待办、催办、超时和其他通知的类型、收件人、文案、路由均不得变化。
  • 通知渠道 schema 在新代码发布后必须接受三个新类型;旧配置无需 migration,现有渠道在未授权修改前保持原路由效果。

Acceptance Criteria

  • AC1:通知类型测试逐项证明三个新类型同时存在于 NOTIFICATION_TYPES、IM_SUPPORTED_NOTIFICATION_TYPES、NOTIFICATION_TYPE_TO_DOMAIN_SCOPE 三处注册表,并分别解析到 leave、procurement、travel;以已验证的 reimbursement.returned 为参照,既有类型矩阵不变。
  • AC2:workflow outcome 测试证明 leave/purchase/travel 的 returned 分别解析到新类型,reimbursement returned 仍为原类型,attendance returned 仍为空。
  • AC3:三类 return 路径分别创建目标用户站内通知,类型、标题、“已退回,可修改后重新提交”能力提示、退回意见、targetType、targetId 和 deeplink 准确;申请人与提交人相同时无重复通知。
  • AC4:同一结果投影同时构造 email task 与 IM dispatch task;测试证明邮件/IM 失败不回滚业务退回结果,且不泄露 provider 原始错误或收件人敏感信息。
  • AC5:IM 路由测试覆盖三个新类型的允许与拒绝分支;显式 domain 或 allowedTypes 白名单缺少目标值时固定返回不匹配,配置完整时只为预期渠道入队;保留空数组代表该维度不限制的既有语义。
  • AC6:请假假期释放、采购/差旅预算释放、四类实体状态与 history、全部 approved/rejected 结果通知和报销 returned 回归通过。
  • AC7:docs/领域事件清单.md、docs/集成边界.md 与审批 workflow spec 明确三种 returned 结果通知及“通知失败不回滚业务”的稳定合同。
  • AC8:实施完成后 Prisma validate、Node 语法检查、聚焦后端测试、process contract、docs registry、Trellis governance 与 git diff --check 通过;因无前端改动,不新增 UI 视觉范围。
  • AC9:部署前获得独立授权;代码部署后 /ready 为候选 SHA,现有通知与审批 smoke 通过,生产 routing 在配置授权前没有变化。
  • AC10:生产 routing 经独立授权后由管理 API 做加法更新;脱敏快照、有效路由差异、读回值和审计记录证明只新增目标三种通知能力,回滚步骤可执行。
  • AC11:若取得受控生产数据写入授权或出现自然 return 样本,站内通知与企业微信真机送达、点击回 OA、delivery 记录和业务状态不回滚均通过;若没有真实样本,只能记录 BLOCKED,不得用单测、通道测试消息或合成 dispatcher 结果冒充真实送达 PASS。

Test Plan

  • 类型合同:三种新通知类型、domain scope、管理 API schema、既有类型负向保护。
  • workflow:五类 returned 映射矩阵;请假/采购/差旅的站内、email task、IM dispatch task;收件人去重、文案和 deeplink。
  • 容错:站内创建异常、邮件发送异常、IM 无匹配渠道或派发失败均不改变退回业务结果。
  • 回归:报销 returned、五类 approved/rejected、请假额度释放、采购/差旅预算释放、补卡无 return。
  • 生产:候选 provenance、/ready、配置前后脱敏矩阵、管理 API 审计、可用真实样本的两渠道送达与点击回流。

Out of Scope

  • 修改审批动作、退回/驳回语义、状态机、预算或假期规则。
  • 历史通知重放、存量补发、主动联系员工或改写业务单据。
  • 未经授权创建真实生产请假、采购或差旅数据。
  • 新增通知表、Outbox 事件、provider、消息总线或前端通知页面改版。
  • 启用或验收邮件 provider。
  • 调整三张 rejected 报销及父任务 AC11。

Risks And Deferred Items

  • 生产渠道同时使用 allowedTypes 与 domainScopes。新增 domain scope 可能让已在 allowedTypes 中的其他类型同时生效,因此必须比较“有效允许矩阵”,不能只比较数组 diff。
  • 代码回滚但 routing 未先恢复,会留下旧版本不认识的配置值;回滚顺序固定为 routing 后应用。
  • 当前没有三类真实 return 样本。代码与 routing 可完整交付,但企业微信真实送达在自然事件或受控生产样本出现前必须保持 BLOCKED。
  • 若未来发现历史受影响对象,是否补发要按通知时效、当前业务状态和打扰风险单独决策,不能自动重放。

Planning Gate

本任务涉及共享 workflow、通知类型合同和生产配置,属于复杂任务。design.md、implement.md 与研究记录齐备后,必须由用户审批最终规划;批准前不得执行 task.py start、修改产品代码或写入生产配置。