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
3a100339
Commit
3a100339
authored
Jan 15, 2026
by
LLLLLLiulei
Browse files
feat: add proxy geo location
parent
47eb3c88
Changes
12
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/mappers.go
View file @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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
}
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -1719,6 +1719,7 @@ export default {
name
:
'
名称
'
,
protocol
:
'
协议
'
,
address
:
'
地址
'
,
location
:
'
地理位置
'
,
status
:
'
状态
'
,
accounts
:
'
账号数
'
,
latency
:
'
延迟
'
,
...
...
frontend/src/types/index.ts
View file @
3a100339
...
...
@@ -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 @
3a100339
...
...
@@ -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