- 审批核心在实例进入提交失败的同一事务内写 submission_failed 决策投递与终态事件,消费入口单独放行 - 核销(4)、退款(7)、扫码注册(3)、资料资格(4)、线下充值(7)、佣金提现(5)新增“审批提交失败”终态并释放预占/冻结 - 三处恢复入口不再重放提交失败;退款关闭类终态按审批尝试定位退款单(M13) - 核销列表与详情批量返回审批提交状态;筛选、字段说明、状态名称、退款导出加入新状态 - 迁移 000239 放宽五张业务表状态约束与决策投递取值约束(测试库已执行 up/down/up) - OpenSpec Change fix-approval-submission-failure-terminal:18/26,测试部署后端到端验证与存量补数据待完成
7.0 KiB
7.0 KiB
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
- 复用既有决策通道,新增决策值而不是新事件类型。
pkg/constants/approval.go增加ApprovalDecisionSubmissionFailed = "submission_failed";markSubmissionState在状态 7 分支、实例更新成功后,同一事务写入决策投递记录与TerminalEventWriter.Append事件(见决策 9),事件号为approval:{实例ID}:submission_failed。不新建订阅链路,幂等、重试、死信全部沿用现有机制。备选是业务模块轮询“实例 7 且业务单待审批”,延迟高且要六处各写一套扫描,放弃。 - 六个消费者同一次交付接入。
default分支返回错误,未接入的业务类型会进入分发失败重试,因此 M8、M14 同批,消费者显式处理新值,不能落入驳回分支。处理前锁审批实例并确认仍为 7。 - 提交失败为终态,去掉重放。 决策异步消费,若业务单变终态前有人点“恢复审批”,实例被重放回提交中,会出现“业务单已终态、企微又生成审批”的分裂。三处恢复入口的 replay 分支一律拒绝;“结果未知”“已有单号同步”分支不变。
- 状态值。 核销 4、退款 7、扫码注册 3、资料资格 4、线下充值 7、佣金提现 5。
- 退款状态 7 的门禁与已拒绝(3)一致。 加入
RefundResubmittableStatuses(),不加入RefundActiveStatuses():既可对原单重新发起,也可为同一订单新建退款单,两张同时生效由uk_refund_request_active_order兜底。状态 7 与 3、4 同属“钱未动的终态”,且不可恢复,无重复退款风险;状态 6 禁止另建新单是因为渠道退款可能已发起,与 7 不同。2026-10-11 已确认按 3、4 处理。换货校验HasUnfinishedByAsset不改,新状态自然不阻断换货。 - M13 照通过分支写法。
ResolveRefundInTx本身不加锁,解析后再按target.ID对退款单SELECT ... FOR UPDATE;其余逻辑不变。重新发起沿用CreationService.Resubmit,已允许状态 3。 - 核销投影一次批量读取。 按
latest_approval_instance_id批量读tb_wecom_approval_context,状态名称映射与脱敏复用退款已有实现(internal/service/refund/service.go:1467-1520)。 - 迁移只放宽约束。 一组新的成对迁移:五张表逐表
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 实施时发现)。 - 写决策时同时建投递记录。 现有终态决策(
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)需注入这两个写入器,同步修改启动装配。 - 只在消费入口放行新决策值。
TerminalDecisionConsumer.Consume(internal/infrastructure/approval/terminal_event.go:69)用approvaldomain.StatusForDecision校验决策值,目前不认submission_failed。该函数也被渠道状态推进ApplyDecision使用,直接加值会让企微回调可以把实例推到提交失败;因此只在Consume中对submission_failed单独放行,StatusForDecision不改。 - 失败原因由业务模块读取。
TerminalDecisionEvent没有原因字段,不扩展事件结构;各业务消费者处理submission_failed时按实例读取tb_wecom_approval_context.last_error,脱敏后写入各自的终态原因字段与审计。
Risks / Trade-offs
- [某个消费者漏接新决策] → 分发失败重试堆积;任务中逐个消费者核对,测试环境对六个业务类型各构造一次提交失败。
- [漏写投递记录或消费入口未放行] → 事件静默结束或被拒;验证时逐个确认投递记录状态为成功、业务单状态已变化。
- [测试库已有“实例 7、业务单待审批”存量] → 代码不自动修正;在测试环境只读统计,需要时写一次性补数据脚本,经确认后执行。
- [佣金提现冻结释放出错影响余额] → 释放与状态更新同事务、条件更新检查
RowsAffected,验证时核对钱包余额与流水。 - [退款状态 7 可同时走重发与新建两条路] → 与已拒绝一致,唯一索引保证至多一张活动申请。
Migration Plan
测试环境:先执行新的成对迁移(只放宽约束,旧代码照常运行),再部署 API 与 Worker。六个消费者与核心决策同一次部署。
Open Questions
无。