diff --git a/.gitignore b/.gitignore
index 978c15ac..48172982 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,6 +83,8 @@ temp/
*.log
*.bak
.cache/
+.dev/
+.serena/
# ===================
# 构建产物
diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go
index 4980fd66..371f4f52 100644
--- a/backend/internal/handler/dto/mappers.go
+++ b/backend/internal/handler/dto/mappers.go
@@ -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,
}
}
diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go
index 0da21233..0cbc809b 100644
--- a/backend/internal/handler/dto/types.go
+++ b/backend/internal/handler/dto/types.go
@@ -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 {
diff --git a/backend/internal/repository/proxy_probe_service.go b/backend/internal/repository/proxy_probe_service.go
index ad7b6e1c..fb6f405e 100644
--- a/backend/internal/repository/proxy_probe_service.go
+++ b/backend/internal/repository/proxy_probe_service.go
@@ -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 = "https://ipinfo.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
}
diff --git a/backend/internal/repository/proxy_probe_service_test.go b/backend/internal/repository/proxy_probe_service_test.go
index fe45adbb..f1cd5721 100644
--- a/backend/internal/repository/proxy_probe_service_test.go
+++ b/backend/internal/repository/proxy_probe_service_test.go
@@ -21,7 +21,7 @@ type ProxyProbeServiceSuite struct {
func (s *ProxyProbeServiceSuite) SetupTest() {
s.ctx = context.Background()
s.prober = &proxyProbeService{
- ipInfoURL: "http://ipinfo.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","regionName":"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, "ipinfo.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")
}
diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go
index 04b367e8..1e32699c 100644
--- a/backend/internal/service/admin_service.go
+++ b/backend/internal/service/admin_service.go
@@ -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 ipinfo.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
}
}
diff --git a/backend/internal/service/proxy.go b/backend/internal/service/proxy.go
index 9cb31808..7eb7728f 100644
--- a/backend/internal/service/proxy.go
+++ b/backend/internal/service/proxy.go
@@ -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 {
diff --git a/backend/internal/service/proxy_latency_cache.go b/backend/internal/service/proxy_latency_cache.go
index 2901df0b..4a1cc77b 100644
--- a/backend/internal/service/proxy_latency_cache.go
+++ b/backend/internal/service/proxy_latency_cache.go
@@ -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 {
diff --git a/frontend/src/api/admin/proxies.ts b/frontend/src/api/admin/proxies.ts
index fc526f7a..1af2ea39 100644
--- a/frontend/src/api/admin/proxies.ts
+++ b/frontend/src/api/admin/proxies.ts
@@ -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
}
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 09ebe8e1..3dfbd834 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -1634,6 +1634,7 @@ export default {
name: 'Name',
protocol: 'Protocol',
address: 'Address',
+ location: 'Location',
status: 'Status',
accounts: 'Accounts',
latency: 'Latency',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index f01c1e4b..daf39939 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -1719,6 +1719,7 @@ export default {
name: '名称',
protocol: '协议',
address: '地址',
+ location: '地理位置',
status: '状态',
accounts: '账号数',
latency: '延迟',
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index dabc260d..5c1e307c 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -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
}
diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue
index 9d876972..3bd766b6 100644
--- a/frontend/src/views/admin/ProxiesView.vue
+++ b/frontend/src/views/admin/ProxiesView.vue
@@ -117,6 +117,21 @@
{{ row.host }}:{{ row.port }}
+
+
+
![]()
+
+ {{ formatLocation(row) }}
+
+
-
+
+
+