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
61f55674
Commit
61f55674
authored
Jan 10, 2026
by
yangjianbo
Browse files
Merge branch 'main' of
https://github.com/mt21625457/aicodex2api
parents
eeb1282f
7d1fe818
Changes
103
Show whitespace changes
Inline
Side-by-side
backend/internal/repository/group_repo.go
View file @
61f55674
...
...
@@ -112,10 +112,10 @@ func (r *groupRepository) Delete(ctx context.Context, id int64) error {
}
func
(
r
*
groupRepository
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
service
.
Group
,
*
pagination
.
PaginationResult
,
error
)
{
return
r
.
ListWithFilters
(
ctx
,
params
,
""
,
""
,
nil
)
return
r
.
ListWithFilters
(
ctx
,
params
,
""
,
""
,
""
,
nil
)
}
func
(
r
*
groupRepository
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
service
.
Group
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
groupRepository
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
service
.
Group
,
*
pagination
.
PaginationResult
,
error
)
{
q
:=
r
.
client
.
Group
.
Query
()
if
platform
!=
""
{
...
...
@@ -124,6 +124,12 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination
if
status
!=
""
{
q
=
q
.
Where
(
group
.
StatusEQ
(
status
))
}
if
search
!=
""
{
q
=
q
.
Where
(
group
.
Or
(
group
.
NameContainsFold
(
search
),
group
.
DescriptionContainsFold
(
search
),
))
}
if
isExclusive
!=
nil
{
q
=
q
.
Where
(
group
.
IsExclusiveEQ
(
*
isExclusive
))
}
...
...
backend/internal/repository/group_repo_integration_test.go
View file @
61f55674
...
...
@@ -131,6 +131,7 @@ func (s *GroupRepoSuite) TestListWithFilters_Platform() {
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
service
.
PlatformOpenAI
,
""
,
""
,
nil
,
)
s
.
Require
()
.
NoError
(
err
,
"ListWithFilters base"
)
...
...
@@ -152,7 +153,7 @@ func (s *GroupRepoSuite) TestListWithFilters_Platform() {
SubscriptionType
:
service
.
SubscriptionTypeStandard
,
}))
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
service
.
PlatformOpenAI
,
""
,
nil
)
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
service
.
PlatformOpenAI
,
""
,
""
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
groups
,
len
(
baseGroups
)
+
1
)
// Verify all groups are OpenAI platform
...
...
@@ -179,7 +180,7 @@ func (s *GroupRepoSuite) TestListWithFilters_Status() {
SubscriptionType
:
service
.
SubscriptionTypeStandard
,
}))
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
""
,
service
.
StatusDisabled
,
nil
)
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
""
,
service
.
StatusDisabled
,
""
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
groups
,
1
)
s
.
Require
()
.
Equal
(
service
.
StatusDisabled
,
groups
[
0
]
.
Status
)
...
...
@@ -204,12 +205,117 @@ func (s *GroupRepoSuite) TestListWithFilters_IsExclusive() {
}))
isExclusive
:=
true
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
""
,
""
,
&
isExclusive
)
groups
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
""
,
""
,
""
,
&
isExclusive
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
groups
,
1
)
s
.
Require
()
.
True
(
groups
[
0
]
.
IsExclusive
)
}
func
(
s
*
GroupRepoSuite
)
TestListWithFilters_Search
()
{
newRepo
:=
func
()
(
*
groupRepository
,
context
.
Context
)
{
tx
:=
testEntTx
(
s
.
T
())
return
newGroupRepositoryWithSQL
(
tx
.
Client
(),
tx
),
context
.
Background
()
}
containsID
:=
func
(
groups
[]
service
.
Group
,
id
int64
)
bool
{
for
i
:=
range
groups
{
if
groups
[
i
]
.
ID
==
id
{
return
true
}
}
return
false
}
mustCreate
:=
func
(
repo
*
groupRepository
,
ctx
context
.
Context
,
g
*
service
.
Group
)
*
service
.
Group
{
s
.
Require
()
.
NoError
(
repo
.
Create
(
ctx
,
g
))
s
.
Require
()
.
NotZero
(
g
.
ID
)
return
g
}
newGroup
:=
func
(
name
string
)
*
service
.
Group
{
return
&
service
.
Group
{
Name
:
name
,
Platform
:
service
.
PlatformAnthropic
,
RateMultiplier
:
1.0
,
IsExclusive
:
false
,
Status
:
service
.
StatusActive
,
SubscriptionType
:
service
.
SubscriptionTypeStandard
,
}
}
s
.
Run
(
"search_name_should_match"
,
func
()
{
repo
,
ctx
:=
newRepo
()
target
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-name-target"
))
other
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-name-other"
))
groups
,
_
,
err
:=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
"name-target"
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
True
(
containsID
(
groups
,
target
.
ID
),
"expected target group to match by name"
)
s
.
Require
()
.
False
(
containsID
(
groups
,
other
.
ID
),
"expected other group to be filtered out"
)
})
s
.
Run
(
"search_description_should_match"
,
func
()
{
repo
,
ctx
:=
newRepo
()
target
:=
newGroup
(
"it-group-search-desc-target"
)
target
.
Description
=
"something about desc-needle in here"
target
=
mustCreate
(
repo
,
ctx
,
target
)
other
:=
newGroup
(
"it-group-search-desc-other"
)
other
.
Description
=
"nothing to see here"
other
=
mustCreate
(
repo
,
ctx
,
other
)
groups
,
_
,
err
:=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
"desc-needle"
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
True
(
containsID
(
groups
,
target
.
ID
),
"expected target group to match by description"
)
s
.
Require
()
.
False
(
containsID
(
groups
,
other
.
ID
),
"expected other group to be filtered out"
)
})
s
.
Run
(
"search_nonexistent_should_return_empty"
,
func
()
{
repo
,
ctx
:=
newRepo
()
_
=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-nonexistent-baseline"
))
search
:=
s
.
T
()
.
Name
()
+
"__no_such_group__"
groups
,
_
,
err
:=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
search
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Empty
(
groups
)
})
s
.
Run
(
"search_should_be_case_insensitive"
,
func
()
{
repo
,
ctx
:=
newRepo
()
target
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"MiXeDCaSe-Needle"
))
other
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-case-other"
))
groups
,
_
,
err
:=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
"mixedcase-needle"
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
True
(
containsID
(
groups
,
target
.
ID
),
"expected case-insensitive match"
)
s
.
Require
()
.
False
(
containsID
(
groups
,
other
.
ID
),
"expected other group to be filtered out"
)
})
s
.
Run
(
"search_should_escape_like_wildcards"
,
func
()
{
repo
,
ctx
:=
newRepo
()
percentTarget
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-100%-target"
))
percentOther
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-100X-other"
))
groups
,
_
,
err
:=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
"100%"
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
True
(
containsID
(
groups
,
percentTarget
.
ID
),
"expected literal %% match"
)
s
.
Require
()
.
False
(
containsID
(
groups
,
percentOther
.
ID
),
"expected %% not to act as wildcard"
)
underscoreTarget
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-ab_cd-target"
))
underscoreOther
:=
mustCreate
(
repo
,
ctx
,
newGroup
(
"it-group-search-abXcd-other"
))
groups
,
_
,
err
=
repo
.
ListWithFilters
(
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
50
},
""
,
""
,
"ab_cd"
,
nil
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
True
(
containsID
(
groups
,
underscoreTarget
.
ID
),
"expected literal _ match"
)
s
.
Require
()
.
False
(
containsID
(
groups
,
underscoreOther
.
ID
),
"expected _ not to act as wildcard"
)
})
}
func
(
s
*
GroupRepoSuite
)
TestListWithFilters_AccountCount
()
{
g1
:=
&
service
.
Group
{
Name
:
"g1"
,
...
...
@@ -244,7 +350,7 @@ func (s *GroupRepoSuite) TestListWithFilters_AccountCount() {
s
.
Require
()
.
NoError
(
err
)
isExclusive
:=
true
groups
,
page
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
service
.
PlatformAnthropic
,
service
.
StatusActive
,
&
isExclusive
)
groups
,
page
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
},
service
.
PlatformAnthropic
,
service
.
StatusActive
,
""
,
&
isExclusive
)
s
.
Require
()
.
NoError
(
err
,
"ListWithFilters"
)
s
.
Require
()
.
Equal
(
int64
(
1
),
page
.
Total
)
s
.
Require
()
.
Len
(
groups
,
1
)
...
...
backend/internal/repository/usage_log_repo.go
View file @
61f55674
...
...
@@ -22,7 +22,7 @@ import (
"github.com/lib/pq"
)
const
usageLogSelectColumns
=
"id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, image_count, image_size, created_at"
const
usageLogSelectColumns
=
"id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent,
ip_address,
image_count, image_size, created_at"
type
usageLogRepository
struct
{
client
*
dbent
.
Client
...
...
@@ -110,6 +110,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
duration_ms,
first_token_ms,
user_agent,
ip_address,
image_count,
image_size,
created_at
...
...
@@ -119,7 +120,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
$8, $9, $10, $11,
$12, $13,
$14, $15, $16, $17, $18, $19,
$20, $21, $22, $23, $24, $25, $26, $27, $28
$20, $21, $22, $23, $24, $25, $26, $27, $28
, $29
)
ON CONFLICT (request_id, api_key_id) DO NOTHING
RETURNING id, created_at
...
...
@@ -130,6 +131,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
duration
:=
nullInt
(
log
.
DurationMs
)
firstToken
:=
nullInt
(
log
.
FirstTokenMs
)
userAgent
:=
nullString
(
log
.
UserAgent
)
ipAddress
:=
nullString
(
log
.
IPAddress
)
imageSize
:=
nullString
(
log
.
ImageSize
)
var
requestIDArg
any
...
...
@@ -163,6 +165,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
duration
,
firstToken
,
userAgent
,
ipAddress
,
log
.
ImageCount
,
imageSize
,
createdAt
,
...
...
@@ -1873,6 +1876,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
durationMs
sql
.
NullInt64
firstTokenMs
sql
.
NullInt64
userAgent
sql
.
NullString
ipAddress
sql
.
NullString
imageCount
int
imageSize
sql
.
NullString
createdAt
time
.
Time
...
...
@@ -1905,6 +1909,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
&
durationMs
,
&
firstTokenMs
,
&
userAgent
,
&
ipAddress
,
&
imageCount
,
&
imageSize
,
&
createdAt
,
...
...
@@ -1959,6 +1964,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
if
userAgent
.
Valid
{
log
.
UserAgent
=
&
userAgent
.
String
}
if
ipAddress
.
Valid
{
log
.
IPAddress
=
&
ipAddress
.
String
}
if
imageSize
.
Valid
{
log
.
ImageSize
=
&
imageSize
.
String
}
...
...
backend/internal/server/api_contract_test.go
View file @
61f55674
...
...
@@ -82,6 +82,8 @@ func TestAPIContracts(t *testing.T) {
"name": "Key One",
"group_id": null,
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
...
...
@@ -116,6 +118,8 @@ func TestAPIContracts(t *testing.T) {
"name": "Key One",
"group_id": null,
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
...
...
@@ -304,6 +308,10 @@ func TestAPIContracts(t *testing.T) {
"turnstile_enabled": true,
"turnstile_site_key": "site-key",
"turnstile_secret_key_configured": true,
"linuxdo_connect_enabled": false,
"linuxdo_connect_client_id": "",
"linuxdo_connect_client_secret_configured": false,
"linuxdo_connect_redirect_url": "",
"site_name": "Sub2API",
"site_logo": "",
"site_subtitle": "Subtitle",
...
...
@@ -390,7 +398,7 @@ func newContractDeps(t *testing.T) *contractDeps {
settingRepo
:=
newStubSettingRepo
()
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
,
settingService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
,
nil
)
...
...
@@ -583,7 +591,7 @@ func (stubGroupRepo) List(ctx context.Context, params pagination.PaginationParam
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
stubGroupRepo
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
service
.
Group
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
stubGroupRepo
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
service
.
Group
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
...
...
backend/internal/server/middleware/api_key_auth.go
View file @
61f55674
...
...
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
...
...
@@ -71,6 +72,17 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
return
}
// 检查 IP 限制(白名单/黑名单)
// 注意:错误信息故意模糊,避免暴露具体的 IP 限制机制
if
len
(
apiKey
.
IPWhitelist
)
>
0
||
len
(
apiKey
.
IPBlacklist
)
>
0
{
clientIP
:=
ip
.
GetClientIP
(
c
)
allowed
,
_
:=
ip
.
CheckIPRestriction
(
clientIP
,
apiKey
.
IPWhitelist
,
apiKey
.
IPBlacklist
)
if
!
allowed
{
AbortWithError
(
c
,
403
,
"ACCESS_DENIED"
,
"Access denied"
)
return
}
}
// 检查关联的用户
if
apiKey
.
User
==
nil
{
AbortWithError
(
c
,
401
,
"USER_NOT_FOUND"
,
"User associated with API key not found"
)
...
...
backend/internal/server/routes/auth.go
View file @
61f55674
...
...
@@ -19,6 +19,8 @@ func RegisterAuthRoutes(
auth
.
POST
(
"/register"
,
h
.
Auth
.
Register
)
auth
.
POST
(
"/login"
,
h
.
Auth
.
Login
)
auth
.
POST
(
"/send-verify-code"
,
h
.
Auth
.
SendVerifyCode
)
auth
.
GET
(
"/oauth/linuxdo/start"
,
h
.
Auth
.
LinuxDoOAuthStart
)
auth
.
GET
(
"/oauth/linuxdo/callback"
,
h
.
Auth
.
LinuxDoOAuthCallback
)
}
// 公开设置(无需认证)
...
...
backend/internal/service/account_service.go
View file @
61f55674
...
...
@@ -49,10 +49,12 @@ type AccountRepository interface {
ListSchedulableByGroupIDAndPlatforms
(
ctx
context
.
Context
,
groupID
int64
,
platforms
[]
string
)
([]
Account
,
error
)
SetRateLimited
(
ctx
context
.
Context
,
id
int64
,
resetAt
time
.
Time
)
error
SetAntigravityQuotaScopeLimit
(
ctx
context
.
Context
,
id
int64
,
scope
AntigravityQuotaScope
,
resetAt
time
.
Time
)
error
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
SetTempUnschedulable
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
,
reason
string
)
error
ClearTempUnschedulable
(
ctx
context
.
Context
,
id
int64
)
error
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
ClearAntigravityQuotaScopes
(
ctx
context
.
Context
,
id
int64
)
error
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
UpdateExtra
(
ctx
context
.
Context
,
id
int64
,
updates
map
[
string
]
any
)
error
BulkUpdate
(
ctx
context
.
Context
,
ids
[]
int64
,
updates
AccountBulkUpdate
)
(
int64
,
error
)
...
...
@@ -66,6 +68,7 @@ type AccountBulkUpdate struct {
Concurrency
*
int
Priority
*
int
Status
*
string
Schedulable
*
bool
Credentials
map
[
string
]
any
Extra
map
[
string
]
any
}
...
...
backend/internal/service/account_service_delete_test.go
View file @
61f55674
...
...
@@ -139,6 +139,10 @@ func (s *accountRepoStub) SetRateLimited(ctx context.Context, id int64, resetAt
panic
(
"unexpected SetRateLimited call"
)
}
func
(
s
*
accountRepoStub
)
SetAntigravityQuotaScopeLimit
(
ctx
context
.
Context
,
id
int64
,
scope
AntigravityQuotaScope
,
resetAt
time
.
Time
)
error
{
panic
(
"unexpected SetAntigravityQuotaScopeLimit call"
)
}
func
(
s
*
accountRepoStub
)
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
{
panic
(
"unexpected SetOverloaded call"
)
}
...
...
@@ -155,6 +159,10 @@ func (s *accountRepoStub) ClearRateLimit(ctx context.Context, id int64) error {
panic
(
"unexpected ClearRateLimit call"
)
}
func
(
s
*
accountRepoStub
)
ClearAntigravityQuotaScopes
(
ctx
context
.
Context
,
id
int64
)
error
{
panic
(
"unexpected ClearAntigravityQuotaScopes call"
)
}
func
(
s
*
accountRepoStub
)
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
{
panic
(
"unexpected UpdateSessionWindow call"
)
}
...
...
backend/internal/service/admin_service.go
View file @
61f55674
...
...
@@ -24,7 +24,7 @@ type AdminService interface {
GetUserUsageStats
(
ctx
context
.
Context
,
userID
int64
,
period
string
)
(
any
,
error
)
// Group management
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
int64
,
error
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
int64
,
error
)
GetAllGroups
(
ctx
context
.
Context
)
([]
Group
,
error
)
GetAllGroupsByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
GetGroup
(
ctx
context
.
Context
,
id
int64
)
(
*
Group
,
error
)
...
...
@@ -168,6 +168,7 @@ type BulkUpdateAccountsInput struct {
Concurrency
*
int
Priority
*
int
Status
string
Schedulable
*
bool
GroupIDs
*
[]
int64
Credentials
map
[
string
]
any
Extra
map
[
string
]
any
...
...
@@ -478,9 +479,9 @@ func (s *adminServiceImpl) GetUserUsageStats(ctx context.Context, userID int64,
}
// Group management implementations
func
(
s
*
adminServiceImpl
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
int64
,
error
)
{
func
(
s
*
adminServiceImpl
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
int64
,
error
)
{
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
groups
,
result
,
err
:=
s
.
groupRepo
.
ListWithFilters
(
ctx
,
params
,
platform
,
status
,
isExclusive
)
groups
,
result
,
err
:=
s
.
groupRepo
.
ListWithFilters
(
ctx
,
params
,
platform
,
status
,
search
,
isExclusive
)
if
err
!=
nil
{
return
nil
,
0
,
err
}
...
...
@@ -910,6 +911,9 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
if
input
.
Status
!=
""
{
repoUpdates
.
Status
=
&
input
.
Status
}
if
input
.
Schedulable
!=
nil
{
repoUpdates
.
Schedulable
=
input
.
Schedulable
}
// Run bulk update for column/jsonb fields first.
if
_
,
err
:=
s
.
accountRepo
.
BulkUpdate
(
ctx
,
input
.
AccountIDs
,
repoUpdates
);
err
!=
nil
{
...
...
backend/internal/service/admin_service_delete_test.go
View file @
61f55674
...
...
@@ -124,7 +124,7 @@ func (s *groupRepoStub) List(ctx context.Context, params pagination.PaginationPa
panic
(
"unexpected List call"
)
}
func
(
s
*
groupRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
s
*
groupRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
...
...
backend/internal/service/admin_service_group_test.go
View file @
61f55674
...
...
@@ -16,6 +16,16 @@ type groupRepoStubForAdmin struct {
updated
*
Group
// 记录 Update 调用的参数
getByID
*
Group
// GetByID 返回值
getErr
error
// GetByID 返回的错误
listWithFiltersCalls
int
listWithFiltersParams
pagination
.
PaginationParams
listWithFiltersPlatform
string
listWithFiltersStatus
string
listWithFiltersSearch
string
listWithFiltersIsExclusive
*
bool
listWithFiltersGroups
[]
Group
listWithFiltersResult
*
pagination
.
PaginationResult
listWithFiltersErr
error
}
func
(
s
*
groupRepoStubForAdmin
)
Create
(
_
context
.
Context
,
g
*
Group
)
error
{
...
...
@@ -47,8 +57,28 @@ func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationP
panic
(
"unexpected List call"
)
}
func
(
s
*
groupRepoStubForAdmin
)
ListWithFilters
(
_
context
.
Context
,
_
pagination
.
PaginationParams
,
_
,
_
string
,
_
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
func
(
s
*
groupRepoStubForAdmin
)
ListWithFilters
(
_
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
listWithFiltersCalls
++
s
.
listWithFiltersParams
=
params
s
.
listWithFiltersPlatform
=
platform
s
.
listWithFiltersStatus
=
status
s
.
listWithFiltersSearch
=
search
s
.
listWithFiltersIsExclusive
=
isExclusive
if
s
.
listWithFiltersErr
!=
nil
{
return
nil
,
nil
,
s
.
listWithFiltersErr
}
result
:=
s
.
listWithFiltersResult
if
result
==
nil
{
result
=
&
pagination
.
PaginationResult
{
Total
:
int64
(
len
(
s
.
listWithFiltersGroups
)),
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
}
}
return
s
.
listWithFiltersGroups
,
result
,
nil
}
func
(
s
*
groupRepoStubForAdmin
)
ListActive
(
_
context
.
Context
)
([]
Group
,
error
)
{
...
...
@@ -195,3 +225,68 @@ func TestAdminService_UpdateGroup_PartialImagePricing(t *testing.T) {
require
.
InDelta
(
t
,
0.15
,
*
repo
.
updated
.
ImagePrice2K
,
0.0001
)
// 原值保持
require
.
Nil
(
t
,
repo
.
updated
.
ImagePrice4K
)
}
func
TestAdminService_ListGroups_WithSearch
(
t
*
testing
.
T
)
{
// 测试:
// 1. search 参数正常传递到 repository 层
// 2. search 为空字符串时的行为
// 3. search 与其他过滤条件组合使用
t
.
Run
(
"search 参数正常传递到 repository 层"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
groupRepoStubForAdmin
{
listWithFiltersGroups
:
[]
Group
{{
ID
:
1
,
Name
:
"alpha"
}},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
1
},
}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
}
groups
,
total
,
err
:=
svc
.
ListGroups
(
context
.
Background
(),
1
,
20
,
""
,
""
,
"alpha"
,
nil
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
1
),
total
)
require
.
Equal
(
t
,
[]
Group
{{
ID
:
1
,
Name
:
"alpha"
}},
groups
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
20
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
"alpha"
,
repo
.
listWithFiltersSearch
)
require
.
Nil
(
t
,
repo
.
listWithFiltersIsExclusive
)
})
t
.
Run
(
"search 为空字符串时传递空字符串"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
groupRepoStubForAdmin
{
listWithFiltersGroups
:
[]
Group
{},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
0
},
}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
}
groups
,
total
,
err
:=
svc
.
ListGroups
(
context
.
Background
(),
2
,
10
,
""
,
""
,
""
,
nil
)
require
.
NoError
(
t
,
err
)
require
.
Empty
(
t
,
groups
)
require
.
Equal
(
t
,
int64
(
0
),
total
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
2
,
PageSize
:
10
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
""
,
repo
.
listWithFiltersSearch
)
require
.
Nil
(
t
,
repo
.
listWithFiltersIsExclusive
)
})
t
.
Run
(
"search 与其他过滤条件组合使用"
,
func
(
t
*
testing
.
T
)
{
isExclusive
:=
true
repo
:=
&
groupRepoStubForAdmin
{
listWithFiltersGroups
:
[]
Group
{{
ID
:
2
,
Name
:
"beta"
}},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
42
},
}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
}
groups
,
total
,
err
:=
svc
.
ListGroups
(
context
.
Background
(),
3
,
50
,
PlatformAntigravity
,
StatusActive
,
"beta"
,
&
isExclusive
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
42
),
total
)
require
.
Equal
(
t
,
[]
Group
{{
ID
:
2
,
Name
:
"beta"
}},
groups
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
3
,
PageSize
:
50
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
PlatformAntigravity
,
repo
.
listWithFiltersPlatform
)
require
.
Equal
(
t
,
StatusActive
,
repo
.
listWithFiltersStatus
)
require
.
Equal
(
t
,
"beta"
,
repo
.
listWithFiltersSearch
)
require
.
NotNil
(
t
,
repo
.
listWithFiltersIsExclusive
)
require
.
True
(
t
,
*
repo
.
listWithFiltersIsExclusive
)
})
}
backend/internal/service/admin_service_search_test.go
0 → 100644
View file @
61f55674
//go:build unit
package
service
import
(
"context"
"testing"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/stretchr/testify/require"
)
type
accountRepoStubForAdminList
struct
{
accountRepoStub
listWithFiltersCalls
int
listWithFiltersParams
pagination
.
PaginationParams
listWithFiltersPlatform
string
listWithFiltersType
string
listWithFiltersStatus
string
listWithFiltersSearch
string
listWithFiltersAccounts
[]
Account
listWithFiltersResult
*
pagination
.
PaginationResult
listWithFiltersErr
error
}
func
(
s
*
accountRepoStubForAdminList
)
ListWithFilters
(
_
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
accountType
,
status
,
search
string
)
([]
Account
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
listWithFiltersCalls
++
s
.
listWithFiltersParams
=
params
s
.
listWithFiltersPlatform
=
platform
s
.
listWithFiltersType
=
accountType
s
.
listWithFiltersStatus
=
status
s
.
listWithFiltersSearch
=
search
if
s
.
listWithFiltersErr
!=
nil
{
return
nil
,
nil
,
s
.
listWithFiltersErr
}
result
:=
s
.
listWithFiltersResult
if
result
==
nil
{
result
=
&
pagination
.
PaginationResult
{
Total
:
int64
(
len
(
s
.
listWithFiltersAccounts
)),
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
}
}
return
s
.
listWithFiltersAccounts
,
result
,
nil
}
type
proxyRepoStubForAdminList
struct
{
proxyRepoStub
listWithFiltersCalls
int
listWithFiltersParams
pagination
.
PaginationParams
listWithFiltersProtocol
string
listWithFiltersStatus
string
listWithFiltersSearch
string
listWithFiltersProxies
[]
Proxy
listWithFiltersResult
*
pagination
.
PaginationResult
listWithFiltersErr
error
listWithFiltersAndAccountCountCalls
int
listWithFiltersAndAccountCountParams
pagination
.
PaginationParams
listWithFiltersAndAccountCountProtocol
string
listWithFiltersAndAccountCountStatus
string
listWithFiltersAndAccountCountSearch
string
listWithFiltersAndAccountCountProxies
[]
ProxyWithAccountCount
listWithFiltersAndAccountCountResult
*
pagination
.
PaginationResult
listWithFiltersAndAccountCountErr
error
}
func
(
s
*
proxyRepoStubForAdminList
)
ListWithFilters
(
_
context
.
Context
,
params
pagination
.
PaginationParams
,
protocol
,
status
,
search
string
)
([]
Proxy
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
listWithFiltersCalls
++
s
.
listWithFiltersParams
=
params
s
.
listWithFiltersProtocol
=
protocol
s
.
listWithFiltersStatus
=
status
s
.
listWithFiltersSearch
=
search
if
s
.
listWithFiltersErr
!=
nil
{
return
nil
,
nil
,
s
.
listWithFiltersErr
}
result
:=
s
.
listWithFiltersResult
if
result
==
nil
{
result
=
&
pagination
.
PaginationResult
{
Total
:
int64
(
len
(
s
.
listWithFiltersProxies
)),
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
}
}
return
s
.
listWithFiltersProxies
,
result
,
nil
}
func
(
s
*
proxyRepoStubForAdminList
)
ListWithFiltersAndAccountCount
(
_
context
.
Context
,
params
pagination
.
PaginationParams
,
protocol
,
status
,
search
string
)
([]
ProxyWithAccountCount
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
listWithFiltersAndAccountCountCalls
++
s
.
listWithFiltersAndAccountCountParams
=
params
s
.
listWithFiltersAndAccountCountProtocol
=
protocol
s
.
listWithFiltersAndAccountCountStatus
=
status
s
.
listWithFiltersAndAccountCountSearch
=
search
if
s
.
listWithFiltersAndAccountCountErr
!=
nil
{
return
nil
,
nil
,
s
.
listWithFiltersAndAccountCountErr
}
result
:=
s
.
listWithFiltersAndAccountCountResult
if
result
==
nil
{
result
=
&
pagination
.
PaginationResult
{
Total
:
int64
(
len
(
s
.
listWithFiltersAndAccountCountProxies
)),
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
}
}
return
s
.
listWithFiltersAndAccountCountProxies
,
result
,
nil
}
type
redeemRepoStubForAdminList
struct
{
redeemRepoStub
listWithFiltersCalls
int
listWithFiltersParams
pagination
.
PaginationParams
listWithFiltersType
string
listWithFiltersStatus
string
listWithFiltersSearch
string
listWithFiltersCodes
[]
RedeemCode
listWithFiltersResult
*
pagination
.
PaginationResult
listWithFiltersErr
error
}
func
(
s
*
redeemRepoStubForAdminList
)
ListWithFilters
(
_
context
.
Context
,
params
pagination
.
PaginationParams
,
codeType
,
status
,
search
string
)
([]
RedeemCode
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
listWithFiltersCalls
++
s
.
listWithFiltersParams
=
params
s
.
listWithFiltersType
=
codeType
s
.
listWithFiltersStatus
=
status
s
.
listWithFiltersSearch
=
search
if
s
.
listWithFiltersErr
!=
nil
{
return
nil
,
nil
,
s
.
listWithFiltersErr
}
result
:=
s
.
listWithFiltersResult
if
result
==
nil
{
result
=
&
pagination
.
PaginationResult
{
Total
:
int64
(
len
(
s
.
listWithFiltersCodes
)),
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
}
}
return
s
.
listWithFiltersCodes
,
result
,
nil
}
func
TestAdminService_ListAccounts_WithSearch
(
t
*
testing
.
T
)
{
t
.
Run
(
"search 参数正常传递到 repository 层"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
accountRepoStubForAdminList
{
listWithFiltersAccounts
:
[]
Account
{{
ID
:
1
,
Name
:
"acc"
}},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
10
},
}
svc
:=
&
adminServiceImpl
{
accountRepo
:
repo
}
accounts
,
total
,
err
:=
svc
.
ListAccounts
(
context
.
Background
(),
1
,
20
,
PlatformGemini
,
AccountTypeOAuth
,
StatusActive
,
"acc"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
10
),
total
)
require
.
Equal
(
t
,
[]
Account
{{
ID
:
1
,
Name
:
"acc"
}},
accounts
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
20
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
PlatformGemini
,
repo
.
listWithFiltersPlatform
)
require
.
Equal
(
t
,
AccountTypeOAuth
,
repo
.
listWithFiltersType
)
require
.
Equal
(
t
,
StatusActive
,
repo
.
listWithFiltersStatus
)
require
.
Equal
(
t
,
"acc"
,
repo
.
listWithFiltersSearch
)
})
}
func
TestAdminService_ListProxies_WithSearch
(
t
*
testing
.
T
)
{
t
.
Run
(
"search 参数正常传递到 repository 层"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
proxyRepoStubForAdminList
{
listWithFiltersProxies
:
[]
Proxy
{{
ID
:
2
,
Name
:
"p1"
}},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
7
},
}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
proxies
,
total
,
err
:=
svc
.
ListProxies
(
context
.
Background
(),
3
,
50
,
"http"
,
StatusActive
,
"p1"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
7
),
total
)
require
.
Equal
(
t
,
[]
Proxy
{{
ID
:
2
,
Name
:
"p1"
}},
proxies
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
3
,
PageSize
:
50
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
"http"
,
repo
.
listWithFiltersProtocol
)
require
.
Equal
(
t
,
StatusActive
,
repo
.
listWithFiltersStatus
)
require
.
Equal
(
t
,
"p1"
,
repo
.
listWithFiltersSearch
)
})
}
func
TestAdminService_ListProxiesWithAccountCount_WithSearch
(
t
*
testing
.
T
)
{
t
.
Run
(
"search 参数正常传递到 repository 层"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
proxyRepoStubForAdminList
{
listWithFiltersAndAccountCountProxies
:
[]
ProxyWithAccountCount
{{
Proxy
:
Proxy
{
ID
:
3
,
Name
:
"p2"
},
AccountCount
:
5
}},
listWithFiltersAndAccountCountResult
:
&
pagination
.
PaginationResult
{
Total
:
9
},
}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
proxies
,
total
,
err
:=
svc
.
ListProxiesWithAccountCount
(
context
.
Background
(),
2
,
10
,
"socks5"
,
StatusDisabled
,
"p2"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
9
),
total
)
require
.
Equal
(
t
,
[]
ProxyWithAccountCount
{{
Proxy
:
Proxy
{
ID
:
3
,
Name
:
"p2"
},
AccountCount
:
5
}},
proxies
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersAndAccountCountCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
2
,
PageSize
:
10
},
repo
.
listWithFiltersAndAccountCountParams
)
require
.
Equal
(
t
,
"socks5"
,
repo
.
listWithFiltersAndAccountCountProtocol
)
require
.
Equal
(
t
,
StatusDisabled
,
repo
.
listWithFiltersAndAccountCountStatus
)
require
.
Equal
(
t
,
"p2"
,
repo
.
listWithFiltersAndAccountCountSearch
)
})
}
func
TestAdminService_ListRedeemCodes_WithSearch
(
t
*
testing
.
T
)
{
t
.
Run
(
"search 参数正常传递到 repository 层"
,
func
(
t
*
testing
.
T
)
{
repo
:=
&
redeemRepoStubForAdminList
{
listWithFiltersCodes
:
[]
RedeemCode
{{
ID
:
4
,
Code
:
"ABC"
}},
listWithFiltersResult
:
&
pagination
.
PaginationResult
{
Total
:
3
},
}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
codes
,
total
,
err
:=
svc
.
ListRedeemCodes
(
context
.
Background
(),
1
,
20
,
RedeemTypeBalance
,
StatusUnused
,
"ABC"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
3
),
total
)
require
.
Equal
(
t
,
[]
RedeemCode
{{
ID
:
4
,
Code
:
"ABC"
}},
codes
)
require
.
Equal
(
t
,
1
,
repo
.
listWithFiltersCalls
)
require
.
Equal
(
t
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
20
},
repo
.
listWithFiltersParams
)
require
.
Equal
(
t
,
RedeemTypeBalance
,
repo
.
listWithFiltersType
)
require
.
Equal
(
t
,
StatusUnused
,
repo
.
listWithFiltersStatus
)
require
.
Equal
(
t
,
"ABC"
,
repo
.
listWithFiltersSearch
)
})
}
backend/internal/service/antigravity_gateway_service.go
View file @
61f55674
...
...
@@ -93,6 +93,7 @@ var antigravityPrefixMapping = []struct {
// 长前缀优先
{
"gemini-2.5-flash-image"
,
"gemini-3-pro-image"
},
// gemini-2.5-flash-image → 3-pro-image
{
"gemini-3-pro-image"
,
"gemini-3-pro-image"
},
// gemini-3-pro-image-preview 等
{
"gemini-3-flash"
,
"gemini-3-flash"
},
// gemini-3-flash-preview 等 → gemini-3-flash
{
"claude-3-5-sonnet"
,
"claude-sonnet-4-5"
},
// 旧版 claude-3-5-sonnet-xxx
{
"claude-sonnet-4-5"
,
"claude-sonnet-4-5"
},
// claude-sonnet-4-5-xxx
{
"claude-haiku-4-5"
,
"claude-sonnet-4-5"
},
// claude-haiku-4-5-xxx → sonnet
...
...
@@ -502,6 +503,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
originalModel
:=
claudeReq
.
Model
mappedModel
:=
s
.
getMappedModel
(
account
,
claudeReq
.
Model
)
quotaScope
,
_
:=
resolveAntigravityQuotaScope
(
originalModel
)
// 获取 access_token
if
s
.
tokenProvider
==
nil
{
...
...
@@ -603,7 +605,7 @@ urlFallbackLoop:
}
// 所有重试都失败,标记限流状态
if
resp
.
StatusCode
==
429
{
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
,
quotaScope
)
}
// 最后一次尝试也失败
resp
=
&
http
.
Response
{
...
...
@@ -696,7 +698,7 @@ urlFallbackLoop:
// 处理错误响应(重试后仍失败或不触发重试)
if
resp
.
StatusCode
>=
400
{
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
,
quotaScope
)
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
...
...
@@ -1021,6 +1023,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
if
len
(
body
)
==
0
{
return
nil
,
s
.
writeGoogleError
(
c
,
http
.
StatusBadRequest
,
"Request body is empty"
)
}
quotaScope
,
_
:=
resolveAntigravityQuotaScope
(
originalModel
)
// 解析请求以获取 image_size(用于图片计费)
imageSize
:=
s
.
extractImageSize
(
body
)
...
...
@@ -1146,7 +1149,7 @@ urlFallbackLoop:
}
// 所有重试都失败,标记限流状态
if
resp
.
StatusCode
==
429
{
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
,
quotaScope
)
}
resp
=
&
http
.
Response
{
StatusCode
:
resp
.
StatusCode
,
...
...
@@ -1200,7 +1203,7 @@ urlFallbackLoop:
goto
handleSuccess
}
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
,
quotaScope
)
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
...
...
@@ -1314,7 +1317,7 @@ func sleepAntigravityBackoffWithContext(ctx context.Context, attempt int) bool {
}
}
func
(
s
*
AntigravityGatewayService
)
handleUpstreamError
(
ctx
context
.
Context
,
prefix
string
,
account
*
Account
,
statusCode
int
,
headers
http
.
Header
,
body
[]
byte
)
{
func
(
s
*
AntigravityGatewayService
)
handleUpstreamError
(
ctx
context
.
Context
,
prefix
string
,
account
*
Account
,
statusCode
int
,
headers
http
.
Header
,
body
[]
byte
,
quotaScope
AntigravityQuotaScope
)
{
// 429 使用 Gemini 格式解析(从 body 解析重置时间)
if
statusCode
==
429
{
resetAt
:=
ParseGeminiRateLimitResetTime
(
body
)
...
...
@@ -1325,13 +1328,23 @@ func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, pre
defaultDur
=
5
*
time
.
Minute
}
ra
:=
time
.
Now
()
.
Add
(
defaultDur
)
log
.
Printf
(
"%s status=429 rate_limited reset_in=%v (fallback)"
,
prefix
,
defaultDur
)
_
=
s
.
accountRepo
.
SetRateLimited
(
ctx
,
account
.
ID
,
ra
)
log
.
Printf
(
"%s status=429 rate_limited scope=%s reset_in=%v (fallback)"
,
prefix
,
quotaScope
,
defaultDur
)
if
quotaScope
==
""
{
return
}
if
err
:=
s
.
accountRepo
.
SetAntigravityQuotaScopeLimit
(
ctx
,
account
.
ID
,
quotaScope
,
ra
);
err
!=
nil
{
log
.
Printf
(
"%s status=429 rate_limit_set_failed scope=%s error=%v"
,
prefix
,
quotaScope
,
err
)
}
return
}
resetTime
:=
time
.
Unix
(
*
resetAt
,
0
)
log
.
Printf
(
"%s status=429 rate_limited reset_at=%v reset_in=%v"
,
prefix
,
resetTime
.
Format
(
"15:04:05"
),
time
.
Until
(
resetTime
)
.
Truncate
(
time
.
Second
))
_
=
s
.
accountRepo
.
SetRateLimited
(
ctx
,
account
.
ID
,
resetTime
)
log
.
Printf
(
"%s status=429 rate_limited scope=%s reset_at=%v reset_in=%v"
,
prefix
,
quotaScope
,
resetTime
.
Format
(
"15:04:05"
),
time
.
Until
(
resetTime
)
.
Truncate
(
time
.
Second
))
if
quotaScope
==
""
{
return
}
if
err
:=
s
.
accountRepo
.
SetAntigravityQuotaScopeLimit
(
ctx
,
account
.
ID
,
quotaScope
,
resetTime
);
err
!=
nil
{
log
.
Printf
(
"%s status=429 rate_limit_set_failed scope=%s error=%v"
,
prefix
,
quotaScope
,
err
)
}
return
}
// 其他错误码继续使用 rateLimitService
...
...
backend/internal/service/antigravity_quota_scope.go
0 → 100644
View file @
61f55674
package
service
import
(
"strings"
"time"
)
const
antigravityQuotaScopesKey
=
"antigravity_quota_scopes"
// AntigravityQuotaScope 表示 Antigravity 的配额域
type
AntigravityQuotaScope
string
const
(
AntigravityQuotaScopeClaude
AntigravityQuotaScope
=
"claude"
AntigravityQuotaScopeGeminiText
AntigravityQuotaScope
=
"gemini_text"
AntigravityQuotaScopeGeminiImage
AntigravityQuotaScope
=
"gemini_image"
)
// resolveAntigravityQuotaScope 根据模型名称解析配额域
func
resolveAntigravityQuotaScope
(
requestedModel
string
)
(
AntigravityQuotaScope
,
bool
)
{
model
:=
normalizeAntigravityModelName
(
requestedModel
)
if
model
==
""
{
return
""
,
false
}
switch
{
case
strings
.
HasPrefix
(
model
,
"claude-"
)
:
return
AntigravityQuotaScopeClaude
,
true
case
strings
.
HasPrefix
(
model
,
"gemini-"
)
:
if
isImageGenerationModel
(
model
)
{
return
AntigravityQuotaScopeGeminiImage
,
true
}
return
AntigravityQuotaScopeGeminiText
,
true
default
:
return
""
,
false
}
}
func
normalizeAntigravityModelName
(
model
string
)
string
{
normalized
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
model
))
normalized
=
strings
.
TrimPrefix
(
normalized
,
"models/"
)
return
normalized
}
// IsSchedulableForModel 结合 Antigravity 配额域限流判断是否可调度
func
(
a
*
Account
)
IsSchedulableForModel
(
requestedModel
string
)
bool
{
if
a
==
nil
{
return
false
}
if
!
a
.
IsSchedulable
()
{
return
false
}
if
a
.
Platform
!=
PlatformAntigravity
{
return
true
}
scope
,
ok
:=
resolveAntigravityQuotaScope
(
requestedModel
)
if
!
ok
{
return
true
}
resetAt
:=
a
.
antigravityQuotaScopeResetAt
(
scope
)
if
resetAt
==
nil
{
return
true
}
now
:=
time
.
Now
()
return
!
now
.
Before
(
*
resetAt
)
}
func
(
a
*
Account
)
antigravityQuotaScopeResetAt
(
scope
AntigravityQuotaScope
)
*
time
.
Time
{
if
a
==
nil
||
a
.
Extra
==
nil
||
scope
==
""
{
return
nil
}
rawScopes
,
ok
:=
a
.
Extra
[
antigravityQuotaScopesKey
]
.
(
map
[
string
]
any
)
if
!
ok
{
return
nil
}
rawScope
,
ok
:=
rawScopes
[
string
(
scope
)]
.
(
map
[
string
]
any
)
if
!
ok
{
return
nil
}
resetAtRaw
,
ok
:=
rawScope
[
"rate_limit_reset_at"
]
.
(
string
)
if
!
ok
||
strings
.
TrimSpace
(
resetAtRaw
)
==
""
{
return
nil
}
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtRaw
)
if
err
!=
nil
{
return
nil
}
return
&
resetAt
}
backend/internal/service/api_key.go
View file @
61f55674
...
...
@@ -9,6 +9,8 @@ type APIKey struct {
Name
string
GroupID
*
int64
Status
string
IPWhitelist
[]
string
IPBlacklist
[]
string
CreatedAt
time
.
Time
UpdatedAt
time
.
Time
User
*
User
...
...
backend/internal/service/api_key_service.go
View file @
61f55674
...
...
@@ -9,6 +9,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
)
...
...
@@ -20,6 +21,7 @@ var (
ErrAPIKeyTooShort
=
infraerrors
.
BadRequest
(
"API_KEY_TOO_SHORT"
,
"api key must be at least 16 characters"
)
ErrAPIKeyInvalidChars
=
infraerrors
.
BadRequest
(
"API_KEY_INVALID_CHARS"
,
"api key can only contain letters, numbers, underscores, and hyphens"
)
ErrAPIKeyRateLimited
=
infraerrors
.
TooManyRequests
(
"API_KEY_RATE_LIMITED"
,
"too many failed attempts, please try again later"
)
ErrInvalidIPPattern
=
infraerrors
.
BadRequest
(
"INVALID_IP_PATTERN"
,
"invalid IP or CIDR pattern"
)
)
const
(
...
...
@@ -60,6 +62,8 @@ type CreateAPIKeyRequest struct {
Name
string
`json:"name"`
GroupID
*
int64
`json:"group_id"`
CustomKey
*
string
`json:"custom_key"`
// 可选的自定义key
IPWhitelist
[]
string
`json:"ip_whitelist"`
// IP 白名单
IPBlacklist
[]
string
`json:"ip_blacklist"`
// IP 黑名单
}
// UpdateAPIKeyRequest 更新API Key请求
...
...
@@ -67,6 +71,8 @@ type UpdateAPIKeyRequest struct {
Name
*
string
`json:"name"`
GroupID
*
int64
`json:"group_id"`
Status
*
string
`json:"status"`
IPWhitelist
[]
string
`json:"ip_whitelist"`
// IP 白名单(空数组清空)
IPBlacklist
[]
string
`json:"ip_blacklist"`
// IP 黑名单(空数组清空)
}
// APIKeyService API Key服务
...
...
@@ -186,6 +192,20 @@ func (s *APIKeyService) Create(ctx context.Context, userID int64, req CreateAPIK
return
nil
,
fmt
.
Errorf
(
"get user: %w"
,
err
)
}
// 验证 IP 白名单格式
if
len
(
req
.
IPWhitelist
)
>
0
{
if
invalid
:=
ip
.
ValidateIPPatterns
(
req
.
IPWhitelist
);
len
(
invalid
)
>
0
{
return
nil
,
fmt
.
Errorf
(
"%w: %v"
,
ErrInvalidIPPattern
,
invalid
)
}
}
// 验证 IP 黑名单格式
if
len
(
req
.
IPBlacklist
)
>
0
{
if
invalid
:=
ip
.
ValidateIPPatterns
(
req
.
IPBlacklist
);
len
(
invalid
)
>
0
{
return
nil
,
fmt
.
Errorf
(
"%w: %v"
,
ErrInvalidIPPattern
,
invalid
)
}
}
// 验证分组权限(如果指定了分组)
if
req
.
GroupID
!=
nil
{
group
,
err
:=
s
.
groupRepo
.
GetByID
(
ctx
,
*
req
.
GroupID
)
...
...
@@ -241,6 +261,8 @@ func (s *APIKeyService) Create(ctx context.Context, userID int64, req CreateAPIK
Name
:
req
.
Name
,
GroupID
:
req
.
GroupID
,
Status
:
StatusActive
,
IPWhitelist
:
req
.
IPWhitelist
,
IPBlacklist
:
req
.
IPBlacklist
,
}
if
err
:=
s
.
apiKeyRepo
.
Create
(
ctx
,
apiKey
);
err
!=
nil
{
...
...
@@ -312,6 +334,20 @@ func (s *APIKeyService) Update(ctx context.Context, id int64, userID int64, req
return
nil
,
ErrInsufficientPerms
}
// 验证 IP 白名单格式
if
len
(
req
.
IPWhitelist
)
>
0
{
if
invalid
:=
ip
.
ValidateIPPatterns
(
req
.
IPWhitelist
);
len
(
invalid
)
>
0
{
return
nil
,
fmt
.
Errorf
(
"%w: %v"
,
ErrInvalidIPPattern
,
invalid
)
}
}
// 验证 IP 黑名单格式
if
len
(
req
.
IPBlacklist
)
>
0
{
if
invalid
:=
ip
.
ValidateIPPatterns
(
req
.
IPBlacklist
);
len
(
invalid
)
>
0
{
return
nil
,
fmt
.
Errorf
(
"%w: %v"
,
ErrInvalidIPPattern
,
invalid
)
}
}
// 更新字段
if
req
.
Name
!=
nil
{
apiKey
.
Name
=
*
req
.
Name
...
...
@@ -344,6 +380,10 @@ func (s *APIKeyService) Update(ctx context.Context, id int64, userID int64, req
}
}
// 更新 IP 限制(空数组会清空设置)
apiKey
.
IPWhitelist
=
req
.
IPWhitelist
apiKey
.
IPBlacklist
=
req
.
IPBlacklist
if
err
:=
s
.
apiKeyRepo
.
Update
(
ctx
,
apiKey
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"update api key: %w"
,
err
)
}
...
...
backend/internal/service/auth_service.go
View file @
61f55674
...
...
@@ -2,9 +2,13 @@ package service
import
(
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log"
"net/mail"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
...
...
@@ -18,6 +22,7 @@ var (
ErrInvalidCredentials
=
infraerrors
.
Unauthorized
(
"INVALID_CREDENTIALS"
,
"invalid email or password"
)
ErrUserNotActive
=
infraerrors
.
Forbidden
(
"USER_NOT_ACTIVE"
,
"user is not active"
)
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrEmailReserved
=
infraerrors
.
BadRequest
(
"EMAIL_RESERVED"
,
"email is reserved"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenTooLarge
=
infraerrors
.
BadRequest
(
"TOKEN_TOO_LARGE"
,
"token too large"
)
...
...
@@ -80,6 +85,11 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
return
""
,
nil
,
ErrRegDisabled
}
// 防止用户注册 LinuxDo OAuth 合成邮箱,避免第三方登录与本地账号发生碰撞。
if
isReservedEmail
(
email
)
{
return
""
,
nil
,
ErrEmailReserved
}
// 检查是否需要邮件验证
if
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsEmailVerifyEnabled
(
ctx
)
{
// 如果邮件验证已开启但邮件服务未配置,拒绝注册
...
...
@@ -161,6 +171,10 @@ func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error {
return
ErrRegDisabled
}
if
isReservedEmail
(
email
)
{
return
ErrEmailReserved
}
// 检查邮箱是否已存在
existsEmail
,
err
:=
s
.
userRepo
.
ExistsByEmail
(
ctx
,
email
)
if
err
!=
nil
{
...
...
@@ -195,6 +209,10 @@ func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*S
return
nil
,
ErrRegDisabled
}
if
isReservedEmail
(
email
)
{
return
nil
,
ErrEmailReserved
}
// 检查邮箱是否已存在
existsEmail
,
err
:=
s
.
userRepo
.
ExistsByEmail
(
ctx
,
email
)
if
err
!=
nil
{
...
...
@@ -319,6 +337,102 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
return
token
,
user
,
nil
}
// LoginOrRegisterOAuth 用于第三方 OAuth/SSO 登录:
// - 如果邮箱已存在:直接登录(不需要本地密码)
// - 如果邮箱不存在:创建新用户并登录
//
// 注意:该函数用于“终端用户登录 Sub2API 本身”的场景(不同于上游账号的 OAuth,例如 OpenAI/Gemini)。
// 为了满足现有数据库约束(需要密码哈希),新用户会生成随机密码并进行哈希保存。
func
(
s
*
AuthService
)
LoginOrRegisterOAuth
(
ctx
context
.
Context
,
email
,
username
string
)
(
string
,
*
User
,
error
)
{
email
=
strings
.
TrimSpace
(
email
)
if
email
==
""
||
len
(
email
)
>
255
{
return
""
,
nil
,
infraerrors
.
BadRequest
(
"INVALID_EMAIL"
,
"invalid email"
)
}
if
_
,
err
:=
mail
.
ParseAddress
(
email
);
err
!=
nil
{
return
""
,
nil
,
infraerrors
.
BadRequest
(
"INVALID_EMAIL"
,
"invalid email"
)
}
username
=
strings
.
TrimSpace
(
username
)
if
len
([]
rune
(
username
))
>
100
{
username
=
string
([]
rune
(
username
)[
:
100
])
}
user
,
err
:=
s
.
userRepo
.
GetByEmail
(
ctx
,
email
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
ErrUserNotFound
)
{
// OAuth 首次登录视为注册。
if
s
.
settingService
!=
nil
&&
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
return
""
,
nil
,
ErrRegDisabled
}
randomPassword
,
err
:=
randomHexString
(
32
)
if
err
!=
nil
{
log
.
Printf
(
"[Auth] Failed to generate random password for oauth signup: %v"
,
err
)
return
""
,
nil
,
ErrServiceUnavailable
}
hashedPassword
,
err
:=
s
.
HashPassword
(
randomPassword
)
if
err
!=
nil
{
return
""
,
nil
,
fmt
.
Errorf
(
"hash password: %w"
,
err
)
}
// 新用户默认值。
defaultBalance
:=
s
.
cfg
.
Default
.
UserBalance
defaultConcurrency
:=
s
.
cfg
.
Default
.
UserConcurrency
if
s
.
settingService
!=
nil
{
defaultBalance
=
s
.
settingService
.
GetDefaultBalance
(
ctx
)
defaultConcurrency
=
s
.
settingService
.
GetDefaultConcurrency
(
ctx
)
}
newUser
:=
&
User
{
Email
:
email
,
Username
:
username
,
PasswordHash
:
hashedPassword
,
Role
:
RoleUser
,
Balance
:
defaultBalance
,
Concurrency
:
defaultConcurrency
,
Status
:
StatusActive
,
}
if
err
:=
s
.
userRepo
.
Create
(
ctx
,
newUser
);
err
!=
nil
{
if
errors
.
Is
(
err
,
ErrEmailExists
)
{
// 并发场景:GetByEmail 与 Create 之间用户被创建。
user
,
err
=
s
.
userRepo
.
GetByEmail
(
ctx
,
email
)
if
err
!=
nil
{
log
.
Printf
(
"[Auth] Database error getting user after conflict: %v"
,
err
)
return
""
,
nil
,
ErrServiceUnavailable
}
}
else
{
log
.
Printf
(
"[Auth] Database error creating oauth user: %v"
,
err
)
return
""
,
nil
,
ErrServiceUnavailable
}
}
else
{
user
=
newUser
}
}
else
{
log
.
Printf
(
"[Auth] Database error during oauth login: %v"
,
err
)
return
""
,
nil
,
ErrServiceUnavailable
}
}
if
!
user
.
IsActive
()
{
return
""
,
nil
,
ErrUserNotActive
}
// 尽力补全:当用户名为空时,使用第三方返回的用户名回填。
if
user
.
Username
==
""
&&
username
!=
""
{
user
.
Username
=
username
if
err
:=
s
.
userRepo
.
Update
(
ctx
,
user
);
err
!=
nil
{
log
.
Printf
(
"[Auth] Failed to update username after oauth login: %v"
,
err
)
}
}
token
,
err
:=
s
.
GenerateToken
(
user
)
if
err
!=
nil
{
return
""
,
nil
,
fmt
.
Errorf
(
"generate token: %w"
,
err
)
}
return
token
,
user
,
nil
}
// ValidateToken 验证JWT token并返回用户声明
func
(
s
*
AuthService
)
ValidateToken
(
tokenString
string
)
(
*
JWTClaims
,
error
)
{
// 先做长度校验,尽早拒绝异常超长 token,降低 DoS 风险。
...
...
@@ -361,6 +475,22 @@ func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
return
nil
,
ErrInvalidToken
}
func
randomHexString
(
byteLength
int
)
(
string
,
error
)
{
if
byteLength
<=
0
{
byteLength
=
16
}
buf
:=
make
([]
byte
,
byteLength
)
if
_
,
err
:=
rand
.
Read
(
buf
);
err
!=
nil
{
return
""
,
err
}
return
hex
.
EncodeToString
(
buf
),
nil
}
func
isReservedEmail
(
email
string
)
bool
{
normalized
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
email
))
return
strings
.
HasSuffix
(
normalized
,
LinuxDoConnectSyntheticEmailDomain
)
}
// GenerateToken 生成JWT token
func
(
s
*
AuthService
)
GenerateToken
(
user
*
User
)
(
string
,
error
)
{
now
:=
time
.
Now
()
...
...
backend/internal/service/auth_service_register_test.go
View file @
61f55674
...
...
@@ -182,6 +182,16 @@ func TestAuthService_Register_CheckEmailError(t *testing.T) {
require
.
ErrorIs
(
t
,
err
,
ErrServiceUnavailable
)
}
func
TestAuthService_Register_ReservedEmail
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
},
nil
)
_
,
_
,
err
:=
service
.
Register
(
context
.
Background
(),
"linuxdo-123@linuxdo-connect.invalid"
,
"password"
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailReserved
)
}
func
TestAuthService_Register_CreateError
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
createErr
:
errors
.
New
(
"create failed"
)}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
...
...
backend/internal/service/domain_constants.go
View file @
61f55674
...
...
@@ -105,7 +105,17 @@ const (
// Request identity patch (Claude -> Gemini systemInstruction injection)
SettingKeyEnableIdentityPatch
=
"enable_identity_patch"
SettingKeyIdentityPatchPrompt
=
"identity_patch_prompt"
// LinuxDo Connect OAuth 登录(终端用户 SSO)
SettingKeyLinuxDoConnectEnabled
=
"linuxdo_connect_enabled"
SettingKeyLinuxDoConnectClientID
=
"linuxdo_connect_client_id"
SettingKeyLinuxDoConnectClientSecret
=
"linuxdo_connect_client_secret"
SettingKeyLinuxDoConnectRedirectURL
=
"linuxdo_connect_redirect_url"
)
// LinuxDoConnectSyntheticEmailDomain 是 LinuxDo Connect 用户的合成邮箱后缀(RFC 保留域名)。
// 目的:避免第三方登录返回的用户标识与本地真实邮箱发生碰撞,进而造成账号被接管的风险。
const
LinuxDoConnectSyntheticEmailDomain
=
"@linuxdo-connect.invalid"
// AdminAPIKeyPrefix is the prefix for admin API keys (distinct from user "sk-" keys).
const
AdminAPIKeyPrefix
=
"admin-"
backend/internal/service/gateway_multiplatform_test.go
View file @
61f55674
...
...
@@ -136,6 +136,9 @@ func (m *mockAccountRepoForPlatform) ListSchedulableByGroupIDAndPlatforms(ctx co
func
(
m
*
mockAccountRepoForPlatform
)
SetRateLimited
(
ctx
context
.
Context
,
id
int64
,
resetAt
time
.
Time
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForPlatform
)
SetAntigravityQuotaScopeLimit
(
ctx
context
.
Context
,
id
int64
,
scope
AntigravityQuotaScope
,
resetAt
time
.
Time
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForPlatform
)
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
{
return
nil
}
...
...
@@ -148,6 +151,9 @@ func (m *mockAccountRepoForPlatform) ClearTempUnschedulable(ctx context.Context,
func
(
m
*
mockAccountRepoForPlatform
)
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForPlatform
)
ClearAntigravityQuotaScopes
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForPlatform
)
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
{
return
nil
}
...
...
Prev
1
2
3
4
5
6
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