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
0b746501
Commit
0b746501
authored
Apr 16, 2026
by
陈曦
Browse files
1. merge upstream v0.1.113 2.提交migration相关文件
parents
45061102
be7551b9
Changes
225
Show whitespace changes
Inline
Side-by-side
backend/go.sum
View file @
0b746501
...
...
@@ -183,6 +183,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
...
...
@@ -218,6 +220,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
...
...
@@ -251,6 +255,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
...
...
@@ -280,6 +286,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
...
...
@@ -312,6 +320,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
...
...
backend/internal/config/config.go
View file @
0b746501
...
...
@@ -28,7 +28,7 @@ const (
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
const
DefaultCSPPolicy
=
"default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
const
DefaultCSPPolicy
=
"default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com
https://*.stripe.com
; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com
https://*.stripe.com
; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
// UMQ(用户消息队列)模式常量
const
(
...
...
backend/internal/config/config_test.go
View file @
0b746501
...
...
@@ -233,12 +233,13 @@ func TestLoadForcedCodexInstructionsTemplate(t *testing.T) {
configPath
:=
filepath
.
Join
(
tempDir
,
"config.yaml"
)
require
.
NoError
(
t
,
os
.
WriteFile
(
templatePath
,
[]
byte
(
"server-prefix
\n\n
{{ .ExistingInstructions }}"
),
0
o644
))
require
.
NoError
(
t
,
os
.
WriteFile
(
configPath
,
[]
byte
(
"gateway:
\n
forced_codex_instructions_template_file:
\"
"
+
templatePath
+
"
\"\n
"
),
0
o644
))
yamlSafePath
:=
filepath
.
ToSlash
(
templatePath
)
require
.
NoError
(
t
,
os
.
WriteFile
(
configPath
,
[]
byte
(
"gateway:
\n
forced_codex_instructions_template_file:
\"
"
+
yamlSafePath
+
"
\"\n
"
),
0
o644
))
t
.
Setenv
(
"DATA_DIR"
,
tempDir
)
cfg
,
err
:=
Load
()
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
templat
ePath
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
)
require
.
Equal
(
t
,
yamlSaf
ePath
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
)
require
.
Equal
(
t
,
"server-prefix
\n\n
{{ .ExistingInstructions }}"
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplate
)
}
...
...
backend/internal/handler/admin/account_handler.go
View file @
0b746501
...
...
@@ -1412,6 +1412,12 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
c
.
JSON
(
409
,
gin
.
H
{
"error"
:
"mixed_channel_warning"
,
"message"
:
mixedErr
.
Error
(),
"details"
:
gin
.
H
{
"group_id"
:
mixedErr
.
GroupID
,
"group_name"
:
mixedErr
.
GroupName
,
"current_platform"
:
mixedErr
.
CurrentPlatform
,
"other_platform"
:
mixedErr
.
OtherPlatform
,
},
})
return
}
...
...
backend/internal/handler/admin/channel_handler.go
View file @
0b746501
package
admin
import
(
"fmt"
"strconv"
"strings"
...
...
@@ -33,6 +34,10 @@ type createChannelRequest struct {
ModelMapping
map
[
string
]
map
[
string
]
string
`json:"model_mapping"`
BillingModelSource
string
`json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
RestrictModels
bool
`json:"restrict_models"`
Features
string
`json:"features"`
FeaturesConfig
map
[
string
]
any
`json:"features_config"`
ApplyPricingToAccountStats
bool
`json:"apply_pricing_to_account_stats"`
AccountStatsPricingRules
[]
accountStatsPricingRuleRequest
`json:"account_stats_pricing_rules"`
}
type
updateChannelRequest
struct
{
...
...
@@ -44,6 +49,10 @@ type updateChannelRequest struct {
ModelMapping
map
[
string
]
map
[
string
]
string
`json:"model_mapping"`
BillingModelSource
string
`json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
RestrictModels
*
bool
`json:"restrict_models"`
Features
*
string
`json:"features"`
FeaturesConfig
map
[
string
]
any
`json:"features_config"`
ApplyPricingToAccountStats
*
bool
`json:"apply_pricing_to_account_stats"`
AccountStatsPricingRules
*
[]
accountStatsPricingRuleRequest
`json:"account_stats_pricing_rules"`
}
type
channelModelPricingRequest
struct
{
...
...
@@ -71,6 +80,13 @@ type pricingIntervalRequest struct {
SortOrder
int
`json:"sort_order"`
}
type
accountStatsPricingRuleRequest
struct
{
Name
string
`json:"name"`
GroupIDs
[]
int64
`json:"group_ids"`
AccountIDs
[]
int64
`json:"account_ids"`
Pricing
[]
channelModelPricingRequest
`json:"pricing"`
}
type
channelResponse
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
...
...
@@ -78,9 +94,13 @@ type channelResponse struct {
Status
string
`json:"status"`
BillingModelSource
string
`json:"billing_model_source"`
RestrictModels
bool
`json:"restrict_models"`
Features
string
`json:"features"`
FeaturesConfig
map
[
string
]
any
`json:"features_config"`
GroupIDs
[]
int64
`json:"group_ids"`
ModelPricing
[]
channelModelPricingResponse
`json:"model_pricing"`
ModelMapping
map
[
string
]
map
[
string
]
string
`json:"model_mapping"`
ApplyPricingToAccountStats
bool
`json:"apply_pricing_to_account_stats"`
AccountStatsPricingRules
[]
accountStatsPricingRuleResponse
`json:"account_stats_pricing_rules"`
CreatedAt
string
`json:"created_at"`
UpdatedAt
string
`json:"updated_at"`
}
...
...
@@ -112,6 +132,14 @@ type pricingIntervalResponse struct {
SortOrder
int
`json:"sort_order"`
}
type
accountStatsPricingRuleResponse
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
GroupIDs
[]
int64
`json:"group_ids"`
AccountIDs
[]
int64
`json:"account_ids"`
Pricing
[]
channelModelPricingResponse
`json:"pricing"`
}
func
channelToResponse
(
ch
*
service
.
Channel
)
*
channelResponse
{
if
ch
==
nil
{
return
nil
...
...
@@ -122,6 +150,8 @@ func channelToResponse(ch *service.Channel) *channelResponse {
Description
:
ch
.
Description
,
Status
:
ch
.
Status
,
RestrictModels
:
ch
.
RestrictModels
,
Features
:
ch
.
Features
,
FeaturesConfig
:
ch
.
FeaturesConfig
,
GroupIDs
:
ch
.
GroupIDs
,
ModelMapping
:
ch
.
ModelMapping
,
CreatedAt
:
ch
.
CreatedAt
.
Format
(
"2006-01-02T15:04:05Z"
),
...
...
@@ -142,6 +172,29 @@ func channelToResponse(ch *service.Channel) *channelResponse {
for
_
,
p
:=
range
ch
.
ModelPricing
{
resp
.
ModelPricing
=
append
(
resp
.
ModelPricing
,
pricingToResponse
(
&
p
))
}
resp
.
ApplyPricingToAccountStats
=
ch
.
ApplyPricingToAccountStats
resp
.
AccountStatsPricingRules
=
make
([]
accountStatsPricingRuleResponse
,
0
,
len
(
ch
.
AccountStatsPricingRules
))
for
_
,
rule
:=
range
ch
.
AccountStatsPricingRules
{
ruleResp
:=
accountStatsPricingRuleResponse
{
ID
:
rule
.
ID
,
Name
:
rule
.
Name
,
GroupIDs
:
rule
.
GroupIDs
,
AccountIDs
:
rule
.
AccountIDs
,
Pricing
:
make
([]
channelModelPricingResponse
,
0
,
len
(
rule
.
Pricing
)),
}
if
ruleResp
.
GroupIDs
==
nil
{
ruleResp
.
GroupIDs
=
[]
int64
{}
}
if
ruleResp
.
AccountIDs
==
nil
{
ruleResp
.
AccountIDs
=
[]
int64
{}
}
for
i
:=
range
rule
.
Pricing
{
ruleResp
.
Pricing
=
append
(
ruleResp
.
Pricing
,
pricingToResponse
(
&
rule
.
Pricing
[
i
]))
}
resp
.
AccountStatsPricingRules
=
append
(
resp
.
AccountStatsPricingRules
,
ruleResp
)
}
return
resp
}
...
...
@@ -200,9 +253,6 @@ func pricingRequestToService(reqs []channelModelPricingRequest) []service.Channe
billingMode
=
service
.
BillingModeToken
}
platform
:=
r
.
Platform
if
platform
==
""
{
platform
=
service
.
PlatformAnthropic
}
intervals
:=
make
([]
service
.
PricingInterval
,
0
,
len
(
r
.
Intervals
))
for
_
,
iv
:=
range
r
.
Intervals
{
intervals
=
append
(
intervals
,
service
.
PricingInterval
{
...
...
@@ -233,6 +283,15 @@ func pricingRequestToService(reqs []channelModelPricingRequest) []service.Channe
return
result
}
func
accountStatsPricingRuleRequestToService
(
r
accountStatsPricingRuleRequest
)
service
.
AccountStatsPricingRule
{
return
service
.
AccountStatsPricingRule
{
Name
:
r
.
Name
,
GroupIDs
:
r
.
GroupIDs
,
AccountIDs
:
r
.
AccountIDs
,
Pricing
:
pricingRequestToService
(
r
.
Pricing
),
}
}
// --- Handlers ---
// List handles listing channels with pagination
...
...
@@ -291,6 +350,29 @@ func (h *ChannelHandler) Create(c *gin.Context) {
}
pricing
:=
pricingRequestToService
(
req
.
ModelPricing
)
// Main model_pricing requires a platform; default to anthropic for backward compatibility.
for
i
:=
range
pricing
{
if
pricing
[
i
]
.
Platform
==
""
{
pricing
[
i
]
.
Platform
=
service
.
PlatformAnthropic
}
}
var
statsRules
[]
service
.
AccountStatsPricingRule
for
i
,
r
:=
range
req
.
AccountStatsPricingRules
{
if
len
(
r
.
GroupIDs
)
==
0
&&
len
(
r
.
AccountIDs
)
==
0
{
response
.
ErrorFrom
(
c
,
infraerrors
.
BadRequest
(
"PRICING_RULE_EMPTY_SCOPE"
,
fmt
.
Sprintf
(
"pricing rule #%d must have at least one group or account"
,
i
+
1
)))
return
}
if
len
(
r
.
Pricing
)
==
0
{
response
.
ErrorFrom
(
c
,
infraerrors
.
BadRequest
(
"PRICING_RULE_EMPTY_PRICING"
,
fmt
.
Sprintf
(
"pricing rule #%d must have at least one pricing entry"
,
i
+
1
)))
return
}
rule
:=
accountStatsPricingRuleRequestToService
(
r
)
rule
.
SortOrder
=
i
statsRules
=
append
(
statsRules
,
rule
)
}
channel
,
err
:=
h
.
channelService
.
Create
(
c
.
Request
.
Context
(),
&
service
.
CreateChannelInput
{
Name
:
req
.
Name
,
...
...
@@ -300,6 +382,10 @@ func (h *ChannelHandler) Create(c *gin.Context) {
ModelMapping
:
req
.
ModelMapping
,
BillingModelSource
:
req
.
BillingModelSource
,
RestrictModels
:
req
.
RestrictModels
,
Features
:
req
.
Features
,
FeaturesConfig
:
req
.
FeaturesConfig
,
ApplyPricingToAccountStats
:
req
.
ApplyPricingToAccountStats
,
AccountStatsPricingRules
:
statsRules
,
})
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
...
...
@@ -332,11 +418,38 @@ func (h *ChannelHandler) Update(c *gin.Context) {
ModelMapping
:
req
.
ModelMapping
,
BillingModelSource
:
req
.
BillingModelSource
,
RestrictModels
:
req
.
RestrictModels
,
Features
:
req
.
Features
,
FeaturesConfig
:
req
.
FeaturesConfig
,
ApplyPricingToAccountStats
:
req
.
ApplyPricingToAccountStats
,
}
if
req
.
ModelPricing
!=
nil
{
pricing
:=
pricingRequestToService
(
*
req
.
ModelPricing
)
for
i
:=
range
pricing
{
if
pricing
[
i
]
.
Platform
==
""
{
pricing
[
i
]
.
Platform
=
service
.
PlatformAnthropic
}
}
input
.
ModelPricing
=
&
pricing
}
if
req
.
AccountStatsPricingRules
!=
nil
{
statsRules
:=
make
([]
service
.
AccountStatsPricingRule
,
0
,
len
(
*
req
.
AccountStatsPricingRules
))
for
i
,
r
:=
range
*
req
.
AccountStatsPricingRules
{
if
len
(
r
.
GroupIDs
)
==
0
&&
len
(
r
.
AccountIDs
)
==
0
{
response
.
ErrorFrom
(
c
,
infraerrors
.
BadRequest
(
"PRICING_RULE_EMPTY_SCOPE"
,
fmt
.
Sprintf
(
"pricing rule #%d must have at least one group or account"
,
i
+
1
)))
return
}
if
len
(
r
.
Pricing
)
==
0
{
response
.
ErrorFrom
(
c
,
infraerrors
.
BadRequest
(
"PRICING_RULE_EMPTY_PRICING"
,
fmt
.
Sprintf
(
"pricing rule #%d must have at least one pricing entry"
,
i
+
1
)))
return
}
rule
:=
accountStatsPricingRuleRequestToService
(
r
)
rule
.
SortOrder
=
i
statsRules
=
append
(
statsRules
,
rule
)
}
input
.
AccountStatsPricingRules
=
&
statsRules
}
channel
,
err
:=
h
.
channelService
.
Update
(
c
.
Request
.
Context
(),
id
,
input
)
if
err
!=
nil
{
...
...
backend/internal/handler/admin/channel_handler_test.go
View file @
0b746501
...
...
@@ -273,13 +273,13 @@ func TestPricingRequestToService_Defaults(t *testing.T) {
wantValue
:
string
(
service
.
BillingModeToken
),
},
{
name
:
"empty platform
defaults to anthropic
"
,
name
:
"empty platform
stays empty
"
,
req
:
channelModelPricingRequest
{
Models
:
[]
string
{
"m1"
},
Platform
:
""
,
},
wantField
:
"Platform"
,
wantValue
:
"
anthropic
"
,
wantValue
:
""
,
},
}
...
...
backend/internal/handler/admin/setting_handler.go
View file @
0b746501
...
...
@@ -5,11 +5,10 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"log
/slog
"
"net/http"
"regexp"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
...
...
@@ -175,6 +174,12 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableFingerprintUnification
:
settings
.
EnableFingerprintUnification
,
EnableMetadataPassthrough
:
settings
.
EnableMetadataPassthrough
,
EnableCCHSigning
:
settings
.
EnableCCHSigning
,
WebSearchEmulationEnabled
:
settings
.
WebSearchEmulationEnabled
,
BalanceLowNotifyEnabled
:
settings
.
BalanceLowNotifyEnabled
,
BalanceLowNotifyThreshold
:
settings
.
BalanceLowNotifyThreshold
,
BalanceLowNotifyRechargeURL
:
settings
.
BalanceLowNotifyRechargeURL
,
AccountQuotaNotifyEnabled
:
settings
.
AccountQuotaNotifyEnabled
,
AccountQuotaNotifyEmails
:
dto
.
NotifyEmailEntriesFromService
(
settings
.
AccountQuotaNotifyEmails
),
PaymentEnabled
:
paymentCfg
.
Enabled
,
PaymentMinAmount
:
paymentCfg
.
MinAmount
,
PaymentMaxAmount
:
paymentCfg
.
MaxAmount
,
...
...
@@ -183,6 +188,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
PaymentMaxPendingOrders
:
paymentCfg
.
MaxPendingOrders
,
PaymentEnabledTypes
:
paymentCfg
.
EnabledTypes
,
PaymentBalanceDisabled
:
paymentCfg
.
BalanceDisabled
,
PaymentBalanceRechargeMultiplier
:
paymentCfg
.
BalanceRechargeMultiplier
,
PaymentRechargeFeeRate
:
paymentCfg
.
RechargeFeeRate
,
PaymentLoadBalanceStrat
:
paymentCfg
.
LoadBalanceStrategy
,
PaymentProductNamePrefix
:
paymentCfg
.
ProductNamePrefix
,
PaymentProductNameSuffix
:
paymentCfg
.
ProductNameSuffix
,
...
...
@@ -304,6 +311,13 @@ type UpdateSettingsRequest struct {
EnableMetadataPassthrough
*
bool
`json:"enable_metadata_passthrough"`
EnableCCHSigning
*
bool
`json:"enable_cch_signing"`
// Balance low notification
BalanceLowNotifyEnabled
*
bool
`json:"balance_low_notify_enabled"`
BalanceLowNotifyThreshold
*
float64
`json:"balance_low_notify_threshold"`
BalanceLowNotifyRechargeURL
*
string
`json:"balance_low_notify_recharge_url"`
AccountQuotaNotifyEnabled
*
bool
`json:"account_quota_notify_enabled"`
AccountQuotaNotifyEmails
*
[]
dto
.
NotifyEmailEntry
`json:"account_quota_notify_emails"`
// Payment configuration (integrated into settings, full replace)
PaymentEnabled
*
bool
`json:"payment_enabled"`
PaymentMinAmount
*
float64
`json:"payment_min_amount"`
...
...
@@ -313,6 +327,8 @@ type UpdateSettingsRequest struct {
PaymentMaxPendingOrders
*
int
`json:"payment_max_pending_orders"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
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"`
PaymentProductNamePrefix
*
string
`json:"payment_product_name_prefix"`
PaymentProductNameSuffix
*
string
`json:"payment_product_name_suffix"`
...
...
@@ -881,6 +897,36 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return
previousSettings
.
EnableCCHSigning
}(),
BalanceLowNotifyEnabled
:
func
()
bool
{
if
req
.
BalanceLowNotifyEnabled
!=
nil
{
return
*
req
.
BalanceLowNotifyEnabled
}
return
previousSettings
.
BalanceLowNotifyEnabled
}(),
BalanceLowNotifyThreshold
:
func
()
float64
{
if
req
.
BalanceLowNotifyThreshold
!=
nil
{
return
*
req
.
BalanceLowNotifyThreshold
}
return
previousSettings
.
BalanceLowNotifyThreshold
}(),
BalanceLowNotifyRechargeURL
:
func
()
string
{
if
req
.
BalanceLowNotifyRechargeURL
!=
nil
{
return
*
req
.
BalanceLowNotifyRechargeURL
}
return
previousSettings
.
BalanceLowNotifyRechargeURL
}(),
AccountQuotaNotifyEnabled
:
func
()
bool
{
if
req
.
AccountQuotaNotifyEnabled
!=
nil
{
return
*
req
.
AccountQuotaNotifyEnabled
}
return
previousSettings
.
AccountQuotaNotifyEnabled
}(),
AccountQuotaNotifyEmails
:
func
()
[]
service
.
NotifyEmailEntry
{
if
req
.
AccountQuotaNotifyEmails
!=
nil
{
return
dto
.
NotifyEmailEntriesToService
(
*
req
.
AccountQuotaNotifyEmails
)
}
return
previousSettings
.
AccountQuotaNotifyEmails
}(),
}
if
err
:=
h
.
settingService
.
UpdateSettings
(
c
.
Request
.
Context
(),
settings
);
err
!=
nil
{
...
...
@@ -900,6 +946,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
MaxPendingOrders
:
req
.
PaymentMaxPendingOrders
,
EnabledTypes
:
req
.
PaymentEnabledTypes
,
BalanceDisabled
:
req
.
PaymentBalanceDisabled
,
BalanceRechargeMultiplier
:
req
.
PaymentBalanceRechargeMultiplier
,
RechargeFeeRate
:
req
.
PaymentRechargeFeeRate
,
LoadBalanceStrategy
:
req
.
PaymentLoadBalanceStrat
,
ProductNamePrefix
:
req
.
PaymentProductNamePrefix
,
ProductNameSuffix
:
req
.
PaymentProductNameSuffix
,
...
...
@@ -1027,6 +1075,11 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableFingerprintUnification
:
updatedSettings
.
EnableFingerprintUnification
,
EnableMetadataPassthrough
:
updatedSettings
.
EnableMetadataPassthrough
,
EnableCCHSigning
:
updatedSettings
.
EnableCCHSigning
,
BalanceLowNotifyEnabled
:
updatedSettings
.
BalanceLowNotifyEnabled
,
BalanceLowNotifyThreshold
:
updatedSettings
.
BalanceLowNotifyThreshold
,
BalanceLowNotifyRechargeURL
:
updatedSettings
.
BalanceLowNotifyRechargeURL
,
AccountQuotaNotifyEnabled
:
updatedSettings
.
AccountQuotaNotifyEnabled
,
AccountQuotaNotifyEmails
:
dto
.
NotifyEmailEntriesFromService
(
updatedSettings
.
AccountQuotaNotifyEmails
),
PaymentEnabled
:
updatedPaymentCfg
.
Enabled
,
PaymentMinAmount
:
updatedPaymentCfg
.
MinAmount
,
PaymentMaxAmount
:
updatedPaymentCfg
.
MaxAmount
,
...
...
@@ -1035,6 +1088,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PaymentMaxPendingOrders
:
updatedPaymentCfg
.
MaxPendingOrders
,
PaymentEnabledTypes
:
updatedPaymentCfg
.
EnabledTypes
,
PaymentBalanceDisabled
:
updatedPaymentCfg
.
BalanceDisabled
,
PaymentBalanceRechargeMultiplier
:
updatedPaymentCfg
.
BalanceRechargeMultiplier
,
PaymentRechargeFeeRate
:
updatedPaymentCfg
.
RechargeFeeRate
,
PaymentLoadBalanceStrat
:
updatedPaymentCfg
.
LoadBalanceStrategy
,
PaymentProductNamePrefix
:
updatedPaymentCfg
.
ProductNamePrefix
,
PaymentProductNameSuffix
:
updatedPaymentCfg
.
ProductNameSuffix
,
...
...
@@ -1054,6 +1109,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
req
.
PaymentMaxAmount
!=
nil
||
req
.
PaymentDailyLimit
!=
nil
||
req
.
PaymentOrderTimeoutMin
!=
nil
||
req
.
PaymentMaxPendingOrders
!=
nil
||
req
.
PaymentEnabledTypes
!=
nil
||
req
.
PaymentBalanceDisabled
!=
nil
||
req
.
PaymentBalanceRechargeMultiplier
!=
nil
||
req
.
PaymentRechargeFeeRate
!=
nil
||
req
.
PaymentLoadBalanceStrat
!=
nil
||
req
.
PaymentProductNamePrefix
!=
nil
||
req
.
PaymentProductNameSuffix
!=
nil
||
req
.
PaymentHelpImageURL
!=
nil
||
req
.
PaymentHelpText
!=
nil
||
req
.
PaymentCancelRateLimitEnabled
!=
nil
||
...
...
@@ -1073,11 +1129,11 @@ func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.Sys
subject
,
_
:=
middleware
.
GetAuthSubjectFromContext
(
c
)
role
,
_
:=
middleware
.
GetUserRoleFromContext
(
c
)
log
.
Printf
(
"AUDIT:
settings updated
at=%s user_id=%d role=%s changed=%v
"
,
time
.
Now
()
.
UTC
()
.
Format
(
time
.
RFC3339
)
,
subject
.
UserID
,
role
,
changed
,
s
log
.
Info
(
"
settings updated"
,
"audit"
,
true
,
"user_id"
,
subject
.
UserID
,
"role"
,
role
,
"changed"
,
changed
,
)
}
...
...
@@ -1092,6 +1148,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
!
equalStringSlice
(
before
.
RegistrationEmailSuffixWhitelist
,
after
.
RegistrationEmailSuffixWhitelist
)
{
changed
=
append
(
changed
,
"registration_email_suffix_whitelist"
)
}
if
before
.
PromoCodeEnabled
!=
after
.
PromoCodeEnabled
{
changed
=
append
(
changed
,
"promo_code_enabled"
)
}
if
before
.
InvitationCodeEnabled
!=
after
.
InvitationCodeEnabled
{
changed
=
append
(
changed
,
"invitation_code_enabled"
)
}
if
before
.
PasswordResetEnabled
!=
after
.
PasswordResetEnabled
{
changed
=
append
(
changed
,
"password_reset_enabled"
)
}
...
...
@@ -1302,6 +1364,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
CustomMenuItems
!=
after
.
CustomMenuItems
{
changed
=
append
(
changed
,
"custom_menu_items"
)
}
if
before
.
CustomEndpoints
!=
after
.
CustomEndpoints
{
changed
=
append
(
changed
,
"custom_endpoints"
)
}
if
before
.
EnableFingerprintUnification
!=
after
.
EnableFingerprintUnification
{
changed
=
append
(
changed
,
"enable_fingerprint_unification"
)
}
...
...
@@ -1311,6 +1376,22 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
EnableCCHSigning
!=
after
.
EnableCCHSigning
{
changed
=
append
(
changed
,
"enable_cch_signing"
)
}
// Balance & quota notification
if
before
.
BalanceLowNotifyEnabled
!=
after
.
BalanceLowNotifyEnabled
{
changed
=
append
(
changed
,
"balance_low_notify_enabled"
)
}
if
before
.
BalanceLowNotifyThreshold
!=
after
.
BalanceLowNotifyThreshold
{
changed
=
append
(
changed
,
"balance_low_notify_threshold"
)
}
if
before
.
BalanceLowNotifyRechargeURL
!=
after
.
BalanceLowNotifyRechargeURL
{
changed
=
append
(
changed
,
"balance_low_notify_recharge_url"
)
}
if
before
.
AccountQuotaNotifyEnabled
!=
after
.
AccountQuotaNotifyEnabled
{
changed
=
append
(
changed
,
"account_quota_notify_enabled"
)
}
if
!
equalNotifyEmailEntries
(
before
.
AccountQuotaNotifyEmails
,
after
.
AccountQuotaNotifyEmails
)
{
changed
=
append
(
changed
,
"account_quota_notify_emails"
)
}
return
changed
}
...
...
@@ -1367,6 +1448,18 @@ func equalIntSlice(a, b []int) bool {
return
true
}
func
equalNotifyEmailEntries
(
a
,
b
[]
service
.
NotifyEmailEntry
)
bool
{
if
len
(
a
)
!=
len
(
b
)
{
return
false
}
for
i
:=
range
a
{
if
a
[
i
]
.
Email
!=
b
[
i
]
.
Email
||
a
[
i
]
.
Verified
!=
b
[
i
]
.
Verified
||
a
[
i
]
.
Disabled
!=
b
[
i
]
.
Disabled
{
return
false
}
}
return
true
}
// TestSMTPRequest 测试SMTP连接请求
type
TestSMTPRequest
struct
{
SMTPHost
string
`json:"smtp_host"`
...
...
@@ -1847,3 +1940,80 @@ func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
ThresholdWindowMinutes
:
updatedSettings
.
ThresholdWindowMinutes
,
})
}
// GetWebSearchEmulationConfig 获取 Web Search 模拟配置
// GET /api/v1/admin/settings/web-search-emulation
func
(
h
*
SettingHandler
)
GetWebSearchEmulationConfig
(
c
*
gin
.
Context
)
{
cfg
,
err
:=
h
.
settingService
.
GetWebSearchEmulationConfig
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
service
.
PopulateWebSearchUsage
(
c
.
Request
.
Context
(),
cfg
))
}
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置
// PUT /api/v1/admin/settings/web-search-emulation
func
(
h
*
SettingHandler
)
UpdateWebSearchEmulationConfig
(
c
*
gin
.
Context
)
{
var
cfg
service
.
WebSearchEmulationConfig
if
err
:=
c
.
ShouldBindJSON
(
&
cfg
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
err
:=
h
.
settingService
.
SaveWebSearchEmulationConfig
(
c
.
Request
.
Context
(),
&
cfg
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
// Re-read (with sanitized api keys) to return current state
updated
,
err
:=
h
.
settingService
.
GetWebSearchEmulationConfig
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
service
.
PopulateWebSearchUsage
(
c
.
Request
.
Context
(),
updated
))
}
// ResetWebSearchUsage 重置指定 provider 的配额用量
// POST /api/v1/admin/settings/web-search-emulation/reset-usage
func
(
h
*
SettingHandler
)
ResetWebSearchUsage
(
c
*
gin
.
Context
)
{
var
req
struct
{
ProviderType
string
`json:"provider_type"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
req
.
ProviderType
==
""
{
response
.
BadRequest
(
c
,
"provider_type is required"
)
return
}
if
err
:=
service
.
ResetWebSearchUsage
(
c
.
Request
.
Context
(),
req
.
ProviderType
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
nil
)
}
// TestWebSearchEmulation 测试 Web Search 搜索
// POST /api/v1/admin/settings/web-search-emulation/test
func
(
h
*
SettingHandler
)
TestWebSearchEmulation
(
c
*
gin
.
Context
)
{
var
req
struct
{
Query
string
`json:"query"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
strings
.
TrimSpace
(
req
.
Query
)
==
""
{
req
.
Query
=
"搜索今年世界大事件"
}
result
,
err
:=
service
.
TestWebSearch
(
c
.
Request
.
Context
(),
req
.
Query
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
result
)
}
backend/internal/handler/dto/mappers.go
View file @
0b746501
...
...
@@ -23,6 +23,11 @@ func UserFromServiceShallow(u *service.User) *User {
AllowedGroups
:
u
.
AllowedGroups
,
CreatedAt
:
u
.
CreatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
BalanceNotifyEnabled
:
u
.
BalanceNotifyEnabled
,
BalanceNotifyThresholdType
:
u
.
BalanceNotifyThresholdType
,
BalanceNotifyThreshold
:
u
.
BalanceNotifyThreshold
,
BalanceNotifyExtraEmails
:
NotifyEmailEntriesFromService
(
u
.
BalanceNotifyExtraEmails
),
TotalRecharged
:
u
.
TotalRecharged
,
}
}
...
...
@@ -322,6 +327,26 @@ func AccountFromServiceShallow(a *service.Account) *Account {
out
.
QuotaWeeklyResetAt
=
&
v
}
}
// 配额通知配置
if
enabled
:=
a
.
GetQuotaNotifyDailyEnabled
();
enabled
{
out
.
QuotaNotifyDailyEnabled
=
&
enabled
}
if
threshold
:=
a
.
GetQuotaNotifyDailyThreshold
();
threshold
>
0
{
out
.
QuotaNotifyDailyThreshold
=
&
threshold
}
if
enabled
:=
a
.
GetQuotaNotifyWeeklyEnabled
();
enabled
{
out
.
QuotaNotifyWeeklyEnabled
=
&
enabled
}
if
threshold
:=
a
.
GetQuotaNotifyWeeklyThreshold
();
threshold
>
0
{
out
.
QuotaNotifyWeeklyThreshold
=
&
threshold
}
if
enabled
:=
a
.
GetQuotaNotifyTotalEnabled
();
enabled
{
out
.
QuotaNotifyTotalEnabled
=
&
enabled
}
if
threshold
:=
a
.
GetQuotaNotifyTotalThreshold
();
threshold
>
0
{
out
.
QuotaNotifyTotalThreshold
=
&
threshold
}
}
return
out
...
...
@@ -603,6 +628,7 @@ func UsageLogFromServiceAdmin(l *service.UsageLog) *AdminUsageLog {
ModelMappingChain
:
l
.
ModelMappingChain
,
BillingTier
:
l
.
BillingTier
,
AccountRateMultiplier
:
l
.
AccountRateMultiplier
,
AccountStatsCost
:
l
.
AccountStatsCost
,
IPAddress
:
l
.
IPAddress
,
Account
:
AccountSummaryFromService
(
l
.
Account
),
}
...
...
backend/internal/handler/dto/notify_email_entry.go
0 → 100644
View file @
0b746501
package
dto
import
"github.com/Wei-Shaw/sub2api/internal/service"
// NotifyEmailEntry represents a notification email with enable/disable and verification state.
// All emails are user-managed; maximum 3 entries per user.
type
NotifyEmailEntry
struct
{
Email
string
`json:"email"`
Disabled
bool
`json:"disabled"`
Verified
bool
`json:"verified"`
}
// NotifyEmailEntriesFromService converts service entries to DTO entries.
func
NotifyEmailEntriesFromService
(
entries
[]
service
.
NotifyEmailEntry
)
[]
NotifyEmailEntry
{
if
entries
==
nil
{
return
nil
}
result
:=
make
([]
NotifyEmailEntry
,
len
(
entries
))
for
i
,
e
:=
range
entries
{
result
[
i
]
=
NotifyEmailEntry
{
Email
:
e
.
Email
,
Disabled
:
e
.
Disabled
,
Verified
:
e
.
Verified
,
}
}
return
result
}
// NotifyEmailEntriesToService converts DTO entries to service entries.
func
NotifyEmailEntriesToService
(
entries
[]
NotifyEmailEntry
)
[]
service
.
NotifyEmailEntry
{
if
entries
==
nil
{
return
nil
}
result
:=
make
([]
service
.
NotifyEmailEntry
,
len
(
entries
))
for
i
,
e
:=
range
entries
{
result
[
i
]
=
service
.
NotifyEmailEntry
{
Email
:
e
.
Email
,
Disabled
:
e
.
Disabled
,
Verified
:
e
.
Verified
,
}
}
return
result
}
backend/internal/handler/dto/settings.go
View file @
0b746501
...
...
@@ -124,6 +124,9 @@ type SystemSettings struct {
EnableMetadataPassthrough
bool
`json:"enable_metadata_passthrough"`
EnableCCHSigning
bool
`json:"enable_cch_signing"`
// Web Search Emulation
WebSearchEmulationEnabled
bool
`json:"web_search_emulation_enabled"`
// Payment configuration
PaymentEnabled
bool
`json:"payment_enabled"`
PaymentMinAmount
float64
`json:"payment_min_amount"`
...
...
@@ -133,6 +136,8 @@ type SystemSettings struct {
PaymentMaxPendingOrders
int
`json:"payment_max_pending_orders"`
PaymentEnabledTypes
[]
string
`json:"payment_enabled_types"`
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"`
PaymentProductNamePrefix
string
`json:"payment_product_name_prefix"`
PaymentProductNameSuffix
string
`json:"payment_product_name_suffix"`
...
...
@@ -145,6 +150,13 @@ type SystemSettings struct {
PaymentCancelRateLimitWindow
int
`json:"payment_cancel_rate_limit_window"`
PaymentCancelRateLimitUnit
string
`json:"payment_cancel_rate_limit_unit"`
PaymentCancelRateLimitMode
string
`json:"payment_cancel_rate_limit_window_mode"`
// Balance low notification
BalanceLowNotifyEnabled
bool
`json:"balance_low_notify_enabled"`
BalanceLowNotifyThreshold
float64
`json:"balance_low_notify_threshold"`
BalanceLowNotifyRechargeURL
string
`json:"balance_low_notify_recharge_url"`
AccountQuotaNotifyEnabled
bool
`json:"account_quota_notify_enabled"`
AccountQuotaNotifyEmails
[]
NotifyEmailEntry
`json:"account_quota_notify_emails"`
}
type
DefaultSubscriptionSetting
struct
{
...
...
@@ -183,6 +195,10 @@ type PublicSettings struct {
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
PaymentEnabled
bool
`json:"payment_enabled"`
Version
string
`json:"version"`
BalanceLowNotifyEnabled
bool
`json:"balance_low_notify_enabled"`
AccountQuotaNotifyEnabled
bool
`json:"account_quota_notify_enabled"`
BalanceLowNotifyThreshold
float64
`json:"balance_low_notify_threshold"`
BalanceLowNotifyRechargeURL
string
`json:"balance_low_notify_recharge_url"`
}
// OverloadCooldownSettings 529过载冷却配置 DTO
...
...
backend/internal/handler/dto/types.go
View file @
0b746501
...
...
@@ -18,6 +18,13 @@ type User struct {
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
// 余额不足通知
BalanceNotifyEnabled
bool
`json:"balance_notify_enabled"`
BalanceNotifyThresholdType
string
`json:"balance_notify_threshold_type"`
BalanceNotifyThreshold
*
float64
`json:"balance_notify_threshold"`
BalanceNotifyExtraEmails
[]
NotifyEmailEntry
`json:"balance_notify_extra_emails"`
TotalRecharged
float64
`json:"total_recharged"`
APIKeys
[]
APIKey
`json:"api_keys,omitempty"`
Subscriptions
[]
UserSubscription
`json:"subscriptions,omitempty"`
}
...
...
@@ -218,6 +225,14 @@ type Account struct {
QuotaDailyResetAt
*
string
`json:"quota_daily_reset_at,omitempty"`
QuotaWeeklyResetAt
*
string
`json:"quota_weekly_reset_at,omitempty"`
// 配额通知配置
QuotaNotifyDailyEnabled
*
bool
`json:"quota_notify_daily_enabled,omitempty"`
QuotaNotifyDailyThreshold
*
float64
`json:"quota_notify_daily_threshold,omitempty"`
QuotaNotifyWeeklyEnabled
*
bool
`json:"quota_notify_weekly_enabled,omitempty"`
QuotaNotifyWeeklyThreshold
*
float64
`json:"quota_notify_weekly_threshold,omitempty"`
QuotaNotifyTotalEnabled
*
bool
`json:"quota_notify_total_enabled,omitempty"`
QuotaNotifyTotalThreshold
*
float64
`json:"quota_notify_total_threshold,omitempty"`
Proxy
*
Proxy
`json:"proxy,omitempty"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
...
...
@@ -412,6 +427,8 @@ type AdminUsageLog struct {
// AccountRateMultiplier 账号计费倍率快照(nil 表示按 1.0 处理)
AccountRateMultiplier
*
float64
`json:"account_rate_multiplier"`
// AccountStatsCost 自定义定价规则计算的账号统计费用(nil 表示使用默认公式)
AccountStatsCost
*
float64
`json:"account_stats_cost,omitempty"`
// IPAddress 用户请求 IP(仅管理员可见)
IPAddress
*
string
`json:"ip_address,omitempty"`
...
...
backend/internal/handler/gateway_handler.go
View file @
0b746501
...
...
@@ -248,6 +248,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
return
}
// 设置请求所属分组 ID(用于渠道级功能判断,如 WebSearch 模拟)
parsedReq
.
GroupID
=
apiKey
.
GroupID
// 计算粘性会话hash
parsedReq
.
SessionContext
=
&
service
.
SessionContext
{
ClientIP
:
ip
.
GetClientIP
(
c
),
...
...
@@ -470,6 +473,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
ParsedRequest
:
parsedReq
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
Account
:
account
,
...
...
@@ -518,7 +522,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
for
{
// 选择支持该模型的账号
selection
,
err
:=
h
.
gatewayService
.
SelectAccountWithLoadAwareness
(
c
.
Request
.
Context
(),
currentAPIKey
.
GroupID
,
sessionKey
,
reqModel
,
fs
.
FailedAccountIDs
,
parsedReq
.
MetadataUserID
,
int64
(
0
)
)
selection
,
err
:=
h
.
gatewayService
.
SelectAccountWithLoadAwareness
(
c
.
Request
.
Context
(),
currentAPIKey
.
GroupID
,
sessionKey
,
reqModel
,
fs
.
FailedAccountIDs
,
parsedReq
.
MetadataUserID
,
subject
.
UserID
)
if
err
!=
nil
{
if
len
(
fs
.
FailedAccountIDs
)
==
0
{
h
.
handleStreamingAwareError
(
c
,
http
.
StatusServiceUnavailable
,
"api_error"
,
"No available accounts: "
+
err
.
Error
(),
streamStarted
)
...
...
@@ -672,6 +676,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
}
// 转发请求 - 根据账号平台分流
c
.
Set
(
"parsed_request"
,
parsedReq
)
var
result
*
service
.
ForwardResult
requestCtx
:=
c
.
Request
.
Context
()
if
fs
.
SwitchCount
>
0
{
...
...
@@ -810,6 +815,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
ParsedRequest
:
parsedReq
,
APIKey
:
currentAPIKey
,
User
:
currentAPIKey
.
User
,
Account
:
account
,
...
...
backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go
View file @
0b746501
...
...
@@ -168,6 +168,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi
nil
,
// tlsFPProfileService
nil
,
// channelService
nil
,
// resolver
nil
,
// balanceNotifyService
)
// RunModeSimple:跳过计费检查,避免引入 repo/cache 依赖。
...
...
backend/internal/handler/payment_handler.go
View file @
0b746501
...
...
@@ -131,6 +131,8 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
GlobalMax
:
limitsResp
.
GlobalMax
,
Plans
:
planList
,
BalanceDisabled
:
cfg
.
BalanceDisabled
,
BalanceRechargeMultiplier
:
cfg
.
BalanceRechargeMultiplier
,
RechargeFeeRate
:
cfg
.
RechargeFeeRate
,
HelpText
:
cfg
.
HelpText
,
HelpImageURL
:
cfg
.
HelpImageURL
,
StripePublishableKey
:
cfg
.
StripePublishableKey
,
...
...
@@ -143,6 +145,8 @@ type checkoutInfoResponse struct {
GlobalMax
float64
`json:"global_max"`
Plans
[]
checkoutPlan
`json:"plans"`
BalanceDisabled
bool
`json:"balance_disabled"`
BalanceRechargeMultiplier
float64
`json:"balance_recharge_multiplier"`
RechargeFeeRate
float64
`json:"recharge_fee_rate"`
HelpText
string
`json:"help_text"`
HelpImageURL
string
`json:"help_image_url"`
StripePublishableKey
string
`json:"stripe_publishable_key"`
...
...
@@ -335,6 +339,16 @@ func (h *PaymentHandler) RequestRefund(c *gin.Context) {
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"refund requested"
})
}
// GetRefundEligibleProviders returns provider instance IDs that allow user refund.
func
(
h
*
PaymentHandler
)
GetRefundEligibleProviders
(
c
*
gin
.
Context
)
{
ids
,
err
:=
h
.
configService
.
GetUserRefundEligibleInstanceIDs
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"provider_instance_ids"
:
ids
})
}
// VerifyOrderRequest is the request body for verifying a payment order.
type
VerifyOrderRequest
struct
{
OutTradeNo
string
`json:"out_trade_no" binding:"required"`
...
...
@@ -371,6 +385,7 @@ type PublicOrderResult struct {
Amount
float64
`json:"amount"`
PayAmount
float64
`json:"pay_amount"`
PaymentType
string
`json:"payment_type"`
OrderType
string
`json:"order_type"`
Status
string
`json:"status"`
}
...
...
@@ -394,6 +409,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
Amount
:
order
.
Amount
,
PayAmount
:
order
.
PayAmount
,
PaymentType
:
order
.
PaymentType
,
OrderType
:
order
.
OrderType
,
Status
:
order
.
Status
,
})
}
...
...
backend/internal/handler/setting_handler.go
View file @
0b746501
...
...
@@ -61,5 +61,9 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
PaymentEnabled
:
settings
.
PaymentEnabled
,
Version
:
h
.
version
,
BalanceLowNotifyEnabled
:
settings
.
BalanceLowNotifyEnabled
,
AccountQuotaNotifyEnabled
:
settings
.
AccountQuotaNotifyEnabled
,
BalanceLowNotifyThreshold
:
settings
.
BalanceLowNotifyThreshold
,
BalanceLowNotifyRechargeURL
:
settings
.
BalanceLowNotifyRechargeURL
,
})
}
backend/internal/handler/user_handler.go
View file @
0b746501
...
...
@@ -12,12 +12,16 @@ import (
// UserHandler handles user-related requests
type
UserHandler
struct
{
userService
*
service
.
UserService
emailService
*
service
.
EmailService
emailCache
service
.
EmailCache
}
// NewUserHandler creates a new UserHandler
func
NewUserHandler
(
userService
*
service
.
UserService
)
*
UserHandler
{
func
NewUserHandler
(
userService
*
service
.
UserService
,
emailService
*
service
.
EmailService
,
emailCache
service
.
EmailCache
)
*
UserHandler
{
return
&
UserHandler
{
userService
:
userService
,
emailService
:
emailService
,
emailCache
:
emailCache
,
}
}
...
...
@@ -30,6 +34,8 @@ type ChangePasswordRequest struct {
// UpdateProfileRequest represents the update profile request payload
type
UpdateProfileRequest
struct
{
Username
*
string
`json:"username"`
BalanceNotifyEnabled
*
bool
`json:"balance_notify_enabled"`
BalanceNotifyThreshold
*
float64
`json:"balance_notify_threshold"`
}
// GetProfile handles getting user profile
...
...
@@ -95,6 +101,8 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
svcReq
:=
service
.
UpdateProfileRequest
{
Username
:
req
.
Username
,
BalanceNotifyEnabled
:
req
.
BalanceNotifyEnabled
,
BalanceNotifyThreshold
:
req
.
BalanceNotifyThreshold
,
}
updatedUser
,
err
:=
h
.
userService
.
UpdateProfile
(
c
.
Request
.
Context
(),
subject
.
UserID
,
svcReq
)
if
err
!=
nil
{
...
...
@@ -104,3 +112,141 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
response
.
Success
(
c
,
dto
.
UserFromService
(
updatedUser
))
}
// SendNotifyEmailCodeRequest represents the request to send notify email verification code
type
SendNotifyEmailCodeRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
}
// SendNotifyEmailCode sends verification code to extra notification email
// POST /api/v1/user/notify-email/send-code
func
(
h
*
UserHandler
)
SendNotifyEmailCode
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
var
req
SendNotifyEmailCodeRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
err
:=
h
.
userService
.
SendNotifyEmailCode
(
c
.
Request
.
Context
(),
subject
.
UserID
,
req
.
Email
,
h
.
emailService
,
h
.
emailCache
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Verification code sent successfully"
})
}
// VerifyNotifyEmailRequest represents the request to verify and add notify email
type
VerifyNotifyEmailRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
Code
string
`json:"code" binding:"required,len=6"`
}
// VerifyNotifyEmail verifies code and adds email to notification list
// POST /api/v1/user/notify-email/verify
func
(
h
*
UserHandler
)
VerifyNotifyEmail
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
var
req
VerifyNotifyEmailRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
err
:=
h
.
userService
.
VerifyAndAddNotifyEmail
(
c
.
Request
.
Context
(),
subject
.
UserID
,
req
.
Email
,
req
.
Code
,
h
.
emailCache
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
// Return updated user
updatedUser
,
err
:=
h
.
userService
.
GetByID
(
c
.
Request
.
Context
(),
subject
.
UserID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
updatedUser
))
}
// RemoveNotifyEmailRequest represents the request to remove a notify email
type
RemoveNotifyEmailRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
}
// RemoveNotifyEmail removes email from notification list
// DELETE /api/v1/user/notify-email
func
(
h
*
UserHandler
)
RemoveNotifyEmail
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
var
req
RemoveNotifyEmailRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
err
:=
h
.
userService
.
RemoveNotifyEmail
(
c
.
Request
.
Context
(),
subject
.
UserID
,
req
.
Email
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
// Return updated user
updatedUser
,
err
:=
h
.
userService
.
GetByID
(
c
.
Request
.
Context
(),
subject
.
UserID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
updatedUser
))
}
// ToggleNotifyEmailRequest represents the request to toggle a notify email's disabled state
type
ToggleNotifyEmailRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
Disabled
bool
`json:"disabled"`
}
// ToggleNotifyEmail toggles the disabled state of a notification email
// PUT /api/v1/user/notify-email/toggle
func
(
h
*
UserHandler
)
ToggleNotifyEmail
(
c
*
gin
.
Context
)
{
subject
,
ok
:=
middleware2
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
{
response
.
Unauthorized
(
c
,
"User not authenticated"
)
return
}
var
req
ToggleNotifyEmailRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
err
:=
h
.
userService
.
ToggleNotifyEmail
(
c
.
Request
.
Context
(),
subject
.
UserID
,
req
.
Email
,
req
.
Disabled
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
updatedUser
,
err
:=
h
.
userService
.
GetByID
(
c
.
Request
.
Context
(),
subject
.
UserID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
updatedUser
))
}
backend/internal/payment/load_balancer.go
View file @
0b746501
...
...
@@ -117,7 +117,13 @@ func (lb *DefaultLoadBalancer) queryEnabledInstances(
var
matched
[]
*
dbent
.
PaymentProviderInstance
for
_
,
inst
:=
range
instances
{
if
InstanceSupportsType
(
inst
.
SupportedTypes
,
paymentType
)
{
// Stripe: match by provider_key because supported_types lists sub-types (card,link,alipay,wxpay),
// not "stripe" itself. The checkout page aggregates all sub-types under "stripe".
if
paymentType
==
TypeStripe
{
if
inst
.
ProviderKey
==
TypeStripe
{
matched
=
append
(
matched
,
inst
)
}
}
else
if
InstanceSupportsType
(
inst
.
SupportedTypes
,
paymentType
)
{
matched
=
append
(
matched
,
inst
)
}
}
...
...
backend/internal/payment/load_balancer_test.go
View file @
0b746501
backend/internal/payment/provider/alipay_test.go
View file @
0b746501
backend/internal/pkg/antigravity/stream_transformer.go
View file @
0b746501
...
...
@@ -18,6 +18,9 @@ const (
BlockTypeFunction
)
// UsageMapHook is a callback that can modify usage data before it's emitted in SSE events.
type
UsageMapHook
func
(
usageMap
map
[
string
]
any
)
// StreamingProcessor 流式响应处理器
type
StreamingProcessor
struct
{
blockType
BlockType
...
...
@@ -30,6 +33,7 @@ type StreamingProcessor struct {
originalModel
string
webSearchQueries
[]
string
groundingChunks
[]
GeminiGroundingChunk
usageMapHook
UsageMapHook
// 累计 usage
inputTokens
int
...
...
@@ -46,6 +50,28 @@ func NewStreamingProcessor(originalModel string) *StreamingProcessor {
}
}
// SetUsageMapHook sets an optional hook that modifies usage maps before they are emitted.
func
(
p
*
StreamingProcessor
)
SetUsageMapHook
(
fn
UsageMapHook
)
{
p
.
usageMapHook
=
fn
}
func
usageToMap
(
u
ClaudeUsage
)
map
[
string
]
any
{
m
:=
map
[
string
]
any
{
"input_tokens"
:
u
.
InputTokens
,
"output_tokens"
:
u
.
OutputTokens
,
}
if
u
.
CacheCreationInputTokens
>
0
{
m
[
"cache_creation_input_tokens"
]
=
u
.
CacheCreationInputTokens
}
if
u
.
CacheReadInputTokens
>
0
{
m
[
"cache_read_input_tokens"
]
=
u
.
CacheReadInputTokens
}
if
u
.
ImageOutputTokens
>
0
{
m
[
"image_output_tokens"
]
=
u
.
ImageOutputTokens
}
return
m
}
// ProcessLine 处理 SSE 行,返回 Claude SSE 事件
func
(
p
*
StreamingProcessor
)
ProcessLine
(
line
string
)
[]
byte
{
line
=
strings
.
TrimSpace
(
line
)
...
...
@@ -172,6 +198,13 @@ func (p *StreamingProcessor) emitMessageStart(v1Resp *V1InternalResponse) []byte
responseID
=
"msg_"
+
generateRandomID
()
}
var
usageValue
any
=
usage
if
p
.
usageMapHook
!=
nil
{
usageMap
:=
usageToMap
(
usage
)
p
.
usageMapHook
(
usageMap
)
usageValue
=
usageMap
}
message
:=
map
[
string
]
any
{
"id"
:
responseID
,
"type"
:
"message"
,
...
...
@@ -180,7 +213,7 @@ func (p *StreamingProcessor) emitMessageStart(v1Resp *V1InternalResponse) []byte
"model"
:
p
.
originalModel
,
"stop_reason"
:
nil
,
"stop_sequence"
:
nil
,
"usage"
:
usage
,
"usage"
:
usage
Value
,
}
event
:=
map
[
string
]
any
{
...
...
@@ -492,13 +525,20 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte {
ImageOutputTokens
:
p
.
imageOutputTokens
,
}
var
usageValue
any
=
usage
if
p
.
usageMapHook
!=
nil
{
usageMap
:=
usageToMap
(
usage
)
p
.
usageMapHook
(
usageMap
)
usageValue
=
usageMap
}
deltaEvent
:=
map
[
string
]
any
{
"type"
:
"message_delta"
,
"delta"
:
map
[
string
]
any
{
"stop_reason"
:
stopReason
,
"stop_sequence"
:
nil
,
},
"usage"
:
usage
,
"usage"
:
usage
Value
,
}
_
,
_
=
result
.
Write
(
p
.
formatSSE
(
"message_delta"
,
deltaEvent
))
...
...
Prev
1
2
3
4
5
6
…
12
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