fix(v2): settle 定位尝试的瞬时读库错误归 SettleFailed(可重试),不再误标 not_found
AttemptByProviderRef 返回非哨兵错误(如 DB 抖动)时原先与"查无此单"终态 一并标 SettleNotFound;调用方按结果值决定 ack,会把已付订单永久丢弃。 非哨兵分支改判 SettleFailed(可重试,渠道会重投),errors.Is(ErrAttemptNotFound) 分支维持 SettleNotFound 不变。新增 TestSettleTransientReadErrorIsFailed 通过 关闭底层连接强制产出瞬时读库错误,验证结果与语义。
This commit is contained in:
@@ -43,7 +43,8 @@ func (g *Gateway) Settle(ctx context.Context, ev *provider.PaidEvent) (SettleRes
|
||||
if errors.Is(err, store.ErrAttemptNotFound) {
|
||||
return SettleNotFound, nil
|
||||
}
|
||||
return SettleNotFound, err
|
||||
// 读库瞬时失败是可重试态,不能与"查无此单"终态混淆。
|
||||
return SettleFailed, err
|
||||
}
|
||||
// 金额/币种核对:币种须一致,实付须 ≥ 应收(允许 crypto 多付,拒少付)。
|
||||
if ev.PaidCurrency != att.Currency || ev.PaidAmountMinor < att.AmountMinor {
|
||||
|
||||
@@ -5,9 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/wangjia/pay/config"
|
||||
"github.com/wangjia/pay/internal/accounts"
|
||||
"github.com/wangjia/pay/internal/gateway"
|
||||
"github.com/wangjia/pay/internal/model"
|
||||
"github.com/wangjia/pay/internal/provider"
|
||||
"github.com/wangjia/pay/internal/provider/fake"
|
||||
"github.com/wangjia/pay/internal/store"
|
||||
)
|
||||
|
||||
func attemptRef(t *testing.T, orders interface {
|
||||
@@ -103,6 +107,41 @@ func TestSettleEnqueueFailureKeepsOrderPending(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 瞬时读库失败(非 store.ErrAttemptNotFound 哨兵)必须归 SettleFailed(可重试),
|
||||
// 不能与"查无此单"的终态 SettleNotFound 混淆——否则调用方按结果值决定 ack,
|
||||
// 会把已付订单永久丢弃。构造方式:先建好订单/尝试,再直接关掉底层连接,
|
||||
// 让 AttemptByProviderRef 打到一个已关闭的 DB 上,产出非哨兵错误。
|
||||
func TestSettleTransientReadErrorIsFailed(t *testing.T) {
|
||||
db := model.OpenTestDB(t)
|
||||
orders := store.NewOrderStore(db)
|
||||
preg := provider.NewRegistry()
|
||||
fp := fake.New()
|
||||
preg.Register(fp)
|
||||
areg := accounts.New([]config.AccountConfig{
|
||||
{AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true, Weight: 1},
|
||||
})
|
||||
spy := &spyEnqueuer{}
|
||||
g := gateway.New(orders, preg, areg, stubResolver{}, spy, "global")
|
||||
|
||||
ctx := context.Background()
|
||||
g.CreateOrder(ctx, gateway.CreateOrderInput{SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1"})
|
||||
ref := attemptRef(t, orders)
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("db.DB(): %v", err)
|
||||
}
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
t.Fatalf("close db: %v", err)
|
||||
}
|
||||
|
||||
ev := &provider.PaidEvent{ProviderRef: ref, Status: provider.PaidSucceeded, PaidAmountMinor: 29990000, PaidCurrency: "USDT"}
|
||||
got, err := g.Settle(ctx, ev)
|
||||
if got != gateway.SettleFailed || err == nil {
|
||||
t.Fatalf("瞬时读库失败应 SettleFailed+err, got %v, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCallbackAndSync(t *testing.T) {
|
||||
g, fp, _, orders := newGateway(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user