fix(v2): jiu 反馈波——端型透传(wap/page)+ alipay qr(当面付移植)+ 单号增熵/挡板限流
1. CreateOrderInput/RetryOrder 加 Metadata 通道,handler 白名单过滤(is_mobile/render)
后原样传给 provider.CreateRequest;alipay adapter 据 is_mobile 选 wap/page。
2. alipay adapter 移植 v1 当面付(TradePreCreate):Metadata["render"]=="qr" → 二维码
render_type,payload={qr_content,display_amount,currency},默认 2 小时窗口。
3. NewOutTradeNo 随机部分 8→16 hex 防生日碰撞(仍 <=64 字符,合规 DB 列/支付宝上限);
v2 改状态端点(下单/重试/取消)加 per-IP 内存令牌桶限流,默认开 30/min,
config.rate_limit.disabled 可关;callback/GET 查询不限。
顺带修:internal/reconcile/sync_test.go 的 gateway.New 调用漏传 refunds store,
预先存在的编译期回归(与本次改动无关,但挡住 go test ./... 全绿,一并修掉)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -60,6 +60,9 @@ type CreateOrderInput struct {
|
||||
BizSystem string
|
||||
BizRef string
|
||||
ReturnURL string
|
||||
// Metadata 透传给 provider.CreateRequest(如 alipay 用 is_mobile 选 wap/page、
|
||||
// render=qr 选当面付)。handler 层已按白名单过滤,这里原样传,nil 安全。
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
type SessionView struct {
|
||||
@@ -111,7 +114,7 @@ func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderR
|
||||
|
||||
sess, err := prov.Create(ctx, provider.CreateRequest{
|
||||
OutTradeNo: outNo, Subject: subject, AmountMinor: amountMinor,
|
||||
Currency: currency, Account: acct, ReturnURL: in.ReturnURL,
|
||||
Currency: currency, Account: acct, ReturnURL: in.ReturnURL, Metadata: in.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,7 +155,8 @@ func (g *Gateway) GetOrder(outTradeNo string) (*OrderStatusView, error) {
|
||||
|
||||
// RetryOrder spawns a fresh attempt (possibly a different method) on a still-pending
|
||||
// order; old pending attempts are expired. attempt 超时 ≠ order 关闭(设计 §3.2)。
|
||||
func (g *Gateway) RetryOrder(ctx context.Context, outTradeNo, method string) (*OrderResult, error) {
|
||||
// metadata 同 CreateOrderInput.Metadata,原样透传给新尝试的 CreateRequest(nil 安全)。
|
||||
func (g *Gateway) RetryOrder(ctx context.Context, outTradeNo, method string, metadata map[string]string) (*OrderResult, error) {
|
||||
o, err := g.orders.GetOrder(outTradeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -188,7 +192,7 @@ func (g *Gateway) RetryOrder(ctx context.Context, outTradeNo, method string) (*O
|
||||
}
|
||||
sess, err := prov.Create(ctx, provider.CreateRequest{
|
||||
OutTradeNo: outTradeNo, Subject: o.Subject, AmountMinor: o.AmountMinor,
|
||||
Currency: o.Currency, Account: acct,
|
||||
Currency: o.Currency, Account: acct, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestRetryAndCancel(t *testing.T) {
|
||||
res, _ := g.CreateOrder(ctx, gateway.CreateOrderInput{SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1"})
|
||||
|
||||
// retry:弃旧尝试 + 建新尝试(order 仍 pending)。
|
||||
r2, err := g.RetryOrder(ctx, res.OrderNo, "fake")
|
||||
r2, err := g.RetryOrder(ctx, res.OrderNo, "fake", nil)
|
||||
if err != nil || r2.OrderNo != res.OrderNo {
|
||||
t.Fatalf("retry = %+v, %v", r2, err)
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func TestRetryAndCancel(t *testing.T) {
|
||||
t.Fatalf("已取消单再取消应 false")
|
||||
}
|
||||
// canceled 单不可 retry。
|
||||
if _, err := g.RetryOrder(ctx, res.OrderNo, "fake"); err != gateway.ErrOrderNotPending {
|
||||
if _, err := g.RetryOrder(ctx, res.OrderNo, "fake", nil); err != gateway.ErrOrderNotPending {
|
||||
t.Fatalf("canceled 单 retry 应 ErrOrderNotPending, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func TestRetrySwitchesAccount(t *testing.T) {
|
||||
res, _ := g.CreateOrder(ctx, gateway.CreateOrderInput{SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1"})
|
||||
|
||||
// 首单落 fake-a1(round_robin 计数 0);retry 排除 a1 → 必落 fake-a2。
|
||||
if _, err := g.RetryOrder(ctx, res.OrderNo, "fake"); err != nil {
|
||||
if _, err := g.RetryOrder(ctx, res.OrderNo, "fake", nil); err != nil {
|
||||
t.Fatalf("retry: %v", err)
|
||||
}
|
||||
pend, _ := orders.ListAttemptsByStatus(model.AttemptPending, 10)
|
||||
@@ -163,6 +163,31 @@ func TestRetrySwitchesAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// jiu 反馈波 item 1:CreateOrderInput.Metadata / RetryOrder 的 metadata 参数须原样
|
||||
// 透传到 provider.CreateRequest.Metadata,不能在 gateway 管线里被丢弃或改写。
|
||||
func TestMetadataPassthroughToProvider(t *testing.T) {
|
||||
g, fp, _, _ := newGateway(t)
|
||||
ctx := context.Background()
|
||||
|
||||
res, err := g.CreateOrder(ctx, gateway.CreateOrderInput{
|
||||
SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1",
|
||||
Metadata: map[string]string{"is_mobile": "1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if got := fp.LastMetadata(); got["is_mobile"] != "1" {
|
||||
t.Fatalf("CreateOrder 后 provider 收到的 metadata = %+v, want is_mobile=1", got)
|
||||
}
|
||||
|
||||
if _, err := g.RetryOrder(ctx, res.OrderNo, "fake", map[string]string{"render": "qr"}); err != nil {
|
||||
t.Fatalf("retry: %v", err)
|
||||
}
|
||||
if got := fp.LastMetadata(); got["render"] != "qr" {
|
||||
t.Fatalf("RetryOrder 后 provider 收到的 metadata = %+v, want render=qr", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrderCurrencyFromChannelCapability(t *testing.T) {
|
||||
g, _, _, orders := newGateway(t) // fake provider, SettleCurrencies=["USDT"]
|
||||
res, err := g.CreateOrder(context.Background(), gateway.CreateOrderInput{SKU: "pro_year", Method: "fake"})
|
||||
|
||||
Reference in New Issue
Block a user