Files
junhong_cmp_fiber/openspec/changes/fix-approval-submission-failure-terminal/design.md
邹进 6298a9061f fix: 第1批 M8、M13、M14 审批提交失败终态与退款关闭类终态定位
- 审批核心在实例进入提交失败的同一事务内写 submission_failed 决策投递与终态事件,消费入口单独放行
- 核销(4)、退款(7)、扫码注册(3)、资料资格(4)、线下充值(7)、佣金提现(5)新增“审批提交失败”终态并释放预占/冻结
- 三处恢复入口不再重放提交失败;退款关闭类终态按审批尝试定位退款单(M13)
- 核销列表与详情批量返回审批提交状态;筛选、字段说明、状态名称、退款导出加入新状态
- 迁移 000239 放宽五张业务表状态约束与决策投递取值约束(测试库已执行 up/down/up)
- OpenSpec Change fix-approval-submission-failure-terminal:18/26,测试部署后端到端验证与存量补数据待完成
2026-10-12 00:03:46 +09:00

54 lines
7.0 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
承接《第1批-8-9月迭代缺陷修复技术方案》(下称“方案”)第 3、7、8 节。
- 提交失败:`approval_submission_consumer.go` 在表单构造失败和企微明确拒绝两处调用 `ApprovalContextRepository.MarkFailed`,其内部 `markSubmissionState` 单事务更新企微上下文与审批实例(7),但不通知业务模块。测试库退款、线下充值、核销、扫码注册均有提交失败实例停在待审批。
- 决策分发:六个业务类型已通过 `DecisionDispatcher` 注册决策消费者,终态经 `TerminalEventWriter`(`internal/infrastructure/approval/terminal_event.go`)写入公共 Outbox;各消费者 `switch` 的 `default` 分支返回错误。
- M13:`internal/service/refund/approval_decision.go:227` 关闭分支用 `event.BusinessID` 直接查退款单,而新流程中 `BusinessID` 是审批尝试 ID;通过分支(:48、:92)已用 `refundapproval.ResolveRefundInTx`。
- 恢复:核销、退款、扫码注册三处恢复入口在“实例为 7 且上下文为提交失败”时走 `RecoverSubmissionEvent` 重放。
- 状态约束:五张业务表有 `CHECK (status IN ...)`,佣金提现表无约束。
- 核销列表投影 `ProjectApplication`(`internal/query/employeecollection/application.go:207`)未给审批提交状态字段赋值。
## Goals / Non-Goals
**Goals:**
- 提交失败的业务单进入明确终态并释放占用,可在卡管重新发起。
- 退款关闭类终态正确结束退款单。
- 核销列表与详情显示真实提交状态。
**Non-Goals:**
- 不改通过、驳回、撤销、通过后撤销的既有处理(M13 只修定位)。
- 不处理提交结果未知(8),仍由恢复任务确认。
- 不支持企微侧重新提交挂接回原单。
- 不新增佣金提现表的状态约束。
## Decisions
1. **复用既有决策通道,新增决策值而不是新事件类型。** `pkg/constants/approval.go` 增加 `ApprovalDecisionSubmissionFailed = "submission_failed"`;`markSubmissionState` 在状态 7 分支、实例更新成功后,同一事务写入决策投递记录与 `TerminalEventWriter.Append` 事件(见决策 9),事件号为 `approval:{实例ID}:submission_failed`。不新建订阅链路,幂等、重试、死信全部沿用现有机制。备选是业务模块轮询“实例 7 且业务单待审批”,延迟高且要六处各写一套扫描,放弃。
2. **六个消费者同一次交付接入。** `default` 分支返回错误,未接入的业务类型会进入分发失败重试,因此 M8、M14 同批,消费者显式处理新值,不能落入驳回分支。处理前锁审批实例并确认仍为 7。
3. **提交失败为终态,去掉重放。** 决策异步消费,若业务单变终态前有人点“恢复审批”,实例被重放回提交中,会出现“业务单已终态、企微又生成审批”的分裂。三处恢复入口的 replay 分支一律拒绝;“结果未知”“已有单号同步”分支不变。
4. **状态值。** 核销 4、退款 7、扫码注册 3、资料资格 4、线下充值 7、佣金提现 5。
5. **退款状态 7 的门禁与已拒绝(3)一致。** 加入 `RefundResubmittableStatuses()`,不加入 `RefundActiveStatuses()`:既可对原单重新发起,也可为同一订单新建退款单,两张同时生效由 `uk_refund_request_active_order` 兜底。状态 7 与 3、4 同属“钱未动的终态”,且不可恢复,无重复退款风险;状态 6 禁止另建新单是因为渠道退款可能已发起,与 7 不同。2026-10-11 已确认按 3、4 处理。换货校验 `HasUnfinishedByAsset` 不改,新状态自然不阻断换货。
6. **M13 照通过分支写法。** `ResolveRefundInTx` 本身不加锁,解析后再按 `target.ID` 对退款单 `SELECT ... FOR UPDATE`;其余逻辑不变。重新发起沿用 `CreationService.Resubmit`,已允许状态 3。
7. **核销投影一次批量读取。** 按 `latest_approval_instance_id` 批量读 `tb_wecom_approval_context`,状态名称映射与脱敏复用退款已有实现(`internal/service/refund/service.go:1467-1520`)。
8. **迁移只放宽约束。** 一组新的成对迁移:五张表逐表 `DROP CONSTRAINT` 后按新值集合重建、更新列注释;佣金提现只更新列注释。down 恢复原约束(执行前须确认没有新状态数据)。资料资格的 `chk_withdrawal_qualification_invalidated` 与 `WHERE status = 1` 唯一索引不受影响。同一迁移放宽 `tb_approval_decision_delivery.ck_approval_decision_delivery_decision`,允许 `submission_failed`,否则决策 9 写投递记录会违反约束(2026-10-11 实施时发现)。
9. **写决策时同时建投递记录。** 现有终态决策(`internal/application/approval/sync_decision.go`)在同一事务里调用 `DecisionDeliveryWriter.Create` 与 `TerminalEventWriter.Append`;分发器处理前 `Claim` 投递记录(`internal/infrastructure/approval/decision_delivery.go`,要求更新到 1 行),没有记录时返回未认领,分发器直接结束,业务单永远不变且不报错。因此 `markSubmissionState` 必须两者都写,事件号沿用 `terminalDecisionEventID` 的格式 `approval:{实例ID}:submission_failed`。`ApprovalContextRepository`(`internal/infrastructure/wecom`)需注入这两个写入器,同步修改启动装配。
10. **只在消费入口放行新决策值。** `TerminalDecisionConsumer.Consume`(`internal/infrastructure/approval/terminal_event.go:69`)用 `approvaldomain.StatusForDecision` 校验决策值,目前不认 `submission_failed`。该函数也被渠道状态推进 `ApplyDecision` 使用,直接加值会让企微回调可以把实例推到提交失败;因此只在 `Consume` 中对 `submission_failed` 单独放行,`StatusForDecision` 不改。
11. **失败原因由业务模块读取。** `TerminalDecisionEvent` 没有原因字段,不扩展事件结构;各业务消费者处理 `submission_failed` 时按实例读取 `tb_wecom_approval_context.last_error`,脱敏后写入各自的终态原因字段与审计。
## Risks / Trade-offs
- [某个消费者漏接新决策] → 分发失败重试堆积;任务中逐个消费者核对,测试环境对六个业务类型各构造一次提交失败。
- [漏写投递记录或消费入口未放行] → 事件静默结束或被拒;验证时逐个确认投递记录状态为成功、业务单状态已变化。
- [测试库已有“实例 7、业务单待审批”存量] → 代码不自动修正;在测试环境只读统计,需要时写一次性补数据脚本,经确认后执行。
- [佣金提现冻结释放出错影响余额] → 释放与状态更新同事务、条件更新检查 `RowsAffected`,验证时核对钱包余额与流水。
- [退款状态 7 可同时走重发与新建两条路] → 与已拒绝一致,唯一索引保证至多一张活动申请。
## Migration Plan
测试环境:先执行新的成对迁移(只放宽约束,旧代码照常运行),再部署 API 与 Worker。六个消费者与核心决策同一次部署。
## Open Questions
无。