fix: 补全 IP2Location 中国城市中文映射并按省消歧

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 08:40:12 +08:00
parent c6ce44f8b4
commit 34f3d088ad
5 changed files with 1080 additions and 77 deletions

View File

@@ -35,7 +35,7 @@
CDN 头(如 `CF-IPCountry`)仅在本地库无国家码时补全。不落 Lat/Lon/Zip/TimeZone。管理端展示**完整客户端 IP**。 CDN 头(如 `CF-IPCountry`)仅在本地库无国家码时补全。不落 Lat/Lon/Zip/TimeZone。管理端展示**完整客户端 IP**。
写入时做本地中文映射(省/常见城市/运营商);`country` 存 ISO2展示用中文名 写入时做本地中文映射(省 / 中国城市按 IP2Location 英文名 / 运营商);同音城市(如苏州/宿州)按省份歧义。`country` 存 ISO2展示用中文名。历史 `page_views` / 请求日志不会回写,仅影响新写入
## 写入路径(性能) ## 写入路径(性能)

View File

@@ -61,45 +61,39 @@ var cnRegionMap = map[string]cnRegionMeta{
"qinghai": {Name: "青海", ISO: "QH"}, "qinghai": {Name: "青海", ISO: "QH"},
"ningxia": {Name: "宁夏", ISO: "NX"}, "ningxia": {Name: "宁夏", ISO: "NX"},
"xinjiang": {Name: "新疆", ISO: "XJ"}, "xinjiang": {Name: "新疆", ISO: "XJ"},
"xinjiang uygur": {Name: "新疆", ISO: "XJ"},
"inner mongolia": {Name: "内蒙古", ISO: "NM"}, "inner mongolia": {Name: "内蒙古", ISO: "NM"},
"nei mongol": {Name: "内蒙古", ISO: "NM"}, "nei mongol": {Name: "内蒙古", ISO: "NM"},
"guangxi zhuangzu": {Name: "广西", ISO: "GX"},
"ningxia huizu": {Name: "宁夏", ISO: "NX"},
"hong kong": {Name: "香港", ISO: "HK"}, "hong kong": {Name: "香港", ISO: "HK"},
"macau": {Name: "澳门", ISO: "MO"}, "macau": {Name: "澳门", ISO: "MO"},
"macao": {Name: "澳门", ISO: "MO"}, "macao": {Name: "澳门", ISO: "MO"},
"taiwan": {Name: "台湾", ISO: "TW"}, "taiwan": {Name: "台湾", ISO: "TW"},
// 台湾各县市IP2Location 常直接给县市名作为 region
"changhua": {Name: "台湾", ISO: "TW"},
"chiayi": {Name: "台湾", ISO: "TW"},
"hsinchu": {Name: "台湾", ISO: "TW"},
"hualien": {Name: "台湾", ISO: "TW"},
"kaohsiung": {Name: "台湾", ISO: "TW"},
"keelung": {Name: "台湾", ISO: "TW"},
"kinmen": {Name: "台湾", ISO: "TW"},
"lienchiang": {Name: "台湾", ISO: "TW"},
"miaoli": {Name: "台湾", ISO: "TW"},
"nantou": {Name: "台湾", ISO: "TW"},
"new taipei": {Name: "台湾", ISO: "TW"},
"penghu": {Name: "台湾", ISO: "TW"},
"pingtung": {Name: "台湾", ISO: "TW"},
"taichung": {Name: "台湾", ISO: "TW"},
"tainan": {Name: "台湾", ISO: "TW"},
"taipei": {Name: "台湾", ISO: "TW"},
"taitung": {Name: "台湾", ISO: "TW"},
"taoyuan": {Name: "台湾", ISO: "TW"},
"yilan": {Name: "台湾", ISO: "TW"},
"yunlin": {Name: "台湾", ISO: "TW"},
} }
// cityZh 常见城市英文名 → 中文 // cityZh 定义见 geo_zh_cities.goLITE 中国城市全表 + 同音复合键)
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 组织名 → 中文运营商 // asnOrgZh 常见 ASN / AS 组织名 → 中文运营商
var asnOrgZh = map[uint]string{ var asnOrgZh = map[uint]string{
@@ -189,7 +183,7 @@ func mapCNRegionCity(g *GeoInfo) {
} }
} }
if g.City != "" { if g.City != "" {
if zh, ok := lookupCityZh(g.City); ok { if zh, ok := lookupCityZh(g.RegionISO, g.Region, g.City); ok {
g.City = zh g.City = zh
} }
} }
@@ -214,7 +208,7 @@ func lookupCNRegion(name string) (cnRegionMeta, bool) {
return meta, true return meta, true
} }
// 去掉常见后缀再试 // 去掉常见后缀再试
for _, suffix := range []string{" province", " sheng", " autonomous region", " municipality"} { for _, suffix := range []string{" province", " sheng", " autonomous region", " municipality", " zhuangzu", " huizu", " uygur"} {
if strings.HasSuffix(key, suffix) { if strings.HasSuffix(key, suffix) {
if meta, ok := cnRegionMap[strings.TrimSuffix(key, suffix)]; ok { if meta, ok := cnRegionMap[strings.TrimSuffix(key, suffix)]; ok {
return meta, true return meta, true
@@ -224,14 +218,69 @@ func lookupCNRegion(name string) (cnRegionMeta, bool) {
return cnRegionMeta{}, false return cnRegionMeta{}, false
} }
func lookupCityZh(name string) (string, bool) { // lookupCityZh 将 IP2Location 英文城市名映射为中文。
key := strings.ToLower(strings.TrimSpace(name)) // 同音城市优先用「省ISO|城市」复合键;并尝试去掉 city/shi 后缀与撇号变体。
if zh, ok := cityZh[key]; ok { func lookupCityZh(regionISO, region, name string) (string, bool) {
key := normalizeCityKey(name)
if key == "" {
return "", false
}
iso := strings.ToLower(strings.TrimSpace(regionISO))
if iso == "" {
if meta, ok := lookupCNRegion(region); ok {
iso = strings.ToLower(meta.ISO)
}
}
candidates := cityKeyCandidates(key)
if iso != "" {
for _, c := range candidates {
if zh, ok := cityZh[iso+"|"+c]; ok {
return zh, true return zh, true
} }
}
}
for _, c := range candidates {
if zh, ok := cityZh[c]; ok {
return zh, true
}
}
return "", false return "", false
} }
func normalizeCityKey(name string) string {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" || key == "-" {
return ""
}
return key
}
func cityKeyCandidates(key string) []string {
seen := map[string]struct{}{}
var out []string
add := func(s string) {
s = strings.TrimSpace(s)
if s == "" {
return
}
if _, ok := seen[s]; ok {
return
}
seen[s] = struct{}{}
out = append(out, s)
}
add(key)
add(strings.ReplaceAll(key, "'", ""))
for _, suffix := range []string{" city", " shi"} {
if strings.HasSuffix(key, suffix) {
base := strings.TrimSuffix(key, suffix)
add(base)
add(strings.ReplaceAll(base, "'", ""))
}
}
return out
}
func mapASOrgZh(org string) string { func mapASOrgZh(org string) string {
l := strings.ToLower(strings.TrimSpace(org)) l := strings.ToLower(strings.TrimSpace(org))
if l == "" { if l == "" {

422
service/geo_zh_cities.go Normal file
View File

@@ -0,0 +1,422 @@
package service
// 由 IP2Location LITE-DB3 中国含港澳台city 采样生成。
// 键为小写英文城市名同音城市使用「省ISO|城市」复合键(如 ah|suzhou
var cityZh = map[string]string{
"ah|suzhou": "宿州",
"aksu": "阿克苏",
"altay": "阿勒泰",
"amoy": "厦门",
"ankang": "安康",
"anqing": "安庆",
"anshan": "鞍山",
"anshun": "安顺",
"anyang": "安阳",
"baicheng": "白城",
"baise": "百色",
"baishan": "白山",
"baiyin": "白银",
"banqiao": "板桥",
"baoding": "保定",
"baoji": "宝鸡",
"baoshan": "保山",
"baotou": "包头",
"barkam": "马尔康",
"bayannur": "巴彦淖尔",
"bazhong": "巴中",
"beihai": "北海",
"beijing": "北京",
"beitun": "北屯",
"bengbu": "蚌埠",
"benxi": "本溪",
"bijie": "毕节",
"binzhou": "滨州",
"bole": "博乐",
"bozhou": "亳州",
"cangzhou": "沧州",
"canton": "广州",
"changchun": "长春",
"changde": "常德",
"changhua": "彰化",
"changji": "昌吉",
"changsha": "长沙",
"changzhi": "长治",
"changzhou": "常州",
"chaoyang": "朝阳",
"chaozhou": "潮州",
"chengde": "承德",
"chengdu": "成都",
"chenzhou": "郴州",
"chiayi": "嘉义",
"chifeng": "赤峰",
"chizhou": "池州",
"chongqing": "重庆",
"chongzuo": "崇左",
"chungking": "重庆",
"chuxiong": "楚雄",
"chuzhou": "滁州",
"dali": "大理",
"dalian": "大连",
"dandong": "丹东",
"danzhou": "儋州",
"daqing": "大庆",
"datong": "大同",
"dazhou": "达州",
"delingha": "德令哈",
"deyang": "德阳",
"dezhou": "德州",
"dingxi": "定西",
"dongguan": "东莞",
"dongying": "东营",
"douliu": "斗六",
"duyun": "都匀",
"emeishan": "峨眉山",
"enshi": "恩施",
"ezhou": "鄂州",
"fangchenggang": "防城港",
"fenyang": "汾阳",
"fj|fuzhou": "福州",
"foshan": "佛山",
"fuqing": "福清",
"fushun": "抚顺",
"fuxin": "阜新",
"fuyang": "阜阳",
"fuzhou": "福州",
"ganzhou": "赣州",
"gaobeidian": "高碑店",
"gongyi": "巩义",
"gs|longnan": "陇南",
"guang'an": "广安",
"guangan": "广安",
"guangyuan": "广元",
"guangzhou": "广州",
"guigang": "贵港",
"guilin": "桂林",
"guiyang": "贵阳",
"guyuan": "固原",
"gx|yulin": "玉林",
"haerbin": "哈尔滨",
"haidong": "海东",
"haikou": "海口",
"haining": "海宁",
"hami": "哈密",
"handan": "邯郸",
"hangzhou": "杭州",
"hanzhong": "汉中",
"harbin": "哈尔滨",
"hebi": "鹤壁",
"hechi": "河池",
"hefei": "合肥",
"hegang": "鹤岗",
"heihe": "黑河",
"hengchun": "恒春",
"hengshui": "衡水",
"hengyang": "衡阳",
"heyuan": "河源",
"heze": "菏泽",
"hezhou": "贺州",
"hl|yichun": "伊春",
"hohhot": "呼和浩特",
"hong kong": "香港",
"honghe": "红河",
"hotan": "和田",
"hsinchu": "新竹",
"huai'an": "淮安",
"huaian": "淮安",
"huaibei": "淮北",
"huaihua": "怀化",
"huainan": "淮南",
"hualien": "花莲",
"huanggang": "黄冈",
"huangshan": "黄山",
"huangshi": "黄石",
"huhehot": "呼和浩特",
"huizhou": "惠州",
"huludao": "葫芦岛",
"hulunbuir": "呼伦贝尔",
"huzhou": "湖州",
"ji'an": "吉安",
"jiamusi": "佳木斯",
"jian": "吉安",
"jiangmen": "江门",
"jiaozuo": "焦作",
"jiaxing": "嘉兴",
"jiayuguan": "嘉峪关",
"jieyang": "揭阳",
"jilin": "吉林",
"jinan": "济南",
"jinchang": "金昌",
"jincheng": "晋城",
"jingdezhen": "景德镇",
"jinghong": "景洪",
"jingmen": "荆门",
"jingzhou": "荆州",
"jinhua": "金华",
"jining": "济宁",
"jinzhong": "晋中",
"jinzhou": "锦州",
"jishou": "吉首",
"jiujiang": "九江",
"jiuquan": "酒泉",
"jixi": "鸡西",
"jiyuan": "济源",
"js|suzhou": "苏州",
"js|taizhou": "泰州",
"jx|fuzhou": "抚州",
"jx|longnan": "龙南",
"jx|yichun": "宜春",
"kaifeng": "开封",
"kaili": "凯里",
"kangding": "康定",
"kaohsiung": "高雄",
"karamay": "克拉玛依",
"kashgar": "喀什",
"keelung": "基隆",
"korla": "库尔勒",
"kuk liu": "谷寮",
"kunming": "昆明",
"kuytun": "奎屯",
"laibin": "来宾",
"laiyang": "莱阳",
"langfang": "廊坊",
"lanzhou": "兰州",
"leshan": "乐山",
"lhasa": "拉萨",
"lianyungang": "连云港",
"liaocheng": "聊城",
"liaoyang": "辽阳",
"liaoyuan": "辽源",
"lichuan": "利川",
"lijiang": "丽江",
"lincang": "临沧",
"linfen": "临汾",
"linqing": "临清",
"linxia": "临夏",
"linyi": "临沂",
"lishui": "丽水",
"liupanshui": "六盘水",
"liuzhou": "柳州",
"lo so shing": "芦须城",
"longnan": "陇南",
"longyan": "龙岩",
"loudi": "娄底",
"lu'an": "六安",
"luan": "六安",
"luohe": "漯河",
"luoyang": "洛阳",
"luzhou": "泸州",
"ma'anshan": "马鞍山",
"maanshan": "马鞍山",
"macao": "澳门",
"macau": "澳门",
"magong": "马公",
"mangshi": "芒市",
"maoming": "茂名",
"meishan": "眉山",
"meizhou": "梅州",
"mianyang": "绵阳",
"miaoli": "苗栗",
"mudanjiang": "牡丹江",
"nagqu": "那曲",
"nanchang": "南昌",
"nanchong": "南充",
"nangan": "南竿",
"nanjing": "南京",
"nanking": "南京",
"nanning": "南宁",
"nanping": "南平",
"nantong": "南通",
"nantou": "南投",
"nanyang": "南阳",
"neijiang": "内江",
"new taipei": "新北",
"ningbo": "宁波",
"ningde": "宁德",
"ordos": "鄂尔多斯",
"pan long wan": "蟠龙湾",
"panjin": "盘锦",
"panzhihua": "攀枝花",
"peking": "北京",
"pengzhou": "彭州",
"pingdingshan": "平顶山",
"pingliang": "平凉",
"pingtung": "屏东",
"pingxiang": "萍乡",
"pu'er": "普洱",
"puer": "普洱",
"puli": "埔里",
"puning": "普宁",
"putian": "莆田",
"puyang": "濮阳",
"qamdo": "昌都",
"qianjiang": "潜江",
"qingdao": "青岛",
"qingyang": "庆阳",
"qingyuan": "清远",
"qinhuangdao": "秦皇岛",
"qinzhou": "钦州",
"qionghai": "琼海",
"qiqihar": "齐齐哈尔",
"qitaihe": "七台河",
"quanzhou": "泉州",
"qujing": "曲靖",
"quzhou": "衢州",
"renqiu": "任丘",
"rizhao": "日照",
"sanmenxia": "三门峡",
"sanming": "三明",
"sanya": "三亚",
"shanghai": "上海",
"shangluo": "商洛",
"shangqiu": "商丘",
"shangrao": "上饶",
"shannan": "山南",
"shantou": "汕头",
"shanwei": "汕尾",
"shaoguan": "韶关",
"shaoxing": "绍兴",
"shaoyang": "邵阳",
"shenyang": "沈阳",
"shenzhen": "深圳",
"shihezi": "石河子",
"shijiazhuang": "石家庄",
"shiyan": "十堰",
"shizuishan": "石嘴山",
"shuangyashan": "双鸭山",
"shuozhou": "朔州",
"siping": "四平",
"sn|yulin": "榆林",
"songyuan": "松原",
"suihua": "绥化",
"suining": "遂宁",
"suizhou": "随州",
"suqian": "宿迁",
"suzhou": "苏州",
"sx|jincheng": "晋城",
"tacheng": "塔城",
"tai wong ha": "大王下",
"tai'an": "泰安",
"taian": "泰安",
"taibao": "太保",
"taichung": "台中",
"tainan": "台南",
"taipei": "台北",
"taitung": "台东",
"taiyuan": "太原",
"taizhou": "台州",
"tangshan": "唐山",
"taoyuan": "桃园",
"tianjin": "天津",
"tianmen": "天门",
"tianshui": "天水",
"tieling": "铁岭",
"to fung shan": "道风山",
"tongchuan": "铜川",
"tonghua": "通化",
"tongliao": "通辽",
"tongling": "铜陵",
"tongren": "铜仁",
"tsim sha tsui": "尖沙咀",
"turpan": "吐鲁番",
"tw|jincheng": "金城",
"ulanhot": "乌兰浩特",
"ulanqab": "乌兰察布",
"urumqi": "乌鲁木齐",
"wafangdian": "瓦房店",
"weifang": "潍坊",
"weihai": "威海",
"weinan": "渭南",
"wenchang": "文昌",
"wenling": "温岭",
"wenshan": "文山",
"wenzhou": "温州",
"wong fung leng": "黄峰岭",
"wuchuan": "吴川",
"wuhai": "乌海",
"wuhan": "武汉",
"wuhu": "芜湖",
"wulumuqi": "乌鲁木齐",
"wuwei": "武威",
"wuxi": "无锡",
"wuzhishan": "五指山",
"wuzhong": "吴忠",
"wuzhou": "梧州",
"xi'an": "西安",
"xiamen": "厦门",
"xian": "西安",
"xiangtan": "湘潭",
"xiangyang": "襄阳",
"xianning": "咸宁",
"xiantao": "仙桃",
"xianyang": "咸阳",
"xiaogan": "孝感",
"xichang": "西昌",
"xilinhot": "锡林浩特",
"xingtai": "邢台",
"xingyi": "兴义",
"xining": "西宁",
"xinxiang": "新乡",
"xinyang": "信阳",
"xinying": "新营",
"xinyu": "新余",
"xinzhou": "忻州",
"xuancheng": "宣城",
"xuchang": "许昌",
"xuzhou": "徐州",
"ya'an": "雅安",
"yaan": "雅安",
"yan'an": "延安",
"yanan": "延安",
"yancheng": "盐城",
"yangjiang": "阳江",
"yangquan": "阳泉",
"yangzhou": "扬州",
"yanji": "延吉",
"yantai": "烟台",
"yibin": "宜宾",
"yichang": "宜昌",
"yichun": "宜春",
"yilan": "宜兰",
"yinchuan": "银川",
"yingkou": "营口",
"yingtan": "鹰潭",
"yiwu": "义乌",
"yiyang": "益阳",
"yongzhou": "永州",
"yuanlin": "员林",
"yueyang": "岳阳",
"yujing": "玉井",
"yulin": "玉林",
"yuncheng": "运城",
"yunfu": "云浮",
"yunlin": "云林",
"yushu": "玉树",
"yuxi": "玉溪",
"yuyao": "余姚",
"zaozhuang": "枣庄",
"zhangjiajie": "张家界",
"zhangjiakou": "张家口",
"zhangshu": "樟树",
"zhangye": "张掖",
"zhangzhou": "漳州",
"zhanjiang": "湛江",
"zhaoqing": "肇庆",
"zhaotong": "昭通",
"zhengzhou": "郑州",
"zhenjiang": "镇江",
"zhongshan": "中山",
"zhongwei": "中卫",
"zhoukou": "周口",
"zhoushan": "舟山",
"zhuhai": "珠海",
"zhuji": "诸暨",
"zhumadian": "驻马店",
"zhuozhou": "涿州",
"zhuzhou": "株洲",
"zibo": "淄博",
"zigong": "自贡",
"ziyang": "资阳",
"zj|taizhou": "台州",
"zunhua": "遵化",
"zunyi": "遵义",
}

View File

@@ -0,0 +1,392 @@
package service
// 由 tmp_cities_only.tsv 生成:断言 LITE 采样到的 CN/HK/MO/TW 城市均可中文化。
var liteCNCities = []struct{ region, city string }{
{"Anhui", "Anqing"},
{"Anhui", "Bengbu"},
{"Anhui", "Bozhou"},
{"Anhui", "Chizhou"},
{"Anhui", "Chuzhou"},
{"Anhui", "Fuyang"},
{"Anhui", "Hefei"},
{"Anhui", "Huaibei"},
{"Anhui", "Huainan"},
{"Anhui", "Huangshan"},
{"Anhui", "Lu'an"},
{"Anhui", "Ma'anshan"},
{"Anhui", "Suzhou"},
{"Anhui", "Tongling"},
{"Anhui", "Wuhu"},
{"Anhui", "Xuancheng"},
{"Beijing", "Beijing"},
{"Changhua", "Changhua"},
{"Changhua", "Yuanlin"},
{"Chiayi", "Chiayi"},
{"Chiayi", "Taibao"},
{"Chongqing", "Chongqing"},
{"Fujian", "Fuqing"},
{"Fujian", "Fuzhou"},
{"Fujian", "Longyan"},
{"Fujian", "Nanping"},
{"Fujian", "Ningde"},
{"Fujian", "Putian"},
{"Fujian", "Quanzhou"},
{"Fujian", "Sanming"},
{"Fujian", "Xiamen"},
{"Fujian", "Zhangzhou"},
{"Gansu", "Baiyin"},
{"Gansu", "Dingxi"},
{"Gansu", "Jiayuguan"},
{"Gansu", "Jinchang"},
{"Gansu", "Jiuquan"},
{"Gansu", "Lanzhou"},
{"Gansu", "Linxia"},
{"Gansu", "Longnan"},
{"Gansu", "Pingliang"},
{"Gansu", "Qingyang"},
{"Gansu", "Tianshui"},
{"Gansu", "Wuwei"},
{"Gansu", "Zhangye"},
{"Guangdong", "Chaozhou"},
{"Guangdong", "Dongguan"},
{"Guangdong", "Foshan"},
{"Guangdong", "Guangzhou"},
{"Guangdong", "Heyuan"},
{"Guangdong", "Huizhou"},
{"Guangdong", "Jiangmen"},
{"Guangdong", "Jieyang"},
{"Guangdong", "Maoming"},
{"Guangdong", "Meizhou"},
{"Guangdong", "Puning"},
{"Guangdong", "Qingyuan"},
{"Guangdong", "Shantou"},
{"Guangdong", "Shanwei"},
{"Guangdong", "Shaoguan"},
{"Guangdong", "Shenzhen"},
{"Guangdong", "Wuchuan"},
{"Guangdong", "Yangjiang"},
{"Guangdong", "Yunfu"},
{"Guangdong", "Zhanjiang"},
{"Guangdong", "Zhaoqing"},
{"Guangdong", "Zhongshan"},
{"Guangdong", "Zhuhai"},
{"Guangxi Zhuangzu", "Baise"},
{"Guangxi Zhuangzu", "Beihai"},
{"Guangxi Zhuangzu", "Chongzuo"},
{"Guangxi Zhuangzu", "Fangchenggang"},
{"Guangxi Zhuangzu", "Guigang"},
{"Guangxi Zhuangzu", "Guilin"},
{"Guangxi Zhuangzu", "Hechi"},
{"Guangxi Zhuangzu", "Hezhou"},
{"Guangxi Zhuangzu", "Laibin"},
{"Guangxi Zhuangzu", "Liuzhou"},
{"Guangxi Zhuangzu", "Nanning"},
{"Guangxi Zhuangzu", "Qinzhou"},
{"Guangxi Zhuangzu", "Wuzhou"},
{"Guangxi Zhuangzu", "Yulin"},
{"Guizhou", "Anshun"},
{"Guizhou", "Bijie"},
{"Guizhou", "Duyun"},
{"Guizhou", "Guiyang"},
{"Guizhou", "Kaili"},
{"Guizhou", "Liupanshui"},
{"Guizhou", "Tongren"},
{"Guizhou", "Xingyi"},
{"Guizhou", "Zunyi"},
{"Hainan", "Danzhou"},
{"Hainan", "Haikou"},
{"Hainan", "Qionghai"},
{"Hainan", "Sanya"},
{"Hainan", "Wenchang"},
{"Hainan", "Wuzhishan"},
{"Hebei", "Baoding"},
{"Hebei", "Cangzhou"},
{"Hebei", "Chengde"},
{"Hebei", "Gaobeidian"},
{"Hebei", "Handan"},
{"Hebei", "Hengshui"},
{"Hebei", "Langfang"},
{"Hebei", "Qinhuangdao"},
{"Hebei", "Renqiu"},
{"Hebei", "Shijiazhuang"},
{"Hebei", "Tangshan"},
{"Hebei", "Xingtai"},
{"Hebei", "Zhangjiakou"},
{"Hebei", "Zhuozhou"},
{"Hebei", "Zunhua"},
{"Heilongjiang", "Daqing"},
{"Heilongjiang", "Harbin"},
{"Heilongjiang", "Hegang"},
{"Heilongjiang", "Heihe"},
{"Heilongjiang", "Jiamusi"},
{"Heilongjiang", "Jixi"},
{"Heilongjiang", "Mudanjiang"},
{"Heilongjiang", "Qiqihar"},
{"Heilongjiang", "Qitaihe"},
{"Heilongjiang", "Shuangyashan"},
{"Heilongjiang", "Suihua"},
{"Heilongjiang", "Yichun"},
{"Henan", "Anyang"},
{"Henan", "Gongyi"},
{"Henan", "Hebi"},
{"Henan", "Jiaozuo"},
{"Henan", "Jiyuan"},
{"Henan", "Kaifeng"},
{"Henan", "Luohe"},
{"Henan", "Luoyang"},
{"Henan", "Nanyang"},
{"Henan", "Pingdingshan"},
{"Henan", "Puyang"},
{"Henan", "Sanmenxia"},
{"Henan", "Shangqiu"},
{"Henan", "Xinxiang"},
{"Henan", "Xinyang"},
{"Henan", "Xuchang"},
{"Henan", "Zhengzhou"},
{"Henan", "Zhoukou"},
{"Henan", "Zhumadian"},
{"Hong Kong", "Hong Kong"},
{"Hong Kong", "Kuk Liu"},
{"Hong Kong", "Lo So Shing"},
{"Hong Kong", "Pan Long Wan"},
{"Hong Kong", "Tai Wong Ha"},
{"Hong Kong", "To Fung Shan"},
{"Hong Kong", "Tsim Sha Tsui"},
{"Hong Kong", "Wong Fung Leng"},
{"Hsinchu", "Hsinchu"},
{"Hualien", "Hualien"},
{"Hubei", "Enshi"},
{"Hubei", "Ezhou"},
{"Hubei", "Huanggang"},
{"Hubei", "Huangshi"},
{"Hubei", "Jingmen"},
{"Hubei", "Jingzhou"},
{"Hubei", "Lichuan"},
{"Hubei", "Qianjiang"},
{"Hubei", "Shiyan"},
{"Hubei", "Suizhou"},
{"Hubei", "Tianmen"},
{"Hubei", "Wuhan"},
{"Hubei", "Xiangyang"},
{"Hubei", "Xianning"},
{"Hubei", "Xiantao"},
{"Hubei", "Xiaogan"},
{"Hubei", "Yichang"},
{"Hunan", "Changde"},
{"Hunan", "Changsha"},
{"Hunan", "Chenzhou"},
{"Hunan", "Hengyang"},
{"Hunan", "Huaihua"},
{"Hunan", "Jishou"},
{"Hunan", "Loudi"},
{"Hunan", "Shaoyang"},
{"Hunan", "Xiangtan"},
{"Hunan", "Yiyang"},
{"Hunan", "Yongzhou"},
{"Hunan", "Yueyang"},
{"Hunan", "Zhangjiajie"},
{"Hunan", "Zhuzhou"},
{"Jiangsu", "Changzhou"},
{"Jiangsu", "Huai'an"},
{"Jiangsu", "Lianyungang"},
{"Jiangsu", "Nanjing"},
{"Jiangsu", "Nantong"},
{"Jiangsu", "Suqian"},
{"Jiangsu", "Suzhou"},
{"Jiangsu", "Taizhou"},
{"Jiangsu", "Wuxi"},
{"Jiangsu", "Xuzhou"},
{"Jiangsu", "Yancheng"},
{"Jiangsu", "Yangzhou"},
{"Jiangsu", "Zhenjiang"},
{"Jiangxi", "Fuzhou"},
{"Jiangxi", "Ganzhou"},
{"Jiangxi", "Ji'an"},
{"Jiangxi", "Jingdezhen"},
{"Jiangxi", "Jiujiang"},
{"Jiangxi", "Longnan"},
{"Jiangxi", "Nanchang"},
{"Jiangxi", "Pingxiang"},
{"Jiangxi", "Shangrao"},
{"Jiangxi", "Xinyu"},
{"Jiangxi", "Yichun"},
{"Jiangxi", "Yingtan"},
{"Jiangxi", "Zhangshu"},
{"Jilin", "Baicheng"},
{"Jilin", "Baishan"},
{"Jilin", "Changchun"},
{"Jilin", "Jilin"},
{"Jilin", "Liaoyuan"},
{"Jilin", "Siping"},
{"Jilin", "Songyuan"},
{"Jilin", "Tonghua"},
{"Jilin", "Yanji"},
{"Kaohsiung", "Kaohsiung"},
{"Keelung", "Keelung"},
{"Kinmen", "Jincheng"},
{"Liaoning", "Anshan"},
{"Liaoning", "Benxi"},
{"Liaoning", "Chaoyang"},
{"Liaoning", "Dalian"},
{"Liaoning", "Dandong"},
{"Liaoning", "Fushun"},
{"Liaoning", "Fuxin"},
{"Liaoning", "Huludao"},
{"Liaoning", "Jinzhou"},
{"Liaoning", "Liaoyang"},
{"Liaoning", "Panjin"},
{"Liaoning", "Shenyang"},
{"Liaoning", "Tieling"},
{"Liaoning", "Wafangdian"},
{"Liaoning", "Yingkou"},
{"Lienchiang", "Nangan"},
{"Macao", "Macau"},
{"Miaoli", "Miaoli"},
{"Nantou", "Nantou"},
{"Nantou", "Puli"},
{"Nei Mongol", "Baotou"},
{"Nei Mongol", "Bayannur"},
{"Nei Mongol", "Chifeng"},
{"Nei Mongol", "Hohhot"},
{"Nei Mongol", "Hulunbuir"},
{"Nei Mongol", "Ordos"},
{"Nei Mongol", "Tongliao"},
{"Nei Mongol", "Ulanhot"},
{"Nei Mongol", "Ulanqab"},
{"Nei Mongol", "Wuhai"},
{"Nei Mongol", "Xilinhot"},
{"New Taipei", "Banqiao"},
{"Ningxia Huizu", "Guyuan"},
{"Ningxia Huizu", "Shizuishan"},
{"Ningxia Huizu", "Wuzhong"},
{"Ningxia Huizu", "Yinchuan"},
{"Ningxia Huizu", "Zhongwei"},
{"Penghu", "Magong"},
{"Pingtung", "Hengchun"},
{"Pingtung", "Pingtung"},
{"Qinghai", "Delingha"},
{"Qinghai", "Haidong"},
{"Qinghai", "Xining"},
{"Qinghai", "Yushu"},
{"Shaanxi", "Ankang"},
{"Shaanxi", "Baoji"},
{"Shaanxi", "Hanzhong"},
{"Shaanxi", "Shangluo"},
{"Shaanxi", "Tongchuan"},
{"Shaanxi", "Weinan"},
{"Shaanxi", "Xi'an"},
{"Shaanxi", "Xianyang"},
{"Shaanxi", "Yan'an"},
{"Shaanxi", "Yulin"},
{"Shandong", "Binzhou"},
{"Shandong", "Dezhou"},
{"Shandong", "Dongying"},
{"Shandong", "Heze"},
{"Shandong", "Jinan"},
{"Shandong", "Jining"},
{"Shandong", "Laiyang"},
{"Shandong", "Liaocheng"},
{"Shandong", "Linqing"},
{"Shandong", "Linyi"},
{"Shandong", "Qingdao"},
{"Shandong", "Rizhao"},
{"Shandong", "Tai'an"},
{"Shandong", "Weifang"},
{"Shandong", "Weihai"},
{"Shandong", "Yantai"},
{"Shandong", "Zaozhuang"},
{"Shandong", "Zibo"},
{"Shanghai", "Shanghai"},
{"Shanxi", "Changzhi"},
{"Shanxi", "Datong"},
{"Shanxi", "Fenyang"},
{"Shanxi", "Jincheng"},
{"Shanxi", "Jinzhong"},
{"Shanxi", "Linfen"},
{"Shanxi", "Shuozhou"},
{"Shanxi", "Taiyuan"},
{"Shanxi", "Xinzhou"},
{"Shanxi", "Yangquan"},
{"Shanxi", "Yuncheng"},
{"Sichuan", "Barkam"},
{"Sichuan", "Bazhong"},
{"Sichuan", "Chengdu"},
{"Sichuan", "Dazhou"},
{"Sichuan", "Deyang"},
{"Sichuan", "Emeishan"},
{"Sichuan", "Guang'an"},
{"Sichuan", "Guangyuan"},
{"Sichuan", "Kangding"},
{"Sichuan", "Leshan"},
{"Sichuan", "Luzhou"},
{"Sichuan", "Meishan"},
{"Sichuan", "Mianyang"},
{"Sichuan", "Nanchong"},
{"Sichuan", "Neijiang"},
{"Sichuan", "Panzhihua"},
{"Sichuan", "Pengzhou"},
{"Sichuan", "Suining"},
{"Sichuan", "Xichang"},
{"Sichuan", "Ya'an"},
{"Sichuan", "Yibin"},
{"Sichuan", "Zigong"},
{"Sichuan", "Ziyang"},
{"Taichung", "Taichung"},
{"Tainan", "Tainan"},
{"Tainan", "Xinying"},
{"Tainan", "Yujing"},
{"Taipei", "Taipei"},
{"Taitung", "Taitung"},
{"Taoyuan", "Taoyuan"},
{"Tianjin", "Tianjin"},
{"Xinjiang Uygur", "Aksu"},
{"Xinjiang Uygur", "Altay"},
{"Xinjiang Uygur", "Beitun"},
{"Xinjiang Uygur", "Bole"},
{"Xinjiang Uygur", "Changji"},
{"Xinjiang Uygur", "Hami"},
{"Xinjiang Uygur", "Hotan"},
{"Xinjiang Uygur", "Karamay"},
{"Xinjiang Uygur", "Kashgar"},
{"Xinjiang Uygur", "Korla"},
{"Xinjiang Uygur", "Kuytun"},
{"Xinjiang Uygur", "Shihezi"},
{"Xinjiang Uygur", "Tacheng"},
{"Xinjiang Uygur", "Turpan"},
{"Xizang", "Lhasa"},
{"Xizang", "Nagqu"},
{"Xizang", "Qamdo"},
{"Xizang", "Shannan"},
{"Yilan", "Yilan"},
{"Yunlin", "Douliu"},
{"Yunnan", "Baoshan"},
{"Yunnan", "Chuxiong"},
{"Yunnan", "Dali"},
{"Yunnan", "Honghe"},
{"Yunnan", "Jinghong"},
{"Yunnan", "Kunming"},
{"Yunnan", "Lijiang"},
{"Yunnan", "Lincang"},
{"Yunnan", "Mangshi"},
{"Yunnan", "Pu'er"},
{"Yunnan", "Qujing"},
{"Yunnan", "Wenshan"},
{"Yunnan", "Yuxi"},
{"Yunnan", "Zhaotong"},
{"Zhejiang", "Haining"},
{"Zhejiang", "Hangzhou"},
{"Zhejiang", "Huzhou"},
{"Zhejiang", "Jiaxing"},
{"Zhejiang", "Jinhua"},
{"Zhejiang", "Lishui"},
{"Zhejiang", "Ningbo"},
{"Zhejiang", "Quzhou"},
{"Zhejiang", "Shaoxing"},
{"Zhejiang", "Taizhou"},
{"Zhejiang", "Wenling"},
{"Zhejiang", "Wenzhou"},
{"Zhejiang", "Yiwu"},
{"Zhejiang", "Yuyao"},
{"Zhejiang", "Zhoushan"},
{"Zhejiang", "Zhuji"},
}

140
service/geo_zh_test.go Normal file
View File

@@ -0,0 +1,140 @@
package service
import "testing"
func TestApplyGeoZh_Huainan(t *testing.T) {
g := &GeoInfo{Country: "CN", Region: "Anhui", City: "Huainan", ASN: 4134, ASOrg: "Chinanet"}
ApplyGeoZh(g)
if g.Region != "安徽" || g.RegionISO != "AH" {
t.Fatalf("region=%q iso=%q", g.Region, g.RegionISO)
}
if g.City != "淮南" {
t.Fatalf("city=%q want 淮南", g.City)
}
if g.ASOrg != "中国电信" {
t.Fatalf("as_org=%q", g.ASOrg)
}
}
func TestApplyGeoZh_AmbiguousSuzhou(t *testing.T) {
js := &GeoInfo{Country: "CN", Region: "Jiangsu", City: "Suzhou"}
ApplyGeoZh(js)
if js.City != "苏州" {
t.Fatalf("jiangsu suzhou=%q", js.City)
}
ah := &GeoInfo{Country: "CN", Region: "Anhui", City: "Suzhou"}
ApplyGeoZh(ah)
if ah.City != "宿州" {
t.Fatalf("anhui suzhou=%q", ah.City)
}
}
func TestApplyGeoZh_AmbiguousTaizhouFuzhouYichunYulin(t *testing.T) {
cases := []struct {
region, city, want string
}{
{"Jiangsu", "Taizhou", "泰州"},
{"Zhejiang", "Taizhou", "台州"},
{"Fujian", "Fuzhou", "福州"},
{"Jiangxi", "Fuzhou", "抚州"},
{"Heilongjiang", "Yichun", "伊春"},
{"Jiangxi", "Yichun", "宜春"},
{"Guangxi Zhuangzu", "Yulin", "玉林"},
{"Shaanxi", "Yulin", "榆林"},
{"Gansu", "Longnan", "陇南"},
{"Jiangxi", "Longnan", "龙南"},
{"Shanxi", "Jincheng", "晋城"},
{"Kinmen", "Jincheng", "金城"},
}
for _, c := range cases {
g := &GeoInfo{Country: "CN", Region: c.region, City: c.city}
if c.region == "Kinmen" {
g.Country = "TW"
}
ApplyGeoZh(g)
if g.City != c.want {
t.Fatalf("%s %s → %q want %q", c.region, c.city, g.City, c.want)
}
}
}
func TestApplyGeoZh_Aliases(t *testing.T) {
for _, city := range []string{"Xi'an", "Xian", "xi'an"} {
g := &GeoInfo{Country: "CN", Region: "Shaanxi", City: city}
ApplyGeoZh(g)
if g.City != "西安" {
t.Fatalf("%q → %q", city, g.City)
}
}
g := &GeoInfo{Country: "CN", Region: "Jiangsu", City: "Huai'an"}
ApplyGeoZh(g)
if g.City != "淮安" {
t.Fatalf("Huai'an → %q", g.City)
}
}
func TestApplyGeoZh_NonCNUnchanged(t *testing.T) {
g := &GeoInfo{Country: "US", Region: "California", City: "San Francisco"}
ApplyGeoZh(g)
if g.City != "San Francisco" || g.Region != "California" {
t.Fatalf("got region=%q city=%q", g.Region, g.City)
}
}
func TestApplyGeoZh_RegionAliases(t *testing.T) {
g := &GeoInfo{Country: "CN", Region: "Guangxi Zhuangzu", City: "Nanning"}
ApplyGeoZh(g)
if g.Region != "广西" || g.RegionISO != "GX" || g.City != "南宁" {
t.Fatalf("got %+v", g)
}
g2 := &GeoInfo{Country: "CN", Region: "Xinjiang Uygur", City: "Kashgar"}
ApplyGeoZh(g2)
if g2.Region != "新疆" || g2.City != "喀什" {
t.Fatalf("got %+v", g2)
}
}
func TestApplyGeoZh_CitySuffix(t *testing.T) {
g := &GeoInfo{Country: "CN", Region: "Anhui", City: "Huainan City"}
ApplyGeoZh(g)
if g.City != "淮南" {
t.Fatalf("city=%q", g.City)
}
}
func TestLookupCityZh_FallbackWithoutRegion(t *testing.T) {
zh, ok := lookupCityZh("", "", "Suzhou")
if !ok || zh != "苏州" {
t.Fatalf("fallback suzhou=%q ok=%v", zh, ok)
}
}
func TestApplyGeoZh_LiteCNCitiesCoverage(t *testing.T) {
var missing []string
for _, row := range liteCNCities {
country := "CN"
rk := row.region
switch rk {
case "Hong Kong":
country = "HK"
case "Macao":
country = "MO"
case "Changhua", "Chiayi", "Hsinchu", "Hualien", "Kaohsiung", "Keelung",
"Kinmen", "Lienchiang", "Miaoli", "Nantou", "New Taipei", "Penghu",
"Pingtung", "Taichung", "Tainan", "Taipei", "Taitung", "Taoyuan",
"Yilan", "Yunlin":
country = "TW"
}
g := &GeoInfo{Country: country, Region: row.region, City: row.city}
ApplyGeoZh(g)
if g.City == "" || g.City == row.city {
// 仍为英文原文则未映射(中文不应等于英文,除非本身就是纯 ASCII 且已是目标——LITE 全是拼音)
if g.City == row.city {
missing = append(missing, row.region+"/"+row.city+"→"+g.City)
}
}
}
if len(missing) > 0 {
t.Fatalf("未中文化 %d 条,示例: %v", len(missing), missing[:min(20, len(missing))])
}
}