Files
AI-CS/backend/infra/geoip/resolver.go
T
2026-05-18 19:26:16 +08:00

146 lines
3.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 geoip
import (
"log"
"net"
"os"
"path/filepath"
"strings"
"sync"
"github.com/lionsoul2014/ip2region/binding/golang/service"
)
// Resolver 根据 IP 解析大致地理位置(离线)。
type Resolver interface {
Lookup(ip string) string
Close()
}
// NoopResolver 未配置 xdb 时使用。
type NoopResolver struct{}
func (NoopResolver) Lookup(string) string { return "" }
func (NoopResolver) Close() {}
type ip2regionResolver struct {
svc *service.Ip2Region
}
func (r *ip2regionResolver) Lookup(ip string) string {
ip = strings.TrimSpace(ip)
if ip == "" {
return ""
}
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
parsed := net.ParseIP(ip)
if parsed == nil {
return ""
}
if parsed.IsLoopback() || parsed.IsPrivate() || parsed.IsLinkLocalUnicast() {
return "内网"
}
region, err := r.svc.Search(ip)
if err != nil || strings.TrimSpace(region) == "" {
return ""
}
return FormatRegion(region)
}
func (r *ip2regionResolver) Close() {
if r.svc != nil {
r.svc.Close()
}
}
var (
globalResolver Resolver = NoopResolver{}
globalMu sync.Mutex
)
// Get 返回全局 Resolver(未初始化时为 Noop)。
func Get() Resolver {
globalMu.Lock()
defer globalMu.Unlock()
return globalResolver
}
// InitFromEnv 按环境变量与默认路径加载 ip2region;失败则降级为 Noop,不阻塞启动。
// IP2REGION_DISABLED=true 时跳过;IP2REGION_V4_XDB 指定 v4 库路径;IP2REGION_V6_XDB 可选 IPv6。
func InitFromEnv() {
globalMu.Lock()
defer globalMu.Unlock()
if old, ok := globalResolver.(*ip2regionResolver); ok {
old.Close()
}
globalResolver = NoopResolver{}
if strings.EqualFold(strings.TrimSpace(os.Getenv("IP2REGION_DISABLED")), "true") {
log.Println("️ IP2REGION_DISABLED=true,跳过访客 IP 地理位置解析")
return
}
v4Path := strings.TrimSpace(os.Getenv("IP2REGION_V4_XDB"))
v6Path := strings.TrimSpace(os.Getenv("IP2REGION_V6_XDB"))
if v4Path == "" {
v4Path = findDefaultXDB("ip2region_v4.xdb")
}
if v6Path == "" {
v6Path = findDefaultXDB("ip2region_v6.xdb")
}
if v4Path == "" && v6Path == "" {
log.Println("⚠️ 未找到 ip2region xdb,访客「位置」将保持为空;请放置 backend/data/ip2region_v4.xdb(及可选 v6)或设置 IP2REGION_V4_XDB / IP2REGION_V6_XDB")
return
}
svc, err := service.NewIp2RegionWithPath(v4Path, v6Path)
if err != nil {
log.Printf("⚠️ 初始化 ip2region 失败: %v", err)
return
}
globalResolver = &ip2regionResolver{svc: svc}
if v4Path != "" {
log.Printf("✅ ip2region 已加载 (v4: %s)", v4Path)
}
if v6Path != "" {
log.Printf("✅ ip2region 已加载 (v6: %s)", v6Path)
} else {
log.Println("️ 未加载 ip2region IPv6 库;访客若为 IPv6 地址将无法解析位置,可下载 ip2region_v6.xdb")
}
}
// LookupLocation 解析 IP;已有 location 非空时原样返回。
func LookupLocation(ip, currentLocation string) string {
if strings.TrimSpace(currentLocation) != "" {
return currentLocation
}
return Get().Lookup(ip)
}
func findDefaultXDB(filename string) string {
candidates := []string{
"data/" + filename,
"backend/data/" + filename,
"/app/data/" + filename,
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates,
filepath.Join(wd, "data", filename),
filepath.Join(wd, "..", "backend", "data", filename),
)
}
for _, p := range candidates {
if st, err := os.Stat(p); err == nil && !st.IsDir() {
abs, err := filepath.Abs(p)
if err == nil {
return abs
}
return p
}
}
return ""
}