Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
c47d9fb5
authored
Sep 12, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(payment): add payment settings configuration and update payment methods handling
parent
c9ffa5a6
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
655 additions
and
275 deletions
+655
-275
controller/channel-billing.go
+2
-2
controller/channel.go
+4
-3
controller/misc.go
+4
-8
controller/topup.go
+55
-8
controller/topup_stripe.go
+10
-1
dto/channel_settings.go
+9
-1
model/channel.go
+2
-1
model/option.go
+12
-12
relay/channel/vertex/adaptor.go
+65
-51
router/api-router.go
+1
-0
service/epay.go
+3
-2
setting/operation_setting/payment_setting.go
+23
-0
setting/operation_setting/payment_setting_old.go
+17
-4
web/src/components/settings/PaymentSetting.jsx
+26
-0
web/src/components/table/channels/modals/EditChannelModal.jsx
+86
-47
web/src/components/topup/RechargeCard.jsx
+108
-74
web/src/components/topup/index.jsx
+116
-57
web/src/components/topup/modals/PaymentConfirmModal.jsx
+36
-3
web/src/helpers/data.js
+0
-1
web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx
+76
-0
No files found.
controller/channel-billing.go
View file @
c47d9fb5
...
...
@@ -10,7 +10,7 @@ import (
"one-api/constant"
"one-api/model"
"one-api/service"
"one-api/setting"
"one-api/setting
/operation_setting
"
"one-api/types"
"strconv"
"time"
...
...
@@ -342,7 +342,7 @@ func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
return
0
,
fmt
.
Errorf
(
"failed to update moonshot balance, status: %v, code: %d, scode: %s"
,
response
.
Status
,
response
.
Code
,
response
.
Scode
)
}
availableBalanceCny
:=
response
.
Data
.
AvailableBalance
availableBalanceUsd
:=
decimal
.
NewFromFloat
(
availableBalanceCny
)
.
Div
(
decimal
.
NewFromFloat
(
setting
.
Price
))
.
InexactFloat64
()
availableBalanceUsd
:=
decimal
.
NewFromFloat
(
availableBalanceCny
)
.
Div
(
decimal
.
NewFromFloat
(
operation_
setting
.
Price
))
.
InexactFloat64
()
channel
.
UpdateBalance
(
availableBalanceUsd
)
return
availableBalanceUsd
,
nil
}
...
...
controller/channel.go
View file @
c47d9fb5
...
...
@@ -6,6 +6,7 @@ import (
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/model"
"strconv"
"strings"
...
...
@@ -560,7 +561,7 @@ func AddChannel(c *gin.Context) {
case
"multi_to_single"
:
addChannelRequest
.
Channel
.
ChannelInfo
.
IsMultiKey
=
true
addChannelRequest
.
Channel
.
ChannelInfo
.
MultiKeyMode
=
addChannelRequest
.
MultiKeyMode
if
addChannelRequest
.
Channel
.
Type
==
constant
.
ChannelTypeVertexAi
{
if
addChannelRequest
.
Channel
.
Type
==
constant
.
ChannelTypeVertexAi
&&
addChannelRequest
.
Channel
.
GetOtherSettings
()
.
VertexKeyType
!=
dto
.
VertexKeyTypeAPIKey
{
array
,
err
:=
getVertexArrayKeys
(
addChannelRequest
.
Channel
.
Key
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
...
...
@@ -585,7 +586,7 @@ func AddChannel(c *gin.Context) {
}
keys
=
[]
string
{
addChannelRequest
.
Channel
.
Key
}
case
"batch"
:
if
addChannelRequest
.
Channel
.
Type
==
constant
.
ChannelTypeVertexAi
{
if
addChannelRequest
.
Channel
.
Type
==
constant
.
ChannelTypeVertexAi
&&
addChannelRequest
.
Channel
.
GetOtherSettings
()
.
VertexKeyType
!=
dto
.
VertexKeyTypeAPIKey
{
// multi json
keys
,
err
=
getVertexArrayKeys
(
addChannelRequest
.
Channel
.
Key
)
if
err
!=
nil
{
...
...
@@ -840,7 +841,7 @@ func UpdateChannel(c *gin.Context) {
}
// 处理 Vertex AI 的特殊情况
if
channel
.
Type
==
constant
.
ChannelTypeVertexAi
{
if
channel
.
Type
==
constant
.
ChannelTypeVertexAi
&&
channel
.
GetOtherSettings
()
.
VertexKeyType
!=
dto
.
VertexKeyTypeAPIKey
{
// 尝试解析新密钥为JSON数组
if
strings
.
HasPrefix
(
strings
.
TrimSpace
(
channel
.
Key
),
"["
)
{
array
,
err
:=
getVertexArrayKeys
(
channel
.
Key
)
...
...
controller/misc.go
View file @
c47d9fb5
...
...
@@ -59,10 +59,6 @@ func GetStatus(c *gin.Context) {
"wechat_qrcode"
:
common
.
WeChatAccountQRCodeImageURL
,
"wechat_login"
:
common
.
WeChatAuthEnabled
,
"server_address"
:
setting
.
ServerAddress
,
"price"
:
setting
.
Price
,
"stripe_unit_price"
:
setting
.
StripeUnitPrice
,
"min_topup"
:
setting
.
MinTopUp
,
"stripe_min_topup"
:
setting
.
StripeMinTopUp
,
"turnstile_check"
:
common
.
TurnstileCheckEnabled
,
"turnstile_site_key"
:
common
.
TurnstileSiteKey
,
"top_up_link"
:
common
.
TopUpLink
,
...
...
@@ -75,15 +71,15 @@ func GetStatus(c *gin.Context) {
"enable_data_export"
:
common
.
DataExportEnabled
,
"data_export_default_time"
:
common
.
DataExportDefaultTime
,
"default_collapse_sidebar"
:
common
.
DefaultCollapseSidebar
,
"enable_online_topup"
:
setting
.
PayAddress
!=
""
&&
setting
.
EpayId
!=
""
&&
setting
.
EpayKey
!=
""
,
"enable_stripe_topup"
:
setting
.
StripeApiSecret
!=
""
&&
setting
.
StripeWebhookSecret
!=
""
&&
setting
.
StripePriceId
!=
""
,
"mj_notify_enabled"
:
setting
.
MjNotifyEnabled
,
"chats"
:
setting
.
Chats
,
"demo_site_enabled"
:
operation_setting
.
DemoSiteEnabled
,
"self_use_mode_enabled"
:
operation_setting
.
SelfUseModeEnabled
,
"default_use_auto_group"
:
setting
.
DefaultUseAutoGroup
,
"pay_methods"
:
setting
.
PayMethods
,
"usd_exchange_rate"
:
setting
.
USDExchangeRate
,
"usd_exchange_rate"
:
operation_setting
.
USDExchangeRate
,
"price"
:
operation_setting
.
Price
,
"stripe_unit_price"
:
setting
.
StripeUnitPrice
,
// 面板启用开关
"api_info_enabled"
:
cs
.
ApiInfoEnabled
,
...
...
controller/topup.go
View file @
c47d9fb5
...
...
@@ -9,6 +9,7 @@ import (
"one-api/model"
"one-api/service"
"one-api/setting"
"one-api/setting/operation_setting"
"strconv"
"sync"
"time"
...
...
@@ -19,6 +20,44 @@ import (
"github.com/shopspring/decimal"
)
func
GetTopUpInfo
(
c
*
gin
.
Context
)
{
// 获取支付方式
payMethods
:=
operation_setting
.
PayMethods
// 如果启用了 Stripe 支付,添加到支付方法列表
if
setting
.
StripeApiSecret
!=
""
&&
setting
.
StripeWebhookSecret
!=
""
&&
setting
.
StripePriceId
!=
""
{
// 检查是否已经包含 Stripe
hasStripe
:=
false
for
_
,
method
:=
range
payMethods
{
if
method
[
"type"
]
==
"stripe"
{
hasStripe
=
true
break
}
}
if
!
hasStripe
{
stripeMethod
:=
map
[
string
]
string
{
"name"
:
"Stripe"
,
"type"
:
"stripe"
,
"color"
:
"rgba(var(--semi-purple-5), 1)"
,
"min_topup"
:
strconv
.
Itoa
(
setting
.
StripeMinTopUp
),
}
payMethods
=
append
(
payMethods
,
stripeMethod
)
}
}
data
:=
gin
.
H
{
"enable_online_topup"
:
operation_setting
.
PayAddress
!=
""
&&
operation_setting
.
EpayId
!=
""
&&
operation_setting
.
EpayKey
!=
""
,
"enable_stripe_topup"
:
setting
.
StripeApiSecret
!=
""
&&
setting
.
StripeWebhookSecret
!=
""
&&
setting
.
StripePriceId
!=
""
,
"pay_methods"
:
payMethods
,
"min_topup"
:
operation_setting
.
MinTopUp
,
"stripe_min_topup"
:
setting
.
StripeMinTopUp
,
"amount_options"
:
operation_setting
.
GetPaymentSetting
()
.
AmountOptions
,
"discount"
:
operation_setting
.
GetPaymentSetting
()
.
AmountDiscount
,
}
common
.
ApiSuccess
(
c
,
data
)
}
type
EpayRequest
struct
{
Amount
int64
`json:"amount"`
PaymentMethod
string
`json:"payment_method"`
...
...
@@ -31,13 +70,13 @@ type AmountRequest struct {
}
func
GetEpayClient
()
*
epay
.
Client
{
if
setting
.
PayAddress
==
""
||
setting
.
EpayId
==
""
||
setting
.
EpayKey
==
""
{
if
operation_setting
.
PayAddress
==
""
||
operation_setting
.
EpayId
==
""
||
operation_
setting
.
EpayKey
==
""
{
return
nil
}
withUrl
,
err
:=
epay
.
NewClient
(
&
epay
.
Config
{
PartnerID
:
setting
.
EpayId
,
Key
:
setting
.
EpayKey
,
},
setting
.
PayAddress
)
PartnerID
:
operation_
setting
.
EpayId
,
Key
:
operation_
setting
.
EpayKey
,
},
operation_
setting
.
PayAddress
)
if
err
!=
nil
{
return
nil
}
...
...
@@ -58,15 +97,23 @@ func getPayMoney(amount int64, group string) float64 {
}
dTopupGroupRatio
:=
decimal
.
NewFromFloat
(
topupGroupRatio
)
dPrice
:=
decimal
.
NewFromFloat
(
setting
.
Price
)
dPrice
:=
decimal
.
NewFromFloat
(
operation_setting
.
Price
)
// apply optional preset discount by the original request amount (if configured), default 1.0
discount
:=
1.0
if
ds
,
ok
:=
operation_setting
.
GetPaymentSetting
()
.
AmountDiscount
[
int
(
amount
)];
ok
{
if
ds
>
0
{
discount
=
ds
}
}
dDiscount
:=
decimal
.
NewFromFloat
(
discount
)
payMoney
:=
dAmount
.
Mul
(
dPrice
)
.
Mul
(
dTopupGroupRatio
)
payMoney
:=
dAmount
.
Mul
(
dPrice
)
.
Mul
(
dTopupGroupRatio
)
.
Mul
(
dDiscount
)
return
payMoney
.
InexactFloat64
()
}
func
getMinTopup
()
int64
{
minTopup
:=
setting
.
MinTopUp
minTopup
:=
operation_
setting
.
MinTopUp
if
!
common
.
DisplayInCurrencyEnabled
{
dMinTopup
:=
decimal
.
NewFromInt
(
int64
(
minTopup
))
dQuotaPerUnit
:=
decimal
.
NewFromFloat
(
common
.
QuotaPerUnit
)
...
...
@@ -99,7 +146,7 @@ func RequestEpay(c *gin.Context) {
return
}
if
!
setting
.
ContainsPayMethod
(
req
.
PaymentMethod
)
{
if
!
operation_
setting
.
ContainsPayMethod
(
req
.
PaymentMethod
)
{
c
.
JSON
(
200
,
gin
.
H
{
"message"
:
"error"
,
"data"
:
"支付方式不存在"
})
return
}
...
...
controller/topup_stripe.go
View file @
c47d9fb5
...
...
@@ -8,6 +8,7 @@ import (
"one-api/common"
"one-api/model"
"one-api/setting"
"one-api/setting/operation_setting"
"strconv"
"strings"
"time"
...
...
@@ -254,6 +255,7 @@ func GetChargedAmount(count float64, user model.User) float64 {
}
func
getStripePayMoney
(
amount
float64
,
group
string
)
float64
{
originalAmount
:=
amount
if
!
common
.
DisplayInCurrencyEnabled
{
amount
=
amount
/
common
.
QuotaPerUnit
}
...
...
@@ -262,7 +264,14 @@ func getStripePayMoney(amount float64, group string) float64 {
if
topupGroupRatio
==
0
{
topupGroupRatio
=
1
}
payMoney
:=
amount
*
setting
.
StripeUnitPrice
*
topupGroupRatio
// apply optional preset discount by the original request amount (if configured), default 1.0
discount
:=
1.0
if
ds
,
ok
:=
operation_setting
.
GetPaymentSetting
()
.
AmountDiscount
[
int
(
originalAmount
)];
ok
{
if
ds
>
0
{
discount
=
ds
}
}
payMoney
:=
amount
*
setting
.
StripeUnitPrice
*
topupGroupRatio
*
discount
return
payMoney
}
...
...
dto/channel_settings.go
View file @
c47d9fb5
...
...
@@ -9,6 +9,14 @@ type ChannelSettings struct {
SystemPromptOverride
bool
`json:"system_prompt_override,omitempty"`
}
type
VertexKeyType
string
const
(
VertexKeyTypeJSON
VertexKeyType
=
"json"
VertexKeyTypeAPIKey
VertexKeyType
=
"api_key"
)
type
ChannelOtherSettings
struct
{
AzureResponsesVersion
string
`json:"azure_responses_version,omitempty"`
AzureResponsesVersion
string
`json:"azure_responses_version,omitempty"`
VertexKeyType
VertexKeyType
`json:"vertex_key_type,omitempty"`
// "json" or "api_key"
}
model/channel.go
View file @
c47d9fb5
...
...
@@ -42,7 +42,6 @@ type Channel struct {
Priority
*
int64
`json:"priority" gorm:"bigint;default:0"`
AutoBan
*
int
`json:"auto_ban" gorm:"default:1"`
OtherInfo
string
`json:"other_info"`
OtherSettings
string
`json:"settings" gorm:"column:settings"`
// 其他设置
Tag
*
string
`json:"tag" gorm:"index"`
Setting
*
string
`json:"setting" gorm:"type:text"`
// 渠道额外设置
ParamOverride
*
string
`json:"param_override" gorm:"type:text"`
...
...
@@ -51,6 +50,8 @@ type Channel struct {
// add after v0.8.5
ChannelInfo
ChannelInfo
`json:"channel_info" gorm:"type:json"`
OtherSettings
string
`json:"settings" gorm:"column:settings"`
// 其他设置,存储azure版本等不需要检索的信息,详见dto.ChannelOtherSettings
// cache info
Keys
[]
string
`json:"-" gorm:"-"`
}
...
...
model/option.go
View file @
c47d9fb5
...
...
@@ -73,9 +73,9 @@ func InitOptionMap() {
common
.
OptionMap
[
"CustomCallbackAddress"
]
=
""
common
.
OptionMap
[
"EpayId"
]
=
""
common
.
OptionMap
[
"EpayKey"
]
=
""
common
.
OptionMap
[
"Price"
]
=
strconv
.
FormatFloat
(
setting
.
Price
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"USDExchangeRate"
]
=
strconv
.
FormatFloat
(
setting
.
USDExchangeRate
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"MinTopUp"
]
=
strconv
.
Itoa
(
setting
.
MinTopUp
)
common
.
OptionMap
[
"Price"
]
=
strconv
.
FormatFloat
(
operation_
setting
.
Price
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"USDExchangeRate"
]
=
strconv
.
FormatFloat
(
operation_
setting
.
USDExchangeRate
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"MinTopUp"
]
=
strconv
.
Itoa
(
operation_
setting
.
MinTopUp
)
common
.
OptionMap
[
"StripeMinTopUp"
]
=
strconv
.
Itoa
(
setting
.
StripeMinTopUp
)
common
.
OptionMap
[
"StripeApiSecret"
]
=
setting
.
StripeApiSecret
common
.
OptionMap
[
"StripeWebhookSecret"
]
=
setting
.
StripeWebhookSecret
...
...
@@ -85,7 +85,7 @@ func InitOptionMap() {
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
common
.
OptionMap
[
"DefaultUseAutoGroup"
]
=
strconv
.
FormatBool
(
setting
.
DefaultUseAutoGroup
)
common
.
OptionMap
[
"PayMethods"
]
=
setting
.
PayMethods2JsonString
()
common
.
OptionMap
[
"PayMethods"
]
=
operation_
setting
.
PayMethods2JsonString
()
common
.
OptionMap
[
"GitHubClientId"
]
=
""
common
.
OptionMap
[
"GitHubClientSecret"
]
=
""
common
.
OptionMap
[
"TelegramBotToken"
]
=
""
...
...
@@ -299,23 +299,23 @@ func updateOptionMap(key string, value string) (err error) {
case
"WorkerValidKey"
:
setting
.
WorkerValidKey
=
value
case
"PayAddress"
:
setting
.
PayAddress
=
value
operation_
setting
.
PayAddress
=
value
case
"Chats"
:
err
=
setting
.
UpdateChatsByJsonString
(
value
)
case
"AutoGroups"
:
err
=
setting
.
UpdateAutoGroupsByJsonString
(
value
)
case
"CustomCallbackAddress"
:
setting
.
CustomCallbackAddress
=
value
operation_
setting
.
CustomCallbackAddress
=
value
case
"EpayId"
:
setting
.
EpayId
=
value
operation_
setting
.
EpayId
=
value
case
"EpayKey"
:
setting
.
EpayKey
=
value
operation_
setting
.
EpayKey
=
value
case
"Price"
:
setting
.
Price
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
operation_
setting
.
Price
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
case
"USDExchangeRate"
:
setting
.
USDExchangeRate
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
operation_
setting
.
USDExchangeRate
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
case
"MinTopUp"
:
setting
.
MinTopUp
,
_
=
strconv
.
Atoi
(
value
)
operation_
setting
.
MinTopUp
,
_
=
strconv
.
Atoi
(
value
)
case
"StripeApiSecret"
:
setting
.
StripeApiSecret
=
value
case
"StripeWebhookSecret"
:
...
...
@@ -413,7 +413,7 @@ func updateOptionMap(key string, value string) (err error) {
case
"StreamCacheQueueLength"
:
setting
.
StreamCacheQueueLength
,
_
=
strconv
.
Atoi
(
value
)
case
"PayMethods"
:
err
=
setting
.
UpdatePayMethodsByJsonString
(
value
)
err
=
operation_
setting
.
UpdatePayMethodsByJsonString
(
value
)
}
return
err
}
...
...
relay/channel/vertex/adaptor.go
View file @
c47d9fb5
...
...
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/claude"
...
...
@@ -80,16 +81,64 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
adc
:=
&
Credentials
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
info
.
ApiKey
),
adc
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to decode credentials file: %w"
,
err
)
}
func
(
a
*
Adaptor
)
getRequestUrl
(
info
*
relaycommon
.
RelayInfo
,
modelName
,
suffix
string
)
(
string
,
error
)
{
region
:=
GetModelRegion
(
info
.
ApiVersion
,
info
.
OriginModelName
)
a
.
AccountCredentials
=
*
adc
if
info
.
ChannelOtherSettings
.
VertexKeyType
!=
dto
.
VertexKeyTypeAPIKey
{
adc
:=
&
Credentials
{}
if
err
:=
common
.
Unmarshal
([]
byte
(
info
.
ApiKey
),
adc
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to decode credentials file: %w"
,
err
)
}
a
.
AccountCredentials
=
*
adc
if
a
.
RequestMode
==
RequestModeLlama
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions"
,
region
,
adc
.
ProjectID
,
region
,
),
nil
}
if
region
==
"global"
{
return
fmt
.
Sprintf
(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s"
,
adc
.
ProjectID
,
modelName
,
suffix
,
),
nil
}
else
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s"
,
region
,
adc
.
ProjectID
,
region
,
modelName
,
suffix
,
),
nil
}
}
else
{
if
region
==
"global"
{
return
fmt
.
Sprintf
(
"https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s"
,
modelName
,
suffix
,
info
.
ApiKey
,
),
nil
}
else
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s"
,
region
,
modelName
,
suffix
,
info
.
ApiKey
,
),
nil
}
}
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
suffix
:=
""
if
a
.
RequestMode
==
RequestModeGemini
{
if
model_setting
.
GetGeminiSettings
()
.
ThinkingAdapterEnabled
{
// 新增逻辑:处理 -thinking-<budget> 格式
if
strings
.
Contains
(
info
.
UpstreamModelName
,
"-thinking-"
)
{
...
...
@@ -112,23 +161,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
suffix
=
"predict"
}
if
region
==
"global"
{
return
fmt
.
Sprintf
(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s"
,
adc
.
ProjectID
,
info
.
UpstreamModelName
,
suffix
,
),
nil
}
else
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s"
,
region
,
adc
.
ProjectID
,
region
,
info
.
UpstreamModelName
,
suffix
,
),
nil
}
return
a
.
getRequestUrl
(
info
,
info
.
UpstreamModelName
,
suffix
)
}
else
if
a
.
RequestMode
==
RequestModeClaude
{
if
info
.
IsStream
{
suffix
=
"streamRawPredict?alt=sse"
...
...
@@ -139,41 +172,22 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if
v
,
ok
:=
claudeModelMap
[
info
.
UpstreamModelName
];
ok
{
model
=
v
}
if
region
==
"global"
{
return
fmt
.
Sprintf
(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/anthropic/models/%s:%s"
,
adc
.
ProjectID
,
model
,
suffix
,
),
nil
}
else
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s"
,
region
,
adc
.
ProjectID
,
region
,
model
,
suffix
,
),
nil
}
return
a
.
getRequestUrl
(
info
,
model
,
suffix
)
}
else
if
a
.
RequestMode
==
RequestModeLlama
{
return
fmt
.
Sprintf
(
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions"
,
region
,
adc
.
ProjectID
,
region
,
),
nil
return
a
.
getRequestUrl
(
info
,
""
,
""
)
}
return
""
,
errors
.
New
(
"unsupported request mode"
)
}
func
(
a
*
Adaptor
)
SetupRequestHeader
(
c
*
gin
.
Context
,
req
*
http
.
Header
,
info
*
relaycommon
.
RelayInfo
)
error
{
channel
.
SetupApiRequestHeader
(
info
,
c
,
req
)
accessToken
,
err
:=
getAccessToken
(
a
,
info
)
if
err
!=
nil
{
return
err
if
info
.
ChannelOtherSettings
.
VertexKeyType
==
"json"
{
accessToken
,
err
:=
getAccessToken
(
a
,
info
)
if
err
!=
nil
{
return
err
}
req
.
Set
(
"Authorization"
,
"Bearer "
+
accessToken
)
}
req
.
Set
(
"Authorization"
,
"Bearer "
+
accessToken
)
return
nil
}
...
...
router/api-router.go
View file @
c47d9fb5
...
...
@@ -60,6 +60,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
selfRoute
.
GET
(
"/aff"
,
controller
.
GetAffCode
)
selfRoute
.
GET
(
"/topup/info"
,
controller
.
GetTopUpInfo
)
selfRoute
.
POST
(
"/topup"
,
middleware
.
CriticalRateLimit
(),
controller
.
TopUp
)
selfRoute
.
POST
(
"/pay"
,
middleware
.
CriticalRateLimit
(),
controller
.
RequestEpay
)
selfRoute
.
POST
(
"/amount"
,
controller
.
RequestAmount
)
...
...
service/epay.go
View file @
c47d9fb5
...
...
@@ -2,11 +2,12 @@ package service
import
(
"one-api/setting"
"one-api/setting/operation_setting"
)
func
GetCallbackAddress
()
string
{
if
setting
.
CustomCallbackAddress
==
""
{
if
operation_
setting
.
CustomCallbackAddress
==
""
{
return
setting
.
ServerAddress
}
return
setting
.
CustomCallbackAddress
return
operation_
setting
.
CustomCallbackAddress
}
setting/operation_setting/payment_setting.go
0 → 100644
View file @
c47d9fb5
package
operation_setting
import
"one-api/setting/config"
type
PaymentSetting
struct
{
AmountOptions
[]
int
`json:"amount_options"`
AmountDiscount
map
[
int
]
float64
`json:"amount_discount"`
// 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠
}
// 默认配置
var
paymentSetting
=
PaymentSetting
{
AmountOptions
:
[]
int
{
10
,
20
,
50
,
100
,
200
,
500
},
AmountDiscount
:
map
[
int
]
float64
{},
}
func
init
()
{
// 注册到全局配置管理器
config
.
GlobalConfig
.
Register
(
"payment_setting"
,
&
paymentSetting
)
}
func
GetPaymentSetting
()
*
PaymentSetting
{
return
&
paymentSetting
}
setting/
payment
.go
→
setting/
operation_setting/payment_setting_old
.go
View file @
c47d9fb5
package
setting
/**
此文件为旧版支付设置文件,如需增加新的参数、变量等,请在 payment_setting.go 中添加
This file is the old version of the payment settings file. If you need to add new parameters, variables, etc., please add them in payment_setting.go
*/
import
"encoding/json"
package
operation_setting
import
(
"one-api/common"
)
var
PayAddress
=
""
var
CustomCallbackAddress
=
""
...
...
@@ -21,15 +28,21 @@ var PayMethods = []map[string]string{
"color"
:
"rgba(var(--semi-green-5), 1)"
,
"type"
:
"wxpay"
,
},
{
"name"
:
"自定义1"
,
"color"
:
"black"
,
"type"
:
"custom1"
,
"min_topup"
:
"50"
,
},
}
func
UpdatePayMethodsByJsonString
(
jsonString
string
)
error
{
PayMethods
=
make
([]
map
[
string
]
string
,
0
)
return
js
on
.
Unmarshal
([]
byte
(
jsonString
),
&
PayMethods
)
return
comm
on
.
Unmarshal
([]
byte
(
jsonString
),
&
PayMethods
)
}
func
PayMethods2JsonString
()
string
{
jsonBytes
,
err
:=
js
on
.
Marshal
(
PayMethods
)
jsonBytes
,
err
:=
comm
on
.
Marshal
(
PayMethods
)
if
err
!=
nil
{
return
"[]"
}
...
...
web/src/components/settings/PaymentSetting.jsx
View file @
c47d9fb5
...
...
@@ -37,6 +37,8 @@ const PaymentSetting = () => {
TopupGroupRatio
:
''
,
CustomCallbackAddress
:
''
,
PayMethods
:
''
,
AmountOptions
:
''
,
AmountDiscount
:
''
,
StripeApiSecret
:
''
,
StripeWebhookSecret
:
''
,
...
...
@@ -66,6 +68,30 @@ const PaymentSetting = () => {
newInputs
[
item
.
key
]
=
item
.
value
;
}
break
;
case
'payment_setting.amount_options'
:
try
{
newInputs
[
'AmountOptions'
]
=
JSON
.
stringify
(
JSON
.
parse
(
item
.
value
),
null
,
2
,
);
}
catch
(
error
)
{
console
.
error
(
'解析AmountOptions出错:'
,
error
);
newInputs
[
'AmountOptions'
]
=
item
.
value
;
}
break
;
case
'payment_setting.amount_discount'
:
try
{
newInputs
[
'AmountDiscount'
]
=
JSON
.
stringify
(
JSON
.
parse
(
item
.
value
),
null
,
2
,
);
}
catch
(
error
)
{
console
.
error
(
'解析AmountDiscount出错:'
,
error
);
newInputs
[
'AmountDiscount'
]
=
item
.
value
;
}
break
;
case
'Price'
:
case
'MinTopUp'
:
case
'StripeUnitPrice'
:
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
c47d9fb5
...
...
@@ -142,6 +142,8 @@ const EditChannelModal = (props) => {
system_prompt
:
''
,
system_prompt_override
:
false
,
settings
:
''
,
// 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
vertex_key_type
:
'json'
,
};
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
multiToSingle
,
setMultiToSingle
]
=
useState
(
false
);
...
...
@@ -409,11 +411,17 @@ const EditChannelModal = (props) => {
const
parsedSettings
=
JSON
.
parse
(
data
.
settings
);
data
.
azure_responses_version
=
parsedSettings
.
azure_responses_version
||
''
;
// 读取 Vertex 密钥格式
data
.
vertex_key_type
=
parsedSettings
.
vertex_key_type
||
'json'
;
}
catch
(
error
)
{
console
.
error
(
'解析其他设置失败:'
,
error
);
data
.
azure_responses_version
=
''
;
data
.
region
=
''
;
data
.
vertex_key_type
=
'json'
;
}
}
else
{
// 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
data
.
vertex_key_type
=
'json'
;
}
setInputs
(
data
);
...
...
@@ -745,59 +753,56 @@ const EditChannelModal = (props) => {
let
localInputs
=
{
...
formValues
};
if
(
localInputs
.
type
===
41
)
{
if
(
useManualInput
)
{
// 手动输入模式
if
(
localInputs
.
key
&&
localInputs
.
key
.
trim
()
!==
''
)
{
try
{
// 验证 JSON 格式
const
parsedKey
=
JSON
.
parse
(
localInputs
.
key
);
// 确保是有效的密钥格式
localInputs
.
key
=
JSON
.
stringify
(
parsedKey
);
}
catch
(
err
)
{
showError
(
t
(
'密钥格式无效,请输入有效的 JSON 格式密钥'
));
return
;
}
}
else
if
(
!
isEdit
)
{
const
keyType
=
localInputs
.
vertex_key_type
||
'json'
;
if
(
keyType
===
'api_key'
)
{
// 直接作为普通字符串密钥处理
if
(
!
isEdit
&&
(
!
localInputs
.
key
||
localInputs
.
key
.
trim
()
===
''
))
{
showInfo
(
t
(
'请输入密钥!'
));
return
;
}
}
else
{
// 文件上传模式
let
keys
=
vertexKeys
;
// 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
if
(
keys
.
length
===
0
&&
vertexFileList
.
length
>
0
)
{
try
{
const
parsed
=
await
Promise
.
all
(
vertexFileList
.
map
(
async
(
item
)
=>
{
const
fileObj
=
item
.
fileInstance
;
if
(
!
fileObj
)
return
null
;
const
txt
=
await
fileObj
.
text
();
return
JSON
.
parse
(
txt
);
}),
);
keys
=
parsed
.
filter
(
Boolean
);
}
catch
(
err
)
{
showError
(
t
(
'解析密钥文件失败: {{msg}}'
,
{
msg
:
err
.
message
}));
return
;
}
}
// 创建模式必须上传密钥;编辑模式可选
if
(
keys
.
length
===
0
)
{
if
(
!
isEdit
)
{
showInfo
(
t
(
'请上传密钥文件!'
));
// JSON 服务账号密钥
if
(
useManualInput
)
{
if
(
localInputs
.
key
&&
localInputs
.
key
.
trim
()
!==
''
)
{
try
{
const
parsedKey
=
JSON
.
parse
(
localInputs
.
key
);
localInputs
.
key
=
JSON
.
stringify
(
parsedKey
);
}
catch
(
err
)
{
showError
(
t
(
'密钥格式无效,请输入有效的 JSON 格式密钥'
));
return
;
}
}
else
if
(
!
isEdit
)
{
showInfo
(
t
(
'请输入密钥!'
));
return
;
}
else
{
// 编辑模式且未上传新密钥,不修改 key
delete
localInputs
.
key
;
}
}
else
{
// 有新密钥,则覆盖
if
(
batch
)
{
localInputs
.
key
=
JSON
.
stringify
(
keys
);
// 文件上传模式
let
keys
=
vertexKeys
;
if
(
keys
.
length
===
0
&&
vertexFileList
.
length
>
0
)
{
try
{
const
parsed
=
await
Promise
.
all
(
vertexFileList
.
map
(
async
(
item
)
=>
{
const
fileObj
=
item
.
fileInstance
;
if
(
!
fileObj
)
return
null
;
const
txt
=
await
fileObj
.
text
();
return
JSON
.
parse
(
txt
);
}),
);
keys
=
parsed
.
filter
(
Boolean
);
}
catch
(
err
)
{
showError
(
t
(
'解析密钥文件失败: {{msg}}'
,
{
msg
:
err
.
message
}));
return
;
}
}
if
(
keys
.
length
===
0
)
{
if
(
!
isEdit
)
{
showInfo
(
t
(
'请上传密钥文件!'
));
return
;
}
else
{
delete
localInputs
.
key
;
}
}
else
{
localInputs
.
key
=
JSON
.
stringify
(
keys
[
0
]);
localInputs
.
key
=
batch
?
JSON
.
stringify
(
keys
)
:
JSON
.
stringify
(
keys
[
0
]);
}
}
}
...
...
@@ -853,6 +858,8 @@ const EditChannelModal = (props) => {
delete
localInputs
.
pass_through_body_enabled
;
delete
localInputs
.
system_prompt
;
delete
localInputs
.
system_prompt_override
;
// 顶层的 vertex_key_type 不应发送给后端
delete
localInputs
.
vertex_key_type
;
let
res
;
localInputs
.
auto_ban
=
localInputs
.
auto_ban
?
1
:
0
;
...
...
@@ -1178,8 +1185,40 @@ const EditChannelModal = (props) => {
autoComplete=
'new-password'
/>
{
inputs
.
type
===
41
&&
(
<
Form
.
Select
field=
'vertex_key_type'
label=
{
t
(
'密钥格式'
)
}
placeholder=
{
t
(
'请选择密钥格式'
)
}
optionList=
{
[
{
label
:
'JSON'
,
value
:
'json'
},
{
label
:
'API Key'
,
value
:
'api_key'
},
]
}
style=
{
{
width
:
'100%'
}
}
value=
{
inputs
.
vertex_key_type
||
'json'
}
onChange=
{
(
value
)
=>
{
// 更新设置中的 vertex_key_type
handleChannelOtherSettingsChange
(
'vertex_key_type'
,
value
);
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
if
(
value
===
'api_key'
)
{
setBatch
(
false
);
setUseManualInput
(
false
);
setVertexKeys
([]);
setVertexFileList
([]);
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'vertex_files'
,
[]);
}
}
}
}
extraText=
{
inputs
.
vertex_key_type
===
'api_key'
?
t
(
'API Key 模式下不支持批量创建'
)
:
t
(
'JSON 模式支持手动输入或上传服务账号 JSON'
)
}
/>
)
}
{
batch
?
(
inputs
.
type
===
41
?
(
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<
Form
.
Upload
field=
'vertex_files'
label=
{
t
(
'密钥文件 (.json)'
)
}
...
...
@@ -1243,7 +1282,7 @@ const EditChannelModal = (props) => {
)
)
:
(
<>
{
inputs
.
type
===
41
?
(
{
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<>
{
!
batch
&&
(
<
div
className=
'flex items-center justify-between mb-3'
>
...
...
web/src/components/topup/RechargeCard.jsx
View file @
c47d9fb5
...
...
@@ -21,6 +21,7 @@ import React, { useRef } from 'react';
import
{
Avatar
,
Typography
,
Tag
,
Card
,
Button
,
Banner
,
...
...
@@ -29,7 +30,7 @@ import {
Space
,
Row
,
Col
,
Spin
,
Spin
,
Tooltip
}
from
'@douyinfe/semi-ui'
;
import
{
SiAlipay
,
SiWechat
,
SiStripe
}
from
'react-icons/si'
;
import
{
CreditCard
,
Coins
,
Wallet
,
BarChart2
,
TrendingUp
}
from
'lucide-react'
;
...
...
@@ -68,6 +69,7 @@ const RechargeCard = ({
userState
,
renderQuota
,
statusLoading
,
topupInfo
,
})
=>
{
const
onlineFormApiRef
=
useRef
(
null
);
const
redeemFormApiRef
=
useRef
(
null
);
...
...
@@ -261,44 +263,58 @@ const RechargeCard = ({
</
Col
>
<
Col
xs=
{
24
}
sm=
{
24
}
md=
{
24
}
lg=
{
14
}
xl=
{
14
}
>
<
Form
.
Slot
label=
{
t
(
'选择支付方式'
)
}
>
<
Space
wrap
>
{
payMethods
.
map
((
payMethod
)
=>
(
<
Button
key=
{
payMethod
.
type
}
theme=
'outline'
type=
'tertiary'
onClick=
{
()
=>
preTopUp
(
payMethod
.
type
)
}
disabled=
{
(
!
enableOnlineTopUp
&&
payMethod
.
type
!==
'stripe'
)
||
(
!
enableStripeTopUp
&&
payMethod
.
type
===
'stripe'
)
}
loading=
{
paymentLoading
&&
payWay
===
payMethod
.
type
}
icon=
{
payMethod
.
type
===
'alipay'
?
(
<
SiAlipay
size=
{
18
}
color=
'#1677FF'
/>
)
:
payMethod
.
type
===
'wxpay'
?
(
<
SiWechat
size=
{
18
}
color=
'#07C160'
/>
)
:
payMethod
.
type
===
'stripe'
?
(
<
SiStripe
size=
{
18
}
color=
'#635BFF'
/>
)
:
(
<
CreditCard
size=
{
18
}
color=
{
payMethod
.
color
||
'var(--semi-color-text-2)'
}
/>
)
}
>
{
payMethod
.
name
}
</
Button
>
))
}
</
Space
>
{
payMethods
&&
payMethods
.
length
>
0
?
(
<
Space
wrap
>
{
payMethods
.
map
((
payMethod
)
=>
{
const
minTopupVal
=
Number
(
payMethod
.
min_topup
)
||
0
;
const
isStripe
=
payMethod
.
type
===
'stripe'
;
const
disabled
=
(
!
enableOnlineTopUp
&&
!
isStripe
)
||
(
!
enableStripeTopUp
&&
isStripe
)
||
minTopupVal
>
Number
(
topUpCount
||
0
);
const
buttonEl
=
(
<
Button
key=
{
payMethod
.
type
}
theme=
'outline'
type=
'tertiary'
onClick=
{
()
=>
preTopUp
(
payMethod
.
type
)
}
disabled=
{
disabled
}
loading=
{
paymentLoading
&&
payWay
===
payMethod
.
type
}
icon=
{
payMethod
.
type
===
'alipay'
?
(
<
SiAlipay
size=
{
18
}
color=
'#1677FF'
/>
)
:
payMethod
.
type
===
'wxpay'
?
(
<
SiWechat
size=
{
18
}
color=
'#07C160'
/>
)
:
payMethod
.
type
===
'stripe'
?
(
<
SiStripe
size=
{
18
}
color=
'#635BFF'
/>
)
:
(
<
CreditCard
size=
{
18
}
color=
{
payMethod
.
color
||
'var(--semi-color-text-2)'
}
/>
)
}
className=
'!rounded-lg !px-4 !py-2'
>
{
payMethod
.
name
}
</
Button
>
);
return
disabled
&&
minTopupVal
>
Number
(
topUpCount
||
0
)
?
(
<
Tooltip
content=
{
t
(
'此支付方式最低充值金额为'
)
+
' '
+
minTopupVal
}
key=
{
payMethod
.
type
}
>
{
buttonEl
}
</
Tooltip
>
)
:
(
<
React
.
Fragment
key=
{
payMethod
.
type
}
>
{
buttonEl
}
</
React
.
Fragment
>
);
})
}
</
Space
>
)
:
(
<
div
className=
'text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'
>
{
t
(
'暂无可用的支付方式,请联系管理员配置'
)
}
</
div
>
)
}
</
Form
.
Slot
>
</
Col
>
</
Row
>
...
...
@@ -306,41 +322,59 @@ const RechargeCard = ({
{
(
enableOnlineTopUp
||
enableStripeTopUp
)
&&
(
<
Form
.
Slot
label=
{
t
(
'选择充值额度'
)
}
>
<
Space
wrap
>
{
presetAmounts
.
map
((
preset
,
index
)
=>
(
<
Button
key=
{
index
}
theme=
{
selectedPreset
===
preset
.
value
?
'solid'
:
'outline'
}
type=
{
selectedPreset
===
preset
.
value
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
selectPresetAmount
(
preset
);
onlineFormApiRef
.
current
?.
setValue
(
'topUpCount'
,
preset
.
value
,
);
}
}
className=
'!rounded-lg !py-2 !px-3'
>
<
div
className=
'flex items-center gap-2'
>
<
Coins
size=
{
14
}
className=
'opacity-80'
/>
<
span
className=
'font-medium'
>
{
formatLargeNumber
(
preset
.
value
)
}
</
span
>
<
span
className=
'text-xs text-gray-500'
>
¥
{
(
preset
.
value
*
priceRatio
).
toFixed
(
2
)
}
</
span
>
</
div
>
</
Button
>
))
}
</
Space
>
<
div
className=
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'
>
{
presetAmounts
.
map
((
preset
,
index
)
=>
{
const
discount
=
preset
.
discount
||
topupInfo
?.
discount
?.[
preset
.
value
]
||
1.0
;
const
originalPrice
=
preset
.
value
*
priceRatio
;
const
discountedPrice
=
originalPrice
*
discount
;
const
hasDiscount
=
discount
<
1.0
;
const
actualPay
=
discountedPrice
;
const
save
=
originalPrice
-
discountedPrice
;
return
(
<
Card
key=
{
index
}
style=
{
{
cursor
:
'pointer'
,
border
:
selectedPreset
===
preset
.
value
?
'2px solid var(--semi-color-primary)'
:
'1px solid var(--semi-color-border)'
,
height
:
'100%'
,
width
:
'100%'
}
}
bodyStyle=
{
{
padding
:
'12px'
}
}
onClick=
{
()
=>
{
selectPresetAmount
(
preset
);
onlineFormApiRef
.
current
?.
setValue
(
'topUpCount'
,
preset
.
value
,
);
}
}
>
<
div
style=
{
{
textAlign
:
'center'
}
}
>
<
Typography
.
Title
heading=
{
6
}
style=
{
{
margin
:
'0 0 8px 0'
}
}
>
{
formatLargeNumber
(
preset
.
value
)
}
{
t
(
'美元额度'
)
}
{
hasDiscount
&&
(
<
Tag
style=
{
{
marginLeft
:
4
}
}
color=
"green"
>
{
t
(
'折'
).
includes
(
'off'
)
?
((
1
-
discount
)
*
100
).
toFixed
(
1
)
:
(
discount
*
10
).
toFixed
(
1
)
}{
t
(
'折'
)
}
</
Tag
>
)
}
</
Typography
.
Title
>
<
div
style=
{
{
color
:
'var(--semi-color-text-2)'
,
fontSize
:
'12px'
,
margin
:
'4px 0'
}
}
>
{
t
(
'实付'
)
}
{
actualPay
.
toFixed
(
2
)
}
,
{
hasDiscount
?
`${t('节省')} ${save.toFixed(2)}`
:
`${t('节省')} 0.00`
}
</
div
>
</
div
>
</
Card
>
);
})
}
</
div
>
</
Form
.
Slot
>
)
}
</
div
>
...
...
web/src/components/topup/index.jsx
View file @
c47d9fb5
...
...
@@ -80,6 +80,12 @@ const TopUp = () => {
// 预设充值额度选项
const
[
presetAmounts
,
setPresetAmounts
]
=
useState
([]);
const
[
selectedPreset
,
setSelectedPreset
]
=
useState
(
null
);
// 充值配置信息
const
[
topupInfo
,
setTopupInfo
]
=
useState
({
amount_options
:
[],
discount
:
{}
});
const
topUp
=
async
()
=>
{
if
(
redemptionCode
===
''
)
{
...
...
@@ -248,6 +254,99 @@ const TopUp = () => {
}
};
// 获取充值配置信息
const
getTopupInfo
=
async
()
=>
{
try
{
const
res
=
await
API
.
get
(
'/api/user/topup/info'
);
const
{
message
,
data
,
success
}
=
res
.
data
;
if
(
success
)
{
setTopupInfo
({
amount_options
:
data
.
amount_options
||
[],
discount
:
data
.
discount
||
{}
});
// 处理支付方式
let
payMethods
=
data
.
pay_methods
||
[];
try
{
if
(
typeof
payMethods
===
'string'
)
{
payMethods
=
JSON
.
parse
(
payMethods
);
}
if
(
payMethods
&&
payMethods
.
length
>
0
)
{
// 检查name和type是否为空
payMethods
=
payMethods
.
filter
((
method
)
=>
{
return
method
.
name
&&
method
.
type
;
});
// 如果没有color,则设置默认颜色
payMethods
=
payMethods
.
map
((
method
)
=>
{
// 规范化最小充值数
const
normalizedMinTopup
=
Number
(
method
.
min_topup
);
method
.
min_topup
=
Number
.
isFinite
(
normalizedMinTopup
)
?
normalizedMinTopup
:
0
;
// Stripe 的最小充值从后端字段回填
if
(
method
.
type
===
'stripe'
&&
(
!
method
.
min_topup
||
method
.
min_topup
<=
0
))
{
const
stripeMin
=
Number
(
data
.
stripe_min_topup
);
if
(
Number
.
isFinite
(
stripeMin
))
{
method
.
min_topup
=
stripeMin
;
}
}
if
(
!
method
.
color
)
{
if
(
method
.
type
===
'alipay'
)
{
method
.
color
=
'rgba(var(--semi-blue-5), 1)'
;
}
else
if
(
method
.
type
===
'wxpay'
)
{
method
.
color
=
'rgba(var(--semi-green-5), 1)'
;
}
else
if
(
method
.
type
===
'stripe'
)
{
method
.
color
=
'rgba(var(--semi-purple-5), 1)'
;
}
else
{
method
.
color
=
'rgba(var(--semi-primary-5), 1)'
;
}
}
return
method
;
});
}
else
{
payMethods
=
[];
}
// 如果启用了 Stripe 支付,添加到支付方法列表
// 这个逻辑现在由后端处理,如果 Stripe 启用,后端会在 pay_methods 中包含它
setPayMethods
(
payMethods
);
const
enableStripeTopUp
=
data
.
enable_stripe_topup
||
false
;
const
enableOnlineTopUp
=
data
.
enable_online_topup
||
false
;
const
minTopUpValue
=
enableOnlineTopUp
?
data
.
min_topup
:
enableStripeTopUp
?
data
.
stripe_min_topup
:
1
;
setEnableOnlineTopUp
(
enableOnlineTopUp
);
setEnableStripeTopUp
(
enableStripeTopUp
);
setMinTopUp
(
minTopUpValue
);
setTopUpCount
(
minTopUpValue
);
// 如果没有自定义充值数量选项,根据最小充值金额生成预设充值额度选项
if
(
topupInfo
.
amount_options
.
length
===
0
)
{
setPresetAmounts
(
generatePresetAmounts
(
minTopUpValue
));
}
// 初始化显示实付金额
getAmount
(
minTopUpValue
);
}
catch
(
e
)
{
console
.
log
(
'解析支付方式失败:'
,
e
);
setPayMethods
([]);
}
// 如果有自定义充值数量选项,使用它们替换默认的预设选项
if
(
data
.
amount_options
&&
data
.
amount_options
.
length
>
0
)
{
const
customPresets
=
data
.
amount_options
.
map
(
amount
=>
({
value
:
amount
,
discount
:
data
.
discount
[
amount
]
||
1.0
}));
setPresetAmounts
(
customPresets
);
}
}
else
{
console
.
error
(
'获取充值配置失败:'
,
data
);
}
}
catch
(
error
)
{
console
.
error
(
'获取充值配置异常:'
,
error
);
}
};
// 获取邀请链接
const
getAffLink
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/user/aff'
);
...
...
@@ -290,52 +389,7 @@ const TopUp = () => {
getUserQuota
().
then
();
}
setTransferAmount
(
getQuotaPerUnit
());
let
payMethods
=
localStorage
.
getItem
(
'pay_methods'
);
try
{
payMethods
=
JSON
.
parse
(
payMethods
);
if
(
payMethods
&&
payMethods
.
length
>
0
)
{
// 检查name和type是否为空
payMethods
=
payMethods
.
filter
((
method
)
=>
{
return
method
.
name
&&
method
.
type
;
});
// 如果没有color,则设置默认颜色
payMethods
=
payMethods
.
map
((
method
)
=>
{
if
(
!
method
.
color
)
{
if
(
method
.
type
===
'alipay'
)
{
method
.
color
=
'rgba(var(--semi-blue-5), 1)'
;
}
else
if
(
method
.
type
===
'wxpay'
)
{
method
.
color
=
'rgba(var(--semi-green-5), 1)'
;
}
else
if
(
method
.
type
===
'stripe'
)
{
method
.
color
=
'rgba(var(--semi-purple-5), 1)'
;
}
else
{
method
.
color
=
'rgba(var(--semi-primary-5), 1)'
;
}
}
return
method
;
});
}
else
{
payMethods
=
[];
}
// 如果启用了 Stripe 支付,添加到支付方法列表
if
(
statusState
?.
status
?.
enable_stripe_topup
)
{
const
hasStripe
=
payMethods
.
some
((
method
)
=>
method
.
type
===
'stripe'
);
if
(
!
hasStripe
)
{
payMethods
.
push
({
name
:
'Stripe'
,
type
:
'stripe'
,
color
:
'rgba(var(--semi-purple-5), 1)'
,
});
}
}
setPayMethods
(
payMethods
);
}
catch
(
e
)
{
console
.
log
(
e
);
showError
(
t
(
'支付方式配置错误, 请联系管理员'
));
}
},
[
statusState
?.
status
?.
enable_stripe_topup
]);
},
[]);
useEffect
(()
=>
{
if
(
affFetchedRef
.
current
)
return
;
...
...
@@ -343,20 +397,18 @@ const TopUp = () => {
getAffLink
().
then
();
},
[]);
// 在 statusState 可用时获取充值信息
useEffect
(()
=>
{
getTopupInfo
().
then
();
},
[]);
useEffect
(()
=>
{
if
(
statusState
?.
status
)
{
const
minTopUpValue
=
statusState
.
status
.
min_topup
||
1
;
setMinTopUp
(
minTopUpValue
);
setTopUpCount
(
minTopUpValue
);
//
const minTopUpValue = statusState.status.min_topup || 1;
//
setMinTopUp(minTopUpValue);
//
setTopUpCount(minTopUpValue);
setTopUpLink
(
statusState
.
status
.
top_up_link
||
''
);
setEnableOnlineTopUp
(
statusState
.
status
.
enable_online_topup
||
false
);
setPriceRatio
(
statusState
.
status
.
price
||
1
);
setEnableStripeTopUp
(
statusState
.
status
.
enable_stripe_topup
||
false
);
// 根据最小充值金额生成预设充值额度选项
setPresetAmounts
(
generatePresetAmounts
(
minTopUpValue
));
// 初始化显示实付金额
getAmount
(
minTopUpValue
);
setStatusLoading
(
false
);
}
...
...
@@ -431,7 +483,11 @@ const TopUp = () => {
const
selectPresetAmount
=
(
preset
)
=>
{
setTopUpCount
(
preset
.
value
);
setSelectedPreset
(
preset
.
value
);
setAmount
(
preset
.
value
*
priceRatio
);
// 计算实际支付金额,考虑折扣
const
discount
=
preset
.
discount
||
topupInfo
.
discount
[
preset
.
value
]
||
1.0
;
const
discountedAmount
=
preset
.
value
*
priceRatio
*
discount
;
setAmount
(
discountedAmount
);
};
// 格式化大数字显示
...
...
@@ -475,6 +531,8 @@ const TopUp = () => {
renderAmount=
{
renderAmount
}
payWay=
{
payWay
}
payMethods=
{
payMethods
}
amountNumber=
{
amount
}
discountRate=
{
topupInfo
?.
discount
?.[
topUpCount
]
||
1.0
}
/>
{
/* 用户信息头部 */
}
...
...
@@ -512,6 +570,7 @@ const TopUp = () => {
userState=
{
userState
}
renderQuota=
{
renderQuota
}
statusLoading=
{
statusLoading
}
topupInfo=
{
topupInfo
}
/>
</
div
>
...
...
web/src/components/topup/modals/PaymentConfirmModal.jsx
View file @
c47d9fb5
...
...
@@ -36,7 +36,13 @@ const PaymentConfirmModal = ({
renderAmount
,
payWay
,
payMethods
,
// 新增:用于显示折扣明细
amountNumber
,
discountRate
,
})
=>
{
const
hasDiscount
=
discountRate
&&
discountRate
>
0
&&
discountRate
<
1
&&
amountNumber
>
0
;
const
originalAmount
=
hasDiscount
?
(
amountNumber
/
discountRate
)
:
0
;
const
discountAmount
=
hasDiscount
?
(
originalAmount
-
amountNumber
)
:
0
;
return
(
<
Modal
title=
{
...
...
@@ -71,11 +77,38 @@ const PaymentConfirmModal = ({
{
amountLoading
?
(
<
Skeleton
.
Title
style=
{
{
width
:
'60px'
,
height
:
'16px'
}
}
/>
)
:
(
<
Text
strong
className=
'font-bold'
style=
{
{
color
:
'red'
}
}
>
{
renderAmount
()
}
</
Text
>
<
div
className=
'flex items-baseline space-x-2'
>
<
Text
strong
className=
'font-bold'
style=
{
{
color
:
'red'
}
}
>
{
renderAmount
()
}
</
Text
>
{
hasDiscount
&&
(
<
Text
size=
'small'
className=
'text-rose-500'
>
{
Math
.
round
(
discountRate
*
100
)
}
%
</
Text
>
)
}
</
div
>
)
}
</
div
>
{
hasDiscount
&&
!
amountLoading
&&
(
<>
<
div
className=
'flex justify-between items-center'
>
<
Text
className=
'text-slate-500 dark:text-slate-400'
>
{
t
(
'原价'
)
}
:
</
Text
>
<
Text
delete
className=
'text-slate-500 dark:text-slate-400'
>
{
`${originalAmount.toFixed(2)} ${t('元')}`
}
</
Text
>
</
div
>
<
div
className=
'flex justify-between items-center'
>
<
Text
className=
'text-slate-500 dark:text-slate-400'
>
{
t
(
'优惠'
)
}
:
</
Text
>
<
Text
className=
'text-emerald-600 dark:text-emerald-400'
>
{
`- ${discountAmount.toFixed(2)} ${t('元')}`
}
</
Text
>
</
div
>
</>
)
}
<
div
className=
'flex justify-between items-center'
>
<
Text
strong
className=
'text-slate-700 dark:text-slate-200'
>
{
t
(
'支付方式'
)
}
:
...
...
web/src/helpers/data.js
View file @
c47d9fb5
...
...
@@ -28,7 +28,6 @@ export function setStatusData(data) {
localStorage
.
setItem
(
'enable_task'
,
data
.
enable_task
);
localStorage
.
setItem
(
'enable_data_export'
,
data
.
enable_data_export
);
localStorage
.
setItem
(
'chats'
,
JSON
.
stringify
(
data
.
chats
));
localStorage
.
setItem
(
'pay_methods'
,
JSON
.
stringify
(
data
.
pay_methods
));
localStorage
.
setItem
(
'data_export_default_time'
,
data
.
data_export_default_time
,
...
...
web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx
View file @
c47d9fb5
...
...
@@ -41,6 +41,8 @@ export default function SettingsPaymentGateway(props) {
TopupGroupRatio
:
''
,
CustomCallbackAddress
:
''
,
PayMethods
:
''
,
AmountOptions
:
''
,
AmountDiscount
:
''
,
});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
const
formApiRef
=
useRef
(
null
);
...
...
@@ -62,7 +64,30 @@ export default function SettingsPaymentGateway(props) {
TopupGroupRatio
:
props
.
options
.
TopupGroupRatio
||
''
,
CustomCallbackAddress
:
props
.
options
.
CustomCallbackAddress
||
''
,
PayMethods
:
props
.
options
.
PayMethods
||
''
,
AmountOptions
:
props
.
options
.
AmountOptions
||
''
,
AmountDiscount
:
props
.
options
.
AmountDiscount
||
''
,
};
// 美化 JSON 展示
try
{
if
(
currentInputs
.
AmountOptions
)
{
currentInputs
.
AmountOptions
=
JSON
.
stringify
(
JSON
.
parse
(
currentInputs
.
AmountOptions
),
null
,
2
,
);
}
}
catch
{}
try
{
if
(
currentInputs
.
AmountDiscount
)
{
currentInputs
.
AmountDiscount
=
JSON
.
stringify
(
JSON
.
parse
(
currentInputs
.
AmountDiscount
),
null
,
2
,
);
}
}
catch
{}
setInputs
(
currentInputs
);
setOriginInputs
({
...
currentInputs
});
formApiRef
.
current
.
setValues
(
currentInputs
);
...
...
@@ -93,6 +118,20 @@ export default function SettingsPaymentGateway(props) {
}
}
if
(
originInputs
[
'AmountOptions'
]
!==
inputs
.
AmountOptions
&&
inputs
.
AmountOptions
.
trim
()
!==
''
)
{
if
(
!
verifyJSON
(
inputs
.
AmountOptions
))
{
showError
(
t
(
'自定义充值数量选项不是合法的 JSON 数组'
));
return
;
}
}
if
(
originInputs
[
'AmountDiscount'
]
!==
inputs
.
AmountDiscount
&&
inputs
.
AmountDiscount
.
trim
()
!==
''
)
{
if
(
!
verifyJSON
(
inputs
.
AmountDiscount
))
{
showError
(
t
(
'充值金额折扣配置不是合法的 JSON 对象'
));
return
;
}
}
setLoading
(
true
);
try
{
const
options
=
[
...
...
@@ -123,6 +162,12 @@ export default function SettingsPaymentGateway(props) {
if
(
originInputs
[
'PayMethods'
]
!==
inputs
.
PayMethods
)
{
options
.
push
({
key
:
'PayMethods'
,
value
:
inputs
.
PayMethods
});
}
if
(
originInputs
[
'AmountOptions'
]
!==
inputs
.
AmountOptions
)
{
options
.
push
({
key
:
'payment_setting.amount_options'
,
value
:
inputs
.
AmountOptions
});
}
if
(
originInputs
[
'AmountDiscount'
]
!==
inputs
.
AmountDiscount
)
{
options
.
push
({
key
:
'payment_setting.amount_discount'
,
value
:
inputs
.
AmountDiscount
});
}
// 发送请求
const
requestQueue
=
options
.
map
((
opt
)
=>
...
...
@@ -228,6 +273,37 @@ export default function SettingsPaymentGateway(props) {
placeholder=
{
t
(
'为一个 JSON 文本'
)
}
autosize
/>
<
Row
gutter=
{
{
xs
:
8
,
sm
:
16
,
md
:
24
,
lg
:
24
,
xl
:
24
,
xxl
:
24
}
}
style=
{
{
marginTop
:
16
}
}
>
<
Col
span=
{
24
}
>
<
Form
.
TextArea
field=
'AmountOptions'
label=
{
t
(
'自定义充值数量选项'
)
}
placeholder=
{
t
(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]'
)
}
autosize
extraText=
{
t
(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]'
)
}
/>
</
Col
>
</
Row
>
<
Row
gutter=
{
{
xs
:
8
,
sm
:
16
,
md
:
24
,
lg
:
24
,
xl
:
24
,
xxl
:
24
}
}
style=
{
{
marginTop
:
16
}
}
>
<
Col
span=
{
24
}
>
<
Form
.
TextArea
field=
'AmountDiscount'
label=
{
t
(
'充值金额折扣配置'
)
}
placeholder=
{
t
(
'为一个
JSON
对象,例如:
{
"100"
:
0.95
,
"200"
:
0.9
,
"500"
:
0.85
}
'
)
}
autosize
extraText=
{
t
(
'设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:
{
"100"
:
0.95
,
"200"
:
0.9
,
"500"
:
0.85
}
'
)
}
/>
</
Col
>
</
Row
>
<
Button
onClick=
{
submitPayAddress
}
>
{
t
(
'更新支付设置'
)
}
</
Button
>
</
Form
.
Section
>
</
Form
>
...
...
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