feat: 排行榜不再展示任何IP,用户列显示地区(ip2region v4 离线库)+浏览器(UA解析);IP仅后台保存,页面上方显示IP+地区

This commit is contained in:
dsh
2026-08-17 23:10:02 -04:00
parent 780e84e67e
commit d8917a666a
9 files changed
+229 -83

No files matched your search

+71 -48
View File
@@ -13,6 +13,7 @@ import (
"speedtest/config"
"speedtest/internal/db"
"speedtest/internal/geo"
"speedtest/internal/store"
"github.com/gin-gonic/gin"
@@ -28,13 +29,18 @@ const (
type Handler struct {
stores *store.Stores
cfg config.SpeedtestConfig
pool [][]byte // 预生成随机数据池(下载测速负载
geo *geo.Geo // IP 地区解析(离线
pool [][]byte // 预生成随机数据池(下载测速负载)
poolNext atomic.Int64
}
// NewHandler creates a new speedtest Handler.
func NewHandler(stores *store.Stores, cfg config.SpeedtestConfig) (*Handler, error) {
h := &Handler{stores: stores, cfg: cfg}
h := &Handler{
stores: stores,
cfg: cfg,
geo: geo.New("data/ip2region_v4.xdb", "/opt/speedtest/data/ip2region_v4.xdb"),
}
if err := h.buildPool(); err != nil {
return nil, err
}
@@ -74,31 +80,16 @@ func clientIP(c *gin.Context) string {
return host
}
// maskIP 排行榜展示时对 IP 打码(保留前 3 段)
func maskIP(ip string) string {
if strings.Contains(ip, ":") {
// IPv6:保留前 3 段
parts := strings.Split(ip, ":")
if len(parts) > 3 {
return strings.Join(parts[:3], ":") + ":****"
}
return ip
}
parts := strings.Split(ip, ".")
if len(parts) == 4 {
return strings.Join(parts[:3], ".") + ".*"
}
return ip
}
// Ping 延迟探测:返回最小响应,供前端计算 RTT;同时回传服务器看到的客户端 IP
// (NAT 环境下可能是内网 IP,前端会用公网探测结果替代)
// Ping 延迟探测:返回最小响应,供前端计算 RTT;
// 同时回传客户端 IP(页面上方展示用)与其地区
func (h *Handler) Ping(c *gin.Context) {
ip := clientIP(c)
c.Header("Cache-Control", "no-store")
c.JSON(http.StatusOK, gin.H{
"pong": true,
"ts": time.Now().UnixMilli(),
"client_ip": clientIP(c),
"client_ip": ip,
"location": h.geo.Search(ip),
})
}
@@ -217,6 +208,8 @@ func (h *Handler) Result(c *gin.Context) {
record := &db.SpeedTestResult{
ClientIP: ip,
ServerIP: serverIP,
Location: h.geo.Search(ip),
UserAgent: truncate(c.Request.UserAgent(), 255),
LatencyMs: req.LatencyMs,
JitterMs: req.JitterMs,
DownloadMbps: req.DownloadMbps,
@@ -230,15 +223,62 @@ func (h *Handler) Result(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "id": record.ID})
}
// RankItem 排行榜展示条目
// ClientIP 打码展示;ServerIP 为来源 IP(内网 IP 完整返回,供站长调试区分设备)
// truncate 截断字符串到 n 个字节
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
// parseBrowser 从 User-Agent 解析简短的浏览器 + 系统信息,如 "Chrome · Windows"
func parseBrowser(ua string) string {
lower := strings.ToLower(ua)
browser := "其他"
switch {
case strings.Contains(lower, "edg/"):
browser = "Edge"
case strings.Contains(lower, "chrome"):
browser = "Chrome"
case strings.Contains(lower, "firefox"):
browser = "Firefox"
case strings.Contains(lower, "micromessenger"):
browser = "微信"
case strings.Contains(lower, "qqbrowser") || strings.Contains(lower, " qq/"):
browser = "QQ浏览器"
case strings.Contains(lower, "ucbrowser"):
browser = "UC浏览器"
case strings.Contains(lower, "opera") || strings.Contains(lower, "opr/"):
browser = "Opera"
case strings.Contains(lower, "safari"):
browser = "Safari"
}
osName := "其他系统"
switch {
case strings.Contains(lower, "windows"):
osName = "Windows"
case strings.Contains(lower, "android"):
osName = "Android"
case strings.Contains(lower, "iphone") || strings.Contains(lower, "ipad") || strings.Contains(lower, "ios"):
osName = "iOS"
case strings.Contains(lower, "mac os") || strings.Contains(lower, "macintosh"):
osName = "macOS"
case strings.Contains(lower, "linux"):
osName = "Linux"
}
return browser + " · " + osName
}
// RankItem 排行榜展示条目(不含任何 IP 信息——IP 仅后台保存,不对外展示)
type RankItem struct {
ID uint `json:"id"`
Rank int `json:"rank"` // 名次(含附加记录的真实名次)
IsMine bool `json:"is_mine"` // 是否请求方指定的记录(前端高亮"我的成绩")
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
IsPrivateIP bool `json:"is_private_ip"` // ClientIP 是否为内网(探测失败回退场景)
Rank int `json:"rank"` // 名次(含附加记录的真实名次)
IsMine bool `json:"is_mine"` // 是否请求方指定的记录(前端高亮"我的成绩")
Location string `json:"location"` // 用户地区(ip2region
Browser string `json:"browser"` // 浏览器 + 系统(由 UA 解析)
LatencyMs float64 `json:"latency_ms"`
JitterMs float64 `json:"jitter_ms"`
DownloadMbps float64 `json:"download_mbps"`
@@ -246,30 +286,13 @@ type RankItem struct {
CreatedAt string `json:"created_at"`
}
// isPrivateIP 判断 IP 是否为内网/保留地址(NAT 网关、局域网、回环等)
func isPrivateIP(ip string) bool {
parsed := net.ParseIP(ip)
if parsed == nil {
return true
}
if parsed.IsLoopback() || parsed.IsLinkLocalUnicast() || parsed.IsLinkLocalMulticast() {
return true
}
if parsed.IsPrivate() || parsed.IsUnspecified() {
return true
}
// IPv4 兼容段(IsPrivate 已覆盖 10/8、172.16/12、192.168/16
return false
}
func toRankItems(rows []db.SpeedTestResult) []RankItem {
items := make([]RankItem, 0, len(rows))
for _, r := range rows {
items = append(items, RankItem{
ID: r.ID,
ClientIP: maskIP(r.ClientIP),
ServerIP: r.ServerIP,
IsPrivateIP: isPrivateIP(r.ClientIP),
Location: r.Location,
Browser: parseBrowser(r.UserAgent),
LatencyMs: r.LatencyMs,
JitterMs: r.JitterMs,
DownloadMbps: r.DownloadMbps,