简介:面向需要实现省市区街道乡镇联动选择、地址库初始化或行政区划检索的Web/移动端开发者及数据维护人员,这份四级城市地区数据资源覆盖国内省市县街道乡镇,可显著降低地址数据整理成本。压缩包共6个文件,包含xlsx表格便于人工查阅与校对,sql脚本可直接导入MySQL等关系型数据库,另附txt字段说明和3张结构预览截图,整体约2.48MB。数据结构包含ID、父ID、名称、联动ID、层级、是否末级等关键字段,层级1~4清晰对应省、市、县(区)、街道(乡镇);联动ID采用如“11-1101-110107-110107001”的层级串联格式,前端可直接按拆分结果实现逐级联动;末级标记为1的条目可用于判断选择器最末一级,无需二次整理,导入即可使用。目前已有603人学习下载,适合作为城市联动组件的数据底座、行政区划数据库设计参考,也可用于前端联动效果演示与教学。
1. 四级地址联动看着简单,真正让你加班的是那四列字段
做物流、门店配送这类业务时,最让后端头疼的往往不是接口性能,而是地址。省市区三级联动做得好好的,一上线发现仓库和驿站按街道乡镇派单,用户只能把街道名硬塞进备注框。这份国内省市县街道乡镇四级地址数据,xlsx 和 sql 文件都有,表面只是几万行文本,真正麻烦的是四列字段怎么消费:名称、联动ID、层级、是否末级。联动ID 决定父子节点怎么查,层级决定你做几级联动,是否末级决定列表末端还能不能继续点。这篇文章按可落地的路径讲:先把字段语义和边界定清楚,再清洗导入数据库建索引,最后落到前端联动查询上。适合正在做四级联动、要把精确到街道的地址作为标准数据落库的工程师。
2. 字段定性决定后续所有查询:名称、联动ID、层级、是否末级的语义边界
拿到这种地址数据,第一件事不是打开 Excel 看有几行,而是先把表的字段语义搞清楚。很多翻车现场并不是数据缺行,而是把“联动ID”理解成了自增主键,把“是否末级”理解成了不能再展开。这两个理解错了,后面的父子查询、递归路径、前端级联组件全部跟着错。
这一章先不写导入脚本,只做一件事:把这四列的真实含义和边界条件钉死。文件里字段名可能会略有出入,比如“联动ID”可能叫 pid、parent_code、p_code,level 可能叫 grade,但表达的东西是一致的。你只需要按这套思路去对应实际文件里的列名就行。
2.1 把一张平面表还原成一棵树,先画父子关系,再谈联动
四级地址表看起来是扁平的行列表,实际是一棵固定深度的树。树的根是省份,第二层是地级市,第三层是区县,第四层是街道乡镇。每一行至少要能回答两个问题:我是谁,我父亲是谁。
如果文件里只有题面列出的四列,通常还缺一个“自身唯一标识”。常见的五列结构是下面这样:
| 列名 | 示例 | 说明 |
|---|---|---|
| code | 330000 | 本行行政区划代码,全表唯一 |
| name | 浙江省 | 本行名称 |
| parent_id | 0 | 父级行政区划代码,省级填 0 |
| level | 1 | 1=省,2=市,3=区县,4=街道乡镇 |
| is_leaf | 0 | 1=末级,0=非末级 |
如果你的文件没有 code 这一列,只有“名称、联动ID、层级、是否末级”四列,就一定要先确认“联动ID”到底指向谁。我见过一种表,它的“联动ID”就是父级的 code,本行自己的 code 被藏在了 Excel 的第一列或者根本没给。这种情况下,导入数据库之前必须自己补一个唯一 ID,可以用父级 ID 顺序生成,也可以直接用“父级 ID + 当前行号”拼一个。千万别拿 name 当主键,全国同名街道、同名区县实在太多。
判断方法很简单:把省级那一行找出来,如果它的联动ID 是 0 或空字符串,而市级行的联动ID 能对应到某个省级行的 code,那联动ID 就是父级 ID。画出来的父子关系就是一棵标准的树。
2.2 用三行数据判断联动ID 到底是父ID 还是路径ID,不看清楚必翻车
同一个“联动ID”这个词,在不同版本的数据文件里可能代表两种完全不同的东西。一种是你常见的父级 ID,查子级时直接WHERE parent_id = ?。另一种是“路径 ID”,也就是这一行从根到自己的整条编码链,比如省级是 33,市级是 3301,区县是 330102,街道是 330102001,那么街道行的联动ID 可能是完整的330102001或者一串用逗号拼接的祖先路径。
区分方法不需要看整个文件,取三行就够了。先看省级这一行,如果它的联动ID 不是空而是自己的 code,那多半是“自身 ID”而不是父级 ID。再看一个市级行,如果它的联动ID 和省级行的联动ID 存在“前缀包含”关系,比如省级是 33,市级是 3301,区县级是 330102,那这组数据用的是路径模式,查询时要按位长截断或者用 LIKE 前缀。
路径模式下,查子级的 SQL 会是这样的:
-- 假设 path_id 是“祖先编码串”,每一层通过加两位或三位扩展 -- 查浙江省下面的地级市:以 33 开头,并且总长度是 4 位 SELECT code, name FROM cn_region WHERE path_id LIKE '33%' AND CHAR_LENGTH(path_id) = 4;这种写法对索引很不友好,能不用尽量不用。如果数据源是路径模式,我一般会在导入阶段把它拆成标准的 parent_id 和 code 两列,而不是让业务查询一直背着 LIKE 前缀。拆列的逻辑不复杂:按层级截取前若干位,上一级就是当前路径去掉最后两位或三位的部分。
2.3 层级与是否末级的四种组合,别被 level=4 的想当然骗了
层级字段的范围一般是 1 到 4,对应省、市、区县、街道乡镇。是否末级字段是 0 和 1,1 表示这一节点下面没有子级了。理想情况下,level=1、2、3 的行 is_leaf 都应该是 0,level=4 的行 is_leaf 是 1。但实际拿到的数据往往不是这样。
我遇到过几种典型偏差:有的文件把“街道”里拆出来的社区也算了一级,导致 level 出现 5;有的文件只在某些区县下补了街道数据,另一些区县在 level=3 时就标了 is_leaf=1;还有的文件把乡镇下面的自然村也整理了,虽然 level 没有 5,但 is_leaf 和 level 对不上。写前端联动组件时,判断“还能不能继续展开”应该优先看 is_leaf,而不是看 level 是否小于 4。最稳的规则是level < 5 AND is_leaf = 0才允许继续加载子级,这样即使数据里多出一层也不会把组件搞死。
3. 落地第一步:把 xlsx 清洗成可执行的 sql 文件,导入 MySQL 与 SQLite 都走通
字段语义确认完,接下来就是把 xlsx 变成能直接跑的表。这一步的坑几乎都集中在数据清洗上。Excel 文件在人工整理时会有大量肉眼看不见的问题:合并单元格、单元格前后空格、数字被存成文本、空白行、全角符号。直接 read 出来拼 INSERT,大概率在导入数据库时才发现问题。
我的习惯是分三段走:先清洗,再生成 SQL,最后导入。清洗用 pandas 最省事,生成 SQL 用 Python 拼字符串,导入阶段再决定用 MySQL 还是 SQLite。下面把每段的关键参数和边界条件都列出来。
3.1 用 pandas 读取 xlsx:先去做三件“不显眼但保命”的清洗
读取 Excel 时第一件事是统一列名和数据类型。我见过太多人直接pd.read_excel把层级列读成了 float,导入 MySQL 后数字全带.0,然后拿着1.0去匹配level = 1,查不出任何结果。
import pandas as pd # dtype 全部先按字符串读,避免 0/1 被识别成 bool 或 float df = pd.read_excel("region.xlsx", dtype=str, header=0) # 统一列名:文件里可能是中文列名,转成小写英文再处理 df.columns = [str(col).strip().lower() for col in df.columns] # 第一件事:去掉整行全空的数据,Excel 里经常有大量无意义空行 df = df.dropna(how="all") # 第二件事:去掉所有单元格首尾空白,中文数据里混全角空格很常见 for col in ["name", "parent_id", "level", "is_leaf"]: if col in df.columns: df[col] = df[col].astype(str).str.strip() # 第三件事:把 level 和 is_leaf 从字符串转成数字,转不了的丢弃 df["level"] = pd.to_numeric(df["level"], errors="coerce") df["is_leaf"] = pd.to_numeric(df["is_leaf"], errors="coerce") # 名称或者层级空的行没有任何保留价值 df = df.dropna(subset=["name", "level"]) df = df[df["name"] != "nan"] print(df.head())这里最关键的参数是dtype=str。Excel 里的数字列如果被 pandas 自动识别成 float,行政区划代码这种“看起来像数字但实际上是字符串”的字段就会被截断或者变成科学计数法。六位的区划代码还好,九位的街道代码一旦被 float 化,精度丢失就直接错了。所以必须先用字符串读,后面再按字段语义去转类型。
errors="coerce"的作用是遇到没法转成数字的值时填 NaN,而不是抛异常中断整个脚本。清零数据里偶尔会有“-”、“空格”这种垃圾值,直接报错的话反而拖慢进度。
3.2 生成 CREATE TABLE 与批量 INSERT,参数和转义一个都不能省
清洗完之后生成 SQL。表结构按章节 2.1 的五列标准设计,其中 code 是主键。建表语句建议直接指定 utf8mb4 字符集,别等到导入时再解决编码问题。
CREATE DATABASE IF NOT EXISTS region_assist DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE region_assist; CREATE TABLE IF NOT EXISTS cn_region ( code VARCHAR(12) NOT NULL COMMENT '行政区划代码', name VARCHAR(64) NOT NULL COMMENT '名称', parent_id VARCHAR(12) NOT NULL DEFAULT '0' COMMENT '父级联动ID,省级为0', level TINYINT NOT NULL COMMENT '1省 2市 3区县 4街道乡镇', is_leaf TINYINT NOT NULL DEFAULT 0 COMMENT '1末级 0非末级', PRIMARY KEY (code), KEY idx_parent (parent_id, level) ) ENGINE=InnoDB COMMENT='中国省市县街道四级地址表';生成 INSERT 时最容易被忽略的是单引号转义。地名里虽然少见,但“某某村'委会”这种数据不是没有。下面这段 Python 负责把清洗后的 DataFrame 拼成批量 INSERT 语句。
def escape_sql(value: str) -> str: # 先把反斜杠和单引号转义,再包一层单引号 return "'" + str(value).replace("\\", "\\\\").replace("'", "\\'") + "'" sql_lines = [] sql_lines.append("USE region_assist;") # 每 500 行拼一条批量 INSERT,减少执行次数 batch_size = 500 for i in range(0, len(df), batch_size): chunk = df.iloc[i:i + batch_size] values = [] for _, row in chunk.iterrows(): # row 里的 code/name 已经是清洗后的值 values.append( ",".join([ escape_sql(row["code"]), escape_sql(row["name"]), escape_sql(row["parent_id"]), str(int(row["level"])), # 层级转成整数 str(int(row["is_leaf"]) if not pd.isna(row["is_leaf"]) else 0) ]) ) sql_lines.append( "INSERT INTO cn_region (code, name, parent_id, level, is_leaf) VALUES\n" + ",\n".join("(" + v + ")" for v in values) + ";" ) with open("region.sql", "w", encoding="utf-8") as f: f.write("\n".join(sql_lines))批量 INSERT 的batch_size建议控制在 200 到 1000 之间,太大容易超过 MySQL 的max_allowed_packet,太小又浪费网络往返。3000 到 5000 行的四级地址表,500 一批是安全值。
3.3 不想引 Python 的工程:操作 Excel 的 JS 工具库 xlsx 读取并转 JSON
不是每个团队都愿意为了一个数据文件引入 Python 依赖。如果你们的工程是纯 Node 技术栈,直接用操作 Excel 的 JS 工具库 xlsx 读取会更顺。这个库的 readFile 方法可以直接读取 .xlsx,配合 sheet_to_json 转出数组。
const XLSX = require("xlsx"); // 读取第一个 sheet const workbook = XLSX.readFile("region.xlsx"); const sheet = workbook.Sheets[workbook.SheetNames[0]]; // defval 让空白单元格得到空字符串而不是 undefined const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" }); // 取前几行确认列名映射 console.log(rows.slice(0, 3)); // 生成 SQL 时同样要注意转义 const escapeSql = (v) => `'${String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`; const inserts = rows .filter((r) => r["name"] && r["level"]) .map((r) => `(${escapeSql(r["code"])}, ${escapeSql(r["name"])}, ${escapeSql(r["parent_id"])}, ${r["level"]}, ${r["is_leaf"] || 0})`) .join(",\n");defval: ""是这里最值得说明的参数。xlsx 库默认会将空白单元格解析为undefined,后续拼字符串时会出现undefined字符串混进 SQL 的诡异现象。显式给空字符串后,后续逻辑可以统一按空值处理。需要注意,xlsx 库的sheet_to_json默认把第一行当列名,如果文件第一行有中文表头,你需要手动改 key,或者在读入时把表头行单独处理。
3.4 SQL 文件导入命令:字符集和 SOURCE 的写法说明
生成好的 region.sql 接下来要导入数据库。如果只在本机验证,SQLite 是最快的路径。如果作为业务正式表,走 MySQL 导入。
# SQLite 快速验证,表结构完全兼容五列模型 sqlite3 region.db < region.sql # MySQL 导入,注意 --default-character-set 必须放在最前面 mysql -uroot -p --default-character-set=utf8mb4 region_assist < region.sqlMySQL 导入时最常见的问题是中文乱码。--default-character-set=utf8mb4要放在region.sql前面,因为它是客户端参数,不是 SQL 语句。如果 SQL 文件非常大,或者你想在导入过程中看每一阶段的报错,用 source 方式更稳。
mysql -uroot -p --default-character-set=utf8mb4USE region_assist; SET NAMES utf8mb4; SOURCE /path/to/region.sql;SET NAMES utf8mb4是让当前会话的客户端字符集和表字符集保持一致,SOURCE则是执行外部文件。如果 source 进来的文件编码是 UTF-8 但文件头带着 BOM,第一行建表语句可能直接报语法错误,这时先用sed -i 's/^\xef\xbb\xbf//' region.sql去掉 BOM 再执行。
4. 查询层设计:一级子节点的 SQL、向上反查的递归 CTE 和索引设置
地址表导入数据库只是第一步,真正让这四列数据产生价值的是查询层。日常业务里最频繁的场景是两类:一类是从省或者市往下拉子级列表,一类是从街道向上反查完整路径。这两类查询的写法完全不同,用错了性能差距会很大。
4.1 按联动ID 查下一级:父子查询只用 parent_id,不要用 level
最常规的下拉列表查询,直接按父级 ID 过滤即可。即使你已经知道当前节点的层级,也不要图省事写WHERE level = 当前层级 + 1,更不要相信“市级行的 level 一定是 2”这种假设。省直辖县级市、部分经济开发区、高新区在数据里挂在省级下面,层级是 3,直接套 level 换算会漏掉一整批节点。
-- 查浙江省的地级市 SELECT code, name FROM cn_region WHERE parent_id = '330000' ORDER BY code; -- 查杭州市的区县,联动ID 指向杭州的行政区划代码 SELECT code, name FROM cn_region WHERE parent_id = '330100' ORDER BY code;这段 SQL 没有限定 level,所以无论子节点是普通地级市还是省直辖县级市,都能被查出来。等宽的 code 排序在同类节点里是稳定的,省级两位、市级四位、区县六位,天然就是正确的显示顺序。
前端级联组件每次切换节点时都调用这种查询,频率会很高。所以第 3 章建表时建的KEY idx_parent (parent_id, level)这时候就起作用了。实际执行时 MySQL 会先按 parent_id 找到所有子节点,再按 level 排序或过滤,两者组合的索引比单列索引更省回表。
4.2 向上拼路径:用 WITH RECURSIVE 从街道回推到省
第二个高频场景是反查路径。比如用户提交了一个街道 ID,后端需要知道这个街道属于哪个省市区,用来做订单区域统计或者权限判断。一条比较快的路径是用 MySQL 8.0 的递归 CTE 从当前行往上逐层找父亲。
WITH RECURSIVE path_cte AS ( -- 初始行:从当前街道开始 SELECT code, name, parent_id, level FROM cn_region WHERE code = '330100001' UNION ALL -- 递归找到父级,条件是父级 code = 当前行的 parent_id SELECT r.code, r.name, r.parent_id, r.level FROM cn_region r INNER JOIN path_cte c ON r.code = c.parent_id WHERE c.parent_id <> '0' ) SELECT GROUP_CONCAT(name ORDER BY level SEPARATOR '/') AS full_path FROM path_cte;这里的关键逻辑是INNER JOIN path_cte c ON r.code = c.parent_id。每次递归取出的是一行父级记录,直到 parent_id 为 0 也就是省级节点的根位置。WHERE c.parent_id <> '0'是为了让递归在到达省级之后停止,避免因为数据错误出现死循环。
如果数据量不大,也可以不用递归,在代码里循环查。但业务查询一旦要批量计算,比如一次给 100 个街道补全路径,循环查询就是 100 次交互,递归 CTE 在数据库里一次性算完,性能明显更好。
4.3 索引参数:parent_id、level、is_leaf 怎么建最实用
四级地址表通常就是几万行,即使不建索引查询也慢不到哪里去,但联动组件的请求频率会放大单次查询的延迟。索引不是越多越好,这表最常用的过滤条件是 parent_id 和 level,最常用的是按 is_leaf 过滤。
-- 常用查询场景: -- 1. WHERE parent_id = ? -- 2. WHERE parent_id = ? AND level = ? -- 3. WHERE is_leaf = 1 AND name LIKE 'xx%' ALTER TABLE cn_region ADD KEY idx_parent_level (parent_id, level); ALTER TABLE cn_region ADD KEY idx_leaf_name (is_leaf, name);idx_parent_level上面的两个条件字段都覆盖了,联合索引在多数情况下能直接用。idx_leaf_name则是给搜索场景用的,当你搜“西湖区有哪些可派送的街道”时,先用 is_leaf 过滤掉非末级节点,再按名称模糊匹配,索引命中率高很多。如果数据库版本支持,把 code 换成更短的整型当然可以,但省级到街道的 code 本身只有 6 到 12 位,VARCHAR 完全够用,省这个优化没必要。
5. 踩坑记录:合并单元格、省直辖县级市、is_leaf 错值与乱码的排查方法
标题里的字段看着清爽,落到真实文件里到处都是看似正常实则害人的数据。这一章是我反复导入地址数据后沉淀下来的排查清单,每一条都按“现象、原因、解决”写,你们遇到类似情况可以直接照着定位。
5.1 现象:level 和 is_leaf 出现大片空值
第一次用 pandas 读取 xlsx 时,发现有些行只有名称,后面的联动ID、层级、是否末级全是 NaN。去 Excel 里看却发现单元格里明明有内容,再仔细看,是“浙江省”这一列被合并单元格了。合并单元格在 pandas 读取时只有左上角第一行有值,其余行都是 NaN,但不影响其他列的数据。
解决方式是在清洗阶段对关键列做前向填充,也就是把上一行非空的值补到当前行。
# 合并单元格的典型表现:名称列或联动ID列整列 NaN # 用 ffill 沿列向下填充 df["name"] = df["name"].ffill() df["parent_id"] = df["parent_id"].ffill()填充之后,第二行的 name 会继承省级名称,parent_id 会继承省级联动ID。对于这种“省市县街道”逐层归属的表格,ffill 是准确的,因为每个省级下面的市级行本来就应该共享省级的父级信息。填充完后再跑一次dropna(subset=["name", "level"]),把真正无用的行清理掉。
5.2 现象:城市列表总是缺几个,怎么都查不出来
项目上线后运营反馈,在城市下拉框里找不到河南济源、湖北仙桃这些城市。起初以为是数据缺行,查了 xlsx 才发现数据在,但这些城市的层级是 3,父级直接是省级,而不是某个地级市。这类“省直辖县级市”在行政体制里就是一种特殊存在,不会经过地级市这一层。
前端组件如果按“省 → 市 → 区县”三级硬编码来加载,看到 level=3 的济源出现在省下面,第一反应是数据错了,于是过滤掉。解决方法是把层级逻辑彻底从查询里拿掉:子级只看 parent_id,不看 level。前端组件展示的仍然是“当前节点的子列表”,只不过省级下面的子列表里既有地级市也有省直辖县级市,这对业务是正确的。
5.3 现象:is_leaf=1 的节点还能查出子级
维护阶段遇到过一个更隐蔽的问题:某个街道节点 is_leaf 标了 1,但按它的 code 查子级还能查出 20 多个社区。原因是有同事在四级地址之外又补了社区级数据,却没有同步更新上一级节点的 is_leaf。联动组件发现 is_leaf=1 后就不再发请求,那些社区数据就成了死数据。
这种问题要用一致性检查提前拦住。SQL 里找出那些“自己标了末级,但表中还有孩子指向它”的节点:
SELECT p.code, p.name, COUNT(c.code) AS child_cnt FROM cn_region p INNER JOIN cn_region c ON c.parent_id = p.code WHERE p.is_leaf = 1 GROUP BY p.code, p.name HAVING COUNT(c.code) > 0;如果这个查询返回了记录,说明 is_leaf 字段和真实父子关系已经脱节。解决时可以按业务需要把 is_leaf 统一改为 0,也可以反方向把社区级数据从表里拆出去单独维护。我一般选择前者,因为前端组件能正常展开总比藏着数据强。
5.4 现象:SQL 文件导入 MySQL 中文全变问号,或者报错“Invalid utf8 character string”
中文乱码是 SQL 文件导入最常见的故障。现象很直接:导入后 select 出来,地名全是??或者锟斤拷。原因通常是两条:一是 SQL 文件本身是 UTF-8,但执行导入的客户端连接字符集不是 utf8mb4;二是文件其实是 GBK 编码,被硬按 UTF-8 解析。
排查时先做两件事。第一件用file region.sql看文件真实编码,第二件确认客户端字符集参数。如果是文件编码问题,用 iconv 转换,而不是在 SQL 里硬塞SET NAMES。
# 查看文件编码 file region.sql # GBK 转 UTF-8 iconv -f GBK -t UTF-8 region.sql > region_utf8.sql # 再导入 mysql -uroot -p --default-character-set=utf8mb4 region_assist < region_utf8.sql注意SET NAMES utf8mb4只能改变当前会话的客户端字符集,改不了 SQL 文件本身的字节。文件是 GBK 的时候,设置SET NAMES等于让数据库把 GBK 字节流按 UTF-8 解码,照样是乱码,必须先转码。
5.5 现象:INSERT 时大量 Duplicate entry 报错
导入到最后阶段报主键重复,通常是同一份文件里出现了重复的行政区划代码。常见原因是数据源在整理时,把某些街道在不同区县下重复录了一次,code 一样但名称不同。生产环境直接执行 result,整批导入会失败。
入库前先单独查一遍重复 code,是成本最低的防线:
SELECT code, COUNT(*) AS cnt FROM cn_region GROUP BY code HAVING COUNT(*) > 1;如果重复记录确实存在,看业务上想保哪一份。我处理这类问题时,优先保留名称和层级与官方区划一致的那条,再用唯一的INSERT ... ON DUPLICATE KEY UPDATE把其余记录覆盖掉。最省事的做法是导入前在临时库里跑同一个检查 SQL,确认无重复主键后再动生产表。
6. 再进一步:用同一份数据生成路径缓存,让搜索和建议框直接用
四级地址表做成了标准库之后,还可以把父子关系预计算成“完整路径”,这样搜索和建议框就不用每次递归拼路径了。路径缓存的核心价值是把查询从递归变成一次字符串匹配,对高频搜索场景帮助明显。
做法也并不复杂。一次性把全表的 code、name、parent_id、level 读进内存,按 code 建一个字典,然后从每个叶子节点向上回溯,拼出“省/市/区/街道”格式的完整路径。
# 一次性读取全表 cur.execute("SELECT code, name, parent_id, level FROM cn_region") rows = cur.fetchall() node_by_code = {row["code"]: row for row in rows} def build_path(code: str) -> str: parts = [] current = code # 向上回溯到省级,parent_id 为 0 时结束 while current and current != "0": node = node_by_code.get(current) if not node: break parts.append(node["name"]) current = node["parent_id"] return "/".join(reversed(parts)) # 只给是末级的节点生成路径缓存 for row in rows: if row["is_leaf"] == 1: path = build_path(row["code"]) # 写入 path_cache 表,字段为 code、full_path、name这个函数有几个参数要注意。while current and current != "0"保证了回溯不会越界,一旦数据里出现某个节点的父级不存在,循环也会因为node_by_code.get(current)返回 None 而安全退出。路径拼接用"/".join(reversed(parts)),因为回溯是从街道往省方向走,天然得到的是逆序,反转一次才能得到正常的省市区顺序。
缓存表建好之后,最直接的应用是“根据用户输入的关键词补全地址”。前端的搜索框可以用SELECT code, full_path FROM path_cache WHERE full_path LIKE '%西湖%' LIMIT 10做模糊匹配,返回的就是一条完整的“浙江省/杭州市/西湖区”,不需要再写递归查询。
我自己的习惯是在生成缓存表之后,再做一次“叶子不一致检查”,如果某个 is_leaf=1 的节点在 path_cache 表中也有子级路径,就把 is_leaf 修正掉。这个自查动作跑一次只要几秒钟,却能在业务出问题之前把脏数据拦下来,省掉很多和运营对线的工夫。
这份数据本身不复杂,复杂的是拿到数据后怎么理解这几列的边界。只要“联动ID”是谁、“是否末级”到底能不能展开这两点不再含糊,后面无论是做三级联动还是四级联动,甚至加一层社区数据,都不会再让你熬夜加班。希望这套流程能帮你少踩几个坑。
本文还有配套的精品资源,点击获取