admin_proxy.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "time"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  12. "github.com/Wei-Shaw/sub2api/internal/util/httputil"
  13. )
  14. // Proxy management implementations
  15. func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) {
  16. params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
  17. proxies, result, err := s.proxyRepo.ListWithFilters(ctx, params, protocol, status, search)
  18. if err != nil {
  19. return nil, 0, err
  20. }
  21. return proxies, result.Total, nil
  22. }
  23. func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error) {
  24. params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
  25. proxies, result, err := s.proxyRepo.ListWithFiltersAndAccountCount(ctx, params, protocol, status, search)
  26. if err != nil {
  27. return nil, 0, err
  28. }
  29. s.attachProxyLatency(ctx, proxies)
  30. return proxies, result.Total, nil
  31. }
  32. func (s *adminServiceImpl) GetAllProxies(ctx context.Context) ([]Proxy, error) {
  33. return s.proxyRepo.ListActive(ctx)
  34. }
  35. func (s *adminServiceImpl) GetAllProxiesWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error) {
  36. proxies, err := s.proxyRepo.ListActiveWithAccountCount(ctx)
  37. if err != nil {
  38. return nil, err
  39. }
  40. s.attachProxyLatency(ctx, proxies)
  41. return proxies, nil
  42. }
  43. func (s *adminServiceImpl) GetProxy(ctx context.Context, id int64) (*Proxy, error) {
  44. return s.proxyRepo.GetByID(ctx, id)
  45. }
  46. func (s *adminServiceImpl) GetProxiesByIDs(ctx context.Context, ids []int64) ([]Proxy, error) {
  47. return s.proxyRepo.ListByIDs(ctx, ids)
  48. }
  49. func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyInput) (*Proxy, error) {
  50. if !isJSONTimeInRange(input.ExpiresAt) {
  51. return nil, infraerrors.BadRequest("PROXY_EXPIRY_INVALID", "proxy expiry year must be between 0 and 9999")
  52. }
  53. // 规范化 fallback_mode
  54. mode := input.FallbackMode
  55. if mode == "" {
  56. mode = FallbackModeNone
  57. }
  58. // 校验:mode=proxy 必须有 backup
  59. if mode == FallbackModeProxy && input.BackupProxyID == nil {
  60. return nil, infraerrors.BadRequest("PROXY_BACKUP_REQUIRED", "backup proxy required when fallback_mode=proxy")
  61. }
  62. if input.ExpiryWarnDays < 0 {
  63. return nil, infraerrors.BadRequest("PROXY_WARN_DAYS_INVALID", "expiry_warn_days must be >= 0")
  64. }
  65. proxy := &Proxy{
  66. Name: input.Name,
  67. Protocol: input.Protocol,
  68. Host: input.Host,
  69. Port: input.Port,
  70. Username: input.Username,
  71. Password: input.Password,
  72. Status: StatusActive,
  73. ExpiresAt: input.ExpiresAt,
  74. FallbackMode: mode,
  75. BackupProxyID: input.BackupProxyID,
  76. ExpiryWarnDays: input.ExpiryWarnDays,
  77. }
  78. if err := s.proxyRepo.Create(ctx, proxy); err != nil {
  79. return nil, err
  80. }
  81. // Probe latency asynchronously so creation isn't blocked by network timeout.
  82. go s.probeProxyLatency(context.Background(), proxy)
  83. return proxy, nil
  84. }
  85. func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*Proxy, error) {
  86. if !isJSONTimeInRange(input.ExpiresAt) {
  87. return nil, infraerrors.BadRequest("PROXY_EXPIRY_INVALID", "proxy expiry year must be between 0 and 9999")
  88. }
  89. // 校验:backup_proxy_id 不能是自身
  90. if input.BackupProxyID != nil && *input.BackupProxyID == id {
  91. return nil, infraerrors.BadRequest("PROXY_BACKUP_SELF", "backup proxy cannot be itself")
  92. }
  93. proxy, err := s.proxyRepo.GetByID(ctx, id)
  94. if err != nil {
  95. return nil, err
  96. }
  97. // Merge only supplied fields, then validate the resulting fallback configuration.
  98. mode := proxy.FallbackMode
  99. if input.FallbackMode != "" {
  100. mode = input.FallbackMode
  101. }
  102. backupID := proxy.BackupProxyID
  103. if input.BackupProxyID != nil || input.ClearBackupID {
  104. backupID = input.BackupProxyID
  105. }
  106. if mode == FallbackModeProxy && backupID == nil {
  107. return nil, infraerrors.BadRequest("PROXY_BACKUP_REQUIRED", "backup proxy required when fallback_mode=proxy")
  108. }
  109. if input.ExpiryWarnDays != nil && *input.ExpiryWarnDays < 0 {
  110. return nil, infraerrors.BadRequest("PROXY_WARN_DAYS_INVALID", "expiry_warn_days must be >= 0")
  111. }
  112. if input.Name != "" {
  113. proxy.Name = input.Name
  114. }
  115. if input.Protocol != "" {
  116. proxy.Protocol = input.Protocol
  117. }
  118. if input.Host != "" {
  119. proxy.Host = input.Host
  120. }
  121. if input.Port != 0 {
  122. proxy.Port = input.Port
  123. }
  124. if input.Username != "" {
  125. proxy.Username = input.Username
  126. }
  127. if input.Password != "" {
  128. proxy.Password = input.Password
  129. }
  130. if input.Status != "" {
  131. proxy.Status = input.Status
  132. }
  133. if input.ExpiresAt != nil || input.ClearExpiresAt {
  134. proxy.ExpiresAt = input.ExpiresAt
  135. }
  136. proxy.FallbackMode = mode
  137. proxy.BackupProxyID = backupID
  138. if input.ExpiryWarnDays != nil {
  139. proxy.ExpiryWarnDays = *input.ExpiryWarnDays
  140. }
  141. if err := s.proxyRepo.Update(ctx, proxy); err != nil {
  142. return nil, err
  143. }
  144. return proxy, nil
  145. }
  146. func (s *adminServiceImpl) DeleteProxy(ctx context.Context, id int64) error {
  147. count, err := s.proxyRepo.CountAccountsByProxyID(ctx, id)
  148. if err != nil {
  149. return err
  150. }
  151. if count > 0 {
  152. return ErrProxyInUse
  153. }
  154. // 分组专属代理功能已下线:分组绑定不再阻止代理删除。
  155. return s.proxyRepo.Delete(ctx, id)
  156. }
  157. func (s *adminServiceImpl) BatchDeleteProxies(ctx context.Context, ids []int64) (*ProxyBatchDeleteResult, error) {
  158. result := &ProxyBatchDeleteResult{}
  159. if len(ids) == 0 {
  160. return result, nil
  161. }
  162. for _, id := range ids {
  163. count, err := s.proxyRepo.CountAccountsByProxyID(ctx, id)
  164. if err != nil {
  165. result.Skipped = append(result.Skipped, ProxyBatchDeleteSkipped{
  166. ID: id,
  167. Reason: err.Error(),
  168. })
  169. continue
  170. }
  171. if count > 0 {
  172. result.Skipped = append(result.Skipped, ProxyBatchDeleteSkipped{
  173. ID: id,
  174. Reason: ErrProxyInUse.Error(),
  175. })
  176. continue
  177. }
  178. // 分组专属代理功能已下线:分组绑定不再阻止代理删除。
  179. if err := s.proxyRepo.Delete(ctx, id); err != nil {
  180. result.Skipped = append(result.Skipped, ProxyBatchDeleteSkipped{
  181. ID: id,
  182. Reason: err.Error(),
  183. })
  184. continue
  185. }
  186. result.DeletedIDs = append(result.DeletedIDs, id)
  187. }
  188. return result, nil
  189. }
  190. func (s *adminServiceImpl) GetProxyAccounts(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) {
  191. return s.proxyRepo.ListAccountSummariesByProxyID(ctx, proxyID)
  192. }
  193. func (s *adminServiceImpl) CheckProxyExists(ctx context.Context, host string, port int, username, password string) (bool, error) {
  194. return s.proxyRepo.ExistsByHostPortAuth(ctx, host, port, username, password)
  195. }
  196. // CheckProxyExistsWithProtocol is an optional protocol-aware variant used by
  197. // batch import. Keep CheckProxyExists unchanged for older service consumers.
  198. func (s *adminServiceImpl) CheckProxyExistsWithProtocol(ctx context.Context, protocol, host string, port int, username, password string) (bool, error) {
  199. if checker, ok := s.proxyRepo.(interface {
  200. ExistsByProtocolHostPortAuth(context.Context, string, string, int, string, string) (bool, error)
  201. }); ok {
  202. return checker.ExistsByProtocolHostPortAuth(ctx, protocol, host, port, username, password)
  203. }
  204. return s.proxyRepo.ExistsByHostPortAuth(ctx, host, port, username, password)
  205. }
  206. func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestResult, error) {
  207. proxy, err := s.proxyRepo.GetByID(ctx, id)
  208. if err != nil {
  209. return nil, err
  210. }
  211. proxyURL := proxy.URL()
  212. exitInfo, latencyMs, err := s.proxyProber.ProbeProxy(ctx, proxyURL)
  213. if err != nil {
  214. s.saveProxyLatency(ctx, id, &ProxyLatencyInfo{
  215. Success: false,
  216. Message: err.Error(),
  217. UpdatedAt: time.Now(),
  218. })
  219. return &ProxyTestResult{
  220. Success: false,
  221. Message: err.Error(),
  222. }, nil
  223. }
  224. latency := latencyMs
  225. s.saveProxyLatency(ctx, id, &ProxyLatencyInfo{
  226. Success: true,
  227. LatencyMs: &latency,
  228. Message: "Proxy is accessible",
  229. IPAddress: exitInfo.IP,
  230. Country: exitInfo.Country,
  231. CountryCode: exitInfo.CountryCode,
  232. Region: exitInfo.Region,
  233. City: exitInfo.City,
  234. UpdatedAt: time.Now(),
  235. })
  236. return &ProxyTestResult{
  237. Success: true,
  238. Message: "Proxy is accessible",
  239. LatencyMs: latencyMs,
  240. IPAddress: exitInfo.IP,
  241. City: exitInfo.City,
  242. Region: exitInfo.Region,
  243. Country: exitInfo.Country,
  244. CountryCode: exitInfo.CountryCode,
  245. }, nil
  246. }
  247. func (s *adminServiceImpl) CheckProxyQuality(ctx context.Context, id int64) (*ProxyQualityCheckResult, error) {
  248. proxy, err := s.proxyRepo.GetByID(ctx, id)
  249. if err != nil {
  250. return nil, err
  251. }
  252. result := &ProxyQualityCheckResult{
  253. ProxyID: id,
  254. Score: 100,
  255. Grade: "A",
  256. CheckedAt: time.Now().Unix(),
  257. Items: make([]ProxyQualityCheckItem, 0, len(proxyQualityTargets)+1),
  258. }
  259. proxyURL := proxy.URL()
  260. if s.proxyProber == nil {
  261. result.Items = append(result.Items, ProxyQualityCheckItem{
  262. Target: "base_connectivity",
  263. Status: "fail",
  264. Message: "代理探测服务未配置",
  265. })
  266. result.FailedCount++
  267. finalizeProxyQualityResult(result)
  268. s.saveProxyQualitySnapshot(ctx, id, result, nil)
  269. return result, nil
  270. }
  271. exitInfo, latencyMs, err := s.proxyProber.ProbeProxy(ctx, proxyURL)
  272. if err != nil {
  273. result.Items = append(result.Items, ProxyQualityCheckItem{
  274. Target: "base_connectivity",
  275. Status: "fail",
  276. LatencyMs: latencyMs,
  277. Message: err.Error(),
  278. })
  279. result.FailedCount++
  280. finalizeProxyQualityResult(result)
  281. s.saveProxyQualitySnapshot(ctx, id, result, nil)
  282. return result, nil
  283. }
  284. result.ExitIP = exitInfo.IP
  285. result.Country = exitInfo.Country
  286. result.CountryCode = exitInfo.CountryCode
  287. result.BaseLatencyMs = latencyMs
  288. result.Items = append(result.Items, ProxyQualityCheckItem{
  289. Target: "base_connectivity",
  290. Status: "pass",
  291. LatencyMs: latencyMs,
  292. Message: "代理出口连通正常",
  293. })
  294. result.PassedCount++
  295. client, err := httpclient.GetClient(httpclient.Options{
  296. ProxyURL: proxyURL,
  297. Timeout: proxyQualityRequestTimeout,
  298. ResponseHeaderTimeout: proxyQualityResponseHeaderTimeout,
  299. })
  300. if err != nil {
  301. result.Items = append(result.Items, ProxyQualityCheckItem{
  302. Target: "http_client",
  303. Status: "fail",
  304. Message: fmt.Sprintf("创建检测客户端失败: %v", err),
  305. })
  306. result.FailedCount++
  307. finalizeProxyQualityResult(result)
  308. s.saveProxyQualitySnapshot(ctx, id, result, exitInfo)
  309. return result, nil
  310. }
  311. for _, target := range proxyQualityTargets {
  312. item := runProxyQualityTarget(ctx, client, target)
  313. result.Items = append(result.Items, item)
  314. switch item.Status {
  315. case "pass":
  316. result.PassedCount++
  317. case "warn":
  318. result.WarnCount++
  319. case "challenge":
  320. result.ChallengeCount++
  321. default:
  322. result.FailedCount++
  323. }
  324. }
  325. finalizeProxyQualityResult(result)
  326. s.saveProxyQualitySnapshot(ctx, id, result, exitInfo)
  327. return result, nil
  328. }
  329. func runProxyQualityTarget(ctx context.Context, client *http.Client, target proxyQualityTarget) ProxyQualityCheckItem {
  330. item := ProxyQualityCheckItem{
  331. Target: target.Target,
  332. }
  333. req, err := http.NewRequestWithContext(ctx, target.Method, target.URL, nil)
  334. if err != nil {
  335. item.Status = "fail"
  336. item.Message = fmt.Sprintf("构建请求失败: %v", err)
  337. return item
  338. }
  339. req.Header.Set("Accept", "application/json,text/html,*/*")
  340. req.Header.Set("User-Agent", proxyQualityClientUserAgent)
  341. start := time.Now()
  342. resp, err := client.Do(req)
  343. if err != nil {
  344. item.Status = "fail"
  345. item.LatencyMs = time.Since(start).Milliseconds()
  346. item.Message = fmt.Sprintf("请求失败: %v", err)
  347. return item
  348. }
  349. defer func() { _ = resp.Body.Close() }()
  350. item.LatencyMs = time.Since(start).Milliseconds()
  351. item.HTTPStatus = resp.StatusCode
  352. body, readErr := io.ReadAll(io.LimitReader(resp.Body, proxyQualityMaxBodyBytes+1))
  353. if readErr != nil {
  354. item.Status = "fail"
  355. item.Message = fmt.Sprintf("读取响应失败: %v", readErr)
  356. return item
  357. }
  358. if int64(len(body)) > proxyQualityMaxBodyBytes {
  359. body = body[:proxyQualityMaxBodyBytes]
  360. }
  361. // Cloudflare challenge 检测
  362. if httputil.IsCloudflareChallengeResponse(resp.StatusCode, resp.Header, body) {
  363. item.Status = "challenge"
  364. item.CFRay = httputil.ExtractCloudflareRayID(resp.Header, body)
  365. item.Message = "命中 Cloudflare challenge"
  366. return item
  367. }
  368. if _, ok := target.AllowedStatuses[resp.StatusCode]; ok {
  369. // 白名单内的状态码均代表目标可达:2xx 表示接口直接可用,
  370. // 401/405 等是无鉴权探测的预期结果,同样视为连通正常,不再扣分。
  371. item.Status = "pass"
  372. if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
  373. item.Message = fmt.Sprintf("HTTP %d", resp.StatusCode)
  374. } else {
  375. item.Message = fmt.Sprintf("HTTP %d(目标可达)", resp.StatusCode)
  376. }
  377. return item
  378. }
  379. if resp.StatusCode == http.StatusTooManyRequests {
  380. item.Status = "warn"
  381. item.Message = "目标返回 429,可能存在频控"
  382. return item
  383. }
  384. item.Status = "fail"
  385. item.Message = fmt.Sprintf("非预期状态码: %d", resp.StatusCode)
  386. return item
  387. }
  388. func finalizeProxyQualityResult(result *ProxyQualityCheckResult) {
  389. if result == nil {
  390. return
  391. }
  392. score := 100 - result.WarnCount*10 - result.FailedCount*22 - result.ChallengeCount*30
  393. if score < 0 {
  394. score = 0
  395. }
  396. result.Score = score
  397. result.Grade = proxyQualityGrade(score)
  398. result.Summary = fmt.Sprintf(
  399. "通过 %d 项,告警 %d 项,失败 %d 项,挑战 %d 项",
  400. result.PassedCount,
  401. result.WarnCount,
  402. result.FailedCount,
  403. result.ChallengeCount,
  404. )
  405. }
  406. func proxyQualityGrade(score int) string {
  407. switch {
  408. case score >= 90:
  409. return "A"
  410. case score >= 75:
  411. return "B"
  412. case score >= 60:
  413. return "C"
  414. case score >= 40:
  415. return "D"
  416. default:
  417. return "F"
  418. }
  419. }
  420. func proxyQualityOverallStatus(result *ProxyQualityCheckResult) string {
  421. if result == nil {
  422. return ""
  423. }
  424. if result.ChallengeCount > 0 {
  425. return "challenge"
  426. }
  427. if result.FailedCount > 0 {
  428. return "failed"
  429. }
  430. if result.WarnCount > 0 {
  431. return "warn"
  432. }
  433. if result.PassedCount > 0 {
  434. return "healthy"
  435. }
  436. return "failed"
  437. }
  438. func proxyQualityFirstCFRay(result *ProxyQualityCheckResult) string {
  439. if result == nil {
  440. return ""
  441. }
  442. for _, item := range result.Items {
  443. if item.CFRay != "" {
  444. return item.CFRay
  445. }
  446. }
  447. return ""
  448. }
  449. func proxyQualityBaseConnectivityPass(result *ProxyQualityCheckResult) bool {
  450. if result == nil {
  451. return false
  452. }
  453. for _, item := range result.Items {
  454. if item.Target == "base_connectivity" {
  455. return item.Status == "pass"
  456. }
  457. }
  458. return false
  459. }
  460. func (s *adminServiceImpl) saveProxyQualitySnapshot(ctx context.Context, proxyID int64, result *ProxyQualityCheckResult, exitInfo *ProxyExitInfo) {
  461. if result == nil {
  462. return
  463. }
  464. score := result.Score
  465. checkedAt := result.CheckedAt
  466. info := &ProxyLatencyInfo{
  467. Success: proxyQualityBaseConnectivityPass(result),
  468. Message: result.Summary,
  469. QualityStatus: proxyQualityOverallStatus(result),
  470. QualityScore: &score,
  471. QualityGrade: result.Grade,
  472. QualitySummary: result.Summary,
  473. QualityCheckedAt: &checkedAt,
  474. QualityCFRay: proxyQualityFirstCFRay(result),
  475. UpdatedAt: time.Now(),
  476. }
  477. if result.BaseLatencyMs > 0 {
  478. latency := result.BaseLatencyMs
  479. info.LatencyMs = &latency
  480. }
  481. if exitInfo != nil {
  482. info.IPAddress = exitInfo.IP
  483. info.Country = exitInfo.Country
  484. info.CountryCode = exitInfo.CountryCode
  485. info.Region = exitInfo.Region
  486. info.City = exitInfo.City
  487. }
  488. s.saveProxyLatency(ctx, proxyID, info)
  489. }
  490. func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy) {
  491. if s.proxyProber == nil || proxy == nil {
  492. return
  493. }
  494. exitInfo, latencyMs, err := s.proxyProber.ProbeProxy(ctx, proxy.URL())
  495. if err != nil {
  496. s.saveProxyLatency(ctx, proxy.ID, &ProxyLatencyInfo{
  497. Success: false,
  498. Message: err.Error(),
  499. UpdatedAt: time.Now(),
  500. })
  501. return
  502. }
  503. latency := latencyMs
  504. s.saveProxyLatency(ctx, proxy.ID, &ProxyLatencyInfo{
  505. Success: true,
  506. LatencyMs: &latency,
  507. Message: "Proxy is accessible",
  508. IPAddress: exitInfo.IP,
  509. Country: exitInfo.Country,
  510. CountryCode: exitInfo.CountryCode,
  511. Region: exitInfo.Region,
  512. City: exitInfo.City,
  513. UpdatedAt: time.Now(),
  514. })
  515. }
  516. func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []ProxyWithAccountCount) {
  517. if s.proxyLatencyCache == nil || len(proxies) == 0 {
  518. return
  519. }
  520. ids := make([]int64, 0, len(proxies))
  521. for i := range proxies {
  522. ids = append(ids, proxies[i].ID)
  523. }
  524. latencies, err := s.proxyLatencyCache.GetProxyLatencies(ctx, ids)
  525. if err != nil {
  526. logger.LegacyPrintf("service.admin", "Warning: load proxy latency cache failed: %v", err)
  527. return
  528. }
  529. for i := range proxies {
  530. info := latencies[proxies[i].ID]
  531. if info == nil {
  532. continue
  533. }
  534. if info.Success {
  535. proxies[i].LatencyStatus = "success"
  536. proxies[i].LatencyMs = info.LatencyMs
  537. } else {
  538. proxies[i].LatencyStatus = "failed"
  539. }
  540. proxies[i].LatencyMessage = info.Message
  541. proxies[i].IPAddress = info.IPAddress
  542. proxies[i].Country = info.Country
  543. proxies[i].CountryCode = info.CountryCode
  544. proxies[i].Region = info.Region
  545. proxies[i].City = info.City
  546. proxies[i].QualityStatus = info.QualityStatus
  547. proxies[i].QualityScore = info.QualityScore
  548. proxies[i].QualityGrade = info.QualityGrade
  549. proxies[i].QualitySummary = info.QualitySummary
  550. proxies[i].QualityChecked = info.QualityCheckedAt
  551. }
  552. }
  553. func (s *adminServiceImpl) saveProxyLatency(ctx context.Context, proxyID int64, info *ProxyLatencyInfo) {
  554. if s.proxyLatencyCache == nil || info == nil {
  555. return
  556. }
  557. merged := *info
  558. if latencies, err := s.proxyLatencyCache.GetProxyLatencies(ctx, []int64{proxyID}); err == nil {
  559. if existing := latencies[proxyID]; existing != nil {
  560. if merged.QualityCheckedAt == nil &&
  561. merged.QualityScore == nil &&
  562. merged.QualityGrade == "" &&
  563. merged.QualityStatus == "" &&
  564. merged.QualitySummary == "" &&
  565. merged.QualityCFRay == "" {
  566. merged.QualityStatus = existing.QualityStatus
  567. merged.QualityScore = existing.QualityScore
  568. merged.QualityGrade = existing.QualityGrade
  569. merged.QualitySummary = existing.QualitySummary
  570. merged.QualityCheckedAt = existing.QualityCheckedAt
  571. merged.QualityCFRay = existing.QualityCFRay
  572. }
  573. }
  574. }
  575. if err := s.proxyLatencyCache.SetProxyLatency(ctx, proxyID, &merged); err != nil {
  576. logger.LegacyPrintf("service.admin", "Warning: store proxy latency cache failed: %v", err)
  577. }
  578. }