Files
server/app/utils/ip_location.go
T
2026-01-16 15:49:34 +08:00

202 lines
5.5 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 utils
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// IPLocation IP 地理位置信息
type IPLocation struct {
Country string `json:"country"` // 国家
Region string `json:"region"` // 省份/州
City string `json:"city"` // 城市
ISP string `json:"isp"` // ISP
CountryCode string `json:"countryCode"` // 国家代码
}
// GetIPLocation 根据 IP 地址获取地理位置信息
// 使用 ip-api.com 免费 API(无需 API Key,有速率限制)
// 如果查询失败,返回空字符串,不影响主流程
func GetIPLocation(ip string) string {
if ip == "" || ip == "127.0.0.1" || ip == "::1" || strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "10.") || strings.HasPrefix(ip, "172.") {
return "内网IP"
}
// 使用 ip-api.com 免费 API
// 格式:http://ip-api.com/json/{ip}?fields=status,message,country,regionName,city,isp,countryCode
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=status,message,country,regionName,city,isp,countryCode&lang=zh-CN", ip)
client := &http.Client{
Timeout: 3 * time.Second, // 3秒超时,避免阻塞
}
resp, err := client.Get(url)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Country string `json:"country"`
RegionName string `json:"regionName"`
City string `json:"city"`
ISP string `json:"isp"`
CountryCode string `json:"countryCode"`
}
if err := json.Unmarshal(body, &result); err != nil {
return ""
}
if result.Status != "success" {
return ""
}
// 构建位置字符串:国家 省份 城市
var locationParts []string
if result.Country != "" {
locationParts = append(locationParts, result.Country)
}
if result.RegionName != "" {
locationParts = append(locationParts, result.RegionName)
}
if result.City != "" {
locationParts = append(locationParts, result.City)
}
location := strings.Join(locationParts, " ")
if location == "" {
return ""
}
// 如果位置信息太长,截断
if len(location) > 100 {
location = location[:100]
}
return location
}
// GetIPLocationAsync 异步获取 IP 地理位置信息
// 用于不阻塞主流程的场景
//
// 安全特性:
// - 使用 context 超时控制(默认 10 秒)
// - 添加 panic recovery,防止 goroutine 崩溃
// - callback 执行超时保护(默认 5 秒),防止阻塞
//
// 参数:
// - ip: IP 地址
// - callback: 回调函数,接收位置信息
func GetIPLocationAsync(ip string, callback func(location string)) {
GetIPLocationAsyncWithTimeout(ip, callback, 10*time.Second, 5*time.Second)
}
// GetIPLocationAsyncWithTimeout 异步获取 IP 地理位置信息(带超时控制)
//
// 参数:
// - ip: IP 地址
// - callback: 回调函数,接收位置信息
// - locationTimeout: IP 查询超时时间(默认 10 秒)
// - callbackTimeout: 回调函数执行超时时间(默认 5 秒)
func GetIPLocationAsyncWithTimeout(ip string, callback func(location string), locationTimeout, callbackTimeout time.Duration) {
// 设置默认超时时间
if locationTimeout <= 0 {
locationTimeout = 10 * time.Second
}
if callbackTimeout <= 0 {
callbackTimeout = 5 * time.Second
}
go func() {
// 添加 panic recovery,防止 goroutine 崩溃导致程序退出
defer func() {
if r := recover(); r != nil {
// 记录 panic 但不影响主流程
// 注意:这里不能使用 facades.Log(),因为可能没有初始化 context
// 如果需要记录日志,可以通过参数传入 logger
}
}()
// 创建带超时的 context(确保总超时时间不超过 locationTimeout
// 注意:GetIPLocation 内部已有 3 秒 HTTP 超时,这里设置外层超时作为兜底
ctx, cancel := context.WithTimeout(context.Background(), locationTimeout)
defer cancel()
// 在 goroutine 中执行 IP 查询
locationChan := make(chan string, 1)
go func() {
defer func() {
if r := recover(); r != nil {
// panic 时发送空字符串,避免阻塞
select {
case locationChan <- "":
default:
}
}
}()
location := GetIPLocation(ip)
// 非阻塞发送,避免 goroutine 泄露
select {
case locationChan <- location:
case <-ctx.Done():
// 如果已经超时,直接返回,不发送结果
return
}
}()
// 等待结果或超时
var location string
select {
case location = <-locationChan:
// 成功获取位置信息
case <-ctx.Done():
// 超时,返回空字符串
// 注意:内部的 goroutine 可能仍在执行,但由于 HTTP client 有 3 秒超时,会很快结束
location = ""
}
// 执行回调(带超时保护)
if callback != nil {
callbackCtx, callbackCancel := context.WithTimeout(context.Background(), callbackTimeout)
defer callbackCancel()
callbackDone := make(chan struct{}, 1)
go func() {
defer func() {
if r := recover(); r != nil {
// callback 中发生 panic,静默处理
}
callbackDone <- struct{}{}
}()
callback(location)
}()
// 等待 callback 完成或超时
select {
case <-callbackDone:
// callback 正常完成
case <-callbackCtx.Done():
// callback 执行超时,不等待(防止阻塞)
// 注意:callback 仍在执行,但不会阻塞当前 goroutine
}
}
}()
}