package utils // IP 地理位置识别:复用 opsx-waf 的 GeoLite2-City 方案(中文优先,内网标记)。 // 数据文件默认 /data/geoip/GeoLite2-City.mmdb(容器卷内),缺失时静默返回空串,不影响业务。 // Logan.Li import ( "backend/internal/service/appconfig" "net/netip" "strings" "sync" geoip2 "github.com/oschwald/geoip2-golang/v2" ) var geoIPReader struct { once sync.Once db *geoip2.Reader } // LookupIPLocation 返回 "国家 / 省份 / 城市"(中文优先);内网返回"内网",无法识别返回空串 // Logan.Li func LookupIPLocation(value string) string { ip, err := netip.ParseAddr(strings.TrimSpace(value)) if err != nil { return "" } if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() { return "内网" } geoIPReader.once.Do(func() { path := appconfig.String("GEOIP_DB_PATH") if path == "" { path = "/data/geoip/GeoLite2-City.mmdb" } geoIPReader.db, _ = geoip2.Open(path) }) if geoIPReader.db == nil { return "" } record, err := geoIPReader.db.City(ip) if err != nil { return "" } parts := localizedNames(record.Country.Names, record.Country.ISOCode) if len(record.Subdivisions) > 0 { if sub := localizedNames(record.Subdivisions[0].Names, record.Subdivisions[0].ISOCode); sub != "" && sub != parts { parts = joinLocation(parts, sub) } } if city := localizedNames(record.City.Names, ""); city != "" { parts = joinLocation(parts, city) } return parts } // localizedNames 中文优先;GeoLite2 少数中国城市只有英文名,走兜底映射 // Logan.Li func localizedNames(names geoip2.Names, fallback string) string { if names.SimplifiedChinese != "" { return names.SimplifiedChinese } if names.English != "" { return names.English } return fallback } func joinLocation(left, right string) string { if left == "" { return right } return left + " / " + right }