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)