From 34f3d088ad3584bd3c834fd655ecaddf44964c7f Mon Sep 17 00:00:00 2001 From: freefire Date: Mon, 31 Aug 2026 08:40:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8=20IP2Location=20?= =?UTF-8?q?=E4=B8=AD=E5=9B=BD=E5=9F=8E=E5=B8=82=E4=B8=AD=E6=96=87=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=B9=B6=E6=8C=89=E7=9C=81=E6=B6=88=E6=AD=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/monitor.md | 2 +- service/geo_zh.go | 201 ++++++++------ service/geo_zh_cities.go | 422 +++++++++++++++++++++++++++++ service/geo_zh_lite_cities_test.go | 392 +++++++++++++++++++++++++++ service/geo_zh_test.go | 140 ++++++++++ 5 files changed, 1080 insertions(+), 77 deletions(-) create mode 100644 service/geo_zh_cities.go create mode 100644 service/geo_zh_lite_cities_test.go create mode 100644 service/geo_zh_test.go diff --git a/docs/monitor.md b/docs/monitor.md index 0e3adeb..14de7db 100644 --- a/docs/monitor.md +++ b/docs/monitor.md @@ -35,7 +35,7 @@ CDN 头(如 `CF-IPCountry`)仅在本地库无国家码时补全。不落 Lat/Lon/Zip/TimeZone。管理端展示**完整客户端 IP**。 -写入时做本地中文映射(省/常见城市/运营商);`country` 存 ISO2,展示用中文名。 +写入时做本地中文映射(省 / 中国城市按 IP2Location 英文名 / 运营商);同音城市(如苏州/宿州)按省份歧义。`country` 存 ISO2,展示用中文名。历史 `page_views` / 请求日志不会回写,仅影响新写入。 ## 写入路径(性能) diff --git a/service/geo_zh.go b/service/geo_zh.go index e74ba65..c3b35fa 100644 --- a/service/geo_zh.go +++ b/service/geo_zh.go @@ -27,79 +27,73 @@ type cnRegionMeta struct { // 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"}, + "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"}, + "gansu": {Name: "甘肃", ISO: "GS"}, + "qinghai": {Name: "青海", ISO: "QH"}, + "ningxia": {Name: "宁夏", ISO: "NX"}, + "xinjiang": {Name: "新疆", ISO: "XJ"}, + "xinjiang uygur": {Name: "新疆", ISO: "XJ"}, + "inner mongolia": {Name: "内蒙古", ISO: "NM"}, + "nei mongol": {Name: "内蒙古", ISO: "NM"}, + "guangxi zhuangzu": {Name: "广西", ISO: "GX"}, + "ningxia huizu": {Name: "宁夏", ISO: "NX"}, + "hong kong": {Name: "香港", ISO: "HK"}, + "macau": {Name: "澳门", ISO: "MO"}, + "macao": {Name: "澳门", ISO: "MO"}, + "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 常见城市英文名 → 中文 -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": "香港", -} +// cityZh 定义见 geo_zh_cities.go(LITE 中国城市全表 + 同音复合键) // asnOrgZh 常见 ASN / AS 组织名 → 中文运营商 var asnOrgZh = map[uint]string{ @@ -189,7 +183,7 @@ func mapCNRegionCity(g *GeoInfo) { } } if g.City != "" { - if zh, ok := lookupCityZh(g.City); ok { + if zh, ok := lookupCityZh(g.RegionISO, g.Region, g.City); ok { g.City = zh } } @@ -214,7 +208,7 @@ func lookupCNRegion(name string) (cnRegionMeta, bool) { 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 meta, ok := cnRegionMap[strings.TrimSuffix(key, suffix)]; ok { return meta, true @@ -224,14 +218,69 @@ func lookupCNRegion(name string) (cnRegionMeta, bool) { return cnRegionMeta{}, false } -func lookupCityZh(name string) (string, bool) { - key := strings.ToLower(strings.TrimSpace(name)) - if zh, ok := cityZh[key]; ok { - return zh, true +// lookupCityZh 将 IP2Location 英文城市名映射为中文。 +// 同音城市优先用「省ISO|城市」复合键;并尝试去掉 city/shi 后缀与撇号变体。 +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 + } + } + } + for _, c := range candidates { + if zh, ok := cityZh[c]; ok { + return zh, true + } } 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 { l := strings.ToLower(strings.TrimSpace(org)) if l == "" { diff --git a/service/geo_zh_cities.go b/service/geo_zh_cities.go new file mode 100644 index 0000000..c1408c3 --- /dev/null +++ b/service/geo_zh_cities.go @@ -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": "遵义", +} diff --git a/service/geo_zh_lite_cities_test.go b/service/geo_zh_lite_cities_test.go new file mode 100644 index 0000000..0e0a7f7 --- /dev/null +++ b/service/geo_zh_lite_cities_test.go @@ -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"}, +} diff --git a/service/geo_zh_test.go b/service/geo_zh_test.go new file mode 100644 index 0000000..0ca6e3b --- /dev/null +++ b/service/geo_zh_test.go @@ -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))]) + } +}