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
10bc7f70
Unverified
Commit
10bc7f70
authored
Jan 15, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 15, 2026
Browse files
Merge pull request #297 from LLLLLLiulei/feat/ip-management-enhancements
feat: add proxy geo location
parents
47eb3c88
aab44f9f
Changes
13
Hide whitespace changes
Inline
Side-by-side
.gitignore
View file @
10bc7f70
...
...
@@ -83,6 +83,8 @@ temp/
*.log
*.bak
.cache/
.dev/
.serena/
# ===================
# 构建产物
...
...
backend/internal/handler/dto/mappers.go
View file @
10bc7f70
...
...
@@ -218,6 +218,11 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
LatencyMs
:
p
.
LatencyMs
,
LatencyStatus
:
p
.
LatencyStatus
,
LatencyMessage
:
p
.
LatencyMessage
,
IPAddress
:
p
.
IPAddress
,
Country
:
p
.
Country
,
CountryCode
:
p
.
CountryCode
,
Region
:
p
.
Region
,
City
:
p
.
City
,
}
}
...
...
backend/internal/handler/dto/types.go
View file @
10bc7f70
...
...
@@ -134,6 +134,11 @@ type ProxyWithAccountCount struct {
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
LatencyStatus
string
`json:"latency_status,omitempty"`
LatencyMessage
string
`json:"latency_message,omitempty"`
IPAddress
string
`json:"ip_address,omitempty"`
Country
string
`json:"country,omitempty"`
CountryCode
string
`json:"country_code,omitempty"`
Region
string
`json:"region,omitempty"`
City
string
`json:"city,omitempty"`
}
type
ProxyAccountSummary
struct
{
...
...
backend/internal/repository/proxy_probe_service.go
View file @
10bc7f70
...
...
@@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
...
...
@@ -35,7 +36,7 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
}
const
(
defaultIPInfoURL
=
"http
s
://ip
info.io/json
"
defaultIPInfoURL
=
"http://ip
-api.com/json/?lang=zh-CN
"
defaultProxyProbeTimeout
=
30
*
time
.
Second
)
...
...
@@ -78,10 +79,14 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
}
var
ipInfo
struct
{
IP
string
`json:"ip"`
City
string
`json:"city"`
Region
string
`json:"region"`
Country
string
`json:"country"`
Status
string
`json:"status"`
Message
string
`json:"message"`
Query
string
`json:"query"`
City
string
`json:"city"`
Region
string
`json:"region"`
RegionName
string
`json:"regionName"`
Country
string
`json:"country"`
CountryCode
string
`json:"countryCode"`
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
...
...
@@ -92,11 +97,22 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
if
err
:=
json
.
Unmarshal
(
body
,
&
ipInfo
);
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to parse response: %w"
,
err
)
}
if
strings
.
ToLower
(
ipInfo
.
Status
)
!=
"success"
{
if
ipInfo
.
Message
==
""
{
ipInfo
.
Message
=
"ip-api request failed"
}
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"ip-api request failed: %s"
,
ipInfo
.
Message
)
}
region
:=
ipInfo
.
RegionName
if
region
==
""
{
region
=
ipInfo
.
Region
}
return
&
service
.
ProxyExitInfo
{
IP
:
ipInfo
.
IP
,
City
:
ipInfo
.
City
,
Region
:
ipInfo
.
Region
,
Country
:
ipInfo
.
Country
,
IP
:
ipInfo
.
Query
,
City
:
ipInfo
.
City
,
Region
:
region
,
Country
:
ipInfo
.
Country
,
CountryCode
:
ipInfo
.
CountryCode
,
},
latencyMs
,
nil
}
backend/internal/repository/proxy_probe_service_test.go
View file @
10bc7f70
...
...
@@ -21,7 +21,7 @@ type ProxyProbeServiceSuite struct {
func
(
s
*
ProxyProbeServiceSuite
)
SetupTest
()
{
s
.
ctx
=
context
.
Background
()
s
.
prober
=
&
proxyProbeService
{
ipInfoURL
:
"http://ip
info
.test/json"
,
ipInfoURL
:
"http://ip
-api
.test/json
/?lang=zh-CN
"
,
allowPrivateHosts
:
true
,
}
}
...
...
@@ -54,7 +54,7 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
seen
<-
r
.
RequestURI
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
`{"
ip
":"1.2.3.4","city":"c","region":"r","country":"cc"}`
)
_
,
_
=
io
.
WriteString
(
w
,
`{"
status":"success","query
":"1.2.3.4","city":"c","region
Name
":"r","country":"cc"
,"countryCode":"CC"
}`
)
}))
info
,
latencyMs
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
...
...
@@ -64,11 +64,12 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
require
.
Equal
(
s
.
T
(),
"c"
,
info
.
City
)
require
.
Equal
(
s
.
T
(),
"r"
,
info
.
Region
)
require
.
Equal
(
s
.
T
(),
"cc"
,
info
.
Country
)
require
.
Equal
(
s
.
T
(),
"CC"
,
info
.
CountryCode
)
// Verify proxy received the request
select
{
case
uri
:=
<-
seen
:
require
.
Contains
(
s
.
T
(),
uri
,
"ip
info
.test"
,
"expected request to go through proxy"
)
require
.
Contains
(
s
.
T
(),
uri
,
"ip
-api
.test"
,
"expected request to go through proxy"
)
default
:
require
.
Fail
(
s
.
T
(),
"expected proxy to receive request"
)
}
...
...
backend/internal/service/admin_service.go
View file @
10bc7f70
...
...
@@ -236,21 +236,23 @@ type ProxyBatchDeleteSkipped struct {
// ProxyTestResult represents the result of testing a proxy
type
ProxyTestResult
struct
{
Success
bool
`json:"success"`
Message
string
`json:"message"`
LatencyMs
int64
`json:"latency_ms,omitempty"`
IPAddress
string
`json:"ip_address,omitempty"`
City
string
`json:"city,omitempty"`
Region
string
`json:"region,omitempty"`
Country
string
`json:"country,omitempty"`
Success
bool
`json:"success"`
Message
string
`json:"message"`
LatencyMs
int64
`json:"latency_ms,omitempty"`
IPAddress
string
`json:"ip_address,omitempty"`
City
string
`json:"city,omitempty"`
Region
string
`json:"region,omitempty"`
Country
string
`json:"country,omitempty"`
CountryCode
string
`json:"country_code,omitempty"`
}
// ProxyExitInfo represents proxy exit information from ip
info.io
// ProxyExitInfo represents proxy exit information from ip
-api.com
type
ProxyExitInfo
struct
{
IP
string
City
string
Region
string
Country
string
IP
string
City
string
Region
string
Country
string
CountryCode
string
}
// ProxyExitInfoProber tests proxy connectivity and retrieves exit information
...
...
@@ -1340,19 +1342,25 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
latency
:=
latencyMs
s
.
saveProxyLatency
(
ctx
,
id
,
&
ProxyLatencyInfo
{
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
UpdatedAt
:
time
.
Now
(),
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
IPAddress
:
exitInfo
.
IP
,
Country
:
exitInfo
.
Country
,
CountryCode
:
exitInfo
.
CountryCode
,
Region
:
exitInfo
.
Region
,
City
:
exitInfo
.
City
,
UpdatedAt
:
time
.
Now
(),
})
return
&
ProxyTestResult
{
Success
:
true
,
Message
:
"Proxy is accessible"
,
LatencyMs
:
latencyMs
,
IPAddress
:
exitInfo
.
IP
,
City
:
exitInfo
.
City
,
Region
:
exitInfo
.
Region
,
Country
:
exitInfo
.
Country
,
Success
:
true
,
Message
:
"Proxy is accessible"
,
LatencyMs
:
latencyMs
,
IPAddress
:
exitInfo
.
IP
,
City
:
exitInfo
.
City
,
Region
:
exitInfo
.
Region
,
Country
:
exitInfo
.
Country
,
CountryCode
:
exitInfo
.
CountryCode
,
},
nil
}
...
...
@@ -1360,7 +1368,7 @@ func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy)
if
s
.
proxyProber
==
nil
||
proxy
==
nil
{
return
}
_
,
latencyMs
,
err
:=
s
.
proxyProber
.
ProbeProxy
(
ctx
,
proxy
.
URL
())
exitInfo
,
latencyMs
,
err
:=
s
.
proxyProber
.
ProbeProxy
(
ctx
,
proxy
.
URL
())
if
err
!=
nil
{
s
.
saveProxyLatency
(
ctx
,
proxy
.
ID
,
&
ProxyLatencyInfo
{
Success
:
false
,
...
...
@@ -1372,10 +1380,15 @@ func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy)
latency
:=
latencyMs
s
.
saveProxyLatency
(
ctx
,
proxy
.
ID
,
&
ProxyLatencyInfo
{
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
UpdatedAt
:
time
.
Now
(),
Success
:
true
,
LatencyMs
:
&
latency
,
Message
:
"Proxy is accessible"
,
IPAddress
:
exitInfo
.
IP
,
Country
:
exitInfo
.
Country
,
CountryCode
:
exitInfo
.
CountryCode
,
Region
:
exitInfo
.
Region
,
City
:
exitInfo
.
City
,
UpdatedAt
:
time
.
Now
(),
})
}
...
...
@@ -1456,6 +1469,11 @@ func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []Pro
proxies
[
i
]
.
LatencyStatus
=
"failed"
}
proxies
[
i
]
.
LatencyMessage
=
info
.
Message
proxies
[
i
]
.
IPAddress
=
info
.
IPAddress
proxies
[
i
]
.
Country
=
info
.
Country
proxies
[
i
]
.
CountryCode
=
info
.
CountryCode
proxies
[
i
]
.
Region
=
info
.
Region
proxies
[
i
]
.
City
=
info
.
City
}
}
...
...
backend/internal/service/proxy.go
View file @
10bc7f70
...
...
@@ -35,6 +35,11 @@ type ProxyWithAccountCount struct {
LatencyMs
*
int64
LatencyStatus
string
LatencyMessage
string
IPAddress
string
Country
string
CountryCode
string
Region
string
City
string
}
type
ProxyAccountSummary
struct
{
...
...
backend/internal/service/proxy_latency_cache.go
View file @
10bc7f70
...
...
@@ -6,10 +6,15 @@ import (
)
type
ProxyLatencyInfo
struct
{
Success
bool
`json:"success"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
Message
string
`json:"message,omitempty"`
UpdatedAt
time
.
Time
`json:"updated_at"`
Success
bool
`json:"success"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
Message
string
`json:"message,omitempty"`
IPAddress
string
`json:"ip_address,omitempty"`
Country
string
`json:"country,omitempty"`
CountryCode
string
`json:"country_code,omitempty"`
Region
string
`json:"region,omitempty"`
City
string
`json:"city,omitempty"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
type
ProxyLatencyCache
interface
{
...
...
frontend/src/api/admin/proxies.ts
View file @
10bc7f70
...
...
@@ -126,6 +126,7 @@ export async function testProxy(id: number): Promise<{
city
?:
string
region
?:
string
country
?:
string
country_code
?:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
success
:
boolean
...
...
@@ -135,6 +136,7 @@ export async function testProxy(id: number): Promise<{
city
?:
string
region
?:
string
country
?:
string
country_code
?:
string
}
>
(
`/admin/proxies/
${
id
}
/test`
)
return
data
}
...
...
frontend/src/i18n/locales/en.ts
View file @
10bc7f70
...
...
@@ -1634,6 +1634,7 @@ export default {
name
:
'
Name
'
,
protocol
:
'
Protocol
'
,
address
:
'
Address
'
,
location
:
'
Location
'
,
status
:
'
Status
'
,
accounts
:
'
Accounts
'
,
latency
:
'
Latency
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
10bc7f70
...
...
@@ -1719,6 +1719,7 @@ export default {
name
:
'
名称
'
,
protocol
:
'
协议
'
,
address
:
'
地址
'
,
location
:
'
地理位置
'
,
status
:
'
状态
'
,
accounts
:
'
账号数
'
,
latency
:
'
延迟
'
,
...
...
frontend/src/types/index.ts
View file @
10bc7f70
...
...
@@ -367,6 +367,11 @@ export interface Proxy {
latency_ms
?:
number
latency_status
?:
'
success
'
|
'
failed
'
latency_message
?:
string
ip_address
?:
string
country
?:
string
country_code
?:
string
region
?:
string
city
?:
string
created_at
:
string
updated_at
:
string
}
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
10bc7f70
...
...
@@ -117,6 +117,21 @@
<code
class=
"code text-xs"
>
{{
row
.
host
}}
:
{{
row
.
port
}}
</code>
</
template
>
<
template
#cell-location=
"{ row }"
>
<div
class=
"flex items-center gap-2"
>
<img
v-if=
"row.country_code"
:src=
"flagUrl(row.country_code)"
:alt=
"row.country || row.country_code"
class=
"h-4 w-6 rounded-sm"
/>
<span
v-if=
"formatLocation(row)"
class=
"text-sm text-gray-700 dark:text-gray-200"
>
{{
formatLocation
(
row
)
}}
</span>
<span
v-else
class=
"text-sm text-gray-400"
>
-
</span>
</div>
</
template
>
<
template
#cell-account_count=
"{ row, value }"
>
<button
v-if=
"(value || 0) > 0"
...
...
@@ -665,6 +680,7 @@ const columns = computed<Column[]>(() => [
{
key
:
'
name
'
,
label
:
t
(
'
admin.proxies.columns.name
'
),
sortable
:
true
}
,
{
key
:
'
protocol
'
,
label
:
t
(
'
admin.proxies.columns.protocol
'
),
sortable
:
true
}
,
{
key
:
'
address
'
,
label
:
t
(
'
admin.proxies.columns.address
'
),
sortable
:
false
}
,
{
key
:
'
location
'
,
label
:
t
(
'
admin.proxies.columns.location
'
),
sortable
:
false
}
,
{
key
:
'
account_count
'
,
label
:
t
(
'
admin.proxies.columns.accounts
'
),
sortable
:
true
}
,
{
key
:
'
latency
'
,
label
:
t
(
'
admin.proxies.columns.latency
'
),
sortable
:
false
}
,
{
key
:
'
status
'
,
label
:
t
(
'
admin.proxies.columns.status
'
),
sortable
:
true
}
,
...
...
@@ -1058,20 +1074,47 @@ const handleUpdateProxy = async () => {
const
applyLatencyResult
=
(
proxyId
:
number
,
result
:
{
success
:
boolean
;
latency_ms
?:
number
;
message
?:
string
}
result
:
{
success
:
boolean
latency_ms
?:
number
message
?:
string
ip_address
?:
string
country
?:
string
country_code
?:
string
region
?:
string
city
?:
string
}
)
=>
{
const
target
=
proxies
.
value
.
find
((
proxy
)
=>
proxy
.
id
===
proxyId
)
if
(
!
target
)
return
if
(
result
.
success
)
{
target
.
latency_status
=
'
success
'
target
.
latency_ms
=
result
.
latency_ms
target
.
ip_address
=
result
.
ip_address
target
.
country
=
result
.
country
target
.
country_code
=
result
.
country_code
target
.
region
=
result
.
region
target
.
city
=
result
.
city
}
else
{
target
.
latency_status
=
'
failed
'
target
.
latency_ms
=
undefined
target
.
ip_address
=
undefined
target
.
country
=
undefined
target
.
country_code
=
undefined
target
.
region
=
undefined
target
.
city
=
undefined
}
target
.
latency_message
=
result
.
message
}
const
formatLocation
=
(
proxy
:
Proxy
)
=>
{
const
parts
=
[
proxy
.
country
,
proxy
.
city
].
filter
(
Boolean
)
as
string
[]
return
parts
.
join
(
'
·
'
)
}
const
flagUrl
=
(
code
:
string
)
=>
`https://unpkg.com/flag-icons/flags/4x3/${code.toLowerCase()
}
.svg`
const
startTestingProxy
=
(
proxyId
:
number
)
=>
{
testingProxyIds
.
value
=
new
Set
([...
testingProxyIds
.
value
,
proxyId
])
}
...
...
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