feat(proxy): 持久化质量检测结果并在列表展示
This commit is contained in:
@@ -303,6 +303,11 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
|
|||||||
CountryCode: p.CountryCode,
|
CountryCode: p.CountryCode,
|
||||||
Region: p.Region,
|
Region: p.Region,
|
||||||
City: p.City,
|
City: p.City,
|
||||||
|
QualityStatus: p.QualityStatus,
|
||||||
|
QualityScore: p.QualityScore,
|
||||||
|
QualityGrade: p.QualityGrade,
|
||||||
|
QualitySummary: p.QualitySummary,
|
||||||
|
QualityChecked: p.QualityChecked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ type ProxyWithAccountCount struct {
|
|||||||
CountryCode string `json:"country_code,omitempty"`
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
City string `json:"city,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 {
|
type ProxyAccountSummary struct {
|
||||||
|
|||||||
@@ -1796,6 +1796,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr
|
|||||||
})
|
})
|
||||||
result.FailedCount++
|
result.FailedCount++
|
||||||
finalizeProxyQualityResult(result)
|
finalizeProxyQualityResult(result)
|
||||||
|
s.saveProxyQualitySnapshot(ctx, id, result, nil)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1809,6 +1810,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr
|
|||||||
})
|
})
|
||||||
result.FailedCount++
|
result.FailedCount++
|
||||||
finalizeProxyQualityResult(result)
|
finalizeProxyQualityResult(result)
|
||||||
|
s.saveProxyQualitySnapshot(ctx, id, result, nil)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1838,6 +1840,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr
|
|||||||
})
|
})
|
||||||
result.FailedCount++
|
result.FailedCount++
|
||||||
finalizeProxyQualityResult(result)
|
finalizeProxyQualityResult(result)
|
||||||
|
s.saveProxyQualitySnapshot(ctx, id, result, exitInfo)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1857,6 +1860,7 @@ func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalizeProxyQualityResult(result)
|
finalizeProxyQualityResult(result)
|
||||||
|
s.saveProxyQualitySnapshot(ctx, id, result, exitInfo)
|
||||||
return result, nil
|
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) {
|
func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy) {
|
||||||
if s.proxyProber == nil || proxy == nil {
|
if s.proxyProber == nil || proxy == nil {
|
||||||
return
|
return
|
||||||
@@ -2064,6 +2142,11 @@ func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []Pro
|
|||||||
proxies[i].CountryCode = info.CountryCode
|
proxies[i].CountryCode = info.CountryCode
|
||||||
proxies[i].Region = info.Region
|
proxies[i].Region = info.Region
|
||||||
proxies[i].City = info.City
|
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 {
|
if s.proxyLatencyCache == nil || info == nil {
|
||||||
return
|
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)
|
logger.LegacyPrintf("service.admin", "Warning: store proxy latency cache failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ type ProxyWithAccountCount struct {
|
|||||||
CountryCode string
|
CountryCode string
|
||||||
Region string
|
Region string
|
||||||
City string
|
City string
|
||||||
|
QualityStatus string
|
||||||
|
QualityScore *int
|
||||||
|
QualityGrade string
|
||||||
|
QualitySummary string
|
||||||
|
QualityChecked *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyAccountSummary struct {
|
type ProxyAccountSummary struct {
|
||||||
|
|||||||
@@ -6,15 +6,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProxyLatencyInfo struct {
|
type ProxyLatencyInfo struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
IPAddress string `json:"ip_address,omitempty"`
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
CountryCode string `json:"country_code,omitempty"`
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
City string `json:"city,omitempty"`
|
City string `json:"city,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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 {
|
type ProxyLatencyCache interface {
|
||||||
|
|||||||
@@ -2181,6 +2181,8 @@ export default {
|
|||||||
qualityTableStatus: 'Status',
|
qualityTableStatus: 'Status',
|
||||||
qualityTableLatency: 'Latency',
|
qualityTableLatency: 'Latency',
|
||||||
qualityTableMessage: 'Message',
|
qualityTableMessage: 'Message',
|
||||||
|
qualityInline: 'Quality {grade}/{score}',
|
||||||
|
qualityStatusHealthy: 'Healthy',
|
||||||
qualityStatusPass: 'Pass',
|
qualityStatusPass: 'Pass',
|
||||||
qualityStatusWarn: 'Warn',
|
qualityStatusWarn: 'Warn',
|
||||||
qualityStatusFail: 'Fail',
|
qualityStatusFail: 'Fail',
|
||||||
|
|||||||
@@ -2310,6 +2310,8 @@ export default {
|
|||||||
qualityTableStatus: '状态',
|
qualityTableStatus: '状态',
|
||||||
qualityTableLatency: '延迟',
|
qualityTableLatency: '延迟',
|
||||||
qualityTableMessage: '说明',
|
qualityTableMessage: '说明',
|
||||||
|
qualityInline: '质量 {grade}/{score}',
|
||||||
|
qualityStatusHealthy: '优质',
|
||||||
qualityStatusPass: '通过',
|
qualityStatusPass: '通过',
|
||||||
qualityStatusWarn: '告警',
|
qualityStatusWarn: '告警',
|
||||||
qualityStatusFail: '失败',
|
qualityStatusFail: '失败',
|
||||||
|
|||||||
@@ -512,6 +512,11 @@ export interface Proxy {
|
|||||||
country_code?: string
|
country_code?: string
|
||||||
region?: string
|
region?: string
|
||||||
city?: string
|
city?: string
|
||||||
|
quality_status?: 'healthy' | 'warn' | 'challenge' | 'failed'
|
||||||
|
quality_score?: number
|
||||||
|
quality_grade?: string
|
||||||
|
quality_summary?: string
|
||||||
|
quality_checked?: number
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,20 +160,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-latency="{ row }">
|
<template #cell-latency="{ row }">
|
||||||
<span
|
<div class="flex flex-col gap-1">
|
||||||
v-if="row.latency_status === 'failed'"
|
<span
|
||||||
class="badge badge-danger"
|
v-if="row.latency_status === 'failed'"
|
||||||
:title="row.latency_message || undefined"
|
class="badge badge-danger"
|
||||||
>
|
:title="row.latency_message || undefined"
|
||||||
{{ t('admin.proxies.latencyFailed') }}
|
>
|
||||||
</span>
|
{{ t('admin.proxies.latencyFailed') }}
|
||||||
<span
|
</span>
|
||||||
v-else-if="typeof row.latency_ms === 'number'"
|
<span
|
||||||
:class="['badge', row.latency_ms < 200 ? 'badge-success' : 'badge-warning']"
|
v-else-if="typeof row.latency_ms === 'number'"
|
||||||
>
|
:class="['badge', row.latency_ms < 200 ? 'badge-success' : 'badge-warning']"
|
||||||
{{ row.latency_ms }}ms
|
>
|
||||||
</span>
|
{{ row.latency_ms }}ms
|
||||||
<span v-else class="text-sm text-gray-400">-</span>
|
</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>
|
||||||
|
|
||||||
<template #cell-status="{ value }">
|
<template #cell-status="{ value }">
|
||||||
@@ -1250,6 +1262,23 @@ const applyLatencyResult = (
|
|||||||
target.latency_message = result.message
|
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 formatLocation = (proxy: Proxy) => {
|
||||||
const parts = [proxy.country, proxy.city].filter(Boolean) as string[]
|
const parts = [proxy.country, proxy.city].filter(Boolean) as string[]
|
||||||
return parts.join(' · ')
|
return parts.join(' · ')
|
||||||
@@ -1330,6 +1359,7 @@ const handleQualityCheck = async (proxy: Proxy) => {
|
|||||||
country_code: result.country_code
|
country_code: result.country_code
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
applyQualityResult(proxy.id, result)
|
||||||
|
|
||||||
appStore.showSuccess(
|
appStore.showSuccess(
|
||||||
t('admin.proxies.qualityCheckDone', { score: result.score, grade: result.grade })
|
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) {
|
if (result.challenge_count > 0) {
|
||||||
challenge++
|
challenge++
|
||||||
} else if (result.failed_count > 0) {
|
} else if (result.failed_count > 0) {
|
||||||
@@ -1422,6 +1453,20 @@ const qualityStatusLabel = (status: string) => {
|
|||||||
return t('admin.proxies.qualityStatusFail')
|
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) => {
|
const qualityTargetLabel = (target: string) => {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case 'base_connectivity':
|
case 'base_connectivity':
|
||||||
|
|||||||
Reference in New Issue
Block a user