文件
hyb-oa/.trellis/tasks/09-04-approval-return-notification-gap/design.md
T

7.0 KiB
原始文件 Blame 文件历史

技术设计:审批退回结果通知缺失治理

1. 决策

沿用既有统一 workflow outcome 通知链,只补齐缺失的运行时通知类型和生产路由,不为三类实体复制 dispatcher:

entityType outcome notificationType domainScope
reimbursement returned reimbursement.returned reimbursement
leave returned leave.returned leave
purchase_request returned purchase_request.returned procurement
travel_request returned travel_request.returned travel
attendance_correction returned 无 无

三种新值是当前通知系统的稳定 notification type,同时补入通知事实源;本任务不额外创建一套业务 Outbox 领域事件。

2. 数据流

POST /workflow/tasks/:taskId/return
  -> 完成 task / 终止 instance / 业务对象回 draft / 释放额度或预算 / 写 history
  -> resolveWorkflowOutcomeNotificationType(entityType, returned)
  -> loadWorkflowNotifyContext + buildWorkflowEntityMeta + buildReturnedCopy
  -> createNotificationsWithRecords(站内通知)
  -> emailTasks[](既有安全邮件路径,非本任务正式渠道)
  -> dispatchTasks[]
  -> flushNotificationDispatchTasks
  -> notification.im Outbox
  -> channel routing: supported type AND allowedTypes AND domainScopes AND roleCodes
  -> 企业微信个人应用消息 / delivery

类型映射是三条渠道共同的唯一开关。禁止在站内、邮件和 IM 分支分别新增实体判断。

3. 收件人、文案与深链

复用 loadWorkflowNotifyContext:

  • leave:owner。
  • purchase_request:applicant 与 submitter,统一去重。
  • travel_request:applicant 与 submitter,统一去重。

复用 buildWorkflowEntityMeta 与 buildReturnedCopy,不新建三套 copy builder。统一退回结果行明确“已退回,可修改后重新提交”,与四类实体回到 draft 的实际能力一致,不复用“驳回”语义。结果通知保留各实体的现有摘要字段;审批意见缺失时沿用“无”的既有兜底。

深链保持现状:请假 /leave-requests、采购 /purchase-requests、差旅 /travel-requests?id=<id>。本任务不改变页面路由或新增前端定位能力。

4. 一致性与失败语义

站内通知记录仍在事务内尝试创建,但 outcome 通知块沿用现有 try/catch:通知构造或记录失败只写脱敏错误,不回滚业务退回。事务提交后:

  • 邮件通过 sendNotificationEmailSafe best-effort 调用;WF8 仍冻结真实邮件验收。
  • IM 通过统一 dispatcher 选择合格用户和匹配渠道并记录 delivery/Outbox;无匹配渠道产生固定 skip reason。
  • provider 错误不得把完整地址、外部 userId、Secret 或原始响应写入日志和验收材料。

不改变业务优先于通知副作用的现有合同。

5. 代码边界

预期产品改动集中于:

  • server/src/constants/notificationTypes.js:新增三种类型、IM 支持表和 domain scope 映射。
  • server/src/routes/workflow.js:扩展 returned outcome 映射。

原则上不改:

  • workflowCopy.js:已有通用文案和实体摘要。
  • imEnqueue.js、imRouting.js、notificationDispatcher.js:已有类型、领域、角色交集逻辑。
  • Prisma schema、数据库表、return 路由的业务状态与释放分支。
  • client 全部代码。

若测试暴露上述既有公共 helper 无法承载三类实体,必须先回到规划说明原因,不得顺手分叉实现。

6. 测试设计

6.1 纯函数合同

  • 每个新类型在 NOTIFICATION_TYPES、IM_SUPPORTED_NOTIFICATION_TYPES 和 domain scope 映射三处同时存在;以 reimbursement.returned 作为完整参照,漏掉任一注册表都会失败。
  • IM_SUPPORTED_NOTIFICATION_TYPES 精确包含新值且无重复,resolveNotificationDomainScope 返回对应领域。
  • returned outcome 五类矩阵准确,attendance 保持 null。
  • buildReturnedCopy 对 reimbursement/leave/purchase/travel 使用正确实体摘要和“可修改后重新提交”能力文案。

6.2 路由与 side-effect 合同

优先通过可测试 helper 或现有 route harness 覆盖一次真实 return 调用,而不是只测映射函数:

  • 三类实体分别写一份站内通知和对应 email/dispatch task。
  • applicant/submitter 重合时去重。
  • 目标类型、ID、路径和意见准确。
  • IM routing 的显式 allowedTypes/domain/role 白名单缺目标值时拒绝,全部匹配时入队;空数组继续表示该维度不限制。
  • 通知/派发失败后业务状态、workflow history 和额度/预算释放结果保持正确。

6.3 回归合同

  • 报销 returned 继续使用旧类型。
  • attendance 不出现 return 通知。
  • approved/rejected 和 task assigned/reminded/overdue 类型不变。
  • return/reject 状态机和 allowedActions 不变。

7. 生产发布与配置

采用两个独立门:

阶段 A:代码发布

  1. 完成代码、测试、只读 review 和本地门禁。
  2. 获得 commit、push、部署授权。
  3. 部署支持新类型的候选,核 /ready、日志、现有审批/通知 smoke。
  4. 只读确认生产渠道 routing 尚未变化。此时站内链路已具备能力,但企业微信三种新类型仍未放行。

阶段 B:生产 routing 放行

  1. 另行取得生产配置写入授权。
  2. 通过管理 API 读取脱敏配置,保存原 roleCodes/domainScopes/allowedTypes 快照。
  3. 计算当前全部 supported type 的有效匹配结果;目标变更集合必须严格等于三种新 returned。
  4. 通过 PATCH /notifications/channels/:id 做加法更新,保留其他配置与 Secret 状态。
  5. 读回并核对 NOTIFICATION_CHANNEL_UPDATED 审计记录。
  6. 再次计算有效匹配矩阵,确认没有额外类型或角色被放行。

管理 API schema 依赖新枚举,因此严禁在阶段 A 前写配置。

8. 真实送达验收

通知渠道测试接口发送的是通用测试消息,不经过 returned 类型解析和业务 return 路径,不能证明本缺陷已端到端修复。

有效证据只能来自:

  • 经另行批准创建并退回的受控生产测试单;或
  • 自然发生的真实请假、采购、差旅 return。

每类样本需核对站内通知、企业微信 delivery、真机消息、点击回 OA 和业务状态。缺少样本时标记 BLOCKED,不得用单测、合成 API 或通道测试冒充 PASS。

9. 回滚

  • 代码阶段无 migration/schema/seed/flag;可回滚应用构建。
  • routing 已更新时,先用管理 API 恢复原 routing 快照,核读回与审计,再回滚应用。
  • 不删除已生成的 Notification、NotificationDelivery、IntegrationOutbox 或 audit 历史。
  • 通知失败不需要回滚业务退回;只修复 routing/provider 后按明确授权决定是否重新触发或补发。

10. 存量处理

规划时生产无三类 return 历史,因此不设计补发器。若发布前复核发现新样本,只做脱敏清单与状态分类,不自动通知。过期事件是否补发由用户另行判断。