mirror of
https://github.com/2930134478/AI-CS.git
synced 2026-06-15 00:44:30 +08:00
146 lines
3.5 KiB
Go
146 lines
3.5 KiB
Go
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 ""
|
||
}
|