优化结构
This commit is contained in:
+130
-44
@@ -12,37 +12,39 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"kra/pkg/paymentkit"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
PaymentAlipay = paymentutil.ProviderAlipay
|
||||
PaymentAlipayV3 = paymentutil.ProviderAlipayV3
|
||||
PaymentWechatV2 = paymentutil.ProviderWechatV2
|
||||
PaymentWechatV3 = paymentutil.ProviderWechatV3
|
||||
PaymentApple = paymentutil.ProviderApple
|
||||
PaymentChinaums = paymentutil.ProviderChinaums
|
||||
PaymentSFT = paymentutil.ProviderSFT
|
||||
PaymentSuperPay = paymentutil.ProviderSuperPay
|
||||
PaymentWechatGame = paymentutil.ProviderWechatGame
|
||||
PaymentDouyinGame = paymentutil.ProviderDouyinGame
|
||||
PaymentAlipay = "alipay"
|
||||
PaymentAlipayV3 = "alipay-v3"
|
||||
PaymentWechatV2 = "wechat-v2"
|
||||
PaymentWechatV3 = "wechat-v3"
|
||||
PaymentApple = "apple-iap"
|
||||
PaymentChinaums = "chinaums"
|
||||
PaymentSFT = "sft"
|
||||
PaymentSuperPay = "supper-pay"
|
||||
PaymentWechatGame = "wechat-game-pay"
|
||||
PaymentDouyinGame = "douyin-game-pay"
|
||||
// GoPay-backed providers. These are kept separate from the existing
|
||||
// configuration-driven game/aggregator protocols above.
|
||||
PaymentDouyin = paymentutil.ProviderDouyin
|
||||
PaymentQQ = paymentutil.ProviderQQ
|
||||
PaymentAllinPay = paymentutil.ProviderAllinPay
|
||||
PaymentLakala = paymentutil.ProviderLakala
|
||||
PaymentPayPal = paymentutil.ProviderPayPal
|
||||
PaymentSaobei = paymentutil.ProviderSaobei
|
||||
PaymentInternal = paymentutil.ProviderInternal
|
||||
PaymentDouyin = "douyin"
|
||||
PaymentQQ = "qq"
|
||||
PaymentAllinPay = "allinpay"
|
||||
PaymentLakala = "lakala"
|
||||
PaymentPayPal = "paypal"
|
||||
PaymentSaobei = "saobei"
|
||||
PaymentInternal = "internal"
|
||||
|
||||
PaymentModeExternal = paymentutil.ModeExternal
|
||||
PaymentModeInternal = paymentutil.ModeInternal
|
||||
PaymentModeExternal = "external"
|
||||
PaymentModeInternal = "internal"
|
||||
)
|
||||
|
||||
var SupportedPaymentProviders = paymentutil.SupportedProviders()
|
||||
var SupportedPaymentProviders = []string{
|
||||
PaymentAlipay, PaymentAlipayV3, PaymentWechatV2, PaymentWechatV3, PaymentApple,
|
||||
PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, PaymentDouyinGame,
|
||||
PaymentDouyin, PaymentQQ, PaymentAllinPay, PaymentLakala, PaymentPayPal, PaymentSaobei,
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPaymentProviderNotFound = errors.New("支付渠道未配置")
|
||||
@@ -203,15 +205,74 @@ type PaymentTestResult struct {
|
||||
Result *PaymentResult `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentCallbackAck = paymentutil.CallbackAck
|
||||
type PaymentCallbackError = paymentutil.CallbackError
|
||||
type PaymentCallbackAck struct {
|
||||
StatusCode int
|
||||
ContentType string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type PaymentCallbackError struct {
|
||||
Cause error
|
||||
Ack PaymentCallbackAck
|
||||
}
|
||||
|
||||
func (e *PaymentCallbackError) Error() string {
|
||||
if e == nil || e.Cause == nil {
|
||||
return "支付回调处理失败"
|
||||
}
|
||||
return e.Cause.Error()
|
||||
}
|
||||
|
||||
func (e *PaymentCallbackError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func CallbackFailure(err error, fallback PaymentCallbackAck) PaymentCallbackAck {
|
||||
return paymentutil.CallbackFailure(err, fallback)
|
||||
var callbackErr *PaymentCallbackError
|
||||
if errors.As(err, &callbackErr) && callbackErr.Ack.StatusCode != 0 {
|
||||
return callbackErr.Ack
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
|
||||
return paymentutil.DefaultCallbackAck(provider, success)
|
||||
status := 200
|
||||
if !success {
|
||||
status = 500
|
||||
}
|
||||
switch provider {
|
||||
case PaymentAlipay, PaymentAlipayV3:
|
||||
body := "success"
|
||||
if !success {
|
||||
body = "failure"
|
||||
}
|
||||
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
|
||||
case PaymentWechatV2:
|
||||
code, message := "SUCCESS", "OK"
|
||||
if !success {
|
||||
code, message = "FAIL", "FAIL"
|
||||
}
|
||||
body := fmt.Sprintf("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", code, message)
|
||||
return PaymentCallbackAck{StatusCode: status, ContentType: "application/xml; charset=utf-8", Body: []byte(body)}
|
||||
case PaymentWechatV3:
|
||||
code, message := "SUCCESS", "成功"
|
||||
if !success {
|
||||
code, message = "FAIL", "失败"
|
||||
}
|
||||
body, _ := json.Marshal(map[string]string{"code": code, "message": message})
|
||||
return PaymentCallbackAck{StatusCode: status, ContentType: "application/json; charset=utf-8", Body: body}
|
||||
case PaymentApple:
|
||||
return PaymentCallbackAck{StatusCode: status}
|
||||
default:
|
||||
body := "success"
|
||||
if !success {
|
||||
body = "failure"
|
||||
}
|
||||
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
|
||||
}
|
||||
}
|
||||
|
||||
type PaymentCallback struct {
|
||||
@@ -228,6 +289,19 @@ type PaymentRepo interface {
|
||||
HandleCallback(context.Context, *PaymentCallback) (*PaymentResult, error)
|
||||
}
|
||||
|
||||
// PaymentAdapter is the integration boundary for provider protocols. SDK
|
||||
// clients and wire formats stay outside the business and data packages.
|
||||
type PaymentAdapter interface {
|
||||
Create(context.Context, *PaymentRequest, map[string]any) (*PaymentResult, error)
|
||||
Query(context.Context, string, map[string]any) (*PaymentResult, error)
|
||||
Refund(context.Context, *PaymentRefundRequest, map[string]any) (*PaymentResult, error)
|
||||
Callback(context.Context, *PaymentCallback, map[string]any) (*PaymentResult, error)
|
||||
}
|
||||
|
||||
type PaymentAdapterFactory interface {
|
||||
New(string) (PaymentAdapter, error)
|
||||
}
|
||||
|
||||
type PaymentProviderTester interface {
|
||||
TestProvider(context.Context, string) (*PaymentTestResult, error)
|
||||
}
|
||||
@@ -240,6 +314,8 @@ type NoopPaymentHooks struct{}
|
||||
|
||||
func (NoopPaymentHooks) BeforeCreate(context.Context, *PaymentRequest) error { return nil }
|
||||
|
||||
func NewPaymentHooks() PaymentHooks { return NoopPaymentHooks{} }
|
||||
|
||||
type PaymentFulfillmentHandler interface {
|
||||
Type() string
|
||||
Fulfill(context.Context, *PaymentConfirmation) error
|
||||
@@ -273,6 +349,15 @@ func (r *PaymentFulfillmentRegistry) Handler(kind string) PaymentFulfillmentHand
|
||||
return r.handlers[kind]
|
||||
}
|
||||
|
||||
func (r *PaymentFulfillmentRegistry) Len() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.handlers)
|
||||
}
|
||||
|
||||
type PaymentUsecase struct {
|
||||
repo PaymentRepo
|
||||
orders PaymentOrderRepo
|
||||
@@ -282,33 +367,34 @@ type PaymentUsecase struct {
|
||||
logger PaymentLogger
|
||||
}
|
||||
|
||||
func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, appLogger *slog.Logger) *PaymentUsecase {
|
||||
func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) *PaymentUsecase {
|
||||
if hooks == nil {
|
||||
hooks = NoopPaymentHooks{}
|
||||
}
|
||||
if sources == nil {
|
||||
sources = NewPaymentOrderSourceRegistry()
|
||||
}
|
||||
if fulfillments == nil {
|
||||
fulfillments = NewPaymentFulfillmentRegistry()
|
||||
}
|
||||
return &PaymentUsecase{
|
||||
repo: repo,
|
||||
orders: orders,
|
||||
hooks: NoopPaymentHooks{},
|
||||
sources: NewPaymentOrderSourceRegistry(),
|
||||
fulfillments: NewPaymentFulfillmentRegistry(),
|
||||
hooks: hooks,
|
||||
sources: sources,
|
||||
fulfillments: fulfillments,
|
||||
logger: NewPaymentLogger(appLogger),
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *PaymentUsecase) SetHooks(hooks PaymentHooks) {
|
||||
if hooks != nil {
|
||||
uc.hooks = hooks
|
||||
func NewConfiguredPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) (*PaymentUsecase, error) {
|
||||
if sources == nil || sources.Len() == 0 {
|
||||
return nil, errors.New("支付业务订单来源未注册,无法启动支付服务")
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *PaymentUsecase) SetOrderSourceRegistry(registry *PaymentOrderSourceRegistry) {
|
||||
if registry != nil {
|
||||
uc.sources = registry
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *PaymentUsecase) SetFulfillmentRegistry(registry *PaymentFulfillmentRegistry) {
|
||||
if registry != nil {
|
||||
uc.fulfillments = registry
|
||||
if fulfillments == nil || fulfillments.Len() == 0 {
|
||||
return nil, errors.New("支付发货 handler 未注册,无法启动支付服务")
|
||||
}
|
||||
return NewPaymentUsecase(repo, orders, hooks, sources, fulfillments, appLogger), nil
|
||||
}
|
||||
|
||||
func (uc *PaymentUsecase) RegisterBusinessModule(module PaymentBusinessModule) error {
|
||||
|
||||
@@ -182,6 +182,15 @@ func (r *PaymentOrderSourceRegistry) Source(kind string) PaymentOrderSource {
|
||||
return r.sources[kind]
|
||||
}
|
||||
|
||||
func (r *PaymentOrderSourceRegistry) Len() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.sources)
|
||||
}
|
||||
|
||||
func (r *PaymentOrderSourceRegistry) Unregister(kind string) {
|
||||
if r == nil {
|
||||
return
|
||||
|
||||
@@ -135,7 +135,12 @@ func (h *paymentFulfillmentStub) Fulfill(_ context.Context, confirmation *Paymen
|
||||
|
||||
func newPaymentUsecaseForTest(repo PaymentRepo) *PaymentUsecase {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, logger)
|
||||
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, nil, nil, nil, logger)
|
||||
}
|
||||
|
||||
func newPaymentUsecaseWithFulfillmentRegistry(repo PaymentRepo, registry *PaymentFulfillmentRegistry) *PaymentUsecase {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, nil, nil, registry, logger)
|
||||
}
|
||||
|
||||
func testBizPaymentOrder() *PaymentOrder {
|
||||
@@ -186,7 +191,7 @@ func TestPaymentCallbackPendingDoesNotFulfill(t *testing.T) {
|
||||
if err := registry.Register(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
|
||||
result, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -200,10 +205,10 @@ func TestPaymentCallbackRejectsAmountMismatch(t *testing.T) {
|
||||
repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
|
||||
handler := &paymentFulfillmentStub{}
|
||||
uc := newPaymentUsecaseForTest(repo)
|
||||
uc.orders.(*paymentOrderRepoStub).order.Amount = 101
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
_ = registry.Register(handler)
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
|
||||
uc.orders.(*paymentOrderRepoStub).order.Amount = 101
|
||||
if _, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}); err == nil {
|
||||
t.Fatal("amount mismatch was accepted")
|
||||
}
|
||||
@@ -226,7 +231,7 @@ func TestPaymentCallbackDuplicateDoesNotFulfillTwice(t *testing.T) {
|
||||
uc := newPaymentUsecaseForTest(repo)
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
_ = registry.Register(handler)
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
|
||||
callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
|
||||
if _, err := uc.Callback(context.Background(), callback); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -246,7 +251,7 @@ func TestPaymentCallbackFulfillmentFailureCanRetry(t *testing.T) {
|
||||
uc := newPaymentUsecaseForTest(repo)
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
_ = registry.Register(handler)
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
|
||||
callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
|
||||
if _, err := uc.Callback(context.Background(), callback); err == nil {
|
||||
t.Fatal("first fulfillment failure was accepted")
|
||||
@@ -290,12 +295,11 @@ func TestPaymentCallbackErrorCarriesSafeAck(t *testing.T) {
|
||||
func TestPersistentPaymentCreateUsesBusinessCanonicalAmount(t *testing.T) {
|
||||
repo := &paymentRepoStub{create: &PaymentResult{Provider: PaymentAlipay, Status: "created", TradeNo: "order-1", Payload: []byte(`{"prepay":"x"}`)}}
|
||||
orders := &paymentOrderRepoStub{}
|
||||
uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
sources := NewPaymentOrderSourceRegistry()
|
||||
if err := sources.Register(&paymentSourceStub{intent: &PaymentIntent{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "canonical item", Amount: 100, Currency: "CNY", Extra: []byte(`{"product_id":"canonical"}`)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uc.SetOrderSourceRegistry(sources)
|
||||
uc := NewPaymentUsecase(repo, orders, nil, sources, nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
result, err := uc.Create(context.Background(), &PaymentRequest{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "forged", Amount: 1, Currency: "USD", Extra: map[string]any{"product_id": "forged"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -309,12 +313,11 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
|
||||
repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
|
||||
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPending, FulfillmentStatus: FulfillmentStatusPending, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
|
||||
handler := &paymentFulfillmentStub{}
|
||||
uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
if err := registry.Register(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc := NewPaymentUsecase(repo, orders, nil, nil, registry, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
first, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -330,13 +333,12 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
|
||||
|
||||
func TestPaymentFulfillRetriesFailedOrder(t *testing.T) {
|
||||
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPaid, FulfillmentStatus: FulfillmentStatusFailed, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
|
||||
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
handler := &paymentFulfillmentStub{}
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
if err := registry.Register(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, nil, nil, registry, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
result, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,4 +5,9 @@ import "github.com/google/wire"
|
||||
// ProviderSet wires the payment domain usecase independently from other
|
||||
// business domains. Persistence and transport layers consume the payment
|
||||
// interfaces without depending on the system package.
|
||||
var ProviderSet = wire.NewSet(NewPaymentUsecase)
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewPaymentHooks,
|
||||
NewPaymentOrderSourceRegistry,
|
||||
NewPaymentFulfillmentRegistry,
|
||||
NewConfiguredPaymentUsecase,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user