From aab44f9fc8e9738898cd113e5af31c8c1c6183f1 Mon Sep 17 00:00:00 2001 From: LLLLLLiulei <1065070665@qq.com> Date: Thu, 15 Jan 2026 15:15:20 +0800 Subject: [PATCH] feat: add proxy geo location --- .gitignore | 2 + backend/internal/handler/dto/mappers.go | 5 ++ backend/internal/handler/dto/types.go | 5 ++ .../repository/proxy_probe_service.go | 34 ++++++--- .../repository/proxy_probe_service_test.go | 7 +- backend/internal/service/admin_service.go | 74 ++++++++++++------- backend/internal/service/proxy.go | 5 ++ .../internal/service/proxy_latency_cache.go | 13 +++- frontend/src/api/admin/proxies.ts | 2 + frontend/src/i18n/locales/en.ts | 1 + frontend/src/i18n/locales/zh.ts | 1 + frontend/src/types/index.ts | 5 ++ frontend/src/views/admin/ProxiesView.vue | 45 ++++++++++- 13 files changed, 154 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index fe715240..f317ed1a 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 }} + +