优化结构

This commit is contained in:
Yvan
2026-08-28 00:29:40 +08:00
parent 04ff85116c
commit 03a4131f6e
118 changed files with 758 additions and 834 deletions
+130 -44
View File
@@ -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 {
+9
View File
@@ -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
+14 -12
View File
@@ -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)
+6 -1
View File
@@ -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,
)