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
28de614d
Unverified
Commit
28de614d
authored
Jan 15, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 15, 2026
Browse files
Merge pull request #282 from LLLLLLiulei/feat/ip-management-enhancements
feat: enhance proxy management
parents
2a5ef6d3
02cb14c7
Changes
21
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
28de614d
...
...
@@ -67,7 +67,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
userHandler
:=
handler
.
NewUserHandler
(
userService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
redeemCodeRepository
:=
repository
.
NewRedeemCodeRepository
(
client
)
...
...
@@ -76,15 +75,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
redeemService
:=
service
.
NewRedeemService
(
redeemCodeRepository
,
userRepository
,
subscriptionService
,
redeemCache
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
dashboardStatsCache
:=
repository
.
NewDashboardCache
(
redisClient
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
timingWheelService
:=
service
.
ProvideTimingWheelService
()
dashboardAggregationService
:=
service
.
ProvideDashboardAggregationService
(
dashboardAggregationRepository
,
timingWheelService
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
dashboardHandler
:=
admin
.
NewDashboardHandler
(
dashboardService
,
dashboardAggregationService
)
accountRepository
:=
repository
.
NewAccountRepository
(
client
,
db
)
proxyRepository
:=
repository
.
NewProxyRepository
(
client
,
db
)
proxyExitInfoProber
:=
repository
.
NewProxyExitInfoProber
(
configConfig
)
adminService
:=
service
.
NewAdminService
(
userRepository
,
groupRepository
,
accountRepository
,
proxyRepository
,
apiKeyRepository
,
redeemCodeRepository
,
billingCacheService
,
proxyExitInfoProber
,
apiKeyAuthCacheInvalidator
)
proxyLatencyCache
:=
repository
.
NewProxyLatencyCache
(
redisClient
)
adminService
:=
service
.
NewAdminService
(
userRepository
,
groupRepository
,
accountRepository
,
proxyRepository
,
apiKeyRepository
,
redeemCodeRepository
,
billingCacheService
,
proxyExitInfoProber
,
proxyLatencyCache
,
apiKeyAuthCacheInvalidator
)
adminUserHandler
:=
admin
.
NewUserHandler
(
adminService
)
groupHandler
:=
admin
.
NewGroupHandler
(
adminService
)
claudeOAuthClient
:=
repository
.
NewClaudeOAuthClient
()
...
...
@@ -112,9 +113,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
accountTestService
:=
service
.
NewAccountTestService
(
accountRepository
,
geminiTokenProvider
,
antigravityGatewayService
,
httpUpstream
,
configConfig
)
concurrencyCache
:=
repository
.
ProvideConcurrencyCache
(
redisClient
,
configConfig
)
concurrencyService
:=
service
.
ProvideConcurrencyService
(
concurrencyCache
,
accountRepository
,
configConfig
)
schedulerCache
:=
repository
.
NewSchedulerCache
(
redisClient
)
schedulerOutboxRepository
:=
repository
.
NewSchedulerOutboxRepository
(
db
)
schedulerSnapshotService
:=
service
.
ProvideSchedulerSnapshotService
(
schedulerCache
,
schedulerOutboxRepository
,
accountRepository
,
groupRepository
,
configConfig
)
crsSyncService
:=
service
.
NewCRSSyncService
(
accountRepository
,
proxyRepository
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
configConfig
)
accountHandler
:=
admin
.
NewAccountHandler
(
adminService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
rateLimitService
,
accountUsageService
,
accountTestService
,
concurrencyService
,
crsSyncService
)
oAuthHandler
:=
admin
.
NewOAuthHandler
(
oAuthService
)
...
...
@@ -125,6 +123,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
adminRedeemHandler
:=
admin
.
NewRedeemHandler
(
adminService
)
promoHandler
:=
admin
.
NewPromoHandler
(
promoService
)
opsRepository
:=
repository
.
NewOpsRepository
(
db
)
schedulerCache
:=
repository
.
NewSchedulerCache
(
redisClient
)
schedulerOutboxRepository
:=
repository
.
NewSchedulerOutboxRepository
(
db
)
schedulerSnapshotService
:=
service
.
ProvideSchedulerSnapshotService
(
schedulerCache
,
schedulerOutboxRepository
,
accountRepository
,
groupRepository
,
configConfig
)
pricingRemoteClient
:=
repository
.
ProvidePricingRemoteClient
(
configConfig
)
pricingService
,
err
:=
service
.
ProvidePricingService
(
configConfig
,
pricingRemoteClient
)
if
err
!=
nil
{
...
...
backend/internal/handler/admin/proxy_handler.go
View file @
28de614d
...
...
@@ -196,6 +196,28 @@ func (h *ProxyHandler) Delete(c *gin.Context) {
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Proxy deleted successfully"
})
}
// BatchDelete handles batch deleting proxies
// POST /api/v1/admin/proxies/batch-delete
func
(
h
*
ProxyHandler
)
BatchDelete
(
c
*
gin
.
Context
)
{
type
BatchDeleteRequest
struct
{
IDs
[]
int64
`json:"ids" binding:"required,min=1"`
}
var
req
BatchDeleteRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
result
,
err
:=
h
.
adminService
.
BatchDeleteProxies
(
c
.
Request
.
Context
(),
req
.
IDs
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
result
)
}
// Test handles testing proxy connectivity
// POST /api/v1/admin/proxies/:id/test
func
(
h
*
ProxyHandler
)
Test
(
c
*
gin
.
Context
)
{
...
...
@@ -243,19 +265,17 @@ func (h *ProxyHandler) GetProxyAccounts(c *gin.Context) {
return
}
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
accounts
,
total
,
err
:=
h
.
adminService
.
GetProxyAccounts
(
c
.
Request
.
Context
(),
proxyID
,
page
,
pageSize
)
accounts
,
err
:=
h
.
adminService
.
GetProxyAccounts
(
c
.
Request
.
Context
(),
proxyID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
out
:=
make
([]
dto
.
Account
,
0
,
len
(
accounts
))
out
:=
make
([]
dto
.
Proxy
Account
Summary
,
0
,
len
(
accounts
))
for
i
:=
range
accounts
{
out
=
append
(
out
,
*
dto
.
AccountFromService
(
&
accounts
[
i
]))
out
=
append
(
out
,
*
dto
.
Proxy
Account
Summary
FromService
(
&
accounts
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
response
.
Success
(
c
,
out
)
}
// BatchCreateProxyItem represents a single proxy in batch create request
...
...
backend/internal/handler/dto/mappers.go
View file @
28de614d
...
...
@@ -213,8 +213,24 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
return
nil
}
return
&
ProxyWithAccountCount
{
Proxy
:
*
ProxyFromService
(
&
p
.
Proxy
),
AccountCount
:
p
.
AccountCount
,
Proxy
:
*
ProxyFromService
(
&
p
.
Proxy
),
AccountCount
:
p
.
AccountCount
,
LatencyMs
:
p
.
LatencyMs
,
LatencyStatus
:
p
.
LatencyStatus
,
LatencyMessage
:
p
.
LatencyMessage
,
}
}
func
ProxyAccountSummaryFromService
(
a
*
service
.
ProxyAccountSummary
)
*
ProxyAccountSummary
{
if
a
==
nil
{
return
nil
}
return
&
ProxyAccountSummary
{
ID
:
a
.
ID
,
Name
:
a
.
Name
,
Platform
:
a
.
Platform
,
Type
:
a
.
Type
,
Notes
:
a
.
Notes
,
}
}
...
...
backend/internal/handler/dto/types.go
View file @
28de614d
...
...
@@ -130,7 +130,18 @@ type Proxy struct {
type
ProxyWithAccountCount
struct
{
Proxy
AccountCount
int64
`json:"account_count"`
AccountCount
int64
`json:"account_count"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
LatencyStatus
string
`json:"latency_status,omitempty"`
LatencyMessage
string
`json:"latency_message,omitempty"`
}
type
ProxyAccountSummary
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
Platform
string
`json:"platform"`
Type
string
`json:"type"`
Notes
*
string
`json:"notes,omitempty"`
}
type
RedeemCode
struct
{
...
...
backend/internal/repository/proxy_latency_cache.go
0 → 100644
View file @
28de614d
package
repository
import
(
"context"
"encoding/json"
"fmt"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9"
)
const
proxyLatencyKeyPrefix
=
"proxy:latency:"
func
proxyLatencyKey
(
proxyID
int64
)
string
{
return
fmt
.
Sprintf
(
"%s%d"
,
proxyLatencyKeyPrefix
,
proxyID
)
}
type
proxyLatencyCache
struct
{
rdb
*
redis
.
Client
}
func
NewProxyLatencyCache
(
rdb
*
redis
.
Client
)
service
.
ProxyLatencyCache
{
return
&
proxyLatencyCache
{
rdb
:
rdb
}
}
func
(
c
*
proxyLatencyCache
)
GetProxyLatencies
(
ctx
context
.
Context
,
proxyIDs
[]
int64
)
(
map
[
int64
]
*
service
.
ProxyLatencyInfo
,
error
)
{
results
:=
make
(
map
[
int64
]
*
service
.
ProxyLatencyInfo
)
if
len
(
proxyIDs
)
==
0
{
return
results
,
nil
}
keys
:=
make
([]
string
,
0
,
len
(
proxyIDs
))
for
_
,
id
:=
range
proxyIDs
{
keys
=
append
(
keys
,
proxyLatencyKey
(
id
))
}
values
,
err
:=
c
.
rdb
.
MGet
(
ctx
,
keys
...
)
.
Result
()
if
err
!=
nil
{
return
results
,
err
}
for
i
,
raw
:=
range
values
{
if
raw
==
nil
{
continue
}
var
payload
[]
byte
switch
v
:=
raw
.
(
type
)
{
case
string
:
payload
=
[]
byte
(
v
)
case
[]
byte
:
payload
=
v
default
:
continue
}
var
info
service
.
ProxyLatencyInfo
if
err
:=
json
.
Unmarshal
(
payload
,
&
info
);
err
!=
nil
{
continue
}
results
[
proxyIDs
[
i
]]
=
&
info
}
return
results
,
nil
}
func
(
c
*
proxyLatencyCache
)
SetProxyLatency
(
ctx
context
.
Context
,
proxyID
int64
,
info
*
service
.
ProxyLatencyInfo
)
error
{
if
info
==
nil
{
return
nil
}
payload
,
err
:=
json
.
Marshal
(
info
)
if
err
!=
nil
{
return
err
}
return
c
.
rdb
.
Set
(
ctx
,
proxyLatencyKey
(
proxyID
),
payload
,
0
)
.
Err
()
}
backend/internal/repository/proxy_probe_service.go
View file @
28de614d
...
...
@@ -34,7 +34,10 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
}
}
const
defaultIPInfoURL
=
"https://ipinfo.io/json"
const
(
defaultIPInfoURL
=
"https://ipinfo.io/json"
defaultProxyProbeTimeout
=
30
*
time
.
Second
)
type
proxyProbeService
struct
{
ipInfoURL
string
...
...
@@ -46,7 +49,7 @@ type proxyProbeService struct {
func
(
s
*
proxyProbeService
)
ProbeProxy
(
ctx
context
.
Context
,
proxyURL
string
)
(
*
service
.
ProxyExitInfo
,
int64
,
error
)
{
client
,
err
:=
httpclient
.
GetClient
(
httpclient
.
Options
{
ProxyURL
:
proxyURL
,
Timeout
:
15
*
time
.
Second
,
Timeout
:
defaultProxyProbeTimeout
,
InsecureSkipVerify
:
s
.
insecureSkipVerify
,
ProxyStrict
:
true
,
ValidateResolvedIP
:
s
.
validateResolvedIP
,
...
...
backend/internal/repository/proxy_repo.go
View file @
28de614d
...
...
@@ -219,12 +219,54 @@ func (r *proxyRepository) ExistsByHostPortAuth(ctx context.Context, host string,
// CountAccountsByProxyID returns the number of accounts using a specific proxy
func
(
r
*
proxyRepository
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
{
var
count
int64
if
err
:=
scanSingleRow
(
ctx
,
r
.
sql
,
"SELECT COUNT(*) FROM accounts WHERE proxy_id = $1"
,
[]
any
{
proxyID
},
&
count
);
err
!=
nil
{
if
err
:=
scanSingleRow
(
ctx
,
r
.
sql
,
"SELECT COUNT(*) FROM accounts WHERE proxy_id = $1
AND deleted_at IS NULL
"
,
[]
any
{
proxyID
},
&
count
);
err
!=
nil
{
return
0
,
err
}
return
count
,
nil
}
func
(
r
*
proxyRepository
)
ListAccountSummariesByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
([]
service
.
ProxyAccountSummary
,
error
)
{
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
`
SELECT id, name, platform, type, notes
FROM accounts
WHERE proxy_id = $1 AND deleted_at IS NULL
ORDER BY id DESC
`
,
proxyID
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
out
:=
make
([]
service
.
ProxyAccountSummary
,
0
)
for
rows
.
Next
()
{
var
(
id
int64
name
string
platform
string
accType
string
notes
sql
.
NullString
)
if
err
:=
rows
.
Scan
(
&
id
,
&
name
,
&
platform
,
&
accType
,
&
notes
);
err
!=
nil
{
return
nil
,
err
}
var
notesPtr
*
string
if
notes
.
Valid
{
notesPtr
=
&
notes
.
String
}
out
=
append
(
out
,
service
.
ProxyAccountSummary
{
ID
:
id
,
Name
:
name
,
Platform
:
platform
,
Type
:
accType
,
Notes
:
notesPtr
,
})
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
// GetAccountCountsForProxies returns a map of proxy ID to account count for all proxies
func
(
r
*
proxyRepository
)
GetAccountCountsForProxies
(
ctx
context
.
Context
)
(
counts
map
[
int64
]
int64
,
err
error
)
{
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
"SELECT proxy_id, COUNT(*) AS count FROM accounts WHERE proxy_id IS NOT NULL AND deleted_at IS NULL GROUP BY proxy_id"
)
...
...
backend/internal/repository/wire.go
View file @
28de614d
...
...
@@ -69,6 +69,7 @@ var ProviderSet = wire.NewSet(
NewGeminiTokenCache
,
NewSchedulerCache
,
NewSchedulerOutboxRepository
,
NewProxyLatencyCache
,
// HTTP service ports (DI Strategy A: return interface directly)
NewTurnstileVerifier
,
...
...
backend/internal/server/api_contract_test.go
View file @
28de614d
...
...
@@ -436,7 +436,7 @@ func newContractDeps(t *testing.T) *contractDeps {
settingRepo
:=
newStubSettingRepo
()
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
adminService
:=
service
.
NewAdminService
(
userRepo
,
groupRepo
,
&
accountRepo
,
proxyRepo
,
apiKeyRepo
,
redeemRepo
,
nil
,
nil
,
nil
)
adminService
:=
service
.
NewAdminService
(
userRepo
,
groupRepo
,
&
accountRepo
,
proxyRepo
,
apiKeyRepo
,
redeemRepo
,
nil
,
nil
,
nil
,
nil
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
,
settingService
,
nil
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
...
...
@@ -859,6 +859,10 @@ func (stubProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyID int64)
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
stubProxyRepo
)
ListAccountSummariesByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
([]
service
.
ProxyAccountSummary
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
}
type
stubRedeemCodeRepo
struct
{}
func
(
stubRedeemCodeRepo
)
Create
(
ctx
context
.
Context
,
code
*
service
.
RedeemCode
)
error
{
...
...
backend/internal/server/routes/admin.go
View file @
28de614d
...
...
@@ -250,6 +250,7 @@ func registerProxyRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
proxies
.
POST
(
"/:id/test"
,
h
.
Admin
.
Proxy
.
Test
)
proxies
.
GET
(
"/:id/stats"
,
h
.
Admin
.
Proxy
.
GetStats
)
proxies
.
GET
(
"/:id/accounts"
,
h
.
Admin
.
Proxy
.
GetProxyAccounts
)
proxies
.
POST
(
"/batch-delete"
,
h
.
Admin
.
Proxy
.
BatchDelete
)
proxies
.
POST
(
"/batch"
,
h
.
Admin
.
Proxy
.
BatchCreate
)
}
}
...
...
backend/internal/service/admin_service.go
View file @
28de614d
...
...
@@ -54,7 +54,8 @@ type AdminService interface {
CreateProxy
(
ctx
context
.
Context
,
input
*
CreateProxyInput
)
(
*
Proxy
,
error
)
UpdateProxy
(
ctx
context
.
Context
,
id
int64
,
input
*
UpdateProxyInput
)
(
*
Proxy
,
error
)
DeleteProxy
(
ctx
context
.
Context
,
id
int64
)
error
GetProxyAccounts
(
ctx
context
.
Context
,
proxyID
int64
,
page
,
pageSize
int
)
([]
Account
,
int64
,
error
)
BatchDeleteProxies
(
ctx
context
.
Context
,
ids
[]
int64
)
(
*
ProxyBatchDeleteResult
,
error
)
GetProxyAccounts
(
ctx
context
.
Context
,
proxyID
int64
)
([]
ProxyAccountSummary
,
error
)
CheckProxyExists
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
TestProxy
(
ctx
context
.
Context
,
id
int64
)
(
*
ProxyTestResult
,
error
)
...
...
@@ -223,6 +224,16 @@ type GenerateRedeemCodesInput struct {
ValidityDays
int
// 订阅类型专用:有效天数
}
type
ProxyBatchDeleteResult
struct
{
DeletedIDs
[]
int64
`json:"deleted_ids"`
Skipped
[]
ProxyBatchDeleteSkipped
`json:"skipped"`
}
type
ProxyBatchDeleteSkipped
struct
{
ID
int64
`json:"id"`
Reason
string
`json:"reason"`
}
// ProxyTestResult represents the result of testing a proxy
type
ProxyTestResult
struct
{
Success
bool
`json:"success"`
...
...
@@ -257,6 +268,7 @@ type adminServiceImpl struct {
redeemCodeRepo
RedeemCodeRepository
billingCacheService
*
BillingCacheService
proxyProber
ProxyExitInfoProber
proxyLatencyCache
ProxyLatencyCache
authCacheInvalidator
APIKeyAuthCacheInvalidator
}
...
...
@@ -270,6 +282,7 @@ func NewAdminService(
redeemCodeRepo
RedeemCodeRepository
,
billingCacheService
*
BillingCacheService
,
proxyProber
ProxyExitInfoProber
,
proxyLatencyCache
ProxyLatencyCache
,
authCacheInvalidator
APIKeyAuthCacheInvalidator
,
)
AdminService
{
return
&
adminServiceImpl
{
...
...
@@ -281,6 +294,7 @@ func NewAdminService(
redeemCodeRepo
:
redeemCodeRepo
,
billingCacheService
:
billingCacheService
,
proxyProber
:
proxyProber
,
proxyLatencyCache
:
proxyLatencyCache
,
authCacheInvalidator
:
authCacheInvalidator
,
}
}
...
...
@@ -1093,6 +1107,7 @@ func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page
if
err
!=
nil
{
return
nil
,
0
,
err
}
s
.
attachProxyLatency
(
ctx
,
proxies
)
return
proxies
,
result
.
Total
,
nil
}
...
...
@@ -1101,7 +1116,12 @@ func (s *adminServiceImpl) GetAllProxies(ctx context.Context) ([]Proxy, error) {
}
func
(
s
*
adminServiceImpl
)
GetAllProxiesWithAccountCount
(
ctx
context
.
Context
)
([]
ProxyWithAccountCount
,
error
)
{
return
s
.
proxyRepo
.
ListActiveWithAccountCount
(
ctx
)
proxies
,
err
:=
s
.
proxyRepo
.
ListActiveWithAccountCount
(
ctx
)
if
err
!=
nil
{
return
nil
,
err
}
s
.
attachProxyLatency
(
ctx
,
proxies
)
return
proxies
,
nil
}
func
(
s
*
adminServiceImpl
)
GetProxy
(
ctx
context
.
Context
,
id
int64
)
(
*
Proxy
,
error
)
{
...
...
@@ -1121,6 +1141,8 @@ func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyIn
if
err
:=
s
.
proxyRepo
.
Create
(
ctx
,
proxy
);
err
!=
nil
{
return
nil
,
err
}
// Probe latency asynchronously so creation isn't blocked by network timeout.
go
s
.
probeProxyLatency
(
context
.
Background
(),
proxy
)
return
proxy
,
nil
}
...
...
@@ -1159,12 +1181,53 @@ func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *Upd
}
func
(
s
*
adminServiceImpl
)
DeleteProxy
(
ctx
context
.
Context
,
id
int64
)
error
{
count
,
err
:=
s
.
proxyRepo
.
CountAccountsByProxyID
(
ctx
,
id
)
if
err
!=
nil
{
return
err
}
if
count
>
0
{
return
ErrProxyInUse
}
return
s
.
proxyRepo
.
Delete
(
ctx
,
id
)
}
func
(
s
*
adminServiceImpl
)
GetProxyAccounts
(
ctx
context
.
Context
,
proxyID
int64
,
page
,
pageSize
int
)
([]
Account
,
int64
,
error
)
{
// Return mock data for now - would need a dedicated repository method
return
[]
Account
{},
0
,
nil
func
(
s
*
adminServiceImpl
)
BatchDeleteProxies
(
ctx
context
.
Context
,
ids
[]
int64
)
(
*
ProxyBatchDeleteResult
,
error
)
{
result
:=
&
ProxyBatchDeleteResult
{}
if
len
(
ids
)
==
0
{
return
result
,
nil
}
for
_
,
id
:=
range
ids
{
count
,
err
:=
s
.
proxyRepo
.
CountAccountsByProxyID
(
ctx
,
id
)
if
err
!=
nil
{
result
.
Skipped
=
append
(
result
.
Skipped
,
ProxyBatchDeleteSkipped
{
ID
:
id
,
Reason
:
err
.
Error
(),
})
continue
}
if
count
>
0
{
result
.
Skipped
=
append
(
result
.
Skipped
,
ProxyBatchDeleteSkipped
{
ID
:
id
,
Reason
:
ErrProxyInUse
.
Error
(),
})
continue
}
if
err
:=
s
.
proxyRepo
.
Delete
(
ctx
,
id
);
err
!=
nil
{
result
.
Skipped
=
append
(
result
.
Skipped
,
ProxyBatchDeleteSkipped
{
ID
:
id
,
Reason
:
err
.
Error
(),
})
continue
}
result
.
DeletedIDs
=
append
(
result
.
DeletedIDs
,
id
)
}
return
result
,
nil
}
func
(
s
*
adminServiceImpl
)
GetProxyAccounts
(
ctx
context
.
Context
,
proxyID
int64
)
([]
ProxyAccountSummary
,
error
)
{
return
s
.
proxyRepo
.
ListAccountSummariesByProxyID
(
ctx
,
proxyID
)
}
func
(
s
*
adminServiceImpl
)
CheckProxyExists
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
{
...
...
@@ -1264,12 +1327,24 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
proxyURL
:=
proxy
.
URL
()
exitInfo
,
latencyMs
,
err
:=
s
.
proxyProber
.
ProbeProxy
(
ctx
,
proxyURL
)
if
err
!=
nil
{
s
.
saveProxyLatency
(
ctx
,
id
,
&
ProxyLatencyInfo
{
Success
:
false
,
Message
:
err
.
Error
(),
UpdatedAt
:
time
.
Now
(),
})
return
&
ProxyTestResult
{
Success
:
false
,
Message
:
err
.
Error
(),
},
nil
}
latency
:=
latencyMs
s
.
saveProxyLatency
(
ctx
,
id
,
&
ProxyLatencyInfo
{
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
UpdatedAt
:
time
.
Now
(),
})
return
&
ProxyTestResult
{
Success
:
true
,
Message
:
"Proxy is accessible"
,
...
...
@@ -1281,6 +1356,29 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
},
nil
}
func
(
s
*
adminServiceImpl
)
probeProxyLatency
(
ctx
context
.
Context
,
proxy
*
Proxy
)
{
if
s
.
proxyProber
==
nil
||
proxy
==
nil
{
return
}
_
,
latencyMs
,
err
:=
s
.
proxyProber
.
ProbeProxy
(
ctx
,
proxy
.
URL
())
if
err
!=
nil
{
s
.
saveProxyLatency
(
ctx
,
proxy
.
ID
,
&
ProxyLatencyInfo
{
Success
:
false
,
Message
:
err
.
Error
(),
UpdatedAt
:
time
.
Now
(),
})
return
}
latency
:=
latencyMs
s
.
saveProxyLatency
(
ctx
,
proxy
.
ID
,
&
ProxyLatencyInfo
{
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
UpdatedAt
:
time
.
Now
(),
})
}
// checkMixedChannelRisk 检查分组中是否存在混合渠道(Antigravity + Anthropic)
// 如果存在混合,返回错误提示用户确认
func
(
s
*
adminServiceImpl
)
checkMixedChannelRisk
(
ctx
context
.
Context
,
currentAccountID
int64
,
currentAccountPlatform
string
,
groupIDs
[]
int64
)
error
{
...
...
@@ -1330,6 +1428,46 @@ func (s *adminServiceImpl) checkMixedChannelRisk(ctx context.Context, currentAcc
return
nil
}
func
(
s
*
adminServiceImpl
)
attachProxyLatency
(
ctx
context
.
Context
,
proxies
[]
ProxyWithAccountCount
)
{
if
s
.
proxyLatencyCache
==
nil
||
len
(
proxies
)
==
0
{
return
}
ids
:=
make
([]
int64
,
0
,
len
(
proxies
))
for
i
:=
range
proxies
{
ids
=
append
(
ids
,
proxies
[
i
]
.
ID
)
}
latencies
,
err
:=
s
.
proxyLatencyCache
.
GetProxyLatencies
(
ctx
,
ids
)
if
err
!=
nil
{
log
.
Printf
(
"Warning: load proxy latency cache failed: %v"
,
err
)
return
}
for
i
:=
range
proxies
{
info
:=
latencies
[
proxies
[
i
]
.
ID
]
if
info
==
nil
{
continue
}
if
info
.
Success
{
proxies
[
i
]
.
LatencyStatus
=
"success"
proxies
[
i
]
.
LatencyMs
=
info
.
LatencyMs
}
else
{
proxies
[
i
]
.
LatencyStatus
=
"failed"
}
proxies
[
i
]
.
LatencyMessage
=
info
.
Message
}
}
func
(
s
*
adminServiceImpl
)
saveProxyLatency
(
ctx
context
.
Context
,
proxyID
int64
,
info
*
ProxyLatencyInfo
)
{
if
s
.
proxyLatencyCache
==
nil
||
info
==
nil
{
return
}
if
err
:=
s
.
proxyLatencyCache
.
SetProxyLatency
(
ctx
,
proxyID
,
info
);
err
!=
nil
{
log
.
Printf
(
"Warning: store proxy latency cache failed: %v"
,
err
)
}
}
// getAccountPlatform 根据账号 platform 判断混合渠道检查用的平台标识
func
getAccountPlatform
(
accountPlatform
string
)
string
{
switch
strings
.
ToLower
(
strings
.
TrimSpace
(
accountPlatform
))
{
...
...
backend/internal/service/admin_service_delete_test.go
View file @
28de614d
...
...
@@ -153,8 +153,10 @@ func (s *groupRepoStub) DeleteAccountGroupsByGroupID(ctx context.Context, groupI
}
type
proxyRepoStub
struct
{
deleteErr
error
deletedIDs
[]
int64
deleteErr
error
countErr
error
accountCount
int64
deletedIDs
[]
int64
}
func
(
s
*
proxyRepoStub
)
Create
(
ctx
context
.
Context
,
proxy
*
Proxy
)
error
{
...
...
@@ -199,7 +201,14 @@ func (s *proxyRepoStub) ExistsByHostPortAuth(ctx context.Context, host string, p
}
func
(
s
*
proxyRepoStub
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected CountAccountsByProxyID call"
)
if
s
.
countErr
!=
nil
{
return
0
,
s
.
countErr
}
return
s
.
accountCount
,
nil
}
func
(
s
*
proxyRepoStub
)
ListAccountSummariesByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
([]
ProxyAccountSummary
,
error
)
{
panic
(
"unexpected ListAccountSummariesByProxyID call"
)
}
type
redeemRepoStub
struct
{
...
...
@@ -409,6 +418,15 @@ func TestAdminService_DeleteProxy_Idempotent(t *testing.T) {
require
.
Equal
(
t
,
[]
int64
{
404
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteProxy_InUse
(
t
*
testing
.
T
)
{
repo
:=
&
proxyRepoStub
{
accountCount
:
2
}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
err
:=
svc
.
DeleteProxy
(
context
.
Background
(),
77
)
require
.
ErrorIs
(
t
,
err
,
ErrProxyInUse
)
require
.
Empty
(
t
,
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteProxy_Error
(
t
*
testing
.
T
)
{
deleteErr
:=
errors
.
New
(
"delete failed"
)
repo
:=
&
proxyRepoStub
{
deleteErr
:
deleteErr
}
...
...
backend/internal/service/proxy.go
View file @
28de614d
...
...
@@ -31,5 +31,16 @@ func (p *Proxy) URL() string {
type
ProxyWithAccountCount
struct
{
Proxy
AccountCount
int64
AccountCount
int64
LatencyMs
*
int64
LatencyStatus
string
LatencyMessage
string
}
type
ProxyAccountSummary
struct
{
ID
int64
Name
string
Platform
string
Type
string
Notes
*
string
}
backend/internal/service/proxy_latency_cache.go
0 → 100644
View file @
28de614d
package
service
import
(
"context"
"time"
)
type
ProxyLatencyInfo
struct
{
Success
bool
`json:"success"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
Message
string
`json:"message,omitempty"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
type
ProxyLatencyCache
interface
{
GetProxyLatencies
(
ctx
context
.
Context
,
proxyIDs
[]
int64
)
(
map
[
int64
]
*
ProxyLatencyInfo
,
error
)
SetProxyLatency
(
ctx
context
.
Context
,
proxyID
int64
,
info
*
ProxyLatencyInfo
)
error
}
backend/internal/service/proxy_service.go
View file @
28de614d
...
...
@@ -10,6 +10,7 @@ import (
var
(
ErrProxyNotFound
=
infraerrors
.
NotFound
(
"PROXY_NOT_FOUND"
,
"proxy not found"
)
ErrProxyInUse
=
infraerrors
.
Conflict
(
"PROXY_IN_USE"
,
"proxy is in use by accounts"
)
)
type
ProxyRepository
interface
{
...
...
@@ -26,6 +27,7 @@ type ProxyRepository interface {
ExistsByHostPortAuth
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
ListAccountSummariesByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
([]
ProxyAccountSummary
,
error
)
}
// CreateProxyRequest 创建代理请求
...
...
frontend/src/api/admin/proxies.ts
View file @
28de614d
...
...
@@ -4,7 +4,13 @@
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
Proxy
,
CreateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Proxy
,
ProxyAccountSummary
,
CreateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
}
from
'
@/types
'
/**
* List all proxies with pagination
...
...
@@ -160,8 +166,8 @@ export async function getStats(id: number): Promise<{
* @param id - Proxy ID
* @returns List of accounts using the proxy
*/
export
async
function
getProxyAccounts
(
id
:
number
):
Promise
<
P
aginatedResponse
<
any
>
>
{
const
{
data
}
=
await
apiClient
.
get
<
P
aginatedResponse
<
any
>
>
(
`/admin/proxies/
${
id
}
/accounts`
)
export
async
function
getProxyAccounts
(
id
:
number
):
Promise
<
P
roxyAccountSummary
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
P
roxyAccountSummary
[]
>
(
`/admin/proxies/
${
id
}
/accounts`
)
return
data
}
...
...
@@ -189,6 +195,17 @@ export async function batchCreate(
return
data
}
export
async
function
batchDelete
(
ids
:
number
[]):
Promise
<
{
deleted_ids
:
number
[]
skipped
:
Array
<
{
id
:
number
;
reason
:
string
}
>
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
deleted_ids
:
number
[]
skipped
:
Array
<
{
id
:
number
;
reason
:
string
}
>
}
>
(
'
/admin/proxies/batch-delete
'
,
{
ids
})
return
data
}
export
const
proxiesAPI
=
{
list
,
getAll
,
...
...
@@ -201,7 +218,8 @@ export const proxiesAPI = {
testProxy
,
getStats
,
getProxyAccounts
,
batchCreate
batchCreate
,
batchDelete
}
export
default
proxiesAPI
frontend/src/components/common/DataTable.vue
View file @
28de614d
...
...
@@ -22,29 +22,36 @@
]"
@click="column.sortable
&&
handleSort(column.key)"
>
<div
class=
"flex items-center space-x-1"
>
<span>
{{
column
.
label
}}
</span>
<span
v-if=
"column.sortable"
class=
"text-gray-400 dark:text-dark-500"
>
<svg
v-if=
"sortKey === column.key"
class=
"h-4 w-4"
:class=
"
{ 'rotate-180 transform': sortOrder === 'desc' }"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule=
"evenodd"
d=
"M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clip-rule=
"evenodd"
/>
</svg>
<svg
v-else
class=
"h-4 w-4"
fill=
"currentColor"
viewBox=
"0 0 20 20"
>
<path
d=
"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
/>
</svg>
</span>
</div>
<slot
:name=
"`header-$
{column.key}`"
:column="column"
:sort-key="sortKey"
:sort-order="sortOrder"
>
<div
class=
"flex items-center space-x-1"
>
<span>
{{
column
.
label
}}
</span>
<span
v-if=
"column.sortable"
class=
"text-gray-400 dark:text-dark-500"
>
<svg
v-if=
"sortKey === column.key"
class=
"h-4 w-4"
:class=
"
{ 'rotate-180 transform': sortOrder === 'desc' }"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule=
"evenodd"
d=
"M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clip-rule=
"evenodd"
/>
</svg>
<svg
v-else
class=
"h-4 w-4"
fill=
"currentColor"
viewBox=
"0 0 20 20"
>
<path
d=
"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
/>
</svg>
</span>
</div>
</slot>
</th>
</tr>
</thead>
...
...
frontend/src/i18n/locales/en.ts
View file @
28de614d
...
...
@@ -1633,11 +1633,29 @@ export default {
address
:
'
Address
'
,
status
:
'
Status
'
,
accounts
:
'
Accounts
'
,
latency
:
'
Latency
'
,
actions
:
'
Actions
'
},
testConnection
:
'
Test Connection
'
,
batchTest
:
'
Test All Proxies
'
,
testFailed
:
'
Failed
'
,
latencyFailed
:
'
Connection failed
'
,
batchTestEmpty
:
'
No proxies available for testing
'
,
batchTestDone
:
'
Batch test completed for {count} proxies
'
,
batchTestFailed
:
'
Batch test failed
'
,
batchDeleteAction
:
'
Delete
'
,
batchDelete
:
'
Batch delete
'
,
batchDeleteConfirm
:
'
Delete {count} selected proxies? In-use ones will be skipped.
'
,
batchDeleteDone
:
'
Deleted {deleted} proxies, skipped {skipped}
'
,
batchDeleteSkipped
:
'
Skipped {skipped} proxies
'
,
batchDeleteFailed
:
'
Batch delete failed
'
,
deleteBlockedInUse
:
'
This proxy is in use and cannot be deleted
'
,
accountsTitle
:
'
Accounts using this IP
'
,
accountsEmpty
:
'
No accounts are using this proxy
'
,
accountsFailed
:
'
Failed to load accounts list
'
,
accountName
:
'
Account
'
,
accountPlatform
:
'
Platform
'
,
accountNotes
:
'
Notes
'
,
name
:
'
Name
'
,
protocol
:
'
Protocol
'
,
host
:
'
Host
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
28de614d
...
...
@@ -1719,6 +1719,7 @@ export default {
address
:
'
地址
'
,
status
:
'
状态
'
,
accounts
:
'
账号数
'
,
latency
:
'
延迟
'
,
actions
:
'
操作
'
,
nameLabel
:
'
名称
'
,
namePlaceholder
:
'
请输入代理名称
'
,
...
...
@@ -1755,11 +1756,32 @@ export default {
enterProxyName
:
'
请输入代理名称
'
,
optionalAuth
:
'
可选认证信息
'
,
leaveEmptyToKeep
:
'
留空保持不变
'
,
form
:
{
hostPlaceholder
:
'
请输入主机地址
'
,
portPlaceholder
:
'
请输入端口
'
},
noProxiesYet
:
'
暂无代理
'
,
createFirstProxy
:
'
添加您的第一个代理以开始使用。
'
,
testConnection
:
'
测试连接
'
,
batchTest
:
'
批量测试
'
,
testFailed
:
'
失败
'
,
latencyFailed
:
'
链接失败
'
,
batchTestEmpty
:
'
暂无可测试的代理
'
,
batchTestDone
:
'
批量测试完成,共测试 {count} 个代理
'
,
batchTestFailed
:
'
批量测试失败
'
,
batchDeleteAction
:
'
删除
'
,
batchDelete
:
'
批量删除
'
,
batchDeleteConfirm
:
'
确定删除选中的 {count} 个代理吗?已被账号使用的将自动跳过。
'
,
batchDeleteDone
:
'
已删除 {deleted} 个代理,跳过 {skipped} 个
'
,
batchDeleteSkipped
:
'
已跳过 {skipped} 个代理
'
,
batchDeleteFailed
:
'
批量删除失败
'
,
deleteBlockedInUse
:
'
该代理已有账号使用,无法删除
'
,
accountsTitle
:
'
使用该IP的账号
'
,
accountsEmpty
:
'
暂无账号使用此代理
'
,
accountsFailed
:
'
获取账号列表失败
'
,
accountName
:
'
账号名称
'
,
accountPlatform
:
'
所属平台
'
,
accountNotes
:
'
备注
'
,
// Batch import
standardAdd
:
'
标准添加
'
,
batchAdd
:
'
快捷添加
'
,
...
...
frontend/src/types/index.ts
View file @
28de614d
...
...
@@ -364,10 +364,21 @@ export interface Proxy {
password
?:
string
|
null
status
:
'
active
'
|
'
inactive
'
account_count
?:
number
// Number of accounts using this proxy
latency_ms
?:
number
latency_status
?:
'
success
'
|
'
failed
'
latency_message
?:
string
created_at
:
string
updated_at
:
string
}
export
interface
ProxyAccountSummary
{
id
:
number
name
:
string
platform
:
AccountPlatform
type
:
AccountType
notes
?:
string
|
null
}
// Gemini credentials structure for OAuth and API Key authentication
export
interface
GeminiCredentials
{
// API Key authentication
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment