fix(v2): P4 终审波——歧义渠道错误留 processing(ErrRefundRejected 哨兵)/ 拒绝fixture签名 / 错误映射测试 / GetRefund 日志

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-10 17:58:42 +08:00
parent 346004adf3
commit e26f995d7f
8 changed files with 277 additions and 6 deletions
+15 -3
View File
@@ -3,6 +3,7 @@ package gateway
import (
"context"
"errors"
"log"
"time"
"github.com/wangjia/pay/internal/model"
@@ -93,9 +94,20 @@ func (g *Gateway) Refund(ctx context.Context, in RefundInput) (*RefundResult, er
// 渠道退款:refundID 作渠道幂等键。
refundRef, pstatus, rerr := rp.Refund(ctx, att.ProviderRef, refundID, in.AmountMinor, in.Reason)
if rerr != nil {
_, _ = g.refunds.MarkRefundStatus(refundID, model.RefundProcessing, model.RefundFailed, "", time.Now())
_ = g.enqueueRefundEvent(o, att, r, "refund.failed", "")
return &RefundResult{RefundID: refundID, Status: string(model.RefundFailed)}, rerr
if errors.Is(rerr, provider.ErrRefundRejected) {
// 渠道明确拒绝:钱确定没退成,安全标 failed 并释放预留额度(RefundSum
// 守卫按 status<>failed 计,见 store.CreateRefundGuarded)。
_, _ = g.refunds.MarkRefundStatus(refundID, model.RefundProcessing, model.RefundFailed, "", time.Now())
_ = g.enqueueRefundEvent(o, att, r, "refund.failed", "")
return &RefundResult{RefundID: refundID, Status: string(model.RefundFailed)}, rerr
}
// 歧义渠道错误(超时/网络/5xx 等结果不确定的失败):不改状态,留 processing
// 继续占用预留额度——渠道那边可能已经实际执行了退款,标 failed 会在没有真退成
// 的情况下释放额度,让同单能再退一次造成超退,而且 P6 只扫 processing 态的
// RefundStuckAlertTask 对账/告警会因为状态被误标 failed 而永远扫不到它。
// 退款结果不确定,留 processing 待人工核实/对账收敛。
log.Printf("[gateway] refund %s 渠道结果不确定(非 ErrRefundRejected),留 processing 待人工/对账: %v", refundID, rerr)
return &RefundResult{RefundID: refundID, Status: string(model.RefundProcessing)}, rerr
}
switch pstatus {
case provider.PaidSucceeded: