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() }