diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 96688de1..dbc7a8bc 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -303,6 +303,11 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi CountryCode: p.CountryCode, Region: p.Region, City: p.City, + QualityStatus: p.QualityStatus, + QualityScore: p.QualityScore, + QualityGrade: p.QualityGrade, + QualitySummary: p.QualitySummary, + QualityChecked: p.QualityChecked, } } diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index a7abbe96..f2605ffc 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -202,6 +202,11 @@ type ProxyWithAccountCount struct { CountryCode string `json:"country_code,omitempty"` Region string `json:"region,omitempty"` City string `json:"city,omitempty"` + QualityStatus string `json:"quality_status,omitempty"` + QualityScore *int `json:"quality_score,omitempty"` + QualityGrade string `json:"quality_grade,omitempty"` + QualitySummary string `json:"quality_summary,omitempty"` + QualityChecked *int64 `json:"quality_checked,omitempty"` } type ProxyAccountSummary struct { diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index cde8a95a..715949a7 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -1796,6 +1796,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr }) result.FailedCount++ finalizeProxyQualityResult(result) + s.saveProxyQualitySnapshot(ctx, id, result, nil) return result, nil } @@ -1809,6 +1810,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr }) result.FailedCount++ finalizeProxyQualityResult(result) + s.saveProxyQualitySnapshot(ctx, id, result, nil) return result, nil } @@ -1838,6 +1840,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr }) result.FailedCount++ finalizeProxyQualityResult(result) + s.saveProxyQualitySnapshot(ctx, id, result, exitInfo) return result, nil } @@ -1857,6 +1860,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr } finalizeProxyQualityResult(result) + s.saveProxyQualitySnapshot(ctx, id, result, exitInfo) return result, nil } @@ -1954,6 +1958,80 @@ func proxyQualityGrade(score int) string { } } +func proxyQualityOverallStatus(result *ProxyQualityCheckResult) string { + if result == nil { + return "" + } + if result.ChallengeCount > 0 { + return "challenge" + } + if result.FailedCount > 0 { + return "failed" + } + if result.WarnCount > 0 { + return "warn" + } + if result.PassedCount > 0 { + return "healthy" + } + return "failed" +} + +func proxyQualityFirstCFRay(result *ProxyQualityCheckResult) string { + if result == nil { + return "" + } + for _, item := range result.Items { + if item.CFRay != "" { + return item.CFRay + } + } + return "" +} + +func proxyQualityBaseConnectivityPass(result *ProxyQualityCheckResult) bool { + if result == nil { + return false + } + for _, item := range result.Items { + if item.Target == "base_connectivity" { + return item.Status == "pass" + } + } + return false +} + +func (s *adminServiceImpl) saveProxyQualitySnapshot(ctx context.Context, proxyID int64, result *ProxyQualityCheckResult, exitInfo *ProxyExitInfo) { + if result == nil { + return + } + score := result.Score + checkedAt := result.CheckedAt + info := &ProxyLatencyInfo{ + Success: proxyQualityBaseConnectivityPass(result), + Message: result.Summary, + QualityStatus: proxyQualityOverallStatus(result), + QualityScore: &score, + QualityGrade: result.Grade, + QualitySummary: result.Summary, + QualityCheckedAt: &checkedAt, + QualityCFRay: proxyQualityFirstCFRay(result), + UpdatedAt: time.Now(), + } + if result.BaseLatencyMs > 0 { + latency := result.BaseLatencyMs + info.LatencyMs = &latency + } + if exitInfo != nil { + info.IPAddress = exitInfo.IP + info.Country = exitInfo.Country + info.CountryCode = exitInfo.CountryCode + info.Region = exitInfo.Region + info.City = exitInfo.City + } + s.saveProxyLatency(ctx, proxyID, info) +} + func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy) { if s.proxyProber == nil || proxy == nil { return @@ -2064,6 +2142,11 @@ func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []Pro proxies[i].CountryCode = info.CountryCode proxies[i].Region = info.Region proxies[i].City = info.City + proxies[i].QualityStatus = info.QualityStatus + proxies[i].QualityScore = info.QualityScore + proxies[i].QualityGrade = info.QualityGrade + proxies[i].QualitySummary = info.QualitySummary + proxies[i].QualityChecked = info.QualityCheckedAt } } @@ -2071,7 +2154,27 @@ func (s *adminServiceImpl) saveProxyLatency(ctx context.Context, proxyID int64, if s.proxyLatencyCache == nil || info == nil { return } - if err := s.proxyLatencyCache.SetProxyLatency(ctx, proxyID, info); err != nil { + + merged := *info + if latencies, err := s.proxyLatencyCache.GetProxyLatencies(ctx, []int64{proxyID}); err == nil { + if existing := latencies[proxyID]; existing != nil { + if merged.QualityCheckedAt == nil && + merged.QualityScore == nil && + merged.QualityGrade == "" && + merged.QualityStatus == "" && + merged.QualitySummary == "" && + merged.QualityCFRay == "" { + merged.QualityStatus = existing.QualityStatus + merged.QualityScore = existing.QualityScore + merged.QualityGrade = existing.QualityGrade + merged.QualitySummary = existing.QualitySummary + merged.QualityCheckedAt = existing.QualityCheckedAt + merged.QualityCFRay = existing.QualityCFRay + } + } + } + + if err := s.proxyLatencyCache.SetProxyLatency(ctx, proxyID, &merged); err != nil { logger.LegacyPrintf("service.admin", "Warning: store proxy latency cache failed: %v", err) } } diff --git a/backend/internal/service/proxy.go b/backend/internal/service/proxy.go index 7eb7728f..fc449091 100644 --- a/backend/internal/service/proxy.go +++ b/backend/internal/service/proxy.go @@ -40,6 +40,11 @@ type ProxyWithAccountCount struct { CountryCode string Region string City string + QualityStatus string + QualityScore *int + QualityGrade string + QualitySummary string + QualityChecked *int64 } type ProxyAccountSummary struct { diff --git a/backend/internal/service/proxy_latency_cache.go b/backend/internal/service/proxy_latency_cache.go index 4a1cc77b..f54bff88 100644 --- a/backend/internal/service/proxy_latency_cache.go +++ b/backend/internal/service/proxy_latency_cache.go @@ -6,15 +6,21 @@ import ( ) type ProxyLatencyInfo struct { - 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"` + 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"` + QualityStatus string `json:"quality_status,omitempty"` + QualityScore *int `json:"quality_score,omitempty"` + QualityGrade string `json:"quality_grade,omitempty"` + QualitySummary string `json:"quality_summary,omitempty"` + QualityCheckedAt *int64 `json:"quality_checked_at,omitempty"` + QualityCFRay string `json:"quality_cf_ray,omitempty"` + UpdatedAt time.Time `json:"updated_at"` } type ProxyLatencyCache interface { diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 33377834..7d9c52cd 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2181,6 +2181,8 @@ export default { qualityTableStatus: 'Status', qualityTableLatency: 'Latency', qualityTableMessage: 'Message', + qualityInline: 'Quality {grade}/{score}', + qualityStatusHealthy: 'Healthy', qualityStatusPass: 'Pass', qualityStatusWarn: 'Warn', qualityStatusFail: 'Fail', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 144e1598..1ca712cb 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2310,6 +2310,8 @@ export default { qualityTableStatus: '状态', qualityTableLatency: '延迟', qualityTableMessage: '说明', + qualityInline: '质量 {grade}/{score}', + qualityStatusHealthy: '优质', qualityStatusPass: '通过', qualityStatusWarn: '告警', qualityStatusFail: '失败', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9d6cf249..b6c7dd42 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -512,6 +512,11 @@ export interface Proxy { country_code?: string region?: string city?: string + quality_status?: 'healthy' | 'warn' | 'challenge' | 'failed' + quality_score?: number + quality_grade?: string + quality_summary?: string + quality_checked?: number created_at: string updated_at: string } diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue index 55c08474..23d73109 100644 --- a/frontend/src/views/admin/ProxiesView.vue +++ b/frontend/src/views/admin/ProxiesView.vue @@ -160,20 +160,32 @@ - - {{ t('admin.proxies.latencyFailed') }} - - - {{ row.latency_ms }}ms - - - + + + {{ t('admin.proxies.latencyFailed') }} + + + {{ row.latency_ms }}ms + + - + + {{ t('admin.proxies.qualityInline', { grade: row.quality_grade || '-', score: row.quality_score ?? '-' }) }} + + {{ qualityOverallLabel(row.quality_status) }} + + + @@ -1250,6 +1262,23 @@ const applyLatencyResult = ( target.latency_message = result.message } +const summarizeQualityStatus = (result: ProxyQualityCheckResult): Proxy['quality_status'] => { + if (result.challenge_count > 0) return 'challenge' + if (result.failed_count > 0) return 'failed' + if (result.warn_count > 0) return 'warn' + return 'healthy' +} + +const applyQualityResult = (proxyId: number, result: ProxyQualityCheckResult) => { + const target = proxies.value.find((proxy) => proxy.id === proxyId) + if (!target) return + target.quality_status = summarizeQualityStatus(result) + target.quality_score = result.score + target.quality_grade = result.grade + target.quality_summary = result.summary + target.quality_checked = result.checked_at +} + const formatLocation = (proxy: Proxy) => { const parts = [proxy.country, proxy.city].filter(Boolean) as string[] return parts.join(' · ') @@ -1330,6 +1359,7 @@ const handleQualityCheck = async (proxy: Proxy) => { country_code: result.country_code }) } + applyQualityResult(proxy.id, result) appStore.showSuccess( t('admin.proxies.qualityCheckDone', { score: result.score, grade: result.grade }) @@ -1374,6 +1404,7 @@ const runBatchProxyQualityChecks = async (ids: number[]) => { }) } } + applyQualityResult(current, result) if (result.challenge_count > 0) { challenge++ } else if (result.failed_count > 0) { @@ -1422,6 +1453,20 @@ const qualityStatusLabel = (status: string) => { return t('admin.proxies.qualityStatusFail') } +const qualityOverallClass = (status?: string) => { + if (status === 'healthy') return 'badge-success' + if (status === 'warn') return 'badge-warning' + if (status === 'challenge') return 'badge-danger' + return 'badge-danger' +} + +const qualityOverallLabel = (status?: string) => { + if (status === 'healthy') return t('admin.proxies.qualityStatusHealthy') + if (status === 'warn') return t('admin.proxies.qualityStatusWarn') + if (status === 'challenge') return t('admin.proxies.qualityStatusChallenge') + return t('admin.proxies.qualityStatusFail') +} + const qualityTargetLabel = (target: string) => { switch (target) { case 'base_connectivity':