From f498eb8fde028c11fa6285238971ebfa86e5f29f Mon Sep 17 00:00:00 2001 From: erio Date: Mon, 13 Apr 2026 14:06:29 +0800 Subject: [PATCH] fix(payment): fix Alipay/Wxpay direct provider type mapping and enable cross-provider load balancing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues fixed: 1. Alipay.SupportedTypes() returned ["alipay_direct"] and Wxpay returned ["wxpay_direct"], but the frontend sends payment_type="alipay"/"wxpay". The registry lookup failed with "payment method (alipay) is not configured". Fix: return the base types ["alipay"]/["wxpay"]. 2. When multiple providers support the same payment type (e.g. EasyPay and Alipay direct both handle "alipay"), only the last-registered provider's instances were reachable — the registry mapped one type to one provider key, and SelectInstance queried by that single key. Fix: bypass the registry in invokeProvider and let SelectInstance query across all providers when providerKey is empty. The selected instance's own ProviderKey (now included in InstanceSelection) is used to create the correct provider, enabling true cross-provider load balancing. Closes #1592 --- backend/internal/payment/load_balancer.go | 21 +++++++++++++-------- backend/internal/payment/provider/alipay.go | 2 +- backend/internal/payment/provider/wxpay.go | 2 +- backend/internal/payment/types.go | 1 + backend/internal/service/payment_order.go | 15 ++++++--------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/backend/internal/payment/load_balancer.go b/backend/internal/payment/load_balancer.go index afe607e0..55cb2043 100644 --- a/backend/internal/payment/load_balancer.go +++ b/backend/internal/payment/load_balancer.go @@ -94,17 +94,21 @@ func (lb *DefaultLoadBalancer) SelectInstance( return lb.buildSelection(selected.inst) } -// queryEnabledInstances returns enabled instances for providerKey that support paymentType. +// queryEnabledInstances returns enabled instances that support paymentType. +// When providerKey is non-empty, only instances with that provider key are considered. +// When providerKey is empty, instances across all providers are considered, +// enabling cross-provider load balancing (e.g. EasyPay + Alipay direct for "alipay"). func (lb *DefaultLoadBalancer) queryEnabledInstances( ctx context.Context, providerKey string, paymentType PaymentType, ) ([]*dbent.PaymentProviderInstance, error) { - instances, err := lb.db.PaymentProviderInstance.Query(). - Where( - paymentproviderinstance.ProviderKey(providerKey), - paymentproviderinstance.Enabled(true), - ). + query := lb.db.PaymentProviderInstance.Query(). + Where(paymentproviderinstance.Enabled(true)) + if providerKey != "" { + query = query.Where(paymentproviderinstance.ProviderKey(providerKey)) + } + instances, err := query. Order(dbent.Asc(paymentproviderinstance.FieldSortOrder)). All(ctx) if err != nil { @@ -113,12 +117,12 @@ func (lb *DefaultLoadBalancer) queryEnabledInstances( var matched []*dbent.PaymentProviderInstance for _, inst := range instances { - if paymentType == providerKey || InstanceSupportsType(inst.SupportedTypes, paymentType) { + if InstanceSupportsType(inst.SupportedTypes, paymentType) { matched = append(matched, inst) } } if len(matched) == 0 { - return nil, fmt.Errorf("no enabled instance for provider %s type %s", providerKey, paymentType) + return nil, fmt.Errorf("no enabled instance for payment type %s", paymentType) } return matched, nil } @@ -258,6 +262,7 @@ func (lb *DefaultLoadBalancer) buildSelection(selected *dbent.PaymentProviderIns return &InstanceSelection{ InstanceID: fmt.Sprintf("%d", selected.ID), + ProviderKey: selected.ProviderKey, Config: config, SupportedTypes: selected.SupportedTypes, PaymentMode: selected.PaymentMode, diff --git a/backend/internal/payment/provider/alipay.go b/backend/internal/payment/provider/alipay.go index 3eca0b2c..af8a90c6 100644 --- a/backend/internal/payment/provider/alipay.go +++ b/backend/internal/payment/provider/alipay.go @@ -76,7 +76,7 @@ func (a *Alipay) getClient() (*alipay.Client, error) { func (a *Alipay) Name() string { return "Alipay" } func (a *Alipay) ProviderKey() string { return payment.TypeAlipay } func (a *Alipay) SupportedTypes() []payment.PaymentType { - return []payment.PaymentType{payment.TypeAlipayDirect} + return []payment.PaymentType{payment.TypeAlipay} } // CreatePayment creates an Alipay payment page URL. diff --git a/backend/internal/payment/provider/wxpay.go b/backend/internal/payment/provider/wxpay.go index 14e51cd2..0b41c4fb 100644 --- a/backend/internal/payment/provider/wxpay.go +++ b/backend/internal/payment/provider/wxpay.go @@ -72,7 +72,7 @@ func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) { func (w *Wxpay) Name() string { return "Wxpay" } func (w *Wxpay) ProviderKey() string { return payment.TypeWxpay } func (w *Wxpay) SupportedTypes() []payment.PaymentType { - return []payment.PaymentType{payment.TypeWxpayDirect} + return []payment.PaymentType{payment.TypeWxpay} } func formatPEM(key, keyType string) string { diff --git a/backend/internal/payment/types.go b/backend/internal/payment/types.go index c413d8f3..5d613a4a 100644 --- a/backend/internal/payment/types.go +++ b/backend/internal/payment/types.go @@ -148,6 +148,7 @@ type RefundResponse struct { // InstanceSelection holds the selected provider instance and its decrypted config. type InstanceSelection struct { InstanceID string + ProviderKey string // Provider key of the selected instance (e.g. "alipay", "easypay") Config map[string]string SupportedTypes string // Comma-separated list of supported payment types from the instance PaymentMode string // Payment display mode: "qrcode", "redirect", "popup" diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go index 2a952ece..ff4dfaa8 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -189,19 +189,16 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user } func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.PaymentOrder, req CreateOrderRequest, cfg *PaymentConfig, payAmountStr string, payAmount float64, plan *dbent.SubscriptionPlan) (*CreateOrderResponse, error) { - s.EnsureProviders(ctx) - providerKey := s.registry.GetProviderKey(req.PaymentType) - if providerKey == "" { - return nil, infraerrors.ServiceUnavailable("PAYMENT_GATEWAY_ERROR", fmt.Sprintf("payment method (%s) is not configured", req.PaymentType)) - } - sel, err := s.loadBalancer.SelectInstance(ctx, providerKey, req.PaymentType, payment.Strategy(cfg.LoadBalanceStrategy), payAmount) + // Select an instance across all providers that support the requested payment type. + // This enables cross-provider load balancing (e.g. EasyPay + Alipay direct for "alipay"). + sel, err := s.loadBalancer.SelectInstance(ctx, "", req.PaymentType, payment.Strategy(cfg.LoadBalanceStrategy), payAmount) if err != nil { - return nil, fmt.Errorf("select provider instance: %w", err) + return nil, infraerrors.ServiceUnavailable("PAYMENT_GATEWAY_ERROR", fmt.Sprintf("payment method (%s) is not configured", req.PaymentType)) } if sel == nil { return nil, infraerrors.TooManyRequests("NO_AVAILABLE_INSTANCE", "no available payment instance") } - prov, err := provider.CreateProvider(providerKey, sel.InstanceID, sel.Config) + prov, err := provider.CreateProvider(sel.ProviderKey, sel.InstanceID, sel.Config) if err != nil { return nil, infraerrors.ServiceUnavailable("PAYMENT_GATEWAY_ERROR", "payment method is temporarily unavailable") } @@ -209,7 +206,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen outTradeNo := order.OutTradeNo pr, err := prov.CreatePayment(ctx, payment.CreatePaymentRequest{OrderID: outTradeNo, Amount: payAmountStr, PaymentType: req.PaymentType, Subject: subject, ClientIP: req.ClientIP, IsMobile: req.IsMobile, InstanceSubMethods: sel.SupportedTypes}) if err != nil { - slog.Error("[PaymentService] CreatePayment failed", "provider", providerKey, "instance", sel.InstanceID, "error", err) + slog.Error("[PaymentService] CreatePayment failed", "provider", sel.ProviderKey, "instance", sel.InstanceID, "error", err) return nil, infraerrors.ServiceUnavailable("PAYMENT_GATEWAY_ERROR", fmt.Sprintf("payment gateway error: %s", err.Error())) } _, err = s.entClient.PaymentOrder.UpdateOneID(order.ID).SetNillablePaymentTradeNo(psNilIfEmpty(pr.TradeNo)).SetNillablePayURL(psNilIfEmpty(pr.PayURL)).SetNillableQrCode(psNilIfEmpty(pr.QRCode)).SetNillableProviderInstanceID(psNilIfEmpty(sel.InstanceID)).Save(ctx)