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

7.0 KiB
Raw Blame History

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

无。