# 技术设计:审批退回结果通知缺失治理 ## 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. 数据流 ```text 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=`。本任务不改变页面路由或新增前端定位能力。 ## 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 历史,因此不设计补发器。若发布前复核发现新样本,只做脱敏清单与状态分类,不自动通知。过期事件是否补发由用户另行判断。