Files
break 4330b8a40c
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
新增设备类型选项接口
2026-09-22 12:35:26 +08:00

83 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
现有员工核销提交在同一事务内创建不可变审批尝试、通用审批实例、企业微信上下文和稳定的审批提交 Outbox申请保存最新尝试与最新实例用于展示。主 Spec 已要求提交失败、回调延迟或结果未知时保持在途并使用查询/恢复机制确认,但当前没有面向员工或管理员的恢复入口,也没有可供页面稳定判断的恢复投影。
通用审批提交事件键为 `approval:{instance_id}:submission`。恢复若绕过该事实直接创建新审批,会在“企微已受理、本地结果未知”时产生重复审批单;若释放账单预占再重建申请,则会改变业务材料与并发余额事实。因此恢复必须落在既有审批实例和可靠投递边界内。
## Goals / Non-Goals
**Goals:**
- 为员工核销建立统一、幂等的原审批恢复用例,覆盖明确失败、结果未知和已有企微单号待同步三类状态。
- 让原申请员工与有权管理员共享同一恢复规则,同时保持原申请人为企微发起人。
- 把可恢复性和安全失败摘要作为读侧投影返回,避免前端复制审批状态机。
- 保持审批尝试、申请材料、账单预占和审计事实可追溯。
**Non-Goals:**
- 不创建新的审批尝试或审批实例,不提供“强制重新提交”或本地人工终审。
- 不允许通过恢复入口修改核销材料、附件、分摊或收款方式,也不释放账单预占。
- 不改变企业微信提交、查询、回调和终态消费的既有渠道契约。
- 不为退款、代理充值或提现同时新增同类入口;通用恢复接缝可复用,但本 Change 只开放员工核销。
## Decisions
### 统一恢复用例只接受核销申请标识
新增 `POST /api/admin/employee-collection-applications/{id}/recover-approval`。Handler 只绑定申请 IDApplication 用例在事务内锁定申请和最新审批尝试,并校验申请仍为审批中、最新实例引用一致及调用方权限。请求不接受审批实例 ID、提交人或材料避免调用方替换恢复目标。
选择单一业务动作而不是分别暴露“重新提交”和“查询状态”:调用方无法可靠知道企业微信是否已受理,分裂入口会把结果未知判定泄漏给前端并放大重复提交风险。
### 恢复决策以原审批实例的渠道事实为准
用例读取通用审批实例、企业微信上下文及稳定提交事件,按固定优先级决策:
1. 已有企业微信审批单号:只请求查询同步原审批单。
2. 无审批单号且提交结果未知:恢复或触发既有结果确认流程,禁止直接创建提交。
3. 无审批单号且明确失败,并能确认未受理:把同一稳定事件恢复为可投递状态。
4. 事件正在等待、重试或处理:不写重复事件,返回正在恢复。
5. 申请、实例或企微审批已终态:返回状态冲突。
恢复实际外部动作继续由 Worker 执行HTTP 请求只原子恢复可靠事实或触发查询事实,不同步调用企微创建接口。这样 HTTP 超时不会造成调用方误认为失败后再次提交。
备选是在接口内同步查询或提交;放弃,因为请求超时和进程中断会重新引入结果未知窗口。
### 稳定事件原位恢复而非新增补偿事件
继续使用 `approval:{instance_id}:submission` 作为唯一提交业务键。对终态失败或耗尽重试的事件执行带当前状态谓词的条件更新,恢复其投递资格和必要的租约字段;对活动事件返回幂等结果。查询同步复用原审批实例的稳定查询/恢复事实,不生成另一个“人工恢复提交”事件键。
并发员工和管理员操作通过审批实例行锁与事件条件更新收敛,最多一个请求改变状态。恢复审计可每次记录触发动作,但业务提交事实保持唯一。
### 权限与目标实例使用确定规则
原申请员工可恢复本人申请;超级管理员和平台用户可代为恢复;代理、企业账号和其他账号拒绝。越权与不存在保持不可区分。管理员仅作为实际操作者写审计,审批实例中的提交人和企业微信发起身份不变。
恢复目标固定为申请最新尝试与最新实例,并校验业务类型为 `employee_collection_approval`、业务标识等于最新尝试标识、尝试关联实例一致。任一引用不一致都返回状态冲突,不按申请主表或其它候选实例猜测恢复目标。
### 可恢复性由服务端投影
核销申请列表和详情增加提交状态、可恢复标记、安全失败摘要及最近恢复时间。投影由申请状态、最新实例、企微上下文和提交事件批量计算;列表不得逐条查询放大。失败摘要使用稳定分类和脱敏信息,不返回附件、完整流水、企微原文或内部队列键。
现有企业微信审批上下文已保存 `last_recovery_at`,可直接作为最近渠道恢复时间;人工触发时间、操作者和动作结果由新增核销恢复审计表达,因此不新增业务列或数据库迁移,也不得使用通用 `updated_at` 冒充恢复时间。
### 恢复失败不改变业务申请与预占
身份、场景、模板映射或渠道暂不可用时,恢复返回明确可处置原因并保留原申请、审批实例和账单预占。配置修复后可再次恢复同一事实。数据库或事件状态更新失败时事务回滚,不形成“已记录恢复但事件未恢复”的分裂状态。
每次人工恢复记录申请、审批实例、操作者、触发前提交状态、选择的恢复动作与结果;审计禁止保存支付凭证、完整外部交易流水号和企微响应原文。
## Risks / Trade-offs
- [平台用户代办可能扩大既有核销写权限] → 恢复入口是独立受控动作,仅允许平台用户恢复原审批事实;不得据此放宽列表、详情、创建、重提或本地终审等其它核销权限。
- [Outbox 终态失败与结果未知目前使用相同状态] → 以企业微信外部单号、提交认领事实和集成结果联合判定;无法证明未受理时一律走查询确认,绝不直接重提。
- [列表投影联查增加查询成本] → 按页批量读取最新审批实例、企微上下文和事件状态,禁止逐条查询。
- [恢复事件后配置仍不可用导致反复失败] → 返回稳定失败分类并保留可再次恢复资格;恢复不增加审批单或账单预占。
- [人工恢复与 Worker 正常重试竞争] → 条件更新和租约状态确保只有一个执行获得处理权,其余返回当前状态。
## Migration Plan
1. 复用企业微信审批上下文既有 `last_recovery_at` 与审计事件时间表达恢复事实,不执行数据库迁移。
2. 发布支持恢复接缝、员工核销恢复用例、读侧投影和新路由的 API/Worker 二进制。
3. 在维护者指定测试环境验证明确失败、结果未知、已有企微单号、重复与并发恢复,核对企微审批单数量、尝试数量和账单预占均不增加。
4. 回滚时停止开放恢复入口并回退二进制;已恢复的原提交事件继续按既有可靠投递语义处理,不删除审批、申请或审计事实。