package clientgeo import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) const DefaultLookupURL = "https://cf-ip-geo.smyhub.com/api" type apiPayload struct { Geo *struct { CountryName string `json:"countryName"` RegionName string `json:"regionName"` CityName string `json:"cityName"` } `json:"geo"` } func FormatDisplay(countryName, regionName, cityName string) string { parts := make([]string, 0, 3) for _, s := range []string{countryName, regionName, cityName} { s = strings.TrimSpace(s) if s != "" { parts = append(parts, s) } } return strings.Join(parts, " ") } // FetchDisplayLocation 使用 cf-ip-geo 的 ?ip= 查询展示用位置(服务端调用,避免浏览器 CORS)。 func FetchDisplayLocation(ctx context.Context, lookupURL, ip string) (string, error) { ip = strings.TrimSpace(ip) if ip == "" { return "", nil } base := strings.TrimSuffix(strings.TrimSpace(lookupURL), "/") u, err := url.Parse(base) if err != nil || u.Scheme == "" || u.Host == "" { return "", fmt.Errorf("invalid lookup URL") } q := u.Query() q.Set("ip", ip) u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return "", err } client := &http.Client{Timeout: 6 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("geo status %d", resp.StatusCode) } var payload apiPayload if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return "", err } if payload.Geo == nil { return "", nil } return FormatDisplay(payload.Geo.CountryName, payload.Geo.RegionName, payload.Geo.CityName), nil }