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
9bdb45be
Commit
9bdb45be
authored
Jan 14, 2026
by
LLLLLLiulei
Browse files
feat: enhance proxy management
parent
b3b2868f
Changes
21
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -212,8 +212,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 @
9bdb45be
...
...
@@ -129,7 +129,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 @
9bdb45be
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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -262,11 +262,11 @@ func TestAPIContracts(t *testing.T) {
name
:
"GET /api/v1/admin/settings"
,
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
t
.
Helper
()
deps
.
settingRepo
.
SetAll
(
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyEmailVerifyEnabled
:
"false"
,
deps
.
settingRepo
.
SetAll
(
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyEmailVerifyEnabled
:
"false"
,
service
.
SettingKeySMTPHost
:
"smtp.example.com"
,
service
.
SettingKeySMTPHost
:
"smtp.example.com"
,
service
.
SettingKeySMTPPort
:
"587"
,
service
.
SettingKeySMTPUsername
:
"user"
,
service
.
SettingKeySMTPPassword
:
"secret"
,
...
...
@@ -285,15 +285,15 @@ func TestAPIContracts(t *testing.T) {
service
.
SettingKeyContactInfo
:
"support"
,
service
.
SettingKeyDocURL
:
"https://docs.example.com"
,
service
.
SettingKeyDefaultConcurrency
:
"5"
,
service
.
SettingKeyDefaultBalance
:
"1.25"
,
service
.
SettingKeyDefaultConcurrency
:
"5"
,
service
.
SettingKeyDefaultBalance
:
"1.25"
,
service
.
SettingKeyOpsMonitoringEnabled
:
"false"
,
service
.
SettingKeyOpsRealtimeMonitoringEnabled
:
"true"
,
service
.
SettingKeyOpsQueryModeDefault
:
"auto"
,
service
.
SettingKeyOpsMetricsIntervalSeconds
:
"60"
,
})
},
service
.
SettingKeyOpsMonitoringEnabled
:
"false"
,
service
.
SettingKeyOpsRealtimeMonitoringEnabled
:
"true"
,
service
.
SettingKeyOpsQueryModeDefault
:
"auto"
,
service
.
SettingKeyOpsMetricsIntervalSeconds
:
"60"
,
})
},
method
:
http
.
MethodGet
,
path
:
"/api/v1/admin/settings"
,
wantStatus
:
http
.
StatusOK
,
...
...
@@ -435,7 +435,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
)
...
...
backend/internal/server/routes/admin.go
View file @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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
)
...
...
@@ -220,6 +221,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"`
...
...
@@ -254,6 +265,7 @@ type adminServiceImpl struct {
redeemCodeRepo
RedeemCodeRepository
billingCacheService
*
BillingCacheService
proxyProber
ProxyExitInfoProber
proxyLatencyCache
ProxyLatencyCache
authCacheInvalidator
APIKeyAuthCacheInvalidator
}
...
...
@@ -267,6 +279,7 @@ func NewAdminService(
redeemCodeRepo
RedeemCodeRepository
,
billingCacheService
*
BillingCacheService
,
proxyProber
ProxyExitInfoProber
,
proxyLatencyCache
ProxyLatencyCache
,
authCacheInvalidator
APIKeyAuthCacheInvalidator
,
)
AdminService
{
return
&
adminServiceImpl
{
...
...
@@ -278,6 +291,7 @@ func NewAdminService(
redeemCodeRepo
:
redeemCodeRepo
,
billingCacheService
:
billingCacheService
,
proxyProber
:
proxyProber
,
proxyLatencyCache
:
proxyLatencyCache
,
authCacheInvalidator
:
authCacheInvalidator
,
}
}
...
...
@@ -1069,6 +1083,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
}
...
...
@@ -1077,7 +1092,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
)
{
...
...
@@ -1097,6 +1117,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
}
...
...
@@ -1135,12 +1157,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
)
{
...
...
@@ -1240,12 +1303,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"
,
...
...
@@ -1257,6 +1332,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
{
...
...
@@ -1306,6 +1404,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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -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 @
9bdb45be
...
...
@@ -1627,11 +1627,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 @
9bdb45be
...
...
@@ -1713,6 +1713,7 @@ export default {
address
:
'
地址
'
,
status
:
'
状态
'
,
accounts
:
'
账号数
'
,
latency
:
'
延迟
'
,
actions
:
'
操作
'
,
nameLabel
:
'
名称
'
,
namePlaceholder
:
'
请输入代理名称
'
,
...
...
@@ -1749,11 +1750,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 @
9bdb45be
...
...
@@ -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