7.0 KiB
技术设计:审批退回结果通知缺失治理
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:通知构造或记录失败只写脱敏错误,不回滚业务退回。事务提交后:
- 邮件通过
sendNotificationEmailSafebest-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:代码发布
- 完成代码、测试、只读 review 和本地门禁。
- 获得 commit、push、部署授权。
- 部署支持新类型的候选,核
/ready、日志、现有审批/通知 smoke。 - 只读确认生产渠道 routing 尚未变化。此时站内链路已具备能力,但企业微信三种新类型仍未放行。
阶段 B:生产 routing 放行
- 另行取得生产配置写入授权。
- 通过管理 API 读取脱敏配置,保存原
roleCodes/domainScopes/allowedTypes快照。 - 计算当前全部 supported type 的有效匹配结果;目标变更集合必须严格等于三种新 returned。
- 通过
PATCH /notifications/channels/:id做加法更新,保留其他配置与 Secret 状态。 - 读回并核对
NOTIFICATION_CHANNEL_UPDATED审计记录。 - 再次计算有效匹配矩阵,确认没有额外类型或角色被放行。
管理 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 历史,因此不设计补发器。若发布前复核发现新样本,只做脱敏清单与状态分类,不自动通知。过期事件是否补发由用户另行判断。