feat(widget): 优化访客在线轨迹、GeoIP与多站点渠道
CI / Frontend (typecheck + lint + build) (push) Successful in 2m5s
CI / Backend (go vet + test) (push) Failing after 5m20s

This commit is contained in:
yml2213
2026-11-06 18:21:30 +08:00
parent 24fac1bcf1
commit 38d6fc1684
37 changed files with 1071 additions and 123 deletions
+46 -20
View File
@@ -5,6 +5,8 @@ import (
"strings"
"github.com/gin-gonic/gin"
"kefu-cloud/server/internal/geoip"
"kefu-cloud/server/internal/service"
)
// resolveVisitorIP 优先信任反代头,回落到 Gin ClientIP。
@@ -46,9 +48,27 @@ func resolveVisitorIP(c *gin.Context) string {
return ip
}
// resolveVisitorRegion 基于 IP 与常见 CDN 国家头做粗粒度地区,无需第三方库。
// 私网/本机显示「内网」;有国家头则映射中文名;否则「未知」。
func resolveVisitorRegion(c *gin.Context, ip string) string {
type visitorLocation struct {
Country string
Province string
City string
Region string
}
// resolveVisitorLocation 优先使用离线 GeoIP,未配置时降级到 CDN 国家头。
func resolveVisitorLocation(c *gin.Context, ip string) visitorLocation {
parsed := net.ParseIP(ip)
if parsed != nil &&
(parsed.IsLoopback() || parsed.IsPrivate() || parsed.IsLinkLocalUnicast() || parsed.IsLinkLocalMulticast()) {
return visitorLocation{Region: "内网"}
}
if location, ok := geoip.Lookup(ip); ok {
return visitorLocation{
Country: location.Country, Province: location.Province,
City: location.City, Region: location.Region,
}
}
// Cloudflare / 部分网关国家码
country := strings.ToUpper(strings.TrimSpace(c.GetHeader("CF-IPCountry")))
if country == "" {
@@ -59,20 +79,15 @@ func resolveVisitorRegion(c *gin.Context, ip string) string {
}
if country != "" && country != "XX" && country != "T1" {
if name, ok := countryNameCN(country); ok {
return name
return visitorLocation{Country: name, Region: name}
}
return country
return visitorLocation{Country: country, Region: country}
}
return visitorLocation{Region: "未知"}
}
parsed := net.ParseIP(ip)
if parsed == nil {
return "未知"
}
if parsed.IsLoopback() || parsed.IsPrivate() || parsed.IsLinkLocalUnicast() || parsed.IsLinkLocalMulticast() {
return "内网"
}
// 未接入 GeoIP 库时,公网 IP 地区暂标未知(可后续接入离线库)
return "未知"
func resolveVisitorRegion(c *gin.Context, ip string) string {
return resolveVisitorLocation(c, ip).Region
}
func countryNameCN(code string) (string, bool) {
@@ -123,13 +138,24 @@ func summarizeUserAgent(ua string) string {
return browser + " / " + osName
}
func captureVisitorMeta(c *gin.Context) (ip, region, ua, device string) {
ip = resolveVisitorIP(c)
region = resolveVisitorRegion(c, ip)
ua = c.Request.UserAgent()
func captureVisitorMeta(c *gin.Context) service.VisitorMeta {
ip := resolveVisitorIP(c)
location := resolveVisitorLocation(c, ip)
clip := func(value string, max int) string {
value = strings.TrimSpace(value)
runes := []rune(value)
if len(runes) > max {
return string(runes[:max])
}
return value
}
ua := c.Request.UserAgent()
if len(ua) > 500 {
ua = ua[:500]
}
device = summarizeUserAgent(ua)
return
return service.VisitorMeta{
IP: ip, Region: clip(location.Region, 100), Country: clip(location.Country, 100),
Province: clip(location.Province, 100), City: clip(location.City, 100),
UserAgent: ua, DeviceLabel: summarizeUserAgent(ua),
}
}