// 只读诊断:检查 Geo 库与 page_views 地理字段,或对指定 IP 试查中文映射结果。 // // go run ./cmd/monitor-geo-check -db data/monitor.db // go run ./cmd/monitor-geo-check -db data/monitor.db -ip 14.109.35.246 package main import ( "flag" "fmt" "os" "path/filepath" "strings" "time" "git.iioio.com/freefire/jiang13-forum/service" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func main() { dbPath := flag.String("db", filepath.Join("data", "monitor.db"), "含 page_views 的 SQLite(默认 data/monitor.db)") ipLookup := flag.String("ip", "", "对指定 IP 试查 BIN+ASN(不写库)") flag.Parse() abs, err := filepath.Abs(*dbPath) if err != nil { fatalf("解析路径失败: %v", err) } dataDir := filepath.Dir(abs) if *ipLookup == "" { if st, err := os.Stat(abs); err != nil || st.IsDir() { fatalf("数据库不存在: %s", abs) } } fmt.Printf("数据库: %s\n", abs) fmt.Printf("数据目录: %s\n\n", dataDir) suite := service.NewGeoIPService(dataDir) defer suite.Close() printGeoFiles(dataDir, suite) if ip := strings.TrimSpace(*ipLookup); ip != "" { lookupIP(suite, ip) fmt.Println() if _, err := os.Stat(abs); err != nil { return } } db, err := gorm.Open(sqlite.Open(abs), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { fatalf("打开数据库失败: %v", err) } sqlDB, err := db.DB() if err != nil { fatalf("获取连接失败: %v", err) } defer sqlDB.Close() sqlDB.SetMaxOpenConns(1) if !tableExists(db, "page_views") { fatalf("表 page_views 不存在(请确认指向 data/monitor.db,或尚未写入过浏览量)") } have := columnSet(db) printColumnPresence(have) since := time.Now().AddDate(0, 0, -30) var total, recentNonBot, withCountry, withRegion, withCity, withASN int64 _ = db.Table("page_views").Count(&total).Error _ = db.Table("page_views").Where("created_at >= ? AND is_bot = ?", since, false).Count(&recentNonBot).Error fmt.Println("=== page_views 统计(近 30 日非 bot)===") fmt.Printf("总量: %d\n", total) fmt.Printf("近 30 日非 bot: %d\n", recentNonBot) if have["country"] { _ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND country <> ''", since, false).Count(&withCountry).Error fmt.Printf("有国家: %d\n", withCountry) } else { fmt.Println("有国家: (列缺失)") } if have["region"] || have["region_iso"] { cond := "created_at >= ? AND is_bot = ?" parts := []string{} if have["region"] { parts = append(parts, "region <> ''") } if have["region_iso"] { parts = append(parts, "region_iso <> ''") } _ = db.Table("page_views"). Where(cond+" AND ("+strings.Join(parts, " OR ")+")", since, false). Count(&withRegion).Error fmt.Printf("有省字段: %d\n", withRegion) } else { fmt.Println("有省字段: (列缺失)") } if have["city"] { _ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND city <> ''", since, false).Count(&withCity).Error fmt.Printf("有城市: %d\n", withCity) } else { fmt.Println("有城市: (列缺失)") } if have["asn"] { _ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND asn > 0", since, false).Count(&withASN).Error fmt.Printf("有 ASN: %d\n", withASN) } else { fmt.Println("有 ASN: (列缺失)") } fmt.Println() fmt.Println("=== 近 30 日省级 Top(country / region / region_iso)===") if !(have["region"] || have["region_iso"]) { fmt.Println("(省列尚未迁移,跳过)") } else { type regionRow struct { Country string Region string RegionISO string `gorm:"column:region_iso"` Count int64 } parts := []string{} if have["region"] { parts = append(parts, "region <> ''") } if have["region_iso"] { parts = append(parts, "region_iso <> ''") } selParts := []string{} groupParts := []string{} if have["country"] { selParts = append(selParts, "country") groupParts = append(groupParts, "country") } else { selParts = append(selParts, "'' as country") } if have["region"] { selParts = append(selParts, "region") groupParts = append(groupParts, "region") } else { selParts = append(selParts, "'' as region") } if have["region_iso"] { selParts = append(selParts, "region_iso") groupParts = append(groupParts, "region_iso") } else { selParts = append(selParts, "'' as region_iso") } selParts = append(selParts, "COUNT(*) as count") var regions []regionRow _ = db.Table("page_views"). Select(strings.Join(selParts, ", ")). Where("created_at >= ? AND is_bot = ? AND ("+strings.Join(parts, " OR ")+")", since, false). Group(strings.Join(groupParts, ", ")). Order("count DESC"). Limit(20). Scan(®ions).Error if len(regions) == 0 { fmt.Println("(空)") } else { for i, r := range regions { fmt.Printf("%2d. %s | %s | %s | %d\n", i+1, r.Country, r.Region, r.RegionISO, r.Count) } } } fmt.Println() fmt.Println("=== 对账抽样(近 30 日非 bot)===") printGeoReconcileSamples(db, have, since) fmt.Println() fmt.Println("=== 样例行(近 30 日,最多 10 条)===") cols := []string{"id", "created_at", "path", "is_bot"} for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} { if have[c] { cols = append(cols, c) } } type sample struct { ID uint CreatedAt time.Time IP string Country string Region string RegionISO string `gorm:"column:region_iso"` City string ASN uint ASOrg string `gorm:"column:as_org"` Path string IsBot bool } var samples []sample _ = db.Table("page_views"). Select(strings.Join(cols, ", ")). Where("created_at >= ?", since). Order("id DESC"). Limit(10). Scan(&samples).Error if len(samples) == 0 { fmt.Println("(无近 30 日 pageview)") } else { for _, s := range samples { fmt.Printf("#%d %s ip=%s country=%q region=%q iso=%q city=%q asn=%d org=%q bot=%v path=%s\n", s.ID, s.CreatedAt.Format("2006-01-02 15:04:05"), s.IP, s.Country, s.Region, s.RegionISO, s.City, s.ASN, s.ASOrg, s.IsBot, s.Path) } } regionReady := have["region"] || have["region_iso"] if !regionReady || withRegion == 0 { fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "提示: 近 30 日无省级字段。中国地图「暂无省级访问数据」属预期。") if !regionReady { fmt.Fprintln(os.Stderr, "当前库缺 region/region_iso 列:请先启动一次主程序以完成 AutoMigrate。") } fmt.Fprintln(os.Stderr, "请确认数据目录已放置 IP2LOCATION-LITE-DB3.BIN 与 GeoLite2-ASN.mmdb,且有公网访问产生的 pageview。") fmt.Fprintln(os.Stderr, "本机/私网 IP 通常解不出省,属正常。") fmt.Fprintln(os.Stderr, "注意: 请求日志有地理 ≠ 概览地图/排行有数据(后者只读 page_views)。") os.Exit(2) } } func printGeoReconcileSamples(db *gorm.DB, have map[string]bool, since time.Time) { type sample struct { ID uint CreatedAt time.Time IP string Country string Region string RegionISO string `gorm:"column:region_iso"` City string ASN uint ASOrg string `gorm:"column:as_org"` Path string } selCols := []string{"id", "created_at", "path"} for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} { if have[c] { selCols = append(selCols, c) } } sel := strings.Join(selCols, ", ") printBlock := func(title string, n int64, rows []sample) { fmt.Printf("%s: %d\n", title, n) if len(rows) == 0 { return } for _, s := range rows { fmt.Printf(" #%d %s ip=%s country=%q region=%q iso=%q city=%q asn=%d org=%q path=%s\n", s.ID, s.CreatedAt.Format("2006-01-02 15:04:05"), s.IP, s.Country, s.Region, s.RegionISO, s.City, s.ASN, s.ASOrg, s.Path) } } if have["city"] { var n int64 _ = db.Table("page_views"). Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false). Count(&n).Error var rows []sample _ = db.Table("page_views"). Select(sel). Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false). Order("id DESC"). Limit(5). Scan(&rows).Error printBlock("英文城市名(可能未中文化)", n, rows) } else { fmt.Println("英文城市名: (city 列缺失)") } if have["region"] || have["region_iso"] { cond := "created_at >= ? AND is_bot = ? AND country <> ''" emptyParts := []string{} if have["region"] { emptyParts = append(emptyParts, "(region = '' OR region IS NULL OR region = '-')") } if have["region_iso"] { emptyParts = append(emptyParts, "(region_iso = '' OR region_iso IS NULL)") } where := cond + " AND (" + strings.Join(emptyParts, " AND ") + ")" var n int64 _ = db.Table("page_views").Where(where, since, false).Count(&n).Error var rows []sample _ = db.Table("page_views"). Select(sel). Where(where, since, false). Order("id DESC"). Limit(5). Scan(&rows).Error printBlock("有国家但省为空", n, rows) } else { fmt.Println("有国家但省为空: (省列缺失)") } if have["asn"] { var n int64 _ = db.Table("page_views"). Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false). Count(&n).Error var rows []sample _ = db.Table("page_views"). Select(sel). Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false). Order("id DESC"). Limit(5). Scan(&rows).Error printBlock("asn=0(无运营商)", n, rows) } else { fmt.Println("asn=0: (asn 列缺失)") } } func printGeoFiles(dataDir string, suite *service.GeoIPService) { fmt.Println("=== Geo 数据文件 ===") v4, v6, asn, country := suite.Paths() for _, item := range []struct { name string path string ok bool }{ {"IP2LOCATION-LITE-DB3.BIN", v4, suite.BINV4Available()}, {"IP2LOCATION-LITE-DB3.IPV6.BIN", v6, suite.BINV6Available()}, {"GeoLite2-ASN.mmdb", asn, suite.ASNAvailable()}, {"GeoLite2-Country.mmdb", country, suite.CountryAvailable()}, } { st, err := os.Stat(item.path) if err != nil { fmt.Printf("%s: 不存在\n", item.name) continue } loaded := "未加载" if item.ok { loaded = "已加载" } fmt.Printf("%s: 存在 (%d bytes, mtime %s) · %s\n", item.name, st.Size(), st.ModTime().Format(time.RFC3339), loaded) } fmt.Println() } func lookupIP(suite *service.GeoIPService, ipStr string) { fmt.Printf("=== IP 试查 %s(中文映射后)===\n", ipStr) info := suite.Lookup(ipStr) if info.Country == "" && info.Region == "" && info.City == "" && info.ASN == 0 { fmt.Println("未解析到地理信息(请确认 BIN/ASN 文件存在且 IP 为公网地址)") return } fmt.Printf("国家 ISO: %s (%s)\n", info.Country, service.CountryLabelZh(info.Country)) if info.Region != "" || info.RegionISO != "" { fmt.Printf("省/州: %s / %s\n", info.Region, info.RegionISO) } if info.City != "" { fmt.Printf("城市: %s\n", info.City) } if info.ASN > 0 || info.ASOrg != "" { fmt.Printf("ASN: AS%d %s\n", info.ASN, info.ASOrg) } } func tableExists(db *gorm.DB, name string) bool { var n int err := db.Raw( `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, name, ).Scan(&n).Error return err == nil && n > 0 } func columnSet(db *gorm.DB) map[string]bool { type colRow struct { Name string `gorm:"column:name"` } var cols []colRow _ = db.Raw(`PRAGMA table_info(page_views)`).Scan(&cols).Error have := map[string]bool{} for _, c := range cols { have[c.Name] = true } return have } func printColumnPresence(have map[string]bool) { need := []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} fmt.Println("=== page_views 列 ===") missing := false for _, n := range need { if have[n] { fmt.Printf("%s: 有\n", n) } else { fmt.Printf("%s: 缺失\n", n) missing = true } } fmt.Println() if missing { fmt.Fprintln(os.Stderr, "提示: 缺列时请先启动一次主程序以完成监控库 AutoMigrate。") fmt.Fprintln(os.Stderr, "") } } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) os.Exit(1) }