Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
9bf079b7
Unverified
Commit
9bf079b7
authored
Apr 15, 2026
by
Wesley Liddick
Committed by
GitHub
Apr 15, 2026
Browse files
Merge pull request #1655 from touwaeriol/feat/payment-fee-multiplier
feat(payment): balance recharge multiplier and fee rate
parents
7c671b53
c2108421
Changes
28
Show whitespace changes
Inline
Side-by-side
Antigravity-Manager
@
a9d96bd5
Subproject commit a9d96bd54978c22d3033830debfe77aeeeee2500
backend/internal/handler/admin/setting_handler.go
View file @
9bf079b7
...
@@ -188,6 +188,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -188,6 +188,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
PaymentMaxPendingOrders
:
paymentCfg
.
MaxPendingOrders
,
PaymentMaxPendingOrders
:
paymentCfg
.
MaxPendingOrders
,
PaymentEnabledTypes
:
paymentCfg
.
EnabledTypes
,
PaymentEnabledTypes
:
paymentCfg
.
EnabledTypes
,
PaymentBalanceDisabled
:
paymentCfg
.
BalanceDisabled
,
PaymentBalanceDisabled
:
paymentCfg
.
BalanceDisabled
,
PaymentBalanceRechargeMultiplier
:
paymentCfg
.
BalanceRechargeMultiplier
,
PaymentRechargeFeeRate
:
paymentCfg
.
RechargeFeeRate
,
PaymentLoadBalanceStrat
:
paymentCfg
.
LoadBalanceStrategy
,
PaymentLoadBalanceStrat
:
paymentCfg
.
LoadBalanceStrategy
,
PaymentProductNamePrefix
:
paymentCfg
.
ProductNamePrefix
,
PaymentProductNamePrefix
:
paymentCfg
.
ProductNamePrefix
,
PaymentProductNameSuffix
:
paymentCfg
.
ProductNameSuffix
,
PaymentProductNameSuffix
:
paymentCfg
.
ProductNameSuffix
,
...
@@ -325,6 +327,8 @@ type UpdateSettingsRequest struct {
...
@@ -325,6 +327,8 @@ type UpdateSettingsRequest struct {
PaymentMaxPendingOrders
*
int
`json:"payment_max_pending_orders"`
PaymentMaxPendingOrders
*
int
`json:"payment_max_pending_orders"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
PaymentBalanceDisabled
*
bool
`json:"payment_balance_disabled"`
PaymentBalanceDisabled
*
bool
`json:"payment_balance_disabled"`
PaymentBalanceRechargeMultiplier
*
float64
`json:"payment_balance_recharge_multiplier"`
PaymentRechargeFeeRate
*
float64
`json:"payment_recharge_fee_rate"`
PaymentLoadBalanceStrat
*
string
`json:"payment_load_balance_strategy"`
PaymentLoadBalanceStrat
*
string
`json:"payment_load_balance_strategy"`
PaymentProductNamePrefix
*
string
`json:"payment_product_name_prefix"`
PaymentProductNamePrefix
*
string
`json:"payment_product_name_prefix"`
PaymentProductNameSuffix
*
string
`json:"payment_product_name_suffix"`
PaymentProductNameSuffix
*
string
`json:"payment_product_name_suffix"`
...
@@ -942,6 +946,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -942,6 +946,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
MaxPendingOrders
:
req
.
PaymentMaxPendingOrders
,
MaxPendingOrders
:
req
.
PaymentMaxPendingOrders
,
EnabledTypes
:
req
.
PaymentEnabledTypes
,
EnabledTypes
:
req
.
PaymentEnabledTypes
,
BalanceDisabled
:
req
.
PaymentBalanceDisabled
,
BalanceDisabled
:
req
.
PaymentBalanceDisabled
,
BalanceRechargeMultiplier
:
req
.
PaymentBalanceRechargeMultiplier
,
RechargeFeeRate
:
req
.
PaymentRechargeFeeRate
,
LoadBalanceStrategy
:
req
.
PaymentLoadBalanceStrat
,
LoadBalanceStrategy
:
req
.
PaymentLoadBalanceStrat
,
ProductNamePrefix
:
req
.
PaymentProductNamePrefix
,
ProductNamePrefix
:
req
.
PaymentProductNamePrefix
,
ProductNameSuffix
:
req
.
PaymentProductNameSuffix
,
ProductNameSuffix
:
req
.
PaymentProductNameSuffix
,
...
@@ -1082,6 +1088,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1082,6 +1088,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PaymentMaxPendingOrders
:
updatedPaymentCfg
.
MaxPendingOrders
,
PaymentMaxPendingOrders
:
updatedPaymentCfg
.
MaxPendingOrders
,
PaymentEnabledTypes
:
updatedPaymentCfg
.
EnabledTypes
,
PaymentEnabledTypes
:
updatedPaymentCfg
.
EnabledTypes
,
PaymentBalanceDisabled
:
updatedPaymentCfg
.
BalanceDisabled
,
PaymentBalanceDisabled
:
updatedPaymentCfg
.
BalanceDisabled
,
PaymentBalanceRechargeMultiplier
:
updatedPaymentCfg
.
BalanceRechargeMultiplier
,
PaymentRechargeFeeRate
:
updatedPaymentCfg
.
RechargeFeeRate
,
PaymentLoadBalanceStrat
:
updatedPaymentCfg
.
LoadBalanceStrategy
,
PaymentLoadBalanceStrat
:
updatedPaymentCfg
.
LoadBalanceStrategy
,
PaymentProductNamePrefix
:
updatedPaymentCfg
.
ProductNamePrefix
,
PaymentProductNamePrefix
:
updatedPaymentCfg
.
ProductNamePrefix
,
PaymentProductNameSuffix
:
updatedPaymentCfg
.
ProductNameSuffix
,
PaymentProductNameSuffix
:
updatedPaymentCfg
.
ProductNameSuffix
,
...
@@ -1101,6 +1109,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
...
@@ -1101,6 +1109,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
req
.
PaymentMaxAmount
!=
nil
||
req
.
PaymentDailyLimit
!=
nil
||
req
.
PaymentMaxAmount
!=
nil
||
req
.
PaymentDailyLimit
!=
nil
||
req
.
PaymentOrderTimeoutMin
!=
nil
||
req
.
PaymentMaxPendingOrders
!=
nil
||
req
.
PaymentOrderTimeoutMin
!=
nil
||
req
.
PaymentMaxPendingOrders
!=
nil
||
req
.
PaymentEnabledTypes
!=
nil
||
req
.
PaymentBalanceDisabled
!=
nil
||
req
.
PaymentEnabledTypes
!=
nil
||
req
.
PaymentBalanceDisabled
!=
nil
||
req
.
PaymentBalanceRechargeMultiplier
!=
nil
||
req
.
PaymentRechargeFeeRate
!=
nil
||
req
.
PaymentLoadBalanceStrat
!=
nil
||
req
.
PaymentProductNamePrefix
!=
nil
||
req
.
PaymentLoadBalanceStrat
!=
nil
||
req
.
PaymentProductNamePrefix
!=
nil
||
req
.
PaymentProductNameSuffix
!=
nil
||
req
.
PaymentHelpImageURL
!=
nil
||
req
.
PaymentProductNameSuffix
!=
nil
||
req
.
PaymentHelpImageURL
!=
nil
||
req
.
PaymentHelpText
!=
nil
||
req
.
PaymentCancelRateLimitEnabled
!=
nil
||
req
.
PaymentHelpText
!=
nil
||
req
.
PaymentCancelRateLimitEnabled
!=
nil
||
...
...
backend/internal/handler/dto/settings.go
View file @
9bf079b7
...
@@ -136,6 +136,8 @@ type SystemSettings struct {
...
@@ -136,6 +136,8 @@ type SystemSettings struct {
PaymentMaxPendingOrders
int
`json:"payment_max_pending_orders"`
PaymentMaxPendingOrders
int
`json:"payment_max_pending_orders"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
PaymentBalanceDisabled
bool
`json:"payment_balance_disabled"`
PaymentBalanceDisabled
bool
`json:"payment_balance_disabled"`
PaymentBalanceRechargeMultiplier
float64
`json:"payment_balance_recharge_multiplier"`
PaymentRechargeFeeRate
float64
`json:"payment_recharge_fee_rate"`
PaymentLoadBalanceStrat
string
`json:"payment_load_balance_strategy"`
PaymentLoadBalanceStrat
string
`json:"payment_load_balance_strategy"`
PaymentProductNamePrefix
string
`json:"payment_product_name_prefix"`
PaymentProductNamePrefix
string
`json:"payment_product_name_prefix"`
PaymentProductNameSuffix
string
`json:"payment_product_name_suffix"`
PaymentProductNameSuffix
string
`json:"payment_product_name_suffix"`
...
...
backend/internal/handler/payment_handler.go
View file @
9bf079b7
...
@@ -131,6 +131,8 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
...
@@ -131,6 +131,8 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
GlobalMax
:
limitsResp
.
GlobalMax
,
GlobalMax
:
limitsResp
.
GlobalMax
,
Plans
:
planList
,
Plans
:
planList
,
BalanceDisabled
:
cfg
.
BalanceDisabled
,
BalanceDisabled
:
cfg
.
BalanceDisabled
,
BalanceRechargeMultiplier
:
cfg
.
BalanceRechargeMultiplier
,
RechargeFeeRate
:
cfg
.
RechargeFeeRate
,
HelpText
:
cfg
.
HelpText
,
HelpText
:
cfg
.
HelpText
,
HelpImageURL
:
cfg
.
HelpImageURL
,
HelpImageURL
:
cfg
.
HelpImageURL
,
StripePublishableKey
:
cfg
.
StripePublishableKey
,
StripePublishableKey
:
cfg
.
StripePublishableKey
,
...
@@ -143,6 +145,8 @@ type checkoutInfoResponse struct {
...
@@ -143,6 +145,8 @@ type checkoutInfoResponse struct {
GlobalMax
float64
`json:"global_max"`
GlobalMax
float64
`json:"global_max"`
Plans
[]
checkoutPlan
`json:"plans"`
Plans
[]
checkoutPlan
`json:"plans"`
BalanceDisabled
bool
`json:"balance_disabled"`
BalanceDisabled
bool
`json:"balance_disabled"`
BalanceRechargeMultiplier
float64
`json:"balance_recharge_multiplier"`
RechargeFeeRate
float64
`json:"recharge_fee_rate"`
HelpText
string
`json:"help_text"`
HelpText
string
`json:"help_text"`
HelpImageURL
string
`json:"help_image_url"`
HelpImageURL
string
`json:"help_image_url"`
StripePublishableKey
string
`json:"stripe_publishable_key"`
StripePublishableKey
string
`json:"stripe_publishable_key"`
...
@@ -381,6 +385,7 @@ type PublicOrderResult struct {
...
@@ -381,6 +385,7 @@ type PublicOrderResult struct {
Amount
float64
`json:"amount"`
Amount
float64
`json:"amount"`
PayAmount
float64
`json:"pay_amount"`
PayAmount
float64
`json:"pay_amount"`
PaymentType
string
`json:"payment_type"`
PaymentType
string
`json:"payment_type"`
OrderType
string
`json:"order_type"`
Status
string
`json:"status"`
Status
string
`json:"status"`
}
}
...
@@ -404,6 +409,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
...
@@ -404,6 +409,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
Amount
:
order
.
Amount
,
Amount
:
order
.
Amount
,
PayAmount
:
order
.
PayAmount
,
PayAmount
:
order
.
PayAmount
,
PaymentType
:
order
.
PaymentType
,
PaymentType
:
order
.
PaymentType
,
OrderType
:
order
.
OrderType
,
Status
:
order
.
Status
,
Status
:
order
.
Status
,
})
})
}
}
...
...
backend/internal/server/api_contract_test.go
View file @
9bf079b7
...
@@ -601,6 +601,8 @@ func TestAPIContracts(t *testing.T) {
...
@@ -601,6 +601,8 @@ func TestAPIContracts(t *testing.T) {
"payment_order_timeout_minutes": 0,
"payment_order_timeout_minutes": 0,
"payment_max_pending_orders": 0,
"payment_max_pending_orders": 0,
"payment_balance_disabled": false,
"payment_balance_disabled": false,
"payment_balance_recharge_multiplier": 0,
"payment_recharge_fee_rate": 0,
"payment_load_balance_strategy": "",
"payment_load_balance_strategy": "",
"payment_product_name_prefix": "",
"payment_product_name_prefix": "",
"payment_product_name_suffix": "",
"payment_product_name_suffix": "",
...
...
backend/internal/service/payment_amounts.go
0 → 100644
View file @
9bf079b7
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
()
}
backend/internal/service/payment_config_service.go
View file @
9bf079b7
...
@@ -3,12 +3,14 @@ package service
...
@@ -3,12 +3,14 @@ package service
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"math"
"strconv"
"strconv"
"strings"
"strings"
dbent
"github.com/Wei-Shaw/sub2api/ent"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/internal/payment"
"github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
)
const
(
const
(
...
@@ -21,6 +23,8 @@ const (
...
@@ -21,6 +23,8 @@ const (
SettingEnabledPaymentTypes
=
"ENABLED_PAYMENT_TYPES"
SettingEnabledPaymentTypes
=
"ENABLED_PAYMENT_TYPES"
SettingLoadBalanceStrategy
=
"LOAD_BALANCE_STRATEGY"
SettingLoadBalanceStrategy
=
"LOAD_BALANCE_STRATEGY"
SettingBalancePayDisabled
=
"BALANCE_PAYMENT_DISABLED"
SettingBalancePayDisabled
=
"BALANCE_PAYMENT_DISABLED"
SettingBalanceRechargeMult
=
"BALANCE_RECHARGE_MULTIPLIER"
SettingRechargeFeeRate
=
"RECHARGE_FEE_RATE"
SettingProductNamePrefix
=
"PRODUCT_NAME_PREFIX"
SettingProductNamePrefix
=
"PRODUCT_NAME_PREFIX"
SettingProductNameSuffix
=
"PRODUCT_NAME_SUFFIX"
SettingProductNameSuffix
=
"PRODUCT_NAME_SUFFIX"
SettingHelpImageURL
=
"PAYMENT_HELP_IMAGE_URL"
SettingHelpImageURL
=
"PAYMENT_HELP_IMAGE_URL"
...
@@ -48,6 +52,8 @@ type PaymentConfig struct {
...
@@ -48,6 +52,8 @@ type PaymentConfig struct {
MaxPendingOrders
int
`json:"max_pending_orders"`
MaxPendingOrders
int
`json:"max_pending_orders"`
EnabledTypes
[]
string
`json:"enabled_payment_types"`
EnabledTypes
[]
string
`json:"enabled_payment_types"`
BalanceDisabled
bool
`json:"balance_disabled"`
BalanceDisabled
bool
`json:"balance_disabled"`
BalanceRechargeMultiplier
float64
`json:"balance_recharge_multiplier"`
RechargeFeeRate
float64
`json:"recharge_fee_rate"`
LoadBalanceStrategy
string
`json:"load_balance_strategy"`
LoadBalanceStrategy
string
`json:"load_balance_strategy"`
ProductNamePrefix
string
`json:"product_name_prefix"`
ProductNamePrefix
string
`json:"product_name_prefix"`
ProductNameSuffix
string
`json:"product_name_suffix"`
ProductNameSuffix
string
`json:"product_name_suffix"`
...
@@ -73,6 +79,8 @@ type UpdatePaymentConfigRequest struct {
...
@@ -73,6 +79,8 @@ type UpdatePaymentConfigRequest struct {
MaxPendingOrders
*
int
`json:"max_pending_orders"`
MaxPendingOrders
*
int
`json:"max_pending_orders"`
EnabledTypes
[]
string
`json:"enabled_payment_types"`
EnabledTypes
[]
string
`json:"enabled_payment_types"`
BalanceDisabled
*
bool
`json:"balance_disabled"`
BalanceDisabled
*
bool
`json:"balance_disabled"`
BalanceRechargeMultiplier
*
float64
`json:"balance_recharge_multiplier"`
RechargeFeeRate
*
float64
`json:"recharge_fee_rate"`
LoadBalanceStrategy
*
string
`json:"load_balance_strategy"`
LoadBalanceStrategy
*
string
`json:"load_balance_strategy"`
ProductNamePrefix
*
string
`json:"product_name_prefix"`
ProductNamePrefix
*
string
`json:"product_name_prefix"`
ProductNameSuffix
*
string
`json:"product_name_suffix"`
ProductNameSuffix
*
string
`json:"product_name_suffix"`
...
@@ -183,7 +191,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
...
@@ -183,7 +191,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
keys
:=
[]
string
{
keys
:=
[]
string
{
SettingPaymentEnabled
,
SettingMinRechargeAmount
,
SettingMaxRechargeAmount
,
SettingPaymentEnabled
,
SettingMinRechargeAmount
,
SettingMaxRechargeAmount
,
SettingDailyRechargeLimit
,
SettingOrderTimeoutMinutes
,
SettingMaxPendingOrders
,
SettingDailyRechargeLimit
,
SettingOrderTimeoutMinutes
,
SettingMaxPendingOrders
,
SettingEnabledPaymentTypes
,
SettingBalancePayDisabled
,
SettingLoadBalanceStrategy
,
SettingEnabledPaymentTypes
,
SettingBalancePayDisabled
,
SettingBalanceRechargeMult
,
SettingRechargeFeeRate
,
SettingLoadBalanceStrategy
,
SettingProductNamePrefix
,
SettingProductNameSuffix
,
SettingProductNamePrefix
,
SettingProductNameSuffix
,
SettingHelpImageURL
,
SettingHelpText
,
SettingHelpImageURL
,
SettingHelpText
,
SettingCancelRateLimitOn
,
SettingCancelRateLimitMax
,
SettingCancelRateLimitOn
,
SettingCancelRateLimitMax
,
...
@@ -208,6 +216,8 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
...
@@ -208,6 +216,8 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
OrderTimeoutMin
:
pcParseInt
(
vals
[
SettingOrderTimeoutMinutes
],
defaultOrderTimeoutMin
),
OrderTimeoutMin
:
pcParseInt
(
vals
[
SettingOrderTimeoutMinutes
],
defaultOrderTimeoutMin
),
MaxPendingOrders
:
pcParseInt
(
vals
[
SettingMaxPendingOrders
],
defaultMaxPendingOrders
),
MaxPendingOrders
:
pcParseInt
(
vals
[
SettingMaxPendingOrders
],
defaultMaxPendingOrders
),
BalanceDisabled
:
vals
[
SettingBalancePayDisabled
]
==
"true"
,
BalanceDisabled
:
vals
[
SettingBalancePayDisabled
]
==
"true"
,
BalanceRechargeMultiplier
:
normalizeBalanceRechargeMultiplier
(
pcParseFloat
(
vals
[
SettingBalanceRechargeMult
],
defaultBalanceRechargeMultiplier
)),
RechargeFeeRate
:
pcParseFloat
(
vals
[
SettingRechargeFeeRate
],
0
),
LoadBalanceStrategy
:
vals
[
SettingLoadBalanceStrategy
],
LoadBalanceStrategy
:
vals
[
SettingLoadBalanceStrategy
],
ProductNamePrefix
:
vals
[
SettingProductNamePrefix
],
ProductNamePrefix
:
vals
[
SettingProductNamePrefix
],
ProductNameSuffix
:
vals
[
SettingProductNameSuffix
],
ProductNameSuffix
:
vals
[
SettingProductNameSuffix
],
...
@@ -256,6 +266,21 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri
...
@@ -256,6 +266,21 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri
// nil-check before serialisation — this is inherent to patch-style update patterns
// nil-check before serialisation — this is inherent to patch-style update patterns
// and cannot be meaningfully decomposed without introducing unnecessary abstraction.
// and cannot be meaningfully decomposed without introducing unnecessary abstraction.
func
(
s
*
PaymentConfigService
)
UpdatePaymentConfig
(
ctx
context
.
Context
,
req
UpdatePaymentConfigRequest
)
error
{
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"
)
}
}
if
req
.
RechargeFeeRate
!=
nil
{
v
:=
*
req
.
RechargeFeeRate
if
math
.
IsNaN
(
v
)
||
math
.
IsInf
(
v
,
0
)
||
v
<
0
||
v
>
100
{
return
infraerrors
.
BadRequest
(
"INVALID_RECHARGE_FEE_RATE"
,
"recharge fee rate must be between 0 and 100"
)
}
// Enforce max 2 decimal places
if
math
.
Round
(
v
*
100
)
!=
v
*
100
{
return
infraerrors
.
BadRequest
(
"INVALID_RECHARGE_FEE_RATE"
,
"recharge fee rate allows at most 2 decimal places"
)
}
}
m
:=
map
[
string
]
string
{
m
:=
map
[
string
]
string
{
SettingPaymentEnabled
:
formatBoolOrEmpty
(
req
.
Enabled
),
SettingPaymentEnabled
:
formatBoolOrEmpty
(
req
.
Enabled
),
SettingMinRechargeAmount
:
formatPositiveFloat
(
req
.
MinAmount
),
SettingMinRechargeAmount
:
formatPositiveFloat
(
req
.
MinAmount
),
...
@@ -264,6 +289,8 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
...
@@ -264,6 +289,8 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
SettingOrderTimeoutMinutes
:
formatPositiveInt
(
req
.
OrderTimeoutMin
),
SettingOrderTimeoutMinutes
:
formatPositiveInt
(
req
.
OrderTimeoutMin
),
SettingMaxPendingOrders
:
formatPositiveInt
(
req
.
MaxPendingOrders
),
SettingMaxPendingOrders
:
formatPositiveInt
(
req
.
MaxPendingOrders
),
SettingBalancePayDisabled
:
formatBoolOrEmpty
(
req
.
BalanceDisabled
),
SettingBalancePayDisabled
:
formatBoolOrEmpty
(
req
.
BalanceDisabled
),
SettingBalanceRechargeMult
:
formatPositiveFloat
(
req
.
BalanceRechargeMultiplier
),
SettingRechargeFeeRate
:
formatNonNegativeFloat
(
req
.
RechargeFeeRate
),
SettingLoadBalanceStrategy
:
derefStr
(
req
.
LoadBalanceStrategy
),
SettingLoadBalanceStrategy
:
derefStr
(
req
.
LoadBalanceStrategy
),
SettingProductNamePrefix
:
derefStr
(
req
.
ProductNamePrefix
),
SettingProductNamePrefix
:
derefStr
(
req
.
ProductNamePrefix
),
SettingProductNameSuffix
:
derefStr
(
req
.
ProductNameSuffix
),
SettingProductNameSuffix
:
derefStr
(
req
.
ProductNameSuffix
),
...
@@ -297,6 +324,13 @@ func formatPositiveFloat(v *float64) string {
...
@@ -297,6 +324,13 @@ func formatPositiveFloat(v *float64) string {
return
strconv
.
FormatFloat
(
*
v
,
'f'
,
2
,
64
)
return
strconv
.
FormatFloat
(
*
v
,
'f'
,
2
,
64
)
}
}
func
formatNonNegativeFloat
(
v
*
float64
)
string
{
if
v
==
nil
||
*
v
<
0
{
return
""
}
return
strconv
.
FormatFloat
(
*
v
,
'f'
,
2
,
64
)
}
func
formatPositiveInt
(
v
*
int
)
string
{
func
formatPositiveInt
(
v
*
int
)
string
{
if
v
==
nil
||
*
v
<=
0
{
if
v
==
nil
||
*
v
<=
0
{
return
""
return
""
...
...
backend/internal/service/payment_fulfillment.go
View file @
9bf079b7
...
@@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde
...
@@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde
if
err
!=
nil
{
if
err
!=
nil
{
return
fmt
.
Errorf
(
"mark completed: %w"
,
err
)
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
return
nil
}
}
...
...
backend/internal/service/payment_order.go
View file @
9bf079b7
...
@@ -43,18 +43,22 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
...
@@ -43,18 +43,22 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
if
user
.
Status
!=
payment
.
EntityStatusActive
{
if
user
.
Status
!=
payment
.
EntityStatusActive
{
return
nil
,
infraerrors
.
Forbidden
(
"USER_INACTIVE"
,
"user account is disabled"
)
return
nil
,
infraerrors
.
Forbidden
(
"USER_INACTIVE"
,
"user account is disabled"
)
}
}
amount
:=
req
.
Amount
orderAmount
:=
req
.
Amount
limitAmount
:=
req
.
Amount
if
plan
!=
nil
{
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
.
ge
t
FeeRate
(
req
.
PaymentType
)
feeRate
:=
cfg
.
Rechar
geFeeRate
payAmountStr
:=
payment
.
CalculatePayAmount
(
a
mount
,
feeRate
)
payAmountStr
:=
payment
.
CalculatePayAmount
(
limitA
mount
,
feeRate
)
payAmount
,
_
:=
strconv
.
ParseFloat
(
payAmountStr
,
64
)
payAmount
,
_
:=
strconv
.
ParseFloat
(
payAmountStr
,
64
)
order
,
err
:=
s
.
createOrderInTx
(
ctx
,
req
,
user
,
plan
,
cfg
,
a
mount
,
feeRate
,
payAmount
)
order
,
err
:=
s
.
createOrderInTx
(
ctx
,
req
,
user
,
plan
,
cfg
,
orderAmount
,
limitA
mount
,
feeRate
,
payAmount
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
resp
,
err
:=
s
.
invokeProvider
(
ctx
,
order
,
req
,
cfg
,
payAmountStr
,
payAmount
,
plan
)
resp
,
err
:=
s
.
invokeProvider
(
ctx
,
order
,
req
,
cfg
,
limitAmount
,
payAmountStr
,
payAmount
,
plan
)
if
err
!=
nil
{
if
err
!=
nil
{
_
,
_
=
s
.
entClient
.
PaymentOrder
.
UpdateOneID
(
order
.
ID
)
.
_
,
_
=
s
.
entClient
.
PaymentOrder
.
UpdateOneID
(
order
.
ID
)
.
SetStatus
(
OrderStatusFailed
)
.
SetStatus
(
OrderStatusFailed
)
.
...
@@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe
...
@@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe
return
plan
,
nil
return
plan
,
nil
}
}
func
(
s
*
PaymentService
)
createOrderInTx
(
ctx
context
.
Context
,
req
CreateOrderRequest
,
user
*
User
,
plan
*
dbent
.
SubscriptionPlan
,
cfg
*
PaymentConfig
,
a
mount
,
feeRate
,
payAmount
float64
)
(
*
dbent
.
PaymentOrder
,
error
)
{
func
(
s
*
PaymentService
)
createOrderInTx
(
ctx
context
.
Context
,
req
CreateOrderRequest
,
user
*
User
,
plan
*
dbent
.
SubscriptionPlan
,
cfg
*
PaymentConfig
,
orderAmount
,
limitA
mount
,
feeRate
,
payAmount
float64
)
(
*
dbent
.
PaymentOrder
,
error
)
{
tx
,
err
:=
s
.
entClient
.
Tx
(
ctx
)
tx
,
err
:=
s
.
entClient
.
Tx
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"begin transaction: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"begin transaction: %w"
,
err
)
...
@@ -108,7 +112,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
...
@@ -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
{
if
err
:=
s
.
checkPendingLimit
(
ctx
,
tx
,
req
.
UserID
,
cfg
.
MaxPendingOrders
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
err
:=
s
.
checkDailyLimit
(
ctx
,
tx
,
req
.
UserID
,
a
mount
,
cfg
.
DailyLimit
);
err
!=
nil
{
if
err
:=
s
.
checkDailyLimit
(
ctx
,
tx
,
req
.
UserID
,
limitA
mount
,
cfg
.
DailyLimit
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
tm
:=
cfg
.
OrderTimeoutMin
tm
:=
cfg
.
OrderTimeoutMin
...
@@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
...
@@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
SetUserEmail
(
user
.
Email
)
.
SetUserEmail
(
user
.
Email
)
.
SetUserName
(
user
.
Username
)
.
SetUserName
(
user
.
Username
)
.
SetNillableUserNotes
(
psNilIfEmpty
(
user
.
Notes
))
.
SetNillableUserNotes
(
psNilIfEmpty
(
user
.
Notes
))
.
SetAmount
(
a
mount
)
.
SetAmount
(
orderA
mount
)
.
SetPayAmount
(
payAmount
)
.
SetPayAmount
(
payAmount
)
.
SetFeeRate
(
feeRate
)
.
SetFeeRate
(
feeRate
)
.
SetRechargeCode
(
""
)
.
SetRechargeCode
(
""
)
.
...
@@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
...
@@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
}
}
var
used
float64
var
used
float64
for
_
,
o
:=
range
orders
{
for
_
,
o
:=
range
orders
{
if
o
.
OrderType
==
payment
.
OrderTypeBalance
{
used
+=
o
.
PayAmount
continue
}
used
+=
o
.
Amount
used
+=
o
.
Amount
}
}
if
used
+
amount
>
limit
{
if
used
+
amount
>
limit
{
...
@@ -188,7 +196,7 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
...
@@ -188,7 +196,7 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
return
nil
return
nil
}
}
func
(
s
*
PaymentService
)
invokeProvider
(
ctx
context
.
Context
,
order
*
dbent
.
PaymentOrder
,
req
CreateOrderRequest
,
cfg
*
PaymentConfig
,
payAmountStr
string
,
payAmount
float64
,
plan
*
dbent
.
SubscriptionPlan
)
(
*
CreateOrderResponse
,
error
)
{
func
(
s
*
PaymentService
)
invokeProvider
(
ctx
context
.
Context
,
order
*
dbent
.
PaymentOrder
,
req
CreateOrderRequest
,
cfg
*
PaymentConfig
,
limitAmount
float64
,
payAmountStr
string
,
payAmount
float64
,
plan
*
dbent
.
SubscriptionPlan
)
(
*
CreateOrderResponse
,
error
)
{
// Select an instance across all providers that support the requested payment type.
// 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").
// 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
)
sel
,
err
:=
s
.
loadBalancer
.
SelectInstance
(
ctx
,
""
,
req
.
PaymentType
,
payment
.
Strategy
(
cfg
.
LoadBalanceStrategy
),
payAmount
)
...
@@ -202,7 +210,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
...
@@ -202,7 +210,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"PAYMENT_GATEWAY_ERROR"
,
"payment method is temporarily unavailable"
)
return
nil
,
infraerrors
.
ServiceUnavailable
(
"PAYMENT_GATEWAY_ERROR"
,
"payment method is temporarily unavailable"
)
}
}
subject
:=
s
.
buildPaymentSubject
(
plan
,
pay
Amount
Str
,
cfg
)
subject
:=
s
.
buildPaymentSubject
(
plan
,
limit
Amount
,
cfg
)
outTradeNo
:=
order
.
OutTradeNo
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
})
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
{
if
err
!=
nil
{
...
@@ -213,23 +221,30 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
...
@@ -213,23 +221,30 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"update order with payment details: %w"
,
err
)
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
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
,
pay
Amount
Str
string
,
cfg
*
PaymentConfig
)
string
{
func
(
s
*
PaymentService
)
buildPaymentSubject
(
plan
*
dbent
.
SubscriptionPlan
,
limit
Amount
float64
,
cfg
*
PaymentConfig
)
string
{
if
plan
!=
nil
{
if
plan
!=
nil
{
if
plan
.
ProductName
!=
""
{
if
plan
.
ProductName
!=
""
{
return
plan
.
ProductName
return
plan
.
ProductName
}
}
return
"Sub2API Subscription "
+
plan
.
Name
return
"Sub2API Subscription "
+
plan
.
Name
}
}
amountStr
:=
strconv
.
FormatFloat
(
limitAmount
,
'f'
,
2
,
64
)
pf
:=
strings
.
TrimSpace
(
cfg
.
ProductNamePrefix
)
pf
:=
strings
.
TrimSpace
(
cfg
.
ProductNamePrefix
)
sf
:=
strings
.
TrimSpace
(
cfg
.
ProductNameSuffix
)
sf
:=
strings
.
TrimSpace
(
cfg
.
ProductNameSuffix
)
if
pf
!=
""
||
sf
!=
""
{
if
pf
!=
""
||
sf
!=
""
{
return
strings
.
TrimSpace
(
pf
+
" "
+
payA
mountStr
+
" "
+
sf
)
return
strings
.
TrimSpace
(
pf
+
" "
+
a
mountStr
+
" "
+
sf
)
}
}
return
"Sub2API "
+
payA
mountStr
+
" CNY"
return
"Sub2API "
+
a
mountStr
+
" CNY"
}
}
// --- Order Queries ---
// --- Order Queries ---
...
...
backend/internal/service/payment_refund.go
View file @
9bf079b7
...
@@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
...
@@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
if
amt
-
o
.
Amount
>
amountToleranceCNY
{
if
amt
-
o
.
Amount
>
amountToleranceCNY
{
return
nil
,
nil
,
infraerrors
.
BadRequest
(
"REFUND_AMOUNT_EXCEEDED"
,
"refund amount exceeds recharge"
)
return
nil
,
nil
,
infraerrors
.
BadRequest
(
"REFUND_AMOUNT_EXCEEDED"
,
"refund amount exceeds recharge"
)
}
}
// Full refund: use actual pay_amount for gateway (includes fees)
ga
:=
calculateGatewayRefundAmount
(
o
.
Amount
,
o
.
PayAmount
,
amt
)
ga
:=
amt
if
math
.
Abs
(
amt
-
o
.
Amount
)
<=
amountToleranceCNY
{
ga
=
o
.
PayAmount
}
rr
:=
strings
.
TrimSpace
(
reason
)
rr
:=
strings
.
TrimSpace
(
reason
)
if
rr
==
""
&&
o
.
RefundRequestReason
!=
nil
{
if
rr
==
""
&&
o
.
RefundRequestReason
!=
nil
{
rr
=
*
o
.
RefundRequestReason
rr
=
*
o
.
RefundRequestReason
...
...
backend/internal/service/payment_service.go
View file @
9bf079b7
...
@@ -288,8 +288,6 @@ func psComputeValidityDays(days int, unit string) int {
...
@@ -288,8 +288,6 @@ func psComputeValidityDays(days int, unit string) int {
}
}
}
}
func
(
s
*
PaymentService
)
getFeeRate
(
_
string
)
float64
{
return
0
}
func
psStartOfDayUTC
(
t
time
.
Time
)
time
.
Time
{
func
psStartOfDayUTC
(
t
time
.
Time
)
time
.
Time
{
y
,
m
,
d
:=
t
.
UTC
()
.
Date
()
y
,
m
,
d
:=
t
.
UTC
()
.
Date
()
return
time
.
Date
(
y
,
m
,
d
,
0
,
0
,
0
,
0
,
time
.
UTC
)
return
time
.
Date
(
y
,
m
,
d
,
0
,
0
,
0
,
0
,
time
.
UTC
)
...
...
frontend/src/api/admin/payment.ts
View file @
9bf079b7
...
@@ -23,6 +23,7 @@ export interface AdminPaymentConfig {
...
@@ -23,6 +23,7 @@ export interface AdminPaymentConfig {
max_pending_orders
:
number
max_pending_orders
:
number
enabled_payment_types
:
string
[]
enabled_payment_types
:
string
[]
balance_disabled
:
boolean
balance_disabled
:
boolean
balance_recharge_multiplier
:
number
load_balance_strategy
:
string
load_balance_strategy
:
string
product_name_prefix
:
string
product_name_prefix
:
string
product_name_suffix
:
string
product_name_suffix
:
string
...
@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest {
...
@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest {
max_pending_orders
?:
number
max_pending_orders
?:
number
enabled_payment_types
?:
string
[]
enabled_payment_types
?:
string
[]
balance_disabled
?:
boolean
balance_disabled
?:
boolean
balance_recharge_multiplier
?:
number
load_balance_strategy
?:
string
load_balance_strategy
?:
string
product_name_prefix
?:
string
product_name_prefix
?:
string
product_name_suffix
?:
string
product_name_suffix
?:
string
...
...
frontend/src/api/admin/settings.ts
View file @
9bf079b7
...
@@ -125,6 +125,8 @@ export interface SystemSettings {
...
@@ -125,6 +125,8 @@ export interface SystemSettings {
payment_max_pending_orders
:
number
payment_max_pending_orders
:
number
payment_enabled_types
:
string
[]
payment_enabled_types
:
string
[]
payment_balance_disabled
:
boolean
payment_balance_disabled
:
boolean
payment_balance_recharge_multiplier
:
number
payment_recharge_fee_rate
:
number
payment_load_balance_strategy
:
string
payment_load_balance_strategy
:
string
payment_product_name_prefix
:
string
payment_product_name_prefix
:
string
payment_product_name_suffix
:
string
payment_product_name_suffix
:
string
...
@@ -231,6 +233,8 @@ export interface UpdateSettingsRequest {
...
@@ -231,6 +233,8 @@ export interface UpdateSettingsRequest {
payment_max_pending_orders
?:
number
payment_max_pending_orders
?:
number
payment_enabled_types
?:
string
[]
payment_enabled_types
?:
string
[]
payment_balance_disabled
?:
boolean
payment_balance_disabled
?:
boolean
payment_balance_recharge_multiplier
?:
number
payment_recharge_fee_rate
?:
number
payment_load_balance_strategy
?:
string
payment_load_balance_strategy
?:
string
payment_product_name_prefix
?:
string
payment_product_name_prefix
?:
string
payment_product_name_suffix
?:
string
payment_product_name_suffix
?:
string
...
...
frontend/src/components/admin/payment/AdminOrderDetail.vue
View file @
9bf079b7
...
@@ -18,12 +18,20 @@
...
@@ -18,12 +18,20 @@
</span>
</span>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.baseAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
$
{{
order
.
amount
.
toFixed
(
2
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
baseAmount
.
toFixed
(
2
)
}}
</p>
</div>
<div
v-if=
"order.fee_rate > 0"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.fee
'
)
}}
(
{{
order
.
fee_rate
}}
%)
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
feeAmount
.
toFixed
(
2
)
}}
</p>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
$
{{
order
.
pay_amount
.
toFixed
(
2
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
order
.
pay_amount
.
toFixed
(
2
)
}}
</p>
</div>
<div
v-if=
"order.amount !== order.pay_amount"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
{{
order
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
order
.
amount
.
toFixed
(
2
)
}}
</p>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.paymentMethod
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.paymentMethod
'
)
}}
</p>
...
@@ -31,10 +39,6 @@
...
@@ -31,10 +39,6 @@
{{
t
(
'
payment.methods.
'
+
order
.
payment_type
,
order
.
payment_type
)
}}
{{
t
(
'
payment.methods.
'
+
order
.
payment_type
,
order
.
payment_type
)
}}
</p>
</p>
</div>
</div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.feeRate
'
)
}}
</p>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
(
order
.
fee_rate
*
100
).
toFixed
(
1
)
}}
%
</p>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderType
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderType
'
)
}}
</p>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
...
@@ -73,7 +77,7 @@
...
@@ -73,7 +77,7 @@
<div
class=
"grid grid-cols-2 gap-2 text-sm"
>
<div
class=
"grid grid-cols-2 gap-2 text-sm"
>
<div>
<div>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
:
</span>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
:
</span>
<span
class=
"ml-1 font-medium text-red-700 dark:text-red-300"
>
$
{{
order
.
refund_amount
.
toFixed
(
2
)
}}
</span>
<span
class=
"ml-1 font-medium text-red-700 dark:text-red-300"
>
{{
order
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}
{{
order
.
refund_amount
.
toFixed
(
2
)
}}
</span>
</div>
</div>
<div
v-if=
"order.refund_reason"
class=
"col-span-2"
>
<div
v-if=
"order.refund_reason"
class=
"col-span-2"
>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundReason
'
)
}}
:
</span>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundReason
'
)
}}
:
</span>
...
@@ -110,6 +114,7 @@
...
@@ -110,6 +114,7 @@
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
...
@@ -117,11 +122,24 @@ import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } f
...
@@ -117,11 +122,24 @@ import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } f
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
defineProps
<
{
const
props
=
defineProps
<
{
show
:
boolean
show
:
boolean
order
:
PaymentOrder
|
null
order
:
PaymentOrder
|
null
}
>
()
}
>
()
/** 充值金额 (base amount before fee) = pay_amount - fee = pay_amount / (1 + fee_rate/100) */
const
baseAmount
=
computed
(()
=>
{
if
(
!
props
.
order
)
return
0
if
(
props
.
order
.
fee_rate
<=
0
)
return
props
.
order
.
pay_amount
return
props
.
order
.
pay_amount
/
(
1
+
props
.
order
.
fee_rate
/
100
)
})
/** 手续费 = pay_amount - baseAmount */
const
feeAmount
=
computed
(()
=>
{
if
(
!
props
.
order
||
props
.
order
.
fee_rate
<=
0
)
return
0
return
props
.
order
.
pay_amount
-
baseAmount
.
value
})
const
emit
=
defineEmits
<
{
const
emit
=
defineEmits
<
{
(
e
:
'
close
'
):
void
(
e
:
'
close
'
):
void
(
e
:
'
cancel
'
,
order
:
PaymentOrder
):
void
(
e
:
'
cancel
'
,
order
:
PaymentOrder
):
void
...
...
frontend/src/components/admin/payment/AdminOrderTable.vue
View file @
9bf079b7
...
@@ -51,12 +51,15 @@
...
@@ -51,12 +51,15 @@
<span
class=
"text-sm text-gray-600 dark:text-gray-400"
>
#
{{
value
}}
</span>
<span
class=
"text-sm text-gray-600 dark:text-gray-400"
>
#
{{
value
}}
</span>
</
template
>
</
template
>
<
template
#cell-amount=
"{ value, row }"
>
<
template
#cell-
pay_
amount=
"{ value, row }"
>
<div
class=
"text-sm"
>
<div
class=
"text-sm"
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
value
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
value
.
toFixed
(
2
)
}}
</span>
<span
v-if=
"row.
pay_amount !== value
"
class=
"ml-1 text-xs text-gray-
5
00"
>
<span
v-if=
"row.
fee_rate > 0
"
class=
"ml-1 text-xs text-gray-
4
00"
:title=
"t('payment.orders.fee') + ': ' + row.fee_rate + '%'"
>
(
{{
t
(
'
payment.orders.payAmount
'
)
}}
: $
{{
row
.
pay_amount
.
toFixed
(
2
)
}}
)
(
{{
row
.
fee_rate
}}
%
)
</span>
</span>
<div
v-if=
"row.amount !== row.pay_amount"
class=
"text-xs text-gray-500"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
:
{{
row
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
row
.
amount
.
toFixed
(
2
)
}}
</div>
</div>
</div>
</
template
>
</
template
>
...
@@ -183,7 +186,7 @@ function emitFiltersChanged() {
...
@@ -183,7 +186,7 @@ function emitFiltersChanged() {
const
columns
=
computed
<
Column
[]
>
(()
=>
[
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
id
'
,
label
:
t
(
'
payment.orders.orderId
'
)
},
{
key
:
'
id
'
,
label
:
t
(
'
payment.orders.orderId
'
)
},
{
key
:
'
user_id
'
,
label
:
t
(
'
payment.orders.userId
'
)
},
{
key
:
'
user_id
'
,
label
:
t
(
'
payment.orders.userId
'
)
},
{
key
:
'
amount
'
,
label
:
t
(
'
payment.orders.
a
mount
'
)
},
{
key
:
'
pay_
amount
'
,
label
:
t
(
'
payment.orders.
payA
mount
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
order_type
'
,
label
:
t
(
'
payment.orders.orderType
'
)
},
{
key
:
'
order_type
'
,
label
:
t
(
'
payment.orders.orderType
'
)
},
...
...
frontend/src/components/admin/payment/AdminRefundDialog.vue
View file @
9bf079b7
...
@@ -34,12 +34,16 @@
...
@@ -34,12 +34,16 @@
<span
class=
"font-mono text-gray-900 dark:text-white"
>
#
{{
order
?.
id
}}
</span>
<span
class=
"font-mono text-gray-900 dark:text-white"
>
#
{{
order
?.
id
}}
</span>
</div>
</div>
<div
class=
"mt-1 flex justify-between text-sm"
>
<div
class=
"mt-1 flex justify-between text-sm"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
order
?.
pay_amount
?.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
order
?.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
order
?.
amount
?.
toFixed
(
2
)
}}
</span>
</div>
<div
class=
"mt-1 flex justify-between text-sm"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
order
?.
pay_amount
?.
toFixed
(
2
)
}}
</span>
</div>
</div>
<div
v-if=
"actuallyRefunded > 0"
class=
"mt-1 flex justify-between text-sm"
>
<div
v-if=
"actuallyRefunded > 0"
class=
"mt-1 flex justify-between text-sm"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.alreadyRefunded
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.alreadyRefunded
'
)
}}
</span>
<span
class=
"font-medium text-red-600 dark:text-red-400"
>
$
{{
actuallyRefunded
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-red-600 dark:text-red-400"
>
{{
order
?.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}
{{
actuallyRefunded
.
toFixed
(
2
)
}}
</span>
</div>
</div>
</div>
</div>
...
@@ -66,7 +70,7 @@
...
@@ -66,7 +70,7 @@
</div>
</div>
<div
class=
"rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700"
>
<div
class=
"rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700"
>
<div
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderAmount
'
)
}}
</div>
<div
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderAmount
'
)
}}
</div>
<div
class=
"mt-1 font-semibold text-gray-900 dark:text-white"
>
$
{{
order
?.
pay_
amount
?.
toFixed
(
2
)
}}
</div>
<div
class=
"mt-1 font-semibold text-gray-900 dark:text-white"
>
{{
order
?.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
order
?.
amount
?.
toFixed
(
2
)
}}
</div>
</div>
</div>
</div>
</div>
...
@@ -91,7 +95,7 @@
...
@@ -91,7 +95,7 @@
<div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
</label>
<label
class=
"input-label"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
</label>
<div
class=
"relative"
>
<div
class=
"relative"
>
<span
class=
"absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
>
$
</span>
<span
class=
"absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
>
{{
order
?.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}
</span>
<input
<input
v-model.number=
"form.amount"
v-model.number=
"form.amount"
type=
"number"
type=
"number"
...
@@ -103,7 +107,7 @@
...
@@ -103,7 +107,7 @@
/>
/>
</div>
</div>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.maxRefundable
'
)
}}
:
$
{{
maxRefundable
.
toFixed
(
2
)
}}
{{
t
(
'
payment.admin.maxRefundable
'
)
}}
:
{{
order
?.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}
{{
maxRefundable
.
toFixed
(
2
)
}}
</p>
</p>
</div>
</div>
...
@@ -200,12 +204,12 @@ const actuallyRefunded = computed(() => {
...
@@ -200,12 +204,12 @@ const actuallyRefunded = computed(() => {
const
maxRefundable
=
computed
(()
=>
{
const
maxRefundable
=
computed
(()
=>
{
if
(
!
props
.
order
)
return
0
if
(
!
props
.
order
)
return
0
return
props
.
order
.
pay_
amount
-
actuallyRefunded
.
value
return
props
.
order
.
amount
-
actuallyRefunded
.
value
})
})
const
balanceInsufficient
=
computed
(()
=>
{
const
balanceInsufficient
=
computed
(()
=>
{
if
(
props
.
userBalance
==
null
||
!
props
.
order
)
return
false
if
(
props
.
userBalance
==
null
||
!
props
.
order
)
return
false
return
props
.
userBalance
<
props
.
order
.
pay_
amount
return
props
.
userBalance
<
props
.
order
.
amount
})
})
watch
(()
=>
props
.
show
,
(
val
)
=>
{
watch
(()
=>
props
.
show
,
(
val
)
=>
{
...
...
frontend/src/components/payment/OrderTable.vue
View file @
9bf079b7
...
@@ -12,10 +12,15 @@
...
@@ -12,10 +12,15 @@
<span
v-if=
"row.user_notes"
class=
"ml-1 text-xs text-gray-400"
>
(
{{
row
.
user_notes
}}
)
</span>
<span
v-if=
"row.user_notes"
class=
"ml-1 text-xs text-gray-400"
>
(
{{
row
.
user_notes
}}
)
</span>
</div>
</div>
</
template
>
</
template
>
<
template
#cell-amount=
"{ value, row }"
>
<
template
#cell-
pay_
amount=
"{ value, row }"
>
<div
class=
"text-sm"
>
<div
class=
"text-sm"
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
value
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
value
.
toFixed
(
2
)
}}
</span>
<span
v-if=
"row.pay_amount !== value"
class=
"ml-1 text-xs text-gray-500"
>
($
{{
row
.
pay_amount
.
toFixed
(
2
)
}}
)
</span>
<span
v-if=
"row.fee_rate > 0"
class=
"ml-1 text-xs text-gray-400"
:title=
"t('payment.orders.fee') + ': ' + row.fee_rate + '%'"
>
(
{{
t
(
'
payment.orders.fee
'
)
}}
{{
row
.
fee_rate
}}
%)
</span>
<div
v-if=
"row.amount !== row.pay_amount"
class=
"text-xs text-gray-500"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
:
{{
row
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
row
.
amount
.
toFixed
(
2
)
}}
</div>
</div>
</div>
</
template
>
</
template
>
<
template
#cell-payment_type=
"{ value }"
>
<
template
#cell-payment_type=
"{ value }"
>
...
@@ -60,7 +65,7 @@ const columns = computed((): Column[] => {
...
@@ -60,7 +65,7 @@ const columns = computed((): Column[] => {
cols
.
push
({
key
:
'
user_email
'
,
label
:
t
(
'
payment.admin.colUser
'
)
})
cols
.
push
({
key
:
'
user_email
'
,
label
:
t
(
'
payment.admin.colUser
'
)
})
}
}
cols
.
push
(
cols
.
push
(
{
key
:
'
amount
'
,
label
:
t
(
'
payment.orders.
a
mount
'
)
},
{
key
:
'
pay_
amount
'
,
label
:
t
(
'
payment.orders.
payA
mount
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
created_at
'
,
label
:
t
(
'
payment.orders.createdAt
'
)
},
{
key
:
'
created_at
'
,
label
:
t
(
'
payment.orders.createdAt
'
)
},
...
...
frontend/src/components/payment/PaymentQRDialog.vue
View file @
9bf079b7
...
@@ -45,7 +45,11 @@
...
@@ -45,7 +45,11 @@
</div>
</div>
<div
class=
"flex justify-between"
>
<div
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{ t('payment.orders.amount') }}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{ t('payment.orders.amount') }}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
${{ paidOrder.pay_amount.toFixed(2) }}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{ paidOrder.order_type === 'balance' ? '$' : '¥' }}{{ paidOrder.amount.toFixed(2) }}
</span>
</div>
<div
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{ t('payment.orders.payAmount') }}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥{{ paidOrder.pay_amount.toFixed(2) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
frontend/src/components/payment/PaymentStatusPanel.vue
View file @
9bf079b7
...
@@ -22,7 +22,11 @@
...
@@ -22,7 +22,11 @@
</div>
</div>
<div
class=
"flex justify-between"
>
<div
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
paidOrder
.
pay_amount
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
paidOrder
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
paidOrder
.
amount
.
toFixed
(
2
)
}}
</span>
</div>
<div
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
paidOrder
.
pay_amount
.
toFixed
(
2
)
}}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
frontend/src/components/payment/StripePaymentInline.vue
View file @
9bf079b7
...
@@ -21,9 +21,13 @@
...
@@ -21,9 +21,13 @@
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.orderId
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.orderId
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
#
{{
orderId
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
#
{{
orderId
}}
</span>
</div>
</div>
<div
class=
"flex justify-between"
>
<div
v-if=
"amount > 0"
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
payAmount
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
orderType
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
amount
.
toFixed
(
2
)
}}
</span>
</div>
<div
class=
"flex justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
payAmount
.
toFixed
(
2
)
}}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -36,7 +40,7 @@
...
@@ -36,7 +40,7 @@
<div
class=
"card overflow-hidden"
>
<div
class=
"card overflow-hidden"
>
<div
class=
"bg-gradient-to-br from-[#635bff] to-[#4f46e5] px-6 py-5 text-center"
>
<div
class=
"bg-gradient-to-br from-[#635bff] to-[#4f46e5] px-6 py-5 text-center"
>
<p
class=
"text-sm font-medium text-indigo-200"
>
{{
t
(
'
payment.actualPay
'
)
}}
</p>
<p
class=
"text-sm font-medium text-indigo-200"
>
{{
t
(
'
payment.actualPay
'
)
}}
</p>
<p
class=
"mt-1 text-3xl font-bold text-white"
>
$
{{
payAmount
.
toFixed
(
2
)
}}
</p>
<p
class=
"mt-1 text-3xl font-bold text-white"
>
¥
{{
payAmount
.
toFixed
(
2
)
}}
</p>
</div>
</div>
</div>
</div>
<!-- Stripe Payment Element -->
<!-- Stripe Payment Element -->
...
@@ -75,7 +79,9 @@ const POPUP_METHODS = new Set(['alipay', 'wechat_pay'])
...
@@ -75,7 +79,9 @@ const POPUP_METHODS = new Set(['alipay', 'wechat_pay'])
const
props
=
defineProps
<
{
const
props
=
defineProps
<
{
orderId
:
number
orderId
:
number
amount
:
number
clientSecret
:
string
clientSecret
:
string
orderType
?:
'
balance
'
|
'
subscription
'
publishableKey
:
string
publishableKey
:
string
payAmount
:
number
payAmount
:
number
}
>
()
}
>
()
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment