Files
screen/scripts/backfill-province.cjs
2026-06-02 15:44:00 +08:00

419 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 客户省份回填脚本
*
* 从 crm_customer.address 字段中提取省份信息,回填到 province 字段。
* 匹配优先级:省份前缀 > 地级市名 > 县级市/区名
*
* 用法: node scripts/backfill-province.js
*/
const mysql = require('mysql2')
// ===== 省份名称(与 china.json 完全一致) =====
const PROVINCES = [
'北京市', '天津市', '上海市', '重庆市',
'河北省', '山西省', '辽宁省', '吉林省', '黑龙江省',
'江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省',
'河南省', '湖北省', '湖南省', '广东省', '海南省',
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
'台湾省',
'内蒙古自治区', '广西壮族自治区', '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区',
'香港特别行政区', '澳门特别行政区'
]
// ===== 省份简称 → 全称映射 =====
const SHORT_PROVINCE = {
'北京': '北京市', '天津': '天津市', '上海': '上海市', '重庆': '重庆市',
'河北': '河北省', '山西': '山西省', '辽宁': '辽宁省', '吉林': '吉林省',
'黑龙江': '黑龙江省', '江苏': '江苏省', '浙江': '浙江省', '安徽': '安徽省',
'福建': '福建省', '江西': '江西省', '山东': '山东省', '河南': '河南省',
'湖北': '湖北省', '湖南': '湖南省', '广东': '广东省', '海南': '海南省',
'四川': '四川省', '贵州': '贵州省', '云南': '云南省', '陕西': '陕西省',
'甘肃': '甘肃省', '青海': '青海省', '台湾': '台湾省',
'内蒙古': '内蒙古自治区', '广西': '广西壮族自治区',
'西藏': '西藏自治区', '宁夏': '宁夏回族自治区', '新疆': '新疆维吾尔自治区',
'香港': '香港特别行政区', '澳门': '澳门特别行政区'
}
// ===== 城市 → 省份映射(覆盖全部地级行政区和常见县级市) =====
const CITY_PROVINCE = {
// ----- 北京市 -----
'北京': '北京市',
// ----- 天津市 -----
'天津': '天津市',
// ----- 上海市 -----
'上海': '上海市',
// ----- 重庆市 -----
'重庆': '重庆市',
// ===== 河北省 =====
'石家庄': '河北省', '唐山': '河北省', '秦皇岛': '河北省', '邯郸': '河北省',
'邢台': '河北省', '保定': '河北省', '张家口': '河北省', '承德': '河北省',
'沧州': '河北省', '廊坊': '河北省', '衡水': '河北省',
// ===== 山西省 =====
'太原': '山西省', '大同': '山西省', '阳泉': '山西省', '长治': '山西省',
'晋城': '山西省', '朔州': '山西省', '晋中': '山西省', '运城': '山西省',
'忻州': '山西省', '临汾': '山西省', '吕梁': '山西省',
// ===== 辽宁省 =====
'沈阳': '辽宁省', '大连': '辽宁省', '鞍山': '辽宁省', '抚顺': '辽宁省',
'本溪': '辽宁省', '丹东': '辽宁省', '锦州': '辽宁省', '营口': '辽宁省',
'阜新': '辽宁省', '辽阳': '辽宁省', '盘锦': '辽宁省', '铁岭': '辽宁省',
'朝阳': '辽宁省', '葫芦岛': '辽宁省',
// ===== 吉林省 =====
'长春': '吉林省', '吉林': '吉林省', '四平': '吉林省', '辽源': '吉林省',
'通化': '吉林省', '白山': '吉林省', '松原': '吉林省', '白城': '吉林省',
'延边': '吉林省',
// ===== 黑龙江省 =====
'哈尔滨': '黑龙江省', '齐齐哈尔': '黑龙江省', '鸡西': '黑龙江省',
'鹤岗': '黑龙江省', '双鸭山': '黑龙江省', '大庆': '黑龙江省',
'伊春': '黑龙江省', '佳木斯': '黑龙江省', '七台河': '黑龙江省',
'牡丹江': '黑龙江省', '黑河': '黑龙江省', '绥化': '黑龙江省',
'大兴安岭': '黑龙江省',
// ===== 江苏省 =====
'南京': '江苏省', '无锡': '江苏省', '徐州': '江苏省', '常州': '江苏省',
'苏州': '江苏省', '南通': '江苏省', '连云港': '江苏省', '淮安': '江苏省',
'盐城': '江苏省', '扬州': '江苏省', '镇江': '江苏省', '泰州': '江苏省',
'宿迁': '江苏省',
// 江苏县级市/县
'常熟': '江苏省', '张家港': '江苏省', '昆山': '江苏省', '太仓': '江苏省',
'江阴': '江苏省', '宜兴': '江苏省', '丹阳': '江苏省', '扬中': '江苏省',
'句容': '江苏省', '靖江': '江苏省', '泰兴': '江苏省', '兴化': '江苏省',
'沭阳': '江苏省', '泗阳': '江苏省', '泗洪': '江苏省', '沛县': '江苏省',
'丰县': '江苏省', '睢宁': '江苏省', '东海': '江苏省', '灌云': '江苏省',
'灌南': '江苏省', '涟水': '江苏省', '洪泽': '江苏省', '盱眙': '江苏省',
'金湖': '江苏省', '滨海': '江苏省', '阜宁': '江苏省', '射阳': '江苏省',
'建湖': '江苏省', '东台': '江苏省', '宝应': '江苏省', '仪征': '江苏省',
'高邮': '江苏省', '溧阳': '江苏省', '如东': '江苏省', '启东': '江苏省',
'如皋': '江苏省', '海门': '江苏省', '海安': '江苏省',
// ===== 浙江省 =====
'杭州': '浙江省', '宁波': '浙江省', '温州': '浙江省', '嘉兴': '浙江省',
'湖州': '浙江省', '绍兴': '浙江省', '金华': '浙江省', '衢州': '浙江省',
'舟山': '浙江省', '台州': '浙江省', '丽水': '浙江省',
// 浙江县级市/县
'海宁': '浙江省', '嘉善': '浙江省', '平湖': '浙江省', '桐乡': '浙江省',
'德清': '浙江省', '长兴': '浙江省', '安吉': '浙江省', '诸暨': '浙江省',
'上虞': '浙江省', '嵊州': '浙江省', '新昌': '浙江省', '兰溪': '浙江省',
'义乌': '浙江省', '东阳': '浙江省', '永康': '浙江省', '武义': '浙江省',
'浦江': '浙江省', '江山': '浙江省', '常山': '浙江省', '开化': '浙江省',
'龙游': '浙江省', '临海': '浙江省', '温岭': '浙江省', '玉环': '浙江省',
'三门': '浙江省', '天台': '浙江省', '仙居': '浙江省', '青田': '浙江省',
'缙云': '浙江省', '遂昌': '浙江省', '松阳': '浙江省', '云和': '浙江省',
'庆元': '浙江省', '景宁': '浙江省', '龙泉': '浙江省',
// ===== 安徽省 =====
'合肥': '安徽省', '芜湖': '安徽省', '蚌埠': '安徽省', '淮南': '安徽省',
'马鞍山': '安徽省', '淮北': '安徽省', '铜陵': '安徽省', '安庆': '安徽省',
'黄山': '安徽省', '滁州': '安徽省', '阜阳': '安徽省', '宿州': '安徽省',
'六安': '安徽省', '亳州': '安徽省', '池州': '安徽省', '宣城': '安徽省',
// 安徽县级市/县
'界首': '安徽省',
// ===== 福建省 =====
'福州': '福建省', '厦门': '福建省', '莆田': '福建省', '三明': '福建省',
'泉州': '福建省', '漳州': '福建省', '南平': '福建省', '龙岩': '福建省',
'宁德': '福建省',
// ===== 江西省 =====
'南昌': '江西省', '景德镇': '江西省', '萍乡': '江西省', '九江': '江西省',
'新余': '江西省', '鹰潭': '江西省', '赣州': '江西省', '吉安': '江西省',
'宜春': '江西省', '抚州': '江西省', '上饶': '江西省',
// ===== 山东省 =====
'济南': '山东省', '青岛': '山东省', '淄博': '山东省', '枣庄': '山东省',
'东营': '山东省', '烟台': '山东省', '潍坊': '山东省', '济宁': '山东省',
'泰安': '山东省', '威海': '山东省', '日照': '山东省', '临沂': '山东省',
'德州': '山东省', '聊城': '山东省', '滨州': '山东省', '菏泽': '山东省',
// 山东县级市/县
'莱芜': '山东省', '博兴': '山东省', '冠县': '山东省', '梁山': '山东省',
'曹县': '山东省', '滕州': '山东省', '肥城': '山东省', '新泰': '山东省',
'宁阳': '山东省', '东平': '山东省', '沂水': '山东省', '沂南': '山东省',
'莒南': '山东省', '临沭': '山东省', '郯城': '山东省', '费县': '山东省',
'平邑': '山东省', '蒙阴': '山东省', '沂源': '山东省', '临朐': '山东省',
'昌乐': '山东省', '青州': '山东省', '诸城': '山东省', '寿光': '山东省',
'安丘': '山东省', '高密': '山东省', '昌邑': '山东省', '曲阜': '山东省',
'兖州': '山东省', '邹城': '山东省', '微山': '山东省', '鱼台': '山东省',
'金乡': '山东省', '嘉祥': '山东省', '汶上': '山东省', '泗水': '山东省',
'梁山县': '山东省',
// ===== 河南省 =====
'郑州': '河南省', '开封': '河南省', '洛阳': '河南省', '平顶山': '河南省',
'安阳': '河南省', '鹤壁': '河南省', '新乡': '河南省', '焦作': '河南省',
'濮阳': '河南省', '许昌': '河南省', '漯河': '河南省', '三门峡': '河南省',
'南阳': '河南省', '商丘': '河南省', '信阳': '河南省', '周口': '河南省',
'驻马店': '河南省',
// 河南县级市/县
'济源': '河南省', '巩义': '河南省', '新郑': '河南省', '新密': '河南省',
'登封': '河南省', '荥阳': '河南省', '中牟': '河南省', '偃师': '河南省',
'孟津': '河南省', '新安': '河南省', '栾川': '河南省', '嵩县': '河南省',
'汝阳': '河南省', '宜阳': '河南省', '洛宁': '河南省', '伊川': '河南省',
'林州': '河南省', '滑县': '河南省', '内黄': '河南省', '汤阴': '河南省',
'浚县': '河南省', '淇县': '河南省', '原阳': '河南省', '延津': '河南省',
'封丘': '河南省', '长垣': '河南省', '卫辉': '河南省', '辉县': '河南省',
'修武': '河南省', '博爱': '河南省', '武陟': '河南省', '温县': '河南省',
'沁阳': '河南省', '孟州': '河南省', '南乐': '河南省', '范县': '河南省',
'台前': '河南省', '禹州': '河南省', '长葛': '河南省', '鄢陵': '河南省',
'襄城': '河南省', '舞阳': '河南省', '临颍': '河南省', '义马': '河南省',
'灵宝': '河南省', '卢氏': '河南省', '邓州': '河南省', '镇平': '河南省',
'内乡': '河南省', '淅川': '河南省', '西峡': '河南省', '唐河': '河南省',
'新野': '河南省', '方城': '河南省', '社旗': '河南省', '桐柏': '河南省',
'永城': '河南省', '虞城': '河南省', '民权': '河南省', '宁陵': '河南省',
'睢县': '河南省', '夏邑': '河南省', '柘城': '河南省', '罗山': '河南省',
'光山': '河南省', '新县': '河南省', '商城': '河南省', '固始': '河南省',
'潢川': '河南省', '淮滨': '河南省', '息县': '河南省', '扶沟': '河南省',
'西华': '河南省', '商水': '河南省', '沈丘': '河南省', '郸城': '河南省',
'淮阳': '河南省', '太康': '河南省', '鹿邑': '河南省', '项城': '河南省',
'西平': '河南省', '上蔡': '河南省', '平舆': '河南省', '正阳': '河南省',
'确山': '河南省', '泌阳': '河南省', '汝南': '河南省', '遂平': '河南省',
'新蔡': '河南省',
// ===== 湖北省 =====
'武汉': '湖北省', '黄石': '湖北省', '十堰': '湖北省', '宜昌': '湖北省',
'襄阳': '湖北省', '鄂州': '湖北省', '荆门': '湖北省', '孝感': '湖北省',
'荆州': '湖北省', '黄冈': '湖北省', '咸宁': '湖北省', '随州': '湖北省',
'恩施': '湖北省',
// ===== 湖南省 =====
'长沙': '湖南省', '株洲': '湖南省', '湘潭': '湖南省', '衡阳': '湖南省',
'邵阳': '湖南省', '岳阳': '湖南省', '常德': '湖南省', '张家界': '湖南省',
'益阳': '湖南省', '郴州': '湖南省', '永州': '湖南省', '怀化': '湖南省',
'娄底': '湖南省', '湘西': '湖南省',
// ===== 广东省 =====
'广州': '广东省', '韶关': '广东省', '深圳': '广东省', '珠海': '广东省',
'汕头': '广东省', '佛山': '广东省', '江门': '广东省', '湛江': '广东省',
'茂名': '广东省', '肇庆': '广东省', '惠州': '广东省', '梅州': '广东省',
'汕尾': '广东省', '河源': '广东省', '阳江': '广东省', '清远': '广东省',
'东莞': '广东省', '中山': '广东省', '潮州': '广东省', '揭阳': '广东省',
'云浮': '广东省',
// ===== 海南省 =====
'海口': '海南省', '三亚': '海南省', '三沙': '海南省', '儋州': '海南省',
// ===== 四川省 =====
'成都': '四川省', '自贡': '四川省', '攀枝花': '四川省', '泸州': '四川省',
'德阳': '四川省', '绵阳': '四川省', '广元': '四川省', '遂宁': '四川省',
'内江': '四川省', '乐山': '四川省', '南充': '四川省', '眉山': '四川省',
'宜宾': '四川省', '广安': '四川省', '达州': '四川省', '雅安': '四川省',
'巴中': '四川省', '资阳': '四川省', '阿坝': '四川省', '甘孜': '四川省',
'凉山': '四川省',
// ===== 贵州省 =====
'贵阳': '贵州省', '六盘水': '贵州省', '遵义': '贵州省', '安顺': '贵州省',
'毕节': '贵州省', '铜仁': '贵州省', '黔西南': '贵州省', '黔东南': '贵州省',
'黔南': '贵州省',
// ===== 云南省 =====
'昆明': '云南省', '曲靖': '云南省', '玉溪': '云南省', '保山': '云南省',
'昭通': '云南省', '丽江': '云南省', '普洱': '云南省', '临沧': '云南省',
'楚雄': '云南省', '红河': '云南省', '文山': '云南省', '西双版纳': '云南省',
'大理': '云南省', '德宏': '云南省', '怒江': '云南省', '迪庆': '云南省',
// ===== 陕西省 =====
'西安': '陕西省', '铜川': '陕西省', '宝鸡': '陕西省', '咸阳': '陕西省',
'渭南': '陕西省', '延安': '陕西省', '汉中': '陕西省', '榆林': '陕西省',
'安康': '陕西省', '商洛': '陕西省',
// ===== 甘肃省 =====
'兰州': '甘肃省', '嘉峪关': '甘肃省', '金昌': '甘肃省', '白银': '甘肃省',
'天水': '甘肃省', '武威': '甘肃省', '张掖': '甘肃省', '平凉': '甘肃省',
'酒泉': '甘肃省', '庆阳': '甘肃省', '定西': '甘肃省', '陇南': '甘肃省',
'临夏': '甘肃省', '甘南': '甘肃省',
// ===== 青海省 =====
'西宁': '青海省', '海东': '青海省', '海北': '青海省', '黄南': '青海省',
'海南州': '青海省', '果洛': '青海省', '玉树': '青海省', '海西': '青海省',
// ===== 台湾省 =====
'台北': '台湾省', '高雄': '台湾省', '台中': '台湾省',
// ===== 内蒙古自治区 =====
'呼和浩特': '内蒙古自治区', '包头': '内蒙古自治区', '乌海': '内蒙古自治区',
'赤峰': '内蒙古自治区', '通辽': '内蒙古自治区', '鄂尔多斯': '内蒙古自治区',
'呼伦贝尔': '内蒙古自治区', '巴彦淖尔': '内蒙古自治区', '乌兰察布': '内蒙古自治区',
'兴安': '内蒙古自治区', '锡林郭勒': '内蒙古自治区', '阿拉善': '内蒙古自治区',
// ===== 广西壮族自治区 =====
'南宁': '广西壮族自治区', '柳州': '广西壮族自治区', '桂林': '广西壮族自治区',
'梧州': '广西壮族自治区', '北海': '广西壮族自治区', '防城港': '广西壮族自治区',
'钦州': '广西壮族自治区', '贵港': '广西壮族自治区', '玉林': '广西壮族自治区',
'百色': '广西壮族自治区', '贺州': '广西壮族自治区', '河池': '广西壮族自治区',
'来宾': '广西壮族自治区', '崇左': '广西壮族自治区',
// ===== 西藏自治区 =====
'拉萨': '西藏自治区', '日喀则': '西藏自治区', '昌都': '西藏自治区',
'林芝': '西藏自治区', '山南': '西藏自治区', '那曲': '西藏自治区',
'阿里': '西藏自治区',
// ===== 宁夏回族自治区 =====
'银川': '宁夏回族自治区', '石嘴山': '宁夏回族自治区', '吴忠': '宁夏回族自治区',
'固原': '宁夏回族自治区', '中卫': '宁夏回族自治区',
// ===== 新疆维吾尔自治区 =====
'乌鲁木齐': '新疆维吾尔自治区', '克拉玛依': '新疆维吾尔自治区',
'吐鲁番': '新疆维吾尔自治区', '哈密': '新疆维吾尔自治区',
'昌吉': '新疆维吾尔自治区', '博尔塔拉': '新疆维吾尔自治区',
'巴音郭楞': '新疆维吾尔自治区', '阿克苏': '新疆维吾尔自治区',
'克孜勒苏': '新疆维吾尔自治区', '喀什': '新疆维吾尔自治区',
'和田': '新疆维吾尔自治区', '伊犁': '新疆维吾尔自治区',
'塔城': '新疆维吾尔自治区', '阿勒泰': '新疆维吾尔自治区',
'石河子': '新疆维吾尔自治区', '阿拉尔': '新疆维吾尔自治区',
'图木舒克': '新疆维吾尔自治区', '五家渠': '新疆维吾尔自治区',
'北屯': '新疆维吾尔自治区', '铁门关': '新疆维吾尔自治区',
'双河': '新疆维吾尔自治区', '可克达拉': '新疆维吾尔自治区',
'昆玉': '新疆维吾尔自治区', '胡杨河': '新疆维吾尔自治区',
// ===== 香港特别行政区 =====
'香港': '香港特别行政区',
// ===== 澳门特别行政区 =====
'澳门': '澳门特别行政区',
}
/**
* 从地址中提取省份名称
*/
function extractProvince(address) {
if (!address) return ''
// 清理地址前缀
let addr = address.trim()
// 去掉 "地址:" 等前缀
addr = addr.replace(/^地址[:]?\s*/, '')
// 去掉首尾空格
addr = addr.trim()
// ---- 特殊模式 1: "中国(山东)自由贸易试验区..." ----
const chinaMatch = addr.match(/^中国\((\S+?)\)/)
if (chinaMatch) {
const shortName = chinaMatch[1]
if (SHORT_PROVINCE[shortName]) return SHORT_PROVINCE[shortName]
}
// ---- 模式 2: 地址以完整省份名开头 ----
// 按长度降序排序(避免"宁夏回族自治区"被"宁夏"提前匹配)
const sortedProvinces = [...PROVINCES].sort((a, b) => b.length - a.length)
for (const p of sortedProvinces) {
if (addr.startsWith(p)) return p
}
// ---- 模式 3: 地址以省份简称开头(如"山东济南市"、"河南洛阳" ----
const sortedShorts = Object.keys(SHORT_PROVINCE).sort((a, b) => b.length - a.length)
for (const s of sortedShorts) {
if (addr.startsWith(s)) return SHORT_PROVINCE[s]
}
// ---- 模式 4: 地址以城市名开头 ----
// 按长度降序排序,避免"张家港"被"张家"错误匹配
const sortedCities = Object.keys(CITY_PROVINCE).sort((a, b) => b.length - a.length)
for (const city of sortedCities) {
if (addr.startsWith(city)) return CITY_PROVINCE[city]
}
// ---- 模式 5: 地址以区号/代码开头(如 [370000/370800/370829] ----
// 行政区划代码前两位 = 省代码
const codeMatch = addr.match(/^\[?(\d{2})/)
if (codeMatch) {
const codeMap = {
'11': '北京市', '12': '天津市', '13': '河北省', '14': '山西省',
'15': '内蒙古自治区', '21': '辽宁省', '22': '吉林省', '23': '黑龙江省',
'31': '上海市', '32': '江苏省', '33': '浙江省', '34': '安徽省',
'35': '福建省', '36': '江西省', '37': '山东省', '41': '河南省',
'42': '湖北省', '43': '湖南省', '44': '广东省', '45': '广西壮族自治区',
'46': '海南省', '50': '重庆市', '51': '四川省', '52': '贵州省',
'53': '云南省', '54': '西藏自治区', '61': '陕西省', '62': '甘肃省',
'63': '青海省', '64': '宁夏回族自治区', '65': '新疆维吾尔自治区',
'71': '台湾省', '81': '香港特别行政区', '82': '澳门特别行政区',
}
const code = codeMatch[1]
if (codeMap[code]) return codeMap[code]
}
return ''
}
// ---- 主流程 ----
async function main() {
const conn = mysql.createConnection({
host: '140.143.206.120', port: 13306,
user: 'klp', password: 'KeLunPu@123',
database: 'klp-oa-test', connectTimeout: 10000,
supportBigNumbers: true,
bigNumberStrings: true
})
const connPromise = conn.promise()
// 查询需要回填的客户
const [rows] = await connPromise.query(
"SELECT customer_id, company_name, address FROM crm_customer WHERE del_flag = 0 AND (province IS NULL OR province = '') AND address IS NOT NULL AND address != '' ORDER BY customer_id"
)
console.log(`${rows.length} 条客户记录需要回填省份\n`)
let matched = 0
let unmatched = 0
const unmatchedList = []
const updates = []
for (const row of rows) {
const province = extractProvince(row.address)
if (province) {
updates.push({ customer_id: row.customer_id, province, company_name: row.company_name, address: row.address })
matched++
} else {
unmatched++
unmatchedList.push({ company_name: row.company_name, address: row.address })
}
}
console.log(`匹配成功: ${matched}`)
console.log(`无法匹配: ${unmatched}\n`)
if (unmatchedList.length > 0) {
console.log('=== 无法匹配的地址 ===')
unmatchedList.forEach(u => console.log(` ${u.company_name} | ${u.address}`))
console.log('')
}
// 批量更新
if (updates.length > 0) {
console.log('正在回填数据库...')
const sql = 'UPDATE crm_customer SET province = ? WHERE customer_id = ?'
let success = 0
for (const u of updates) {
try {
await connPromise.query(sql, [u.province, u.customer_id])
success++
} catch (err) {
console.log(` 更新失败 ID=${u.customer_id}: ${err.message}`)
}
}
console.log(`回填完成: ${success}/${updates.length} 条已更新`)
}
// 统计各省份分布
const [stats] = await connPromise.query(
"SELECT province, COUNT(*) as cnt FROM crm_customer WHERE del_flag = 0 AND province != '' GROUP BY province ORDER BY cnt DESC"
)
console.log('\n=== 省份分布 ===')
stats.forEach(s => console.log(` ${s.province}: ${s.cnt}`))
await connPromise.end()
console.log('\n完成')
}
main().catch(err => {
console.error('脚本执行失败:', err.message)
process.exit(1)
})