按 PRD 2.3/2.4/2.5 落地套餐退款的方式矩阵与原路渠道退款: - 退款申请派生并冻结权威实收金额(线上取原成功支付记录,钱包/线下取订单实际收款), 提交人不可填写或修改;按来源支付方式生成可选方式矩阵并在创建、提交、执行前重复校验。 - 审批切换为「每次提交一条不可变审批尝试记录 + 独立企业微信审批实例」,业务标识取尝试 记录主键;终态消费按尝试记录优先、退款申请兜底双读,兼容存量无实例与已关联实例申请。 新增活动退款部分唯一索引 (order_id) WHERE status IN (1,5,6)。 - 本地人工终审保持既有开关,补齐通过入口的 approval_instance_id IS NULL 守卫,使三个 入口一致拒绝已关联审批实例的申请;重提按尝试模式重写(仅已拒绝/已退回/原路失败且无异常)。 - 权益时点:企微通过事务写退款终态、按方式确定的订单态、钱包回款、员工账单冲销与可靠 失效事实;套餐失效/接续/停机仍由既有可靠机制最终一致执行,不把外部调用放入资金事务。 订单支付状态按方式置位:凭证退款与退回原钱包在企微通过时置已退款,原路须渠道明确成功。 - 按官方契约实现微信直连 v3、微信 v2(双向证书)、富友(/commonRefund 与 /refundQuery)、 支付宝四类原路退款;能力只由服务商类型与退款必需凭证完整性决定,无人工开关。 渠道请求号在提交时冻结到尝试记录,并以 channel_submitted_at 条件认领保证资金动作至多 提交一次(重复投递只查询不二次提交);不向任何渠道传递退款结果通知地址。 - 新增 refund:channel:recovery 恢复任务只查询回填;本地查询窗口超期(富友 72 小时、 微信 v2 7 天)转原路退款失败、渠道状态已失败、分类超时未知并置异常转人工,不放行自动 重提以避免重复退款。 - 同步退款 DTO/导出/审计资源与审计查询关联、商户凭证文档,并修正 fuiou 集成契约文档。 迁移 000218(退款尝试与渠道退款事实)、000219(微信 v2 客户端证书凭证)成对提供, 未修改既有迁移;测试库 junhong_cmp_test 完成 up/down/up 与行为核对,未调用真实渠道。
25 KiB
Context
现状(开工核对确认,均为可达代码事实):
- 退款创建 100% 经
internal/application/refundapproval/creation.go在同一事务创建退款单与审批实例,business_id取退款单 ID;tb_approval_instance的uq_approval_instance_business UNIQUE (business_type, business_id)(migrations/000170_create_approval_core.up.sql:12)因此结构性地禁止同一退款单拥有第二个审批实例。 - 重提入口
POST /api/admin/refunds/:id/resubmit只接受已退回状态,直接改金额回待审批且不新建实例(internal/service/refund/service.go:720)。 - 本地人工终审默认开放(
legacy_refund_manual_enabled: true);Approve的条件更新缺少approval_instance_id IS NULL,Reject/Return已具备(service.go:315、:641、:685)。 - 实收金额由请求体自填且不校验(
internal/model/dto/refund_dto.go:6),退款金额上限只比order.actual_paid_amount。 - 活动退款互斥只有
pg_advisory_xact_lock(order_id)加计数(creation.go:181-188),无数据库约束。 - 企微终态唯一入口
internal/service/refund/approval_decision.go:事务内写退款单、订单、钱包回款、员工账单冲销、通知/佣金/资产 Outbox 与审计;套餐失效、接续与停机在事务外由 Outbox→Worker 执行(service.go:1125);revoked_after_approved当前直接return nil。 - 渠道侧完全没有退款代码:微信 v3 仅下单/查单/关单/回调,自研微信 v2 仅
unifiedorder/orderquery,支付宝仅TradeWapPay,富友仅preCreate/wxPreCreate/commonQuery。退款能力判定已存在且只依据服务商类型与凭证完整性(internal/service/refund/payment_merchant.go:64,无人工开关)。 - 商户加载不过滤状态,停用商户仍可加载(
internal/application/merchantpayment/routing.go:81)。
关键数据边界(测试库 junhong_cmp_test 只读核对):43 条退款单中 12 条无审批实例、其中 4 条待审批;2 条申请金额为 0;无订单存在两张活动退款单。
Goals / Non-Goals
Goals:
- 让原路退款在企微通过后由原实际收款商户的当前凭证可靠执行,结果未知时可恢复且绝不重复扣款。
- 让每次提交持有独立的、不可变的审批材料与审批实例,且不破坏存量已关联实例的审批消费。
- 让退款状态机、活动申请互斥与冻结实收金额在数据库层和条件更新层同时闭合。
Non-Goals:
- AUG26-012 佣金回溯明细(本 Change 只提供事实契约)。
- 代理充值预存款业务单的退款。
- 真实支付渠道实测(见「不做外部渠道实测」)。
- 对既有状态显示名、既有
approval_instance_id语义或其唯一约束做破坏性改名或重构。 - 新增任何退款相关的运行时开关(含渠道能力开关、退款方式开关、恢复开关)。
Decisions
1. 渠道退款能力判定只由服务商类型与退款必需凭证完整性决定
在既有 validateFrozenMerchantRefundCredentials 的判定上把全部三类服务商都视为具备原路退款能力,并按下列口径修正:
| 服务商类型 | 原路退款能力 | 依据 |
|---|---|---|
wechat(微信直连 v3) |
支持 | 凭证含 wx_mch_id/wx_api_v3_key/wx_cert_content/wx_key_content/wx_serial_no/wx_notify_url 时可用 v3 退款接口 |
wechat_v2 |
支持(需 APIv2 Key + API 客户端证书) | v2 退款走双向证书接口(/secapi/pay/refund,渠道文档明确「请求需要双向证书」),故必需凭证含 wx_client_cert_content 与 wx_client_key_content |
fuiou |
支持 | 既有凭证键集合已含 /commonRefund 与 /refundQuery 所需全部参数 |
alipay |
支持 | 凭证含 ali_app_id/ali_private_key/ali_public_key 时可签名退款请求 |
结论(已裁决):微信支付 v2 本身支持退款,其退款接口 /secapi/pay/refund 按渠道文档「请求需要双向证书」。因此本 Change 为 wechat_v2 扩展两个凭证键并实现该接口:
- 新增凭证键
wx_client_cert_content(apiclient_cert.pem)与wx_client_key_content(apiclient_key.pem),二者对支付/查单非必填,只用于退款能力判定与退款调用;不破坏既有 v2 商户的支付与回调。 - 判定规则:v2 商户凭证含 APIv2 Key 且含上述两项证书内容时原路退款可用;缺证书时按凭证不完整判定为不可用,仅客户收款信息退款,并返回说明「缺少 API 客户端证书」。
- 迁移
000219在历史tb_wechat_config上补齐同名列(存量行为空,补齐后生效);商户池路径的商户凭证存于tb_payment_merchant.credentials(JSONB),无需改表,由merchantpayment.MerchantConfig自动映射。 - 证书装载失败(内容非法/不匹配)不阻断 v2 支付与查单:实例不装载证书,退款能力随之判定为不可用。
- 不提供人工开关;能力仍只由服务商类型与凭证完整性决定。
渠道结果语义:渠道文档规定 v2 退款申请接口「返回仅代表业务的受理情况,具体退款是否成功需要通过退款查询接口获取结果」。因此 v2 受理成功映射为结果未知(保持原路处理中),由恢复任务查询收敛;refund_status=SUCCESS 才算明确成功,REFUNDCLOSE/CHANGE 为明确失败,PROCESSING 保持未知。这一点与微信 v3 不同(v3 申请接口直接返回 status)。
幂等复用:v2 的 out_refund_no 同样取持久化的渠道退款请求号,重试复用、重提更换,与微信 v3、富友、支付宝一致。
2.1 本地查询窗口:富友 72 小时、微信 v2 7 天
channel_submitted_at 之外的第二个时间边界是本地查询窗口:
| 服务商 | 本地窗口 | 性质 |
|---|---|---|
| 富友 | 72 小时 | 渠道硬约束:其退款查询接口只支持 3 日内的退款交易,超期后无法再查询 |
| 微信 v2 | 7 天(168 小时) | 本地阈值:渠道侧无查询时限,此处按本地放弃阈值避免一笔未知结果被无限轮询 |
| 微信 v3 / 支付宝 | 无 | 持续查询直到渠道给出终态 |
超期后果(终止本次渠道执行并转人工):退款申请转 status=6(原路退款失败)、渠道退款状态转 3(已失败)、failure_reason=timeout_unknown、anomaly_flag=1 并写一次异常审计;此后退出轮询,不再查询渠道。
三点必须保持的设计约束:
- 「未确认」的性质由
failure_reason承载,不由status承载。RefundFailureReasonIsDefinitive(timeout_unknown)为 false,因此该退款不会被后续佣金回溯判为「明确失败可回溯」;status=6只表达该尝试的渠道路径已终止。 - 不放行自动重提(保留
anomaly_flag=1):本次渠道请求可能已被受理但结果未知,放行重提会以新的请求号再次提交资金动作,构成重复退款风险。由人工先向渠道核对再决定处置。 - 账号级重复保护仍然生效:
status=6属于活动集合{1,5,6},因此同一订单仍被uk_refund_request_active_order阻断新建申请。
窗口起算点为「进入原路退款处理中」的时点。两条保证该点不会被隐式重置:结果未知的回写与窗口超期终止均使用 UpdateColumns,不隐式推进 updated_at。
2.2 资金动作至多提交一次:提交认领
refund.channel.refund.requested 由 Outbox 至少一次投递(重投、人工重放都可能),因此 Execute 必须区分「首次提交」与「重复投递」:
- 提交前先以
UPDATE ... SET channel_submitted_at = now() WHERE id = ? AND status = 原路处理中 AND channel_submitted_at IS NULL认领提交权; - 认领成功才调用渠道退款接口;认领失败表示该尝试已提交过,本次只调用退款查询并回填,绝不再次提交资金动作;
- 认领与结果回写同以
status = 原路处理中为谓词,因此并发执行也至多有一次认领成功。
这样「至多提交一次可确认的请求」不依赖渠道自身的幂等去重,而是本地持久化事实保证;渠道请求号只是第二道防线。
2. 富友退款按官方契约实现
- 申请退款:
POST /commonRefund。必填version、ins_cd、mchnt_cd、term_id、mchnt_order_no、random_str、sign、order_type、refund_order_no、total_amt、refund_amt;选填operator_id、reserved_fy_term_id、reserved_origi_dt、reserved_addn_inf、reserved_refund_desc。响应result_code = 000000时取refund_id、transaction_id、reserved_refund_amt、reserved_fy_settle_dt。 - 退款查询:
POST /refundQuery,入参refund_order_no;trans_stat取值SUCCESS或PAYERROR;仅支持 3 日内查询。 - 全局约束:
mchnt_order_no与refund_order_no全局永久唯一;流水号格式为「机构码(4) + 日期(yyyyMMdd) + 随机(8-18 位字母数字)」;重复提交直接拒绝;支持全额退款与多次部分退款。 - 原交易日期:不传
reserved_origi_dt仅支持 30 天内原交易,传了可退 360 天内原交易。因此原路执行 MUST 回传原支付成功时间作为原交易日期,以覆盖 30–360 天区间;超出 360 天的原交易在创建/提交预检即禁用原路。 - 回传的原交易标识:
mchnt_order_no取原支付单的商户订单号(现实现为tb_payment.payment_no,internal/infrastructure/payment/fuiou_scan.go:60),order_type取原交易使用的值(现实现唯一可达值为WECHAT主扫,pkg/fuiou/scan.go:20)。本 Change 按现值回传,并把「未来引入其他富友order_type时必须冻结原交易order_type」记为已知约束,不回填历史。
3. 渠道请求号:一次尝试一个稳定值,按富友规则生成
每次审批尝试在提交时生成并持久化 channel_refund_request_no,同一尝试的重试复用该值,重提生成新值。生成规则统一采用富友规则(机构码 4 位 + 上海时区 yyyyMMdd + 8–18 位字母数字,总长 ≤ 30):该值同时满足微信 v3 与支付宝的字母数字与长度约束,也满足微信 v2 的 32 位上限,因此三类渠道共用一个生成器,无需按渠道分支。
生成时点选在提交而非执行,是因为提交已必须加载冻结实际商户做能力判定,fy_ins_cd 在该时点可得;若在执行时才生成,重试路径将无法保证复用同一值。
4. 不做外部渠道实测
渠道适配按官方 SDK/接口契约实现(微信 v3 PowerWeChat、微信 v2 自研 XML、富友自研 XML/RSA、支付宝 smartwalle/alipay/v3)。Agent MUST NOT 调用任何真实支付渠道完成验证;本地验证使用受控适配器桩,覆盖明确成功、渠道明确拒绝、超时、结果未知、凭证失效、余额不足与查询恢复。真实渠道可退款性由维护者后续手工验证。文档只记录「按契约实现,未做外部渠道实测」,不列为阻塞、未完成任务或上线前置。
5. 审批尝试模型:新增 tb_refund_request_attempt,business_id 取尝试记录主键
沿用已确立的提现审批尝试模式(tb_commission_withdrawal_request_attempt,migrations/000215;消费侧 internal/application/distributionwithdrawal/withdrawal_approval.go:32-74)。
新表字段:id(主键,同时是审批 business_id)、refund_id、attempt_no、退款方式、退款金额、冻结实收金额、退款原因、客户收款信息、客户凭证键、套餐使用快照、渠道退款请求号、提交人账号、approval_instance_id、创建时间。约束:UNIQUE(refund_id, attempt_no)、approval_instance_id 部分唯一、approval_instance_id IS NULL OR > 0。
退款单追加(既有列与语义不变):latest_attempt_id、latest_approval_instance_id(仅展示,仿提现)、method、frozen_actual_received_amount、customer_account_info、channel_refund_status、channel_refund_no、channel_refund_request_no、channel_failure_reason、anomaly_flag、anomaly_reason。approval_instance_id 保持「首次接入企业微信审批的实例」语义,其既有唯一索引与本地人工终审的 IS NULL 保护全部不动。
business_type 保持 refund_approval 不变,因此 tb_wecom_approval_scene 的 chk_wecom_approval_scene_business 无需扩展,企微场景字段映射也不变。
业务标识解析(实例优先、双读兼容):
tb_refund_request_attempt WHERE id = business_id AND approval_instance_id = instance_id→ 新路径;tb_refund_request WHERE id = business_id AND approval_instance_id = instance_id→ 存量兼容;- 两者均不匹配 → 稳定冲突错误,不得回落。
用 approval_instance_id 作为唯一判别式是必需的:尝试记录与退款单来自两个独立序列,必然存在同值,仅凭 business_id 无法区分。
备选方案与取舍:曾考虑直接为退款单开放多实例(放宽 uq_approval_instance_business),被否决——该约束是全部六个审批场景共享的不变量,放宽会波及已上线的核销、提现、分销与资格审批;尝试记录模式改动面局限在退款自身。
6. 状态集、活动集合与活动退款唯一索引
RefundRequest.Status 保持 int(ENG-STATE-001),沿用既有 1–4 语义并新增 5、6:
| 值 | 常量名 | 名称 | 语义 | 可否重提 | 是否阻断同订单新申请 |
|---|---|---|---|---|---|
| 1 | RefundStatusPending |
待审批 | 企微审批在途 | 否 | 是 |
| 2 | RefundStatusApproved |
已通过 | 审批通过且退款已完成(原路须渠道明确成功) | 否 | 是 |
| 3 | RefundStatusRejected |
已拒绝 | 企微驳回、撤销或删除 | 是 | 否 |
| 4 | RefundStatusReturned |
已退回 | 本地退回,仅存量无实例申请可达 | 是 | 否 |
| 5 | RefundStatusChannelProcessing |
原路退款处理中 | 企微已通过,渠道结果未确认 | 否 | 是 |
| 6 | RefundStatusChannelFailed |
原路退款失败 | 渠道明确失败或超时保留的可恢复失败 | 是 | 是 |
- 活动集合(同订单互斥与唯一索引谓词):
{1, 5, 6},与 PRD 2.3.6 的「审批中、原路退款处理中、原路退款失败」一一对应。 - 可重提集合:
{3, 4, 6}且anomaly_flag = 0。 - 既有状态 2 的显示名保持「已通过」,不做破坏性改名;其新增语义为「退款已完成」。
anomaly_flag ∈ {0,1}与状态正交(沿用提现异常标记形态),因此「企微通过后撤销」不改变状态,而是写异常标记并禁止重提。- 契约上原路退款在企微通过后、渠道确认前订单仍为已支付,订单支付状态不再能阻止第二张申请,因此活动退款唯一索引是必需的主守卫:
CREATE UNIQUE INDEX uk_refund_request_active_order
ON tb_refund_request (order_id)
WHERE deleted_at IS NULL AND status IN (1, 5, 6);
迁移 MUST 在创建索引前探测活动集合内的重复 order_id,存在时明确失败并中止整次迁移,不自动改写历史数据。活动集合在迁移内写死并加注释说明与常量对应(迁移不引用 Go 常量)。
7. 权益时点:企微通过写可靠失效事实,最终一致执行
企微通过事务内只写可回滚的本地事实:退款申请终态、按方式确定的订单支付状态、原钱包退款回款、员工账单冲销、通知/佣金/资产三类 Outbox 与审计。套餐失效、接续下一套餐与停机评估保留既有 Outbox→Worker 路径(refund.asset.process.requested → handleRefundAssetProcessing),不在事务内执行运营商停机等外部调用(ENG-TX-001)。
与原设计措辞的偏离:原 design.md 与 spec 写作「企业微信通过事务内完成套餐失效/接续/停机」。照此实现会把运营商停机调用放进资金事务,违反 ENG-TX-001,且套餐失效本身已是可靠的最终一致流程,无需迁入事务。本 Change 改为契约层的「企微通过即写可靠失效事实」,对外可观察语义不变(企微通过后权益必然失效,渠道后续失败不回滚)。
8. 订单支付状态时点按退款方式确定
| 退款方式 | 置订单已退款的时点 | 说明 |
|---|---|---|
| 客户收款信息 | 企微通过 | 退款单同时标记已通过,不等待线下付款 |
| 退回原钱包(资产钱包/代理主钱包) | 企微通过 | 回款在同一事务完成 |
| 原路退款 | 渠道明确成功 | 此前订单保持已支付,由活动退款唯一索引阻止第二张活动申请 |
退款单一律按 PRD 2.3.2:仅渠道明确成功才标记已通过并保存渠道退款流水。既有实现的订单态更新位于企微通过事务(approval_decision.go:90),在原路方式下必须改为条件更新并在渠道成功回填路径执行;这也使既有 changed 门与重复投递幂等语义需要一并复核。
9. 本地人工终审与存量
保留既有 Approval.LegacyRefundManualEnabled 开关与入口,不新增任何运行时开关:
Approve的条件更新补齐approval_instance_id IS NULL(ENG-CONC-001 expected-status),使其与Reject/Return一致;- 已关联审批实例的申请在三个入口一律拒绝;
- 存量无实例的待审批退款(测试库 4 条、生产 9 条)保持可终结路径,不强制接入企业微信;
Resubmit按审批尝试模式重写,不再原地改金额。
零金额语义保留:既有 refund-approval 能力要求接受零金额退款通过,且存量零金额待审批申请可经既有补发入口接入企业微信。新申请金额必须为正分,企微路径的批准金额等于申请金额,因此零金额只可能来自存量申请,其方式限客户收款信息,不产生钱包或渠道资金动作。
10. 结构化失败分类
channel_failure_reason 稳定枚举(refund_failure_reason 列,空表示无失败):
| 值 | 名称 | 触发 | 判定为明确失败 |
|---|---|---|---|
channel_rejected |
渠道明确拒绝 | 渠道返回拒绝或业务错误码 | 是 |
credential_invalid |
凭证失效或缺失 | 商户退款必需凭证不完整或已失效 | 是 |
insufficient_balance |
渠道余额不足 | 渠道返回余额不足 | 是 |
timeout_unknown |
超时或结果未知 | 调用超时、网络错误或渠道仍处理中 | 否,保持可恢复 |
approval_rejected |
企微驳回或关闭 | 企业微信驳回、撤销或删除 | 是 |
revoked_after_approved |
企微通过后撤销 | 企业微信通过后撤销 | 否,转人工异常 |
payment_fact_invalid |
原支付事实不可用 | 原支付单、实际商户、原渠道流水或可退金额校验不通过 | 是 |
payment_fact_invalid 是超出既定六项分类的补充项,已裁决保留并与 credential_invalid 并列、语义不合并:credential_invalid 指该商户退款必需凭证缺失或失效(渠道侧会拒绝),payment_fact_invalid 指本地原支付单、实际收款商户、原渠道流水或可退金额校验不通过(本地即不可执行)。两者失败位置与处置不同,合并会丢失该区分;第 7 项据本分类判定可回溯性:标记为「是」的分类表示该退款已明确失败、可进入回溯判定,「否」表示仍在途或需人工处理。
渠道退款状态 channel_refund_status:0 未发起或不适用、1 处理中、2 明确成功、3 明确失败。纯钱包与客户收款信息方式固定为 0。
富友查询窗口超期(结果永久未知)不增设新状态:保留状态 5 并置 anomaly_flag = 1,anomaly_reason 记录「富友退款查询窗口已过」,转人工处理。
11. 恢复任务形态
新增 refund:channel:recovery 定时任务,复刻既有代理在线充值恢复形态(cmd/worker/main.go:465:@every 1m、Unique、MaxRetry、批次扫描、不重发资金动作)。任务只按 channel_refund_request_no 查询渠道并回填结果:明确成功→保存流水并把退款单置已通过、按方式置订单已退款;明确失败→写失败分类并置状态 6;仍未知→保持状态 5。富友仅在提交后 3 日内查询,超期转人工异常。
12. 与第 7 项(AUG26-012)的事实契约
本 Change 不实施佣金回溯明细,只固定并提供:退款完成事件键(保留既有 refund.commission.deduct.requested + refund:<id> 兼容,需要区分原路成功与客户收款信息完成时新增独立事件);refund_id 与 order_id;成功退款金额(回溯比例分子);冻结实收金额(回溯比例分母);终态时点 processed_at;结构化失败分类(区分明确失败可回溯与在途不可回溯);幂等唯一键为「一次退款一次回溯」,不得依赖 commission_deducted 布尔(该标记在既有实现中可能被重复置位)。
现有实现的全额回扣(deductAllCommission 把原佣金置失效并扣佣金钱包)与 PRD 2.14 的「原佣金不变 + 另建负数明细 + 按比例回溯 + 新增回溯终态」存在差异,本 Change 不改变该行为。
13. 渠道退款结果通知:不传递、不接收、只查询
决定(已裁决):
- 不新增任何渠道退款结果通知路由;
- 不把退款通知指向既有支付回调 URL(既有回调只承载支付业务,退款通知会污染其幂等与状态判定);
- 微信 v3 退款请求不传
notify_url(渠道/商户/支付宝特有的通知字段一律不写入退款请求); - 富友无退款通知能力,结果只经
/refundQuery获取; - 支付宝退款结果取调用同步响应;
- 退款结果的唯一确认路径为「渠道同步响应 + 主动查询 + 恢复任务」;
- 不修改
cmd/api/docs.go与cmd/gendocs/main.go。
理由:新增通知入口需要注册路由并同步两处文档生成装配(ENG-ROUTE-001),且通知会与既有支付回调的验签、幂等和状态推进逻辑纠缠。退款终态本就要求「仅渠道明确成功才标记成功」,而同步响应与主动查询已能给出该结论;恢复任务本就定义为「只查询与回填」,因此通知路径是冗余的。代价是终态确认最坏延迟到一个恢复周期,可接受。
已知约束(渠道侧未实测的前提):不传 notify_url 在渠道侧的行为未经真实调用验证。执行时按契约调用;若维护者后续实测发现渠道强制要求该字段,须新增受控通知入口并同步文档生成入口——属独立任务扩张,需另行确认。
Risks / Trade-offs
- 不传
notify_url的渠道行为未经实测 → 若某渠道在实际环境中强制要求通知字段,退款调用可能被拒绝或结果只能靠查询收敛;缓解:按契约不传该字段,终态由同步响应与恢复查询确认,未实测仅作记录、不作为阻塞。 - [活动集合含状态 5/6] 与原路退款处理期间订单保持已支付组合后,唯一索引成为唯一守卫 → 索引谓词与 Go 常量漂移会导致并发重复申请;缓解:迁移内写死集合并加注释,测试断言集合与常量一致。
- 富友 30/360 天与 3 日查询窗口是渠道硬约束 → 超期原交易无法原路、超期未知结果永久未知;缓解:创建/提交预检禁用超期原路,超期未知转人工异常,绝不重发。
- 微信 v2 商户退款能力默认不可用 → 存量 v2 商户原路退款被禁用,可能被误判为功能缺失;缓解:在错误文案中说明凭证不完整,spec 已明确能力只由凭证完整性决定。
- 尝试模式切换后存量审批终态可能失配 → 业务标识解析只凭
business_id会因两个序列同值而误判;缓解:解析必须同时匹配approval_instance_id,双读顺序与冲突失败已写入 spec。 - 订单支付状态在原路方式下延后置位 → 期间订单显示已支付但企微已通过、套餐已失效;缓解:退款单状态 5 与渠道退款状态在列表/详情可见,spec 已把该期间的可观察行为写死。
- 不做外部渠道实测 → 契实现正确性无渠道证据;缓解:按契约实现并记录,实际可退款性由维护者手工验证,不阻塞实施、验证或归档。
Migration Plan
- 新增成对迁移(编号按实施开始时
migrations/目录最大编号顺延,不预占):tb_refund_request_attempt表、退款单列扩展、活动退款部分唯一索引、失败分类与渠道状态 CHECK。既有迁移与既有approval_instance_id约束不改;不把任何既有实例的business_id回填改写为尝试记录标识。 - 迁移前探测活动集合内重复
order_id,存在则明确失败中止。 - 先部署兼容读写的 API/Worker(尝试模式写入 + 业务标识双读 + 恢复任务注册),再实现渠道退款调用与回填;本地人工终审入口保持开启,行为仅收紧为「已关联实例一律拒绝」。
- down 迁移须在存在尝试记录或渠道退款事实时拒绝执行,避免丢失不可重建的资金事实。
- 验证面按 ENG-TEST-001:
junhong_cmp_testPostgreSQL + Redis DB 6,本地工作区以显式DB_*执行scripts/migrate.sh完成 up/down/up,只创建与删除本 Change 自有 fixture,禁止重置整库;渠道适配使用受控桩,不调用真实渠道。