返回博客

地址下拉出现「市辖区」「县」:GB/T 2260 占位词污染数据库的三层防线

某天做门店管理后台的城市筛选时,发现一个诡异现象:重庆市下面并列着「重庆市」「市辖区」「县」三个选项,天津市下面只有孤零零的「市辖区」,北京和上海也好不到哪去。前端同学第一反应:地址库是不是给了脏数据?排查之后发现,脏数据确实存在——但根源不在前端,而在 GB/T 2260 这份国家标准本身。

现象

省市区三级联动的城市层,GROUP BY city 之后,直辖市下面出现了额外的分组:

重庆市
  ├── 重庆市   (8 条门店)
  ├── 市辖区   (105 条门店)   ← 幽灵分组
  └── 县       (3 条门店)     ← 幽灵分组
天津市
  └── 市辖区   (4 条门店)     ← 正常门店全被归到这里

根因

中国行政区划是四级:省 → 地级市 → 县 / 区 → 乡镇。但北京、上海、天津、重庆四个直辖市直接管辖区县,中间没有地级市这一层。

GB/T 2260(国家行政区划代码标准)为了保持数据结构统一,在直辖市的「地级市」位置填入了占位词:

| 占位词 | 语义 | |--------|------| | 市辖区 | 该直辖市下辖的各城区 | | | 该直辖市下辖的县 | | 市辖县 | 较老的写法,同上 | | 省直辖县级行政区划 | 省直管县(如湖北省神农架等) |

这些词本是层级槽的填充符,不是真实地名。但很多 POS 机、CRM、第三方地址库在导出数据时会把它们原样写入 city 字段。进了数据库,城市列就被污染了。

坑:只在前端过滤治标不治本

看到这个现象,直觉反应是在前端联动选项里 filter 掉这些词:

const placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
cities = cities.filter(c => !placeholders.includes(c.name));

这么做有三个隐患:

  1. Excel 导出、BI 报表、其他后台消费点仍然读到脏数据
  2. 后续同步/导入脏数据时会继续污染,前端过滤只是掩盖
  3. 按城市统计门店数永远是错的——同一批门店被拆进了多个分组

正解

必须在存储、写入、读取三层同时封堵,缺一不可。

第一层:存量 DML 清洗

-- 直辖市:把占位词折叠回省名
UPDATE store_info
SET city = province
WHERE province IN ('北京市', '上海市', '天津市', '重庆市')
  AND city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');
 
-- 省直管县兜底
UPDATE store_info
SET city = province
WHERE city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');
 
-- 验证:应返回 0 行
SELECT COUNT(*) FROM store_info
WHERE city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');

第二层:写入侧拦截(同步/导入接口边界)

$placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
$province = $data['province'] ?? '';
if (in_array($data['city'] ?? '', $placeholders, true) && $province !== '') {
    $data['city'] = $province;  // 占位词 → 省名
}

第三层:读取侧归并(不要用 SQL GROUP BY city)

// SQL 只 SELECT province, city,不做 GROUP BY
$rows = $model->field('province, city')->select();
 
$grouped = [];
$placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
foreach ($rows as $r) {
    // 应用层归并,确保 (重庆市, 市辖区) 和 (重庆市, 重庆市) 合并
    $city = in_array($r['city'], $placeholders, true)
        ? $r['province']
        : $r['city'];
    $grouped[$r['province']][$city] = ($grouped[$r['province']][$city] ?? 0) + 1;
}

若直接在 SQL 里 GROUP BY city(重庆市, 重庆市)(重庆市, 市辖区) 会被当成两组,归并逻辑失效。

正解:三层同时封堵,占位词折叠回省名
  • 存量:DML 一次性清洗,验证归零
  • 增量写入:在接口边界把占位词替换为省名,阻断再污染
  • 读取:应用层归并,不依赖 SQL GROUP BY

三层缺任何一层,另外两层的修复都会因为"漏网之鱼"逐渐失效。

一句话外卖

GB/T 2260 的「市辖区」「县」是行政层级的占位符,不是城市名——进库前折叠成省名,写入、读取、存量三层同时封堵,缺一不可。