feat(proxy): 持久化质量检测结果并在列表展示

This commit is contained in:
yangjianbo
2026-02-20 12:13:04 +08:00
parent 48dc011b2a
commit d14c24bbf3
9 changed files with 202 additions and 24 deletions

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -2181,6 +2181,8 @@ export default {
qualityTableStatus: 'Status',
qualityTableLatency: 'Latency',
qualityTableMessage: 'Message',
qualityInline: 'Quality {grade}/{score}',
qualityStatusHealthy: 'Healthy',
qualityStatusPass: 'Pass',
qualityStatusWarn: 'Warn',
qualityStatusFail: 'Fail',

View File

@@ -2310,6 +2310,8 @@ export default {
qualityTableStatus: '状态',
qualityTableLatency: '延迟',
qualityTableMessage: '说明',
qualityInline: '质量 {grade}/{score}',
qualityStatusHealthy: '优质',
qualityStatusPass: '通过',
qualityStatusWarn: '告警',
qualityStatusFail: '失败',

View File

@@ -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
}

View File

@@ -160,20 +160,32 @@
</template>
<template #cell-latency="{ row }">
<span
v-if="row.latency_status === 'failed'"
class="badge badge-danger"
:title="row.latency_message || undefined"
>
{{ t('admin.proxies.latencyFailed') }}
</span>
<span
v-else-if="typeof row.latency_ms === 'number'"
:class="['badge', row.latency_ms < 200 ? 'badge-success' : 'badge-warning']"
>
{{ row.latency_ms }}ms
</span>
<span v-else class="text-sm text-gray-400">-</span>
<div class="flex flex-col gap-1">
<span
v-if="row.latency_status === 'failed'"
class="badge badge-danger"
:title="row.latency_message || undefined"
>
{{ t('admin.proxies.latencyFailed') }}
</span>
<span
v-else-if="typeof row.latency_ms === 'number'"
:class="['badge', row.latency_ms < 200 ? 'badge-success' : 'badge-warning']"
>
{{ row.latency_ms }}ms
</span>
<span v-else class="text-sm text-gray-400">-</span>
<div
v-if="typeof row.quality_checked === 'number'"
class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400"
:title="row.quality_summary || undefined"
>
<span>{{ t('admin.proxies.qualityInline', { grade: row.quality_grade || '-', score: row.quality_score ?? '-' }) }}</span>
<span class="badge" :class="qualityOverallClass(row.quality_status)">
{{ qualityOverallLabel(row.quality_status) }}
</span>
</div>
</div>
</template>
<template #cell-status="{ value }">
@@ -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':