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

258
service/geo_zh.go Normal file
View File

@@ -0,0 +1,258 @@
package service
import (
"strconv"
"strings"
)
// countryZh ISO2 → 中文国名(展示用;存储仍用 ISO
var countryZh = map[string]string{
"CN": "中国", "HK": "中国香港", "MO": "中国澳门", "TW": "中国台湾",
"US": "美国", "JP": "日本", "KR": "韩国", "SG": "新加坡",
"GB": "英国", "DE": "德国", "FR": "法国", "RU": "俄罗斯",
"CA": "加拿大", "AU": "澳大利亚", "IN": "印度", "TH": "泰国",
"VN": "越南", "MY": "马来西亚", "ID": "印度尼西亚", "PH": "菲律宾",
"NL": "荷兰", "IT": "意大利", "ES": "西班牙", "BR": "巴西",
"MX": "墨西哥", "SE": "瑞典", "CH": "瑞士", "PL": "波兰",
"TR": "土耳其", "SA": "沙特阿拉伯", "AE": "阿联酋", "NZ": "新西兰",
"IE": "爱尔兰", "BE": "比利时", "AT": "奥地利", "NO": "挪威",
"DK": "丹麦", "FI": "芬兰", "PT": "葡萄牙", "CZ": "捷克",
"UA": "乌克兰", "IL": "以色列", "ZA": "南非", "AR": "阿根廷",
}
type cnRegionMeta struct {
Name string
ISO string
}
// cnRegionMap 中国省级英文名/别名 → 中文名 + 地图 ISO
var cnRegionMap = map[string]cnRegionMeta{
"beijing": {Name: "北京", ISO: "BJ"},
"peking": {Name: "北京", ISO: "BJ"},
"tianjin": {Name: "天津", ISO: "TJ"},
"shanghai": {Name: "上海", ISO: "SH"},
"chongqing": {Name: "重庆", ISO: "CQ"},
"chungking": {Name: "重庆", ISO: "CQ"},
"hebei": {Name: "河北", ISO: "HE"},
"shanxi": {Name: "山西", ISO: "SX"},
"liaoning": {Name: "辽宁", ISO: "LN"},
"jilin": {Name: "吉林", ISO: "JL"},
"heilongjiang": {Name: "黑龙江", ISO: "HL"},
"jiangsu": {Name: "江苏", ISO: "JS"},
"zhejiang": {Name: "浙江", ISO: "ZJ"},
"anhui": {Name: "安徽", ISO: "AH"},
"fujian": {Name: "福建", ISO: "FJ"},
"jiangxi": {Name: "江西", ISO: "JX"},
"shandong": {Name: "山东", ISO: "SD"},
"henan": {Name: "河南", ISO: "HA"},
"hubei": {Name: "湖北", ISO: "HB"},
"hunan": {Name: "湖南", ISO: "HN"},
"guangdong": {Name: "广东", ISO: "GD"},
"guangxi": {Name: "广西", ISO: "GX"},
"hainan": {Name: "海南", ISO: "HI"},
"sichuan": {Name: "四川", ISO: "SC"},
"guizhou": {Name: "贵州", ISO: "GZ"},
"yunnan": {Name: "云南", ISO: "YN"},
"xizang": {Name: "西藏", ISO: "XZ"},
"tibet": {Name: "西藏", ISO: "XZ"},
"shaanxi": {Name: "陕西", ISO: "SN"},
"shaanxi province": {Name: "陕西", ISO: "SN"},
"gansu": {Name: "甘肃", ISO: "GS"},
"qinghai": {Name: "青海", ISO: "QH"},
"ningxia": {Name: "宁夏", ISO: "NX"},
"xinjiang": {Name: "新疆", ISO: "XJ"},
"inner mongolia": {Name: "内蒙古", ISO: "NM"},
"nei mongol": {Name: "内蒙古", ISO: "NM"},
"hong kong": {Name: "香港", ISO: "HK"},
"macau": {Name: "澳门", ISO: "MO"},
"macao": {Name: "澳门", ISO: "MO"},
"taiwan": {Name: "台湾", ISO: "TW"},
}
// cityZh 常见城市英文名 → 中文
var cityZh = map[string]string{
"beijing": "北京", "peking": "北京",
"shanghai": "上海",
"chongqing": "重庆", "chungking": "重庆",
"tianjin": "天津",
"hangzhou": "杭州",
"shenzhen": "深圳",
"guangzhou": "广州", "canton": "广州",
"chengdu": "成都",
"wuhan": "武汉",
"xi'an": "西安", "xian": "西安",
"nanjing": "南京", "nanking": "南京",
"suzhou": "苏州",
"qingdao": "青岛",
"dalian": "大连",
"ningbo": "宁波",
"xiamen": "厦门", "amoy": "厦门",
"changsha": "长沙",
"zhengzhou": "郑州",
"jinan": "济南",
"harbin": "哈尔滨",
"shenyang": "沈阳",
"kunming": "昆明",
"nanning": "南宁",
"urumqi": "乌鲁木齐",
"lhasa": "拉萨",
"hohhot": "呼和浩特",
"taipei": "台北",
"hong kong": "香港",
}
// asnOrgZh 常见 ASN / AS 组织名 → 中文运营商
var asnOrgZh = map[uint]string{
4134: "中国电信",
4812: "中国电信",
4837: "中国联通",
4808: "中国联通",
9808: "中国移动",
56040: "中国移动",
56041: "中国移动",
56042: "中国移动",
4538: "教育网",
23910: "教育网",
58539: "教育网",
7497: "教育网",
}
var asnOrgKeywordZh = []struct {
kw string
zh string
}{
{"chinanet", "中国电信"},
{"china telecom", "中国电信"},
{"chinatelecom", "中国电信"},
{"unicom", "中国联通"},
{"china unicom", "中国联通"},
{"mobile", "中国移动"},
{"china mobile", "中国移动"},
{"cernet", "教育网"},
{"education", "教育网"},
}
// CountryLabelZh ISO2 → 中文国名(未知则返回原码)
func CountryLabelZh(iso string) string {
iso = strings.ToUpper(strings.TrimSpace(iso))
if iso == "" {
return ""
}
if v, ok := countryZh[iso]; ok {
return v
}
return iso
}
// ApplyGeoZh 就地补充中文省/市/运营商country 保持 ISO2
func ApplyGeoZh(g *GeoInfo) {
if g == nil {
return
}
g.Country = normalizeCountryCode(g.Country)
if g.Country == "CN" || g.Country == "HK" || g.Country == "MO" || g.Country == "TW" {
mapCNRegionCity(g)
}
if g.ASN > 0 {
if zh, ok := asnOrgZh[g.ASN]; ok {
g.ASOrg = zh
} else if g.ASOrg != "" {
g.ASOrg = mapASOrgZh(g.ASOrg)
}
} else if g.ASOrg != "" {
g.ASOrg = mapASOrgZh(g.ASOrg)
}
g.Region = truncateRunes(strings.TrimSpace(g.Region), 64)
g.City = truncateRunes(strings.TrimSpace(g.City), 64)
g.ASOrg = truncateRunes(strings.TrimSpace(g.ASOrg), 128)
}
func mapCNRegionCity(g *GeoInfo) {
if g.RegionISO == "" {
if meta, ok := lookupCNRegion(g.Region); ok {
g.Region = meta.Name
g.RegionISO = meta.ISO
}
} else if g.Region != "" {
if meta, ok := lookupCNRegion(g.Region); ok {
if g.RegionISO == "" {
g.RegionISO = meta.ISO
}
g.Region = meta.Name
}
} else if g.RegionISO != "" {
for _, meta := range cnRegionMap {
if meta.ISO == strings.ToUpper(g.RegionISO) {
g.Region = meta.Name
break
}
}
}
if g.City != "" {
if zh, ok := lookupCityZh(g.City); ok {
g.City = zh
}
}
// 直辖市BIN 有时只给城市不给省
if g.Region == "" && g.City != "" {
switch g.City {
case "北京", "上海", "天津", "重庆":
g.Region = g.City
if meta, ok := cnRegionMap[strings.ToLower(g.City)]; ok {
g.RegionISO = meta.ISO
}
}
}
}
func lookupCNRegion(name string) (cnRegionMeta, bool) {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" {
return cnRegionMeta{}, false
}
if meta, ok := cnRegionMap[key]; ok {
return meta, true
}
// 去掉常见后缀再试
for _, suffix := range []string{" province", " sheng", " autonomous region", " municipality"} {
if strings.HasSuffix(key, suffix) {
if meta, ok := cnRegionMap[strings.TrimSuffix(key, suffix)]; ok {
return meta, true
}
}
}
return cnRegionMeta{}, false
}
func lookupCityZh(name string) (string, bool) {
key := strings.ToLower(strings.TrimSpace(name))
if zh, ok := cityZh[key]; ok {
return zh, true
}
return "", false
}
func mapASOrgZh(org string) string {
l := strings.ToLower(strings.TrimSpace(org))
if l == "" {
return org
}
for _, rule := range asnOrgKeywordZh {
if strings.Contains(l, rule.kw) {
return rule.zh
}
}
// "AS4134 Chinanet" 等形式
if strings.HasPrefix(l, "as") {
num := strings.TrimPrefix(l, "as")
if i := strings.IndexByte(num, ' '); i >= 0 {
num = num[:i]
}
if n, err := strconv.ParseUint(num, 10, 32); err == nil {
if zh, ok := asnOrgZh[uint(n)]; ok {
return zh
}
}
}
return org
}