Files
jiang13-forum/service/geoip.go
freefire 429956c594 feat: 管理端网站监控,浏览量写入独立 monitor.db
请求日志按日 JSONL;page_views 不进主库,避免统计数据撑大 jiang13.db。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 07:55:45 +08:00

334 lines
6.5 KiB
Go
Raw Permalink 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 service
import (
"net"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/ip2location/ip2location-go/v9"
"github.com/oschwald/maxminddb-golang"
)
// GeoInfo IP 地理与运营商解析结果
type GeoInfo struct {
Country string
Region string // 省/州(写入前经 ApplyGeoZh 中文化)
RegionISO string // 省/州 ISO如 GD
City string
ASN uint
ASOrg string
}
type geoIPSuite struct {
dataDir string
mu sync.RWMutex
binV4 *binHandle
binV6 *binHandle
asn *mmdbHandle
country *mmdbHandle // 可选BIN 未命中时国家兜底
}
type binHandle struct {
path string
db *ip2location.DB
mod time.Time
}
type mmdbHandle struct {
path string
db *maxminddb.Reader
mod time.Time
}
func newGeoIPSuite(dataDir string) *geoIPSuite {
s := &geoIPSuite{dataDir: dataDir}
s.binV4 = &binHandle{path: filepath.Join(dataDir, "IP2LOCATION-LITE-DB3.BIN")}
s.binV6 = &binHandle{path: filepath.Join(dataDir, "IP2LOCATION-LITE-DB3.IPV6.BIN")}
s.asn = &mmdbHandle{path: filepath.Join(dataDir, "GeoLite2-ASN.mmdb")}
s.country = &mmdbHandle{path: filepath.Join(dataDir, "GeoLite2-Country.mmdb")}
s.openAll()
return s
}
func (s *geoIPSuite) openAll() {
s.mu.Lock()
defer s.mu.Unlock()
openBIN(s.binV4)
openBIN(s.binV6)
openMMDB(s.asn)
openMMDB(s.country)
}
func openBIN(h *binHandle) {
if h == nil {
return
}
st, err := os.Stat(h.path)
if err != nil {
closeBIN(h)
return
}
if h.db != nil && st.ModTime().Equal(h.mod) {
return
}
db, err := ip2location.OpenDB(h.path)
if err != nil {
closeBIN(h)
return
}
closeBIN(h)
h.db = db
h.mod = st.ModTime()
}
func closeBIN(h *binHandle) {
if h != nil && h.db != nil {
h.db.Close()
h.db = nil
}
}
func openMMDB(h *mmdbHandle) {
if h == nil {
return
}
st, err := os.Stat(h.path)
if err != nil {
closeMMDB(h)
return
}
if h.db != nil && st.ModTime().Equal(h.mod) {
return
}
db, err := maxminddb.Open(h.path)
if err != nil {
closeMMDB(h)
return
}
closeMMDB(h)
h.db = db
h.mod = st.ModTime()
}
func closeMMDB(h *mmdbHandle) {
if h != nil && h.db != nil {
_ = h.db.Close()
h.db = nil
}
}
func (s *geoIPSuite) ReloadIfNeeded() {
s.mu.Lock()
defer s.mu.Unlock()
reloadBIN(s.binV4)
reloadBIN(s.binV6)
reloadMMDB(s.asn)
reloadMMDB(s.country)
}
func reloadBIN(h *binHandle) {
if h == nil {
return
}
st, err := os.Stat(h.path)
if err != nil {
closeBIN(h)
return
}
if h.db != nil && st.ModTime().Equal(h.mod) {
return
}
openBIN(h)
}
func reloadMMDB(h *mmdbHandle) {
if h == nil {
return
}
st, err := os.Stat(h.path)
if err != nil {
closeMMDB(h)
return
}
if h.db != nil && st.ModTime().Equal(h.mod) {
return
}
openMMDB(h)
}
func (s *geoIPSuite) Close() {
s.mu.Lock()
defer s.mu.Unlock()
closeBIN(s.binV4)
closeBIN(s.binV6)
closeMMDB(s.asn)
closeMMDB(s.country)
}
func (s *geoIPSuite) BINV4Available() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.binV4 != nil && s.binV4.db != nil
}
func (s *geoIPSuite) BINV6Available() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.binV6 != nil && s.binV6.db != nil
}
func (s *geoIPSuite) ASNAvailable() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.asn != nil && s.asn.db != nil
}
func (s *geoIPSuite) CountryAvailable() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.country != nil && s.country.db != nil
}
func (s *geoIPSuite) AnyAvailable() bool {
return s.BINV4Available() || s.BINV6Available() || s.ASNAvailable() || s.CountryAvailable()
}
// Paths 返回 v4 BIN、v6 BIN、ASN、Country 兜底库路径
func (s *geoIPSuite) Paths() (v4, v6, asn, country string) {
return filepath.Join(s.dataDir, "IP2LOCATION-LITE-DB3.BIN"),
filepath.Join(s.dataDir, "IP2LOCATION-LITE-DB3.IPV6.BIN"),
filepath.Join(s.dataDir, "GeoLite2-ASN.mmdb"),
filepath.Join(s.dataDir, "GeoLite2-Country.mmdb")
}
type geoCountryOnlyRecord struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
type geoASNRecord struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
}
// Lookup 解析 IP 地理与 ASNBIN 负责国家/省/市ASN 独立查询)
func (s *geoIPSuite) Lookup(ipStr string) GeoInfo {
var out GeoInfo
ip := net.ParseIP(ipStr)
if ip == nil || s == nil {
return out
}
s.mu.RLock()
v4DB := (*ip2location.DB)(nil)
v6DB := (*ip2location.DB)(nil)
asnDB := (*maxminddb.Reader)(nil)
countryDB := (*maxminddb.Reader)(nil)
if s.binV4 != nil {
v4DB = s.binV4.db
}
if s.binV6 != nil {
v6DB = s.binV6.db
}
if s.asn != nil {
asnDB = s.asn.db
}
if s.country != nil {
countryDB = s.country.db
}
s.mu.RUnlock()
binDB := v6DB
if ip.To4() != nil {
binDB = v4DB
}
if binDB != nil {
rec, err := binDB.Get_all(ipStr)
if err == nil {
out.Country = normalizeCountryCode(rec.Country_short)
out.Region = strings.TrimSpace(rec.Region)
out.City = strings.TrimSpace(rec.City)
}
}
if out.Country == "" && countryDB != nil {
var rec geoCountryOnlyRecord
if err := countryDB.Lookup(ip, &rec); err == nil {
out.Country = normalizeCountryCode(rec.Country.ISOCode)
}
}
if asnDB != nil {
var rec geoASNRecord
if err := asnDB.Lookup(ip, &rec); err == nil {
out.ASN = rec.AutonomousSystemNumber
out.ASOrg = strings.TrimSpace(rec.AutonomousSystemOrganization)
}
}
ApplyGeoZh(&out)
return out
}
// GeoIPService 对外暴露的 Geo 查询(诊断命令等)
type GeoIPService struct {
inner *geoIPSuite
}
// NewGeoIPService 打开数据目录下的 BIN/MMDB
func NewGeoIPService(dataDir string) *GeoIPService {
return &GeoIPService{inner: newGeoIPSuite(dataDir)}
}
func (s *GeoIPService) Close() {
if s != nil && s.inner != nil {
s.inner.Close()
}
}
func (s *GeoIPService) Lookup(ip string) GeoInfo {
if s == nil || s.inner == nil {
return GeoInfo{}
}
return s.inner.Lookup(ip)
}
func (s *GeoIPService) Paths() (v4, v6, asn, country string) {
if s == nil || s.inner == nil {
return "", "", "", ""
}
return s.inner.Paths()
}
func (s *GeoIPService) BINV4Available() bool {
if s == nil || s.inner == nil {
return false
}
return s.inner.BINV4Available()
}
func (s *GeoIPService) BINV6Available() bool {
if s == nil || s.inner == nil {
return false
}
return s.inner.BINV6Available()
}
func (s *GeoIPService) ASNAvailable() bool {
if s == nil || s.inner == nil {
return false
}
return s.inner.ASNAvailable()
}
func (s *GeoIPService) CountryAvailable() bool {
if s == nil || s.inner == nil {
return false
}
return s.inner.CountryAvailable()
}