diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 47faaf7..2ea83f7 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -72,7 +72,7 @@ type OrderResult struct { // account, persists a pending Order + Attempt (P1 OrderStore), and returns the // payment session {render_type, payload}. 加渠道不改 client(设计 §4.2)。 func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderResult, error) { - amountMinor, currency, subject, _, err := g.products.Resolve(in.SKU) + amountMinor, currency, subject, bizCode, err := g.products.Resolve(in.SKU) if err != nil { return nil, err // ErrProductNotFound } @@ -93,7 +93,7 @@ func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderR if err := g.orders.CreateOrder(&model.OrderV2{ OutTradeNo: outNo, BizSystem: in.BizSystem, BizRef: in.BizRef, - Subject: subject, AmountMinor: amountMinor, Currency: currency, + BizCode: bizCode, Subject: subject, AmountMinor: amountMinor, Currency: currency, Status: model.OrderPendingV2, }); err != nil { return nil, err diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go index dad3171..4ec78c7 100644 --- a/internal/gateway/gateway_test.go +++ b/internal/gateway/gateway_test.go @@ -81,6 +81,23 @@ func TestCreateOrderPipeline(t *testing.T) { } } +func TestCreateOrderPersistsBizCode(t *testing.T) { + g, _, _, orders := newGateway(t) + res, err := g.CreateOrder(context.Background(), gateway.CreateOrderInput{ + SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1", + }) + if err != nil { + t.Fatalf("create: %v", err) + } + o, err := orders.GetOrder(res.OrderNo) + if err != nil { + t.Fatalf("get order: %v", err) + } + if o.BizCode != "pro_year" { // stubResolver 的 bizCode + t.Fatalf("order.BizCode = %q, want pro_year", o.BizCode) + } +} + func TestCreateOrderErrors(t *testing.T) { g, _, _, _ := newGateway(t) ctx := context.Background() diff --git a/internal/gateway/settle.go b/internal/gateway/settle.go index d19476c..0e1f46a 100644 --- a/internal/gateway/settle.go +++ b/internal/gateway/settle.go @@ -82,14 +82,15 @@ func (g *Gateway) enqueuePaymentSucceeded(att *model.Attempt, paidAt time.Time) return nil // 独立收款无业务方回调;或已翻转/已取消 } data := map[string]any{ - "event_type": "payment.succeeded", - "out_trade_no": o.OutTradeNo, - "biz_system": o.BizSystem, - "biz_ref": o.BizRef, - "amount_minor": o.AmountMinor, - "currency": o.Currency, - "channel": att.Channel, - "paid_at": paidAt.Format(time.RFC3339), + "event_type": "payment.succeeded", + "out_trade_no": o.OutTradeNo, + "biz_system": o.BizSystem, + "biz_ref": o.BizRef, + "product_biz_code": o.BizCode, // 设计 §5:业务方按套餐码映射权益(时长/档位),不硬编码 product_id + "amount_minor": o.AmountMinor, + "currency": o.Currency, + "channel": att.Channel, + "paid_at": paidAt.Format(time.RFC3339), } return g.webhook.Enqueue(o.OutTradeNo, o.BizSystem, "payment.succeeded", data) } diff --git a/internal/gateway/settle_test.go b/internal/gateway/settle_test.go index f040bb9..a996465 100644 --- a/internal/gateway/settle_test.go +++ b/internal/gateway/settle_test.go @@ -53,6 +53,29 @@ func TestSettleHappyIdempotentAndWebhook(t *testing.T) { } } +func TestSettleWebhookCarriesProductBizCode(t *testing.T) { + g, fp, spy, orders := newGateway(t) + ctx := context.Background() + res, _ := g.CreateOrder(ctx, gateway.CreateOrderInput{ + SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-9", + }) + ref := attemptRef(t, orders) + fp.SetQueryResult(ref, provider.PaidEvent{ + ProviderRef: ref, Status: provider.PaidSucceeded, + PaidAmountMinor: 29990000, PaidCurrency: "USDT", + }) + if _, err := g.SyncPendingAttempts(ctx, 10); err != nil { + t.Fatalf("sync: %v", err) + } + if len(spy.calls) != 1 { + t.Fatalf("want 1 webhook, got %d", len(spy.calls)) + } + if spy.calls[0]["product_biz_code"] != "pro_year" { + t.Fatalf("payload product_biz_code = %v, want pro_year", spy.calls[0]["product_biz_code"]) + } + _ = res +} + func TestSettleGuards(t *testing.T) { g, _, spy, orders := newGateway(t) ctx := context.Background() diff --git a/internal/model/v2.go b/internal/model/v2.go index 1aa6547..b73afb6 100644 --- a/internal/model/v2.go +++ b/internal/model/v2.go @@ -47,6 +47,7 @@ type OrderV2 struct { BizSystem string `gorm:"index;size:32"` // 接入方(pangolin/jiu),空=独立收款 BizRef string `gorm:"size:128"` // 接入方业务单号,回调原样带回 ProductID uint64 `gorm:"index"` + BizCode string `gorm:"index;size:64"` // 套餐码副本(下单时从 product 复制),回调带回业务方按码映射权益(设计 §5) Subject string `gorm:"size:128"` // 金额:int64 最小单位 + 币种(presentment/charge/settlement 三态在 charge 落地此处) AmountMinor int64 `gorm:"not null"` @@ -74,7 +75,7 @@ type Attempt struct { AmountMinor int64 `gorm:"not null"` Currency string `gorm:"size:16;not null"` Status AttemptStatus `gorm:"index;size:16;not null"` - ExpiresAt *time.Time // 本次尝试超时 + ExpiresAt *time.Time // 本次尝试超时 PaidAt *time.Time }