package service import ( "context" "net" "strings" "kefu-cloud/server/internal/geoip" "kefu-cloud/server/internal/model" ) const visitorGeoBackfillBatch = 500 var visitorGeoLookup = geoip.Lookup // BackfillVisitorGeoIP 使用历史会话保存的 IP 补齐旧数据中的国家/省市信息。 func BackfillVisitorGeoIP(ctx context.Context) (int, error) { var lastID uint updated := 0 for { select { case <-ctx.Done(): return updated, ctx.Err() default: } var sessions []model.Session if err := model.DB. Select("id", "visitor_ip"). Where("id > ? AND visitor_ip <> ''", lastID). Where("visitor_country = '' AND (visitor_region = '' OR visitor_region = ?)", "未知"). Order("id asc").Limit(visitorGeoBackfillBatch).Find(&sessions).Error; err != nil { return updated, err } if len(sessions) == 0 { return updated, nil } for _, session := range sessions { lastID = session.ID location, ok := visitorGeoLookup(session.VisitorIP) if !ok { if isPrivateIP(session.VisitorIP) { if err := model.DB.Model(&model.Session{}).Where("id = ?", session.ID). Update("visitor_region", "内网").Error; err != nil { return updated, err } updated++ } continue } if strings.TrimSpace(location.Region) == "" { continue } if err := model.DB.Model(&model.Session{}).Where("id = ?", session.ID).Updates(map[string]interface{}{ "visitor_region": location.Region, "visitor_country": location.Country, "visitor_province": location.Province, "visitor_city": location.City, }).Error; err != nil { return updated, err } updated++ } if len(sessions) < visitorGeoBackfillBatch { return updated, nil } } } func isPrivateIP(raw string) bool { ip := net.ParseIP(strings.TrimSpace(raw)) return ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()) }