Files
杨豪 5a34e739bb
Build and Publish Docker Image / build-and-push (pull_request) Successful in 15m54s
feat: HH-773 合集重命名接入 egress geo 缓存、geo 失败降级与每小时补探测
- service: EgressCacheKey 从 handler 下沉复用;MergeCachedEgressGeo 在合集
  重命名前合并缓存 geo 字段(纯缓存读、零网络 I/O);StripEgressGeoFields
  防止 geo 字段泄漏进订阅输出
- filter: geo 检测失败的节点保留 '[别名] 原名'(或纯原名)参与编号/排序,
  不再丢弃
- handler: 每小时 StartEgressRefresher 对启用源补探测缺失/过期的 egress
  缓存(含 TTL 内 error 结果一律跳过);RegisterRoutes 返回 *Deps;
  cmd/server.go 启动定时任务并在 shutdown 时取消
- tests: service 缓存 key 稳定性/合并/管道测试、handler 补探测跳过/TTL/
  过期/取消测试、filter 降级测试;修复 2 个过时测试(mihomo YAML 配置、
  缓存命中路径)
2026-08-28 16:40:11 +08:00

84 lines
2.6 KiB
Go

package service
import (
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/peterqiu0516/sub-store/internal/database"
"github.com/peterqiu0516/sub-store/internal/model"
)
// egressCacheKeySkip lists node fields excluded from the egress cache key so
// the key stays stable across pipeline stages (parse → filter → alias tag →
// rename) and matches the key the background probe wrote.
var egressCacheKeySkip = map[string]bool{
"id": true, "name": true, "_sourceAlias": true, "_previewId": true,
"latencyMs": true, "latencyError": true,
"egressIp": true, "egressCountry": true, "egressRegion": true, "egressError": true,
"country": true, "countryCode": true, "region": true, "city": true, "isp": true, "flag": true, "cached": true,
}
// EgressCacheKey returns the source_cache key for a node's egress probe
// result. Pushed down from the handler (HH-773) so the download pipeline
// reads the exact entries the probe wrote.
func EgressCacheKey(node model.ProxyNode) string {
clean := model.ProxyNode{}
for k, v := range node {
if !egressCacheKeySkip[k] {
clean[k] = v
}
}
data, _ := json.Marshal(clean)
sum := sha256.Sum256(data)
return fmt.Sprintf("egress:%x", sum)
}
// egressGeoFields are the cached egress-probe geo fields merged into nodes
// for collection-level renaming.
var egressGeoFields = []string{"country", "countryCode", "region", "city", "flag"}
// MergeCachedEgressGeo merges cached geo fields into nodes so collection
// renaming can use probed geo data. Pure cache reads — no network I/O, no
// probing inside the download pipeline. geoFromNode in the filter package
// consumes the merged fields.
func MergeCachedEgressGeo(repo *database.CacheRepo, nodes []model.ProxyNode) []model.ProxyNode {
if repo == nil {
return nodes
}
for _, node := range nodes {
if node == nil {
continue
}
entry, ok := repo.SafeGet(EgressCacheKey(node))
if !ok {
continue
}
var cached map[string]any
if json.Unmarshal([]byte(entry.Content), &cached) != nil {
continue
}
for _, field := range egressGeoFields {
if v, ok := cached[field]; ok && v != nil {
node[field] = v
}
}
}
return nodes
}
// StripEgressGeoFields removes the merged geo fields after renaming so they
// do not leak into rendered subscription output. No parser or filter sets
// these fields natively (they exist on nodes only via the egress merge), so
// stripping is safe.
func StripEgressGeoFields(nodes []model.ProxyNode) {
for _, node := range nodes {
if node == nil {
continue
}
for _, field := range egressGeoFields {
delete(node, field)
}
}
}