diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 29c97b4b..d0134953 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -188,6 +188,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders, PaymentEnabledTypes: paymentCfg.EnabledTypes, PaymentBalanceDisabled: paymentCfg.BalanceDisabled, + PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier, PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy, PaymentProductNamePrefix: paymentCfg.ProductNamePrefix, PaymentProductNameSuffix: paymentCfg.ProductNameSuffix, @@ -323,9 +324,10 @@ type UpdateSettingsRequest struct { PaymentDailyLimit *float64 `json:"payment_daily_limit"` PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"` PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"` - PaymentEnabledTypes []string `json:"payment_enabled_types"` - PaymentBalanceDisabled *bool `json:"payment_balance_disabled"` - PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"` + PaymentEnabledTypes []string `json:"payment_enabled_types"` + PaymentBalanceDisabled *bool `json:"payment_balance_disabled"` + PaymentBalanceRechargeMultiplier *float64 `json:"payment_balance_recharge_multiplier"` + PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"` PaymentProductNamePrefix *string `json:"payment_product_name_prefix"` PaymentProductNameSuffix *string `json:"payment_product_name_suffix"` PaymentHelpImageURL *string `json:"payment_help_image_url"` @@ -934,24 +936,25 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { // Skip if no payment fields were provided (prevents accidental wipe). if h.paymentConfigService != nil && hasPaymentFields(req) { paymentReq := service.UpdatePaymentConfigRequest{ - Enabled: req.PaymentEnabled, - MinAmount: req.PaymentMinAmount, - MaxAmount: req.PaymentMaxAmount, - DailyLimit: req.PaymentDailyLimit, - OrderTimeoutMin: req.PaymentOrderTimeoutMin, - MaxPendingOrders: req.PaymentMaxPendingOrders, - EnabledTypes: req.PaymentEnabledTypes, - BalanceDisabled: req.PaymentBalanceDisabled, - LoadBalanceStrategy: req.PaymentLoadBalanceStrat, - ProductNamePrefix: req.PaymentProductNamePrefix, - ProductNameSuffix: req.PaymentProductNameSuffix, - HelpImageURL: req.PaymentHelpImageURL, - HelpText: req.PaymentHelpText, - CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled, - CancelRateLimitMax: req.PaymentCancelRateLimitMax, - CancelRateLimitWindow: req.PaymentCancelRateLimitWindow, - CancelRateLimitUnit: req.PaymentCancelRateLimitUnit, - CancelRateLimitMode: req.PaymentCancelRateLimitMode, + Enabled: req.PaymentEnabled, + MinAmount: req.PaymentMinAmount, + MaxAmount: req.PaymentMaxAmount, + DailyLimit: req.PaymentDailyLimit, + OrderTimeoutMin: req.PaymentOrderTimeoutMin, + MaxPendingOrders: req.PaymentMaxPendingOrders, + EnabledTypes: req.PaymentEnabledTypes, + BalanceDisabled: req.PaymentBalanceDisabled, + BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier, + LoadBalanceStrategy: req.PaymentLoadBalanceStrat, + ProductNamePrefix: req.PaymentProductNamePrefix, + ProductNameSuffix: req.PaymentProductNameSuffix, + HelpImageURL: req.PaymentHelpImageURL, + HelpText: req.PaymentHelpText, + CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled, + CancelRateLimitMax: req.PaymentCancelRateLimitMax, + CancelRateLimitWindow: req.PaymentCancelRateLimitWindow, + CancelRateLimitUnit: req.PaymentCancelRateLimitUnit, + CancelRateLimitMode: req.PaymentCancelRateLimitMode, } if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil { response.ErrorFrom(c, err) @@ -1082,6 +1085,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders, PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes, PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled, + PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier, PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy, PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix, PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix, @@ -1101,6 +1105,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool { req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil || req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil || req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil || + req.PaymentBalanceRechargeMultiplier != nil || req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil || req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil || req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil || diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index ef285a44..2f97b3e0 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -134,9 +134,10 @@ type SystemSettings struct { PaymentDailyLimit float64 `json:"payment_daily_limit"` PaymentOrderTimeoutMin int `json:"payment_order_timeout_minutes"` PaymentMaxPendingOrders int `json:"payment_max_pending_orders"` - PaymentEnabledTypes []string `json:"payment_enabled_types"` - PaymentBalanceDisabled bool `json:"payment_balance_disabled"` - PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"` + PaymentEnabledTypes []string `json:"payment_enabled_types"` + PaymentBalanceDisabled bool `json:"payment_balance_disabled"` + PaymentBalanceRechargeMultiplier float64 `json:"payment_balance_recharge_multiplier"` + PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"` PaymentProductNamePrefix string `json:"payment_product_name_prefix"` PaymentProductNameSuffix string `json:"payment_product_name_suffix"` PaymentHelpImageURL string `json:"payment_help_image_url"` diff --git a/backend/internal/handler/payment_handler.go b/backend/internal/handler/payment_handler.go index 5fde86fa..973ba4a5 100644 --- a/backend/internal/handler/payment_handler.go +++ b/backend/internal/handler/payment_handler.go @@ -126,26 +126,28 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) { } response.Success(c, checkoutInfoResponse{ - Methods: limitsResp.Methods, - GlobalMin: limitsResp.GlobalMin, - GlobalMax: limitsResp.GlobalMax, - Plans: planList, - BalanceDisabled: cfg.BalanceDisabled, - HelpText: cfg.HelpText, - HelpImageURL: cfg.HelpImageURL, - StripePublishableKey: cfg.StripePublishableKey, + Methods: limitsResp.Methods, + GlobalMin: limitsResp.GlobalMin, + GlobalMax: limitsResp.GlobalMax, + Plans: planList, + BalanceDisabled: cfg.BalanceDisabled, + BalanceRechargeMultiplier: cfg.BalanceRechargeMultiplier, + HelpText: cfg.HelpText, + HelpImageURL: cfg.HelpImageURL, + StripePublishableKey: cfg.StripePublishableKey, }) } type checkoutInfoResponse struct { - Methods map[string]service.MethodLimits `json:"methods"` - GlobalMin float64 `json:"global_min"` - GlobalMax float64 `json:"global_max"` - Plans []checkoutPlan `json:"plans"` - BalanceDisabled bool `json:"balance_disabled"` - HelpText string `json:"help_text"` - HelpImageURL string `json:"help_image_url"` - StripePublishableKey string `json:"stripe_publishable_key"` + Methods map[string]service.MethodLimits `json:"methods"` + GlobalMin float64 `json:"global_min"` + GlobalMax float64 `json:"global_max"` + Plans []checkoutPlan `json:"plans"` + BalanceDisabled bool `json:"balance_disabled"` + BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"` + HelpText string `json:"help_text"` + HelpImageURL string `json:"help_image_url"` + StripePublishableKey string `json:"stripe_publishable_key"` } type checkoutPlan struct { @@ -381,6 +383,7 @@ type PublicOrderResult struct { Amount float64 `json:"amount"` PayAmount float64 `json:"pay_amount"` PaymentType string `json:"payment_type"` + OrderType string `json:"order_type"` Status string `json:"status"` } @@ -404,6 +407,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) { Amount: order.Amount, PayAmount: order.PayAmount, PaymentType: order.PaymentType, + OrderType: order.OrderType, Status: order.Status, }) } diff --git a/backend/internal/service/payment_amounts.go b/backend/internal/service/payment_amounts.go new file mode 100644 index 00000000..cc01f6ad --- /dev/null +++ b/backend/internal/service/payment_amounts.go @@ -0,0 +1,37 @@ +package service + +import ( + "math" + + "github.com/shopspring/decimal" +) + +const defaultBalanceRechargeMultiplier = 1.0 + +func normalizeBalanceRechargeMultiplier(multiplier float64) float64 { + if math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 0 { + return defaultBalanceRechargeMultiplier + } + return multiplier +} + +func calculateCreditedBalance(paymentAmount, multiplier float64) float64 { + return decimal.NewFromFloat(paymentAmount). + Mul(decimal.NewFromFloat(normalizeBalanceRechargeMultiplier(multiplier))). + Round(2). + InexactFloat64() +} + +func calculateGatewayRefundAmount(orderAmount, payAmount, refundAmount float64) float64 { + if orderAmount <= 0 || payAmount <= 0 || refundAmount <= 0 { + return 0 + } + if math.Abs(refundAmount-orderAmount) <= amountToleranceCNY { + return decimal.NewFromFloat(payAmount).Round(2).InexactFloat64() + } + return decimal.NewFromFloat(payAmount). + Mul(decimal.NewFromFloat(refundAmount)). + Div(decimal.NewFromFloat(orderAmount)). + Round(2). + InexactFloat64() +} diff --git a/backend/internal/service/payment_config_service.go b/backend/internal/service/payment_config_service.go index cce31f4d..409625ed 100644 --- a/backend/internal/service/payment_config_service.go +++ b/backend/internal/service/payment_config_service.go @@ -3,12 +3,14 @@ package service import ( "context" "fmt" + "math" "strconv" "strings" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance" "github.com/Wei-Shaw/sub2api/internal/payment" + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) const ( @@ -21,6 +23,7 @@ const ( SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES" SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY" SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED" + SettingBalanceRechargeMult = "BALANCE_RECHARGE_MULTIPLIER" SettingProductNamePrefix = "PRODUCT_NAME_PREFIX" SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX" SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL" @@ -46,9 +49,10 @@ type PaymentConfig struct { DailyLimit float64 `json:"daily_limit"` OrderTimeoutMin int `json:"order_timeout_minutes"` MaxPendingOrders int `json:"max_pending_orders"` - EnabledTypes []string `json:"enabled_payment_types"` - BalanceDisabled bool `json:"balance_disabled"` - LoadBalanceStrategy string `json:"load_balance_strategy"` + EnabledTypes []string `json:"enabled_payment_types"` + BalanceDisabled bool `json:"balance_disabled"` + BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"` + LoadBalanceStrategy string `json:"load_balance_strategy"` ProductNamePrefix string `json:"product_name_prefix"` ProductNameSuffix string `json:"product_name_suffix"` HelpImageURL string `json:"help_image_url"` @@ -71,9 +75,10 @@ type UpdatePaymentConfigRequest struct { DailyLimit *float64 `json:"daily_limit"` OrderTimeoutMin *int `json:"order_timeout_minutes"` MaxPendingOrders *int `json:"max_pending_orders"` - EnabledTypes []string `json:"enabled_payment_types"` - BalanceDisabled *bool `json:"balance_disabled"` - LoadBalanceStrategy *string `json:"load_balance_strategy"` + EnabledTypes []string `json:"enabled_payment_types"` + BalanceDisabled *bool `json:"balance_disabled"` + BalanceRechargeMultiplier *float64 `json:"balance_recharge_multiplier"` + LoadBalanceStrategy *string `json:"load_balance_strategy"` ProductNamePrefix *string `json:"product_name_prefix"` ProductNameSuffix *string `json:"product_name_suffix"` HelpImageURL *string `json:"help_image_url"` @@ -183,7 +188,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo keys := []string{ SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount, SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders, - SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy, + SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingBalanceRechargeMult, SettingLoadBalanceStrategy, SettingProductNamePrefix, SettingProductNameSuffix, SettingHelpImageURL, SettingHelpText, SettingCancelRateLimitOn, SettingCancelRateLimitMax, @@ -207,8 +212,9 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0), OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin), MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders), - BalanceDisabled: vals[SettingBalancePayDisabled] == "true", - LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], + BalanceDisabled: vals[SettingBalancePayDisabled] == "true", + BalanceRechargeMultiplier: normalizeBalanceRechargeMultiplier(pcParseFloat(vals[SettingBalanceRechargeMult], defaultBalanceRechargeMultiplier)), + LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], ProductNamePrefix: vals[SettingProductNamePrefix], ProductNameSuffix: vals[SettingProductNameSuffix], HelpImageURL: vals[SettingHelpImageURL], @@ -256,6 +262,11 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri // nil-check before serialisation — this is inherent to patch-style update patterns // and cannot be meaningfully decomposed without introducing unnecessary abstraction. func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error { + if req.BalanceRechargeMultiplier != nil { + if math.IsNaN(*req.BalanceRechargeMultiplier) || math.IsInf(*req.BalanceRechargeMultiplier, 0) || *req.BalanceRechargeMultiplier <= 0 { + return infraerrors.BadRequest("INVALID_BALANCE_RECHARGE_MULTIPLIER", "balance recharge multiplier must be greater than 0") + } + } m := map[string]string{ SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled), SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount), @@ -264,6 +275,7 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin), SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders), SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled), + SettingBalanceRechargeMult: formatPositiveFloat(req.BalanceRechargeMultiplier), SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy), SettingProductNamePrefix: derefStr(req.ProductNamePrefix), SettingProductNameSuffix: derefStr(req.ProductNameSuffix), diff --git a/backend/internal/service/payment_fulfillment.go b/backend/internal/service/payment_fulfillment.go index de41d742..44818b37 100644 --- a/backend/internal/service/payment_fulfillment.go +++ b/backend/internal/service/payment_fulfillment.go @@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde if err != nil { return fmt.Errorf("mark completed: %w", err) } - s.writeAuditLog(ctx, o.ID, auditAction, "system", map[string]any{"rechargeCode": o.RechargeCode, "amount": o.Amount}) + s.writeAuditLog(ctx, o.ID, auditAction, "system", map[string]any{ + "rechargeCode": o.RechargeCode, + "creditedAmount": o.Amount, + "payAmount": o.PayAmount, + }) return nil } diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go index ff4dfaa8..1d5562dd 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -43,14 +43,18 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest if user.Status != payment.EntityStatusActive { return nil, infraerrors.Forbidden("USER_INACTIVE", "user account is disabled") } - amount := req.Amount + orderAmount := req.Amount + limitAmount := req.Amount if plan != nil { - amount = plan.Price + orderAmount = plan.Price + limitAmount = plan.Price + } else if req.OrderType == payment.OrderTypeBalance { + orderAmount = calculateCreditedBalance(req.Amount, cfg.BalanceRechargeMultiplier) } feeRate := s.getFeeRate(req.PaymentType) - payAmountStr := payment.CalculatePayAmount(amount, feeRate) + payAmountStr := payment.CalculatePayAmount(limitAmount, feeRate) payAmount, _ := strconv.ParseFloat(payAmountStr, 64) - order, err := s.createOrderInTx(ctx, req, user, plan, cfg, amount, feeRate, payAmount) + order, err := s.createOrderInTx(ctx, req, user, plan, cfg, orderAmount, limitAmount, feeRate, payAmount) if err != nil { return nil, err } @@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe return plan, nil } -func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderRequest, user *User, plan *dbent.SubscriptionPlan, cfg *PaymentConfig, amount, feeRate, payAmount float64) (*dbent.PaymentOrder, error) { +func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderRequest, user *User, plan *dbent.SubscriptionPlan, cfg *PaymentConfig, orderAmount, limitAmount, feeRate, payAmount float64) (*dbent.PaymentOrder, error) { tx, err := s.entClient.Tx(ctx) if err != nil { return nil, fmt.Errorf("begin transaction: %w", err) @@ -108,7 +112,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq if err := s.checkPendingLimit(ctx, tx, req.UserID, cfg.MaxPendingOrders); err != nil { return nil, err } - if err := s.checkDailyLimit(ctx, tx, req.UserID, amount, cfg.DailyLimit); err != nil { + if err := s.checkDailyLimit(ctx, tx, req.UserID, limitAmount, cfg.DailyLimit); err != nil { return nil, err } tm := cfg.OrderTimeoutMin @@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq SetUserEmail(user.Email). SetUserName(user.Username). SetNillableUserNotes(psNilIfEmpty(user.Notes)). - SetAmount(amount). + SetAmount(orderAmount). SetPayAmount(payAmount). SetFeeRate(feeRate). SetRechargeCode(""). @@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user } var used float64 for _, o := range orders { + if o.OrderType == payment.OrderTypeBalance { + used += o.PayAmount + continue + } used += o.Amount } if used+amount > limit { @@ -213,7 +221,13 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen if err != nil { 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}) + s.writeAuditLog(ctx, order.ID, "ORDER_CREATED", fmt.Sprintf("user:%d", req.UserID), map[string]any{ + "paymentAmount": req.Amount, + "creditedAmount": order.Amount, + "payAmount": order.PayAmount, + "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, PaymentMode: sel.PaymentMode}, nil } diff --git a/backend/internal/service/payment_refund.go b/backend/internal/service/payment_refund.go index 99468433..c5bda763 100644 --- a/backend/internal/service/payment_refund.go +++ b/backend/internal/service/payment_refund.go @@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float if amt-o.Amount > amountToleranceCNY { return nil, nil, infraerrors.BadRequest("REFUND_AMOUNT_EXCEEDED", "refund amount exceeds recharge") } - // Full refund: use actual pay_amount for gateway (includes fees) - ga := amt - if math.Abs(amt-o.Amount) <= amountToleranceCNY { - ga = o.PayAmount - } + ga := calculateGatewayRefundAmount(o.Amount, o.PayAmount, amt) rr := strings.TrimSpace(reason) if rr == "" && o.RefundRequestReason != nil { rr = *o.RefundRequestReason diff --git a/frontend/src/api/admin/payment.ts b/frontend/src/api/admin/payment.ts index 946c91b0..3daf56b2 100644 --- a/frontend/src/api/admin/payment.ts +++ b/frontend/src/api/admin/payment.ts @@ -23,6 +23,7 @@ export interface AdminPaymentConfig { max_pending_orders: number enabled_payment_types: string[] balance_disabled: boolean + balance_recharge_multiplier: number load_balance_strategy: string product_name_prefix: string product_name_suffix: string @@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest { max_pending_orders?: number enabled_payment_types?: string[] balance_disabled?: boolean + balance_recharge_multiplier?: number load_balance_strategy?: string product_name_prefix?: string product_name_suffix?: string diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index aa1d0f82..c0cf16ee 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -125,6 +125,7 @@ export interface SystemSettings { payment_max_pending_orders: number payment_enabled_types: string[] payment_balance_disabled: boolean + payment_balance_recharge_multiplier: number payment_load_balance_strategy: string payment_product_name_prefix: string payment_product_name_suffix: string @@ -231,6 +232,7 @@ export interface UpdateSettingsRequest { payment_max_pending_orders?: number payment_enabled_types?: string[] payment_balance_disabled?: boolean + payment_balance_recharge_multiplier?: number payment_load_balance_strategy?: string payment_product_name_prefix?: string payment_product_name_suffix?: string diff --git a/frontend/src/components/admin/payment/AdminOrderDetail.vue b/frontend/src/components/admin/payment/AdminOrderDetail.vue index de4b00b5..5a07c097 100644 --- a/frontend/src/components/admin/payment/AdminOrderDetail.vue +++ b/frontend/src/components/admin/payment/AdminOrderDetail.vue @@ -19,11 +19,11 @@
{{ t('payment.orders.amount') }}
-${{ order.amount.toFixed(2) }}
+{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}
{{ t('payment.orders.payAmount') }}
-${{ order.pay_amount.toFixed(2) }}
+¥{{ order.pay_amount.toFixed(2) }}
{{ t('payment.orders.paymentMethod') }}
@@ -73,7 +73,7 @@- {{ t('payment.admin.maxRefundable') }}: ${{ maxRefundable.toFixed(2) }} + {{ t('payment.admin.maxRefundable') }}: {{ order?.order_type === 'balance' ? '$' : '¥' }}{{ maxRefundable.toFixed(2) }}
{{ t('payment.actualPay') }}
-${{ payAmount.toFixed(2) }}
+¥{{ payAmount.toFixed(2) }}
{{ t('admin.settings.payment.balanceRechargeMultiplierHint') }}
+{{ t('admin.settings.payment.balanceRechargePreview', { usd: (Number(form.payment_balance_recharge_multiplier) || 1).toFixed(2) }) }}
+{{ t('admin.settings.payment.orderTimeoutHint') }}