Files
tyapi-server/internal/shared/ipgeo/ip_locator.go
2026-03-20 13:24:45 +08:00

135 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ipgeo
import (
"net"
"path/filepath"
"strings"
"tyapi-server/internal/domains/security/entities"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"go.uber.org/zap"
)
// Location IP解析后的地理信息
type Location struct {
Country string
Province string
City string
ISP string
Region string
}
// Locator IP地理定位器
type Locator struct {
logger *zap.Logger
searcher *xdb.Searcher
}
// NewLocator 创建定位器,优先读取 resources/ipgeo/ip2region.xdb
func NewLocator(logger *zap.Logger) *Locator {
locator := &Locator{logger: logger}
dbPath := filepath.Join("resources", "ipgeo", "ip2region.xdb")
cBuff, err := xdb.LoadContentFromFile(dbPath)
if err != nil {
logger.Warn("加载ip2region库失败将使用降级定位", zap.String("db_path", dbPath), zap.Error(err))
return locator
}
header, err := xdb.LoadHeaderFromBuff(cBuff)
if err != nil {
logger.Warn("读取ip2region头信息失败将使用降级定位", zap.Error(err))
return locator
}
version, err := xdb.VersionFromHeader(header)
if err != nil {
logger.Warn("解析ip2region版本失败将使用降级定位", zap.Error(err))
return locator
}
searcher, err := xdb.NewWithBuffer(version, cBuff)
if err != nil {
logger.Warn("初始化ip2region搜索器失败将使用降级定位", zap.Error(err))
return locator
}
locator.searcher = searcher
logger.Info("ip2region定位器初始化成功", zap.String("db_path", dbPath))
return locator
}
// LookupByIP 根据IP定位失败返回 false
func (l *Locator) LookupByIP(ip string) (Location, bool) {
if ip == "" || isPrivateOrLocalIP(ip) || l.searcher == nil {
return Location{}, false
}
region, err := l.searcher.SearchByStr(ip)
if err != nil {
l.logger.Debug("ip2region查询失败", zap.String("ip", ip), zap.Error(err))
return Location{}, false
}
loc := parseRegion(region)
if loc.Region == "" {
return Location{}, false
}
return loc, true
}
// ToGeoPoint 将记录转换为地球飞线起点
func (l *Locator) ToGeoPoint(record entities.SuspiciousIPRecord) (fromName string, lng float64, lat float64) {
// 默认降级坐标:北京
const defaultLng = 116.4074
const defaultLat = 39.9042
loc, ok := l.LookupByIP(record.IP)
if !ok {
return record.IP, defaultLng, defaultLat
}
cityName := strings.TrimSpace(loc.City)
if cityName == "" || cityName == "0" {
cityName = strings.TrimSpace(loc.Province)
}
if cityName == "" || cityName == "0" {
return record.IP, defaultLng, defaultLat
}
coord, exists := CityCoordinates[cityName]
if !exists {
// 降级:未命中城市映射,回默认坐标
return cityName, defaultLng, defaultLat
}
return cityName, coord.Lng, coord.Lat
}
func parseRegion(region string) Location {
parts := strings.Split(region, "|")
for len(parts) < 5 {
parts = append(parts, "")
}
return Location{
Country: normalizeField(parts[0]),
Region: normalizeField(parts[1]),
Province: normalizeField(parts[2]),
City: normalizeField(parts[3]),
ISP: normalizeField(parts[4]),
}
}
func normalizeField(s string) string {
s = strings.TrimSpace(s)
if s == "0" {
return ""
}
return s
}
func isPrivateOrLocalIP(ip string) bool {
parsed := net.ParseIP(ip)
if parsed == nil {
return true
}
return parsed.IsLoopback() || parsed.IsPrivate() || parsed.IsUnspecified() || parsed.IsLinkLocalUnicast()
}