From fafde2a4939f5e19815ea9e0114d37aa1c40f655 Mon Sep 17 00:00:00 2001 From: erio Date: Wed, 8 Apr 2026 18:05:24 +0800 Subject: [PATCH] feat(payment): configurable payment mode (qrcode/redirect/popup) - EasyPay supports 3 modes: qrcode (API/QR), redirect (new tab), popup (small window) - Stripe supports 2 modes: popup (small window, default), redirect (new tab) - Backend passes payment_mode through to create-order response - Frontend opens pay URL based on configured mode --- backend/internal/payment/load_balancer.go | 1 + backend/internal/payment/provider/easypay.go | 5 +-- backend/internal/payment/types.go | 1 + backend/internal/service/payment_order.go | 2 +- backend/internal/service/payment_service.go | 1 + .../payment/PaymentProviderDialog.vue | 31 +++++++++++++------ .../src/components/payment/ProviderCard.vue | 5 +-- .../src/components/payment/providerConfig.ts | 8 +++-- frontend/src/i18n/locales/en.ts | 1 + frontend/src/i18n/locales/zh.ts | 1 + frontend/src/types/payment.ts | 1 + frontend/src/views/user/PaymentView.vue | 16 +++++++--- 12 files changed, 52 insertions(+), 21 deletions(-) diff --git a/backend/internal/payment/load_balancer.go b/backend/internal/payment/load_balancer.go index b6374a74d9..ab55b78816 100644 --- a/backend/internal/payment/load_balancer.go +++ b/backend/internal/payment/load_balancer.go @@ -90,6 +90,7 @@ func (lb *DefaultLoadBalancer) SelectInstance(ctx context.Context, providerKey s InstanceID: fmt.Sprintf("%d", selected.ID), Config: config, SupportedTypes: selected.SupportedTypes, + PaymentMode: selected.PaymentMode, }, nil } diff --git a/backend/internal/payment/provider/easypay.go b/backend/internal/payment/provider/easypay.go index 46150856f3..5cf42c9eea 100644 --- a/backend/internal/payment/provider/easypay.go +++ b/backend/internal/payment/provider/easypay.go @@ -57,8 +57,9 @@ func (e *EasyPay) SupportedTypes() []payment.PaymentType { func (e *EasyPay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { // Payment mode determined by instance config, not payment type. - // "redirect" → hosted page (submit.php); default → API call (mapi.php). - if e.config["paymentMode"] == "redirect" { + // "redirect"/"popup" → hosted page (submit.php); "qrcode"/default → API call (mapi.php). + mode := e.config["paymentMode"] + if mode == "redirect" || mode == "popup" { return e.createRedirectPayment(req) } return e.createAPIPayment(ctx, req) diff --git a/backend/internal/payment/types.go b/backend/internal/payment/types.go index 26b5edde70..ab27dfeda0 100644 --- a/backend/internal/payment/types.go +++ b/backend/internal/payment/types.go @@ -126,6 +126,7 @@ type InstanceSelection struct { InstanceID string Config map[string]string SupportedTypes string // Comma-separated list of supported payment types from the instance + PaymentMode string // Payment display mode: "qrcode", "redirect", "popup" } // Provider defines the interface that all payment providers must implement. diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go index b8416d7412..b1493e9408 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -275,7 +275,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen return nil, fmt.Errorf("update order with payment details: %w", err) } s.writeAuditLog(ctx, order.ID, "ORDER_CREATED", fmt.Sprintf("user:%d", req.UserID), map[string]any{"amount": req.Amount, "paymentType": req.PaymentType, "orderType": req.OrderType}) - return &CreateOrderResponse{OrderID: order.ID, Amount: order.Amount, PayAmount: payAmount, FeeRate: order.FeeRate, Status: OrderStatusPending, PaymentType: req.PaymentType, PayURL: pr.PayURL, QRCode: pr.QRCode, ClientSecret: pr.ClientSecret, ExpiresAt: order.ExpiresAt}, nil + return &CreateOrderResponse{OrderID: order.ID, Amount: order.Amount, PayAmount: payAmount, FeeRate: order.FeeRate, Status: OrderStatusPending, PaymentType: req.PaymentType, PayURL: pr.PayURL, QRCode: pr.QRCode, ClientSecret: pr.ClientSecret, ExpiresAt: order.ExpiresAt, PaymentMode: sel.PaymentMode}, nil } func (s *PaymentService) buildPaymentSubject(plan *dbent.SubscriptionPlan, payAmountStr string, cfg *PaymentConfig) string { diff --git a/backend/internal/service/payment_service.go b/backend/internal/service/payment_service.go index c5dd0ea1c1..d48e6e3c0b 100644 --- a/backend/internal/service/payment_service.go +++ b/backend/internal/service/payment_service.go @@ -81,6 +81,7 @@ type CreateOrderResponse struct { QRCode string `json:"qr_code,omitempty"` ClientSecret string `json:"client_secret,omitempty"` ExpiresAt time.Time `json:"expires_at"` + PaymentMode string `json:"payment_mode,omitempty"` } type OrderListParams struct { diff --git a/frontend/src/components/payment/PaymentProviderDialog.vue b/frontend/src/components/payment/PaymentProviderDialog.vue index cb2871dda7..c101b3c7af 100644 --- a/frontend/src/components/payment/PaymentProviderDialog.vue +++ b/frontend/src/components/payment/PaymentProviderDialog.vue @@ -37,7 +37,7 @@
-
+