feat: 管理端网站监控,浏览量写入独立 monitor.db

请求日志按日 JSONL;page_views 不进主库,避免统计数据撑大 jiang13.db。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 07:55:45 +08:00
parent 16b6a18096
commit 429956c594
54 changed files with 5504 additions and 146 deletions

333
service/geoip.go Normal file
View File

@@ -0,0 +1,333 @@
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()
}