☰
四级地址库建设:行政区划编码、导入校验与地址解析
2026/10/30 11:56:56 网站建设 项目流程

简介:淘宝4级地址库是一份面向电商开发、物流配送与地理信息处理场景的行政区划数据包,适合需要做地址匹配、区域统计或收货地址校验的开发者与数据分析人员使用。压缩包内共1个文件,为sql格式的数据库脚本,体积约351KB,可直接导入数据库后按省、市、县(区)、乡镇(街道)四级结构查询行政区划代码与名称。数据采用国家标准行政区划编码,前六位对应省市区县,并附加街道层级信息,便于精确到乡镇街道的地址补全与校验。资源已有1074人学习下载,可作为地址库建设的轻量参考。读者可获得完整的四级区划表结构、层级关联字段以及街道名称数据,用于订单地址自动填充、区域维度统计、本地化服务划分等任务,也能据此理解行政区划代码的组织方式与查询思路,减少自行采集整理基础地理数据的时间成本。

1. 四级地址库补的是哪一层缺口:省市区之后还差一个街道

做收货地址表单的人都清楚,省、市、区三级下拉只能算及格线。用户选完某个区之后,剩下的全靠一个自由输入框兜底,包裹能不能送到,取决于那行字里街道写没写对。四级地址库把这一层补上:省、市、区县、街道乡镇,每一级都挂国标行政区划代码,前端能做四级联动,后端能把脏地址归一成编码再落库。

这份资源适合三类人:做电商、物流、本地生活后端的,需要一份能直接导入数据库的四级数据;做数据清洗的,手里几十万条地址要反查行政编码;做项目交付的,客户明确要求地址严格按国标走。它的价值不在条数多,而在于街道层没被砍掉、编码没乱填、父子关系能对得上。

下面从表结构怎么定、编码位数怎么读、CSV 怎么灌、地址怎么反查,一路拆到半年后怎么更新。

2. 结构拆解:四级地址库的层级模型与行政区划代码位数

拿到一份地址库,先别急着建表导入。结构没想清楚,后面每一次查询都在补窟窿。四级地址库的核心矛盾只有一个:你是想让写入简单,还是想让查询简单。这两件事在地址这种强层级数据上几乎必然对立,选型错了,后期改表比重新导入还贵。

2.1 邻接表还是宽表:按查询模式定

一份四级地址数据,落到关系型数据库里只有两种主流存法。

邻接表,一行一个节点,用parent_code指向父节点。省级节点父级为空,区县指向市,街道指向区县。写入时每个节点只出现一次,改名只改一行,代价是查一条完整路径要做递归。

宽表,一行一条完整路径,把省市区街道四个名字和四个编码平铺成八列。查一条路径一次SELECT就出来,导出给业务方直接能用,代价是冗余大,街道改名要刷全表,而且一旦某条路径的上级改名,你很难保证八列同步更新。

维度邻接表宽表
单行含义一个行政节点一条四级路径
存储冗余低,每个节点一份高,上级名重复 N 次
级联下拉查询一次查parent_code要DISTINCT去重,慢
完整路径查询递归或应用层循环一次查询
改名成本改一行刷全表
适合场景源数据入库、在线表单离线导出、报表、BI

我一般的做法是两张都留:以邻接表作为唯一数据源,再物化一张宽表给导出和报表用。物化表不进业务写路径,只在数据更新后重建一次,脏了也无所谓。很多项目一开始图省事只用宽表,等到要按parent_code做四级联动时就抓瞎了,只能靠名字LIKE硬拼,同名区县一多就串数据,这是血泪经验。

还有一个折中做法:邻接表加一个path字段,存省码/市码/区码/街道码这样的斜杠串。既能一次LIKE 'xx0000/%'捞出某省全部下级,也能ORDER BY path保证下拉顺序稳定。代价是父级变更时要级联刷子节点的path,但对地址这种几乎只增不迁的数据来说,可以接受。

2.2 六位、九位、十二位:编码长度决定层级

国标行政区划代码是分段编码,长度本身就是层级信号。省、市、区县用六位,前两位是省,中间两位是市,后两位是区县;街道乡镇在六位后面补三位,凑成九位;村和社区再补三位,凑成十二位。

这意味着你不能只用一张表加一个level字段就完事,还必须处理一种情况:同一个库里混着六位码和九位码,parent_code补齐规则不统一,JOIN直接空结果。常见做法是内部统一成六位存省市区,九位存街道,十二位存村社区,父级推导规则固定下来。

def parent_of(code: str): """按编码长度推导父级编码,内部统一 6/9/12 位存储""" c = code.strip() if not c.isdigit(): raise ValueError(f"非数字编码: {code}") n = len(c) if n == 12: # 村/社区 -> 街道 return c[:9] if n == 9: # 街道 -> 区县 return c[:6] if n == 6: if c[2:6] == "0000": # 省级,无父级 return None if c[4:6] == "00": # 市级 -> 省 return c[:2] + "0000" return c[:4] + "00" # 区县 -> 市 raise ValueError(f"非法编码长度: {len(c)}") def level_of(code: str) -> int: """层级编号:1省 2市 3区县 4街道 5村社区""" return {6: None, 9: 4, 12: 5}.get(len(code)) or ( 1 if code[2:6] == "0000" else 2 if code[4:6] == "00" else 3 )

这段代码有两个地方值得说明。第一,parent_of对六位码做了三次判断,顺序不能反:先判省级,再判市级,最后才当区县,否则110000会被当成市级推出一个不存在的父级。第二,level_of里六位码的层级不是靠长度判断的,而是靠尾段是否为零,因为省、市、区县都是六位,长度本身不携带信息。

还有一个容易忽略的边界:省直辖的县级行政区划,六位码的中间两位通常是90这类占位段,它的父级是省而不是市。如果你的推导逻辑硬按"前四位 + 00"去找父级,这类节点会全部断链。稳妥的做法是先按parent_code是否存在于表中做一次全量校验,把断链的节点单独捞出来人工确认,而不是让程序猜。

2.3 街道层的数据校验:四条硬规则

街道层是这份资源最容易出问题的地方,也是最值钱的地方。省级数据全国就三十多条,谁都不会错;街道层动辄几万条,来源杂、更新快、重名多,导入前不做校验,后面查询就是在垃圾上建索引。

我一般会跑四条硬规则,命中任何一条都不让进主表,先落到临时表观察。

校验项规则命中后处理
断链parent_code必须存在于表中补父节点,或整条剔除
长度只允许 6、9、12 三种修正或隔离到临时表
同级重名同一parent_code下name唯一加区分后缀,如"XX街道(新)"
空码段街道码三位不能是000直接丢弃,属于占位脏数据
import pandas as pd df = pd.read_csv("region_4level.csv", dtype=str, encoding="utf-8-sig", keep_default_na=False) # 1. 长度合法性 bad_len = df[~df["code"].str.len().isin([6, 9, 12])] # 2. 父级断链:父码非空但不在 code 集合里 codes = set(df["code"]) bad_link = df[df["parent_code"].ne("") & ~df["parent_code"].isin(codes)] # 3. 同级重名:按父级分组后名字不能重复 dup = df[df.duplicated(subset=["parent_code", "name"], keep=False)] # 4. 街道层占位码 bad_street = df[(df["code"].str.len() == 9) & df["code"].str.endswith("000")] print(len(bad_len), len(bad_link), len(dup), len(bad_street))

encoding="utf-8-sig"是为了吃掉文件头的 BOM,不加这一项,第一条省级编码前面会多出一个不可见字符,导入后code长度变成 7,主键冲突或者查不到,排查起来非常费劲。keep_default_na=False是为了防止名字里恰好是NA、NULL这类字符串被 pandas 自动转成空值,地址库里真出现过这种名字。

同级重名这条规则要留个口子:确实存在同一区县下有两个同名街道的情况,加后缀是最省事的处理,但后缀会污染展示。更干净的做法是保留原名,另加一列display_name存带后缀的版本,展示和匹配分开用。

3. 落地导入:建表、批量灌数与四级联动查询

结构定了,接下来是把这份库真正跑起来。这一段的目标很明确:给它一张能撑住级联查询的表,用最短时间把几万行灌进去,然后验证四级联动和逆序解析两条主链路都通。

3.1 表结构与索引怎么定

表结构不用花哨,字段够用就行,重点是索引要跟查询模式对齐。地址库的查询只有三种:按父级查子级(级联下拉)、按编码查自身(回显)、按名字模糊查(搜索框)。索引就照这三种建。

CREATE TABLE `region` ( `code` CHAR(12) NOT NULL COMMENT '行政区划代码,6/9/12位', `name` VARCHAR(64) NOT NULL COMMENT '标准名称', `short_name` VARCHAR(32) DEFAULT NULL COMMENT '简称,用于展示', `parent_code` CHAR(12) DEFAULT NULL COMMENT '父级编码,省级为NULL', `level` TINYINT NOT NULL COMMENT '1省 2市 3区县 4街道 5村社区', `sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序号', `path` VARCHAR(64) DEFAULT NULL COMMENT '省/市/区/街道 编码路径', PRIMARY KEY (`code`), KEY `idx_parent` (`parent_code`, `sort_no`), KEY `idx_name` (`name`), KEY `idx_path` (`path`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

主键直接用code,不用自增 ID。地址库的特点是编码天然唯一且稳定,用业务编码做主键能省掉一次回表,也让导数据变得可重复执行,重灌直接REPLACE INTO就行,不用先清表。

idx_parent是联合索引,顺序不能反:parent_code在前用于等值过滤,sort_no在后直接覆盖排序,级联下拉那条WHERE parent_code = ? ORDER BY sort_no走的就是这个索引,不会产生额外排序。把sort_no放前面就废了。

idx_name用于搜索框,name上有前缀匹配需求,普通 B-Tree 索引对LIKE 'xx%'有效,对LIKE '%xx%'无效。如果前端要做全模糊搜索,别硬靠这个索引,上全文索引或者把名称搜索放到内存结构里做,后面第 4 章会讲。

path字段是可选优化项,存110000/110100/110105/110105001这样的串,好处是查某省全部下级只要WHERE path LIKE '110000/%',一次搞定,不用递归。代价是父级改名时要级联刷新,只在地址数据基本不变的前提下划算。

3.2 CSV 批量导入的三个参数

几万行数据用INSERT一条条灌是最傻的做法,慢不说还容易把连接池占满。用 MySQL 的LOAD DATA LOCAL INFILE,几秒钟完事。

# 先建表 mysql --default-character-set=utf8mb4 -u root -p addr_db < schema.sql # 客户端和和服务端都要开 local_infile mysql --local-infile=1 --default-character-set=utf8mb4 -u root -p addr_db
LOAD DATA LOCAL INFILE '/data/region_4level.csv' INTO TABLE region CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (@code, @name, @parent_code, @level, @sort_no) SET code = TRIM(BOTH '\r' FROM @code), name = TRIM(@name), parent_code = NULLIF(TRIM(@parent_code), ''), level = CAST(@level AS UNSIGNED), sort_no = CAST(@sort_no AS UNSIGNED);

三个参数值得单独说。CHARACTER SET utf8mb4是给文件内容指定的,不写这一行,服务端会按表的默认字符集解析,中文很容易变问号;OPTIONALLY ENCLOSED BY '"'是因为地址名里偶尔带逗号,比如某些复合名称,不加引号包裹会整行错位;LINES TERMINATED BY '\n'针对的是 Windows 换行,如果文件是\r\n,末尾的\r会粘在最后一列上,所以SET里对code做了TRIM(BOTH '\r' FROM ...)。

NULLIF(TRIM(@parent_code), '')这一句是为了把空字符串转成真正的NULL。省级节点的父级必须是NULL而不是空串,否则递归查询的终止条件失效,会一直往上找,直接死循环。

如果不想碰 MySQL 的命令行工具,用 pandas 也能灌,适合数据量不大或者需要先做清洗的场景:

from sqlalchemy import create_engine import pandas as pd engine = create_engine( "mysql+pymysql://root:pass@127.0.0.1:3306/addr_db?charset=utf8mb4" ) df = pd.read_csv("region_4level.csv", dtype=str, encoding="utf-8-sig", keep_default_na=False) df["parent_code"] = df["parent_code"].replace("", None) df["level"] = df["level"].astype("int8") # chunksize 控制单批行数,太大容易超过 max_allowed_packet df.to_sql("region", engine, if_exists="append", index=False, chunksize=2000, method="multi")

charset=utf8mb4写在连接串里,别指望数据库默认值。chunksize=2000是个经验值,再大容易触发max_allowed_packet报错,报错信息还特别不直观,只会告诉你连接断了。

3.3 四级联动与逆序解析

级联下拉是正向查询,给定父级查子级,一条 SQL 就够:

-- 查某市下所有区县,按 sort_no 排 SELECT code, name FROM region WHERE parent_code = '110100' ORDER BY sort_no, code;

逆序解析是反过来的,给定一个街道编码,要还原出"省/市/区/街道"完整路径。MySQL 8.0 以后用递归 CTE 最省事:

WITH RECURSIVE up AS ( SELECT code, name, parent_code, level FROM region WHERE code = '110105001' UNION ALL SELECT r.code, r.name, r.parent_code, r.level FROM region r JOIN up ON r.code = up.parent_code ) SELECT * FROM up ORDER BY level;

UNION ALL不能换成UNION,后者会做去重排序,白搭一次性能。终止条件是r.code = up.parent_code匹配不到行,所以父级必须是NULL而不是空串,这和上面导入时的NULLIF是配套的。

如果线上是 5.7 或者更早的版本,没有 CTE,两条路:一是在应用层写循环,一次查一级,最多四次查询,配合本地缓存其实很快;二是用前面说的path字段,把路径物化下来,WHERE code = ?查一次拿到path,再按斜杠切分回表查名字。后者更适合高并发场景,因为只有一次查询。

def resolve_path(conn, code: str): """不依赖 CTE 的逆序解析,逐级上溯,最多 4 次查询""" chain, cur = [], code while cur: row = conn.execute( "SELECT code, name, parent_code FROM region WHERE code = %s", (cur,) ).fetchone() if not row: break chain.append({"code": row[0], "name": row[1]}) cur = row[2] return list(reversed(chain)) # 反转成 省->市->区->街道

这段的关键在while cur的循环条件:parent_code为NULL时cur变成None,循环自然退出,不需要额外的层级计数。如果担心脏数据造成死循环,加一个len(chain) > 5的保险,超过就抛异常,比默默转圈强。

4. 地址解析:把用户手填的一行字反查成行政编码

真实业务里,用户不会乖乖走四级下拉,大部分订单的地址是一行自由文本:"某某路某某小区 3 栋"。这一章解决的是怎么把这行字尽量准确地落回编码,落到区县一级算合格,落到街道一级算优秀。

4.1 先切分再匹配,别一上来就上模型

很多人第一反应是上 NLP、上地址解析模型。对四级地址库这个场景,属于杀鸡用牛刀,而且效果未必好:模型对生僻的街道名、新设的开发区几乎无能为力,而你要的恰恰是精确命中一个编码。

可行的路线是"逐级收敛 + 最大匹配"。先按行政级别从高到低匹配,每匹配到一级就把候选集收缩到该节点的子节点里,再匹配下一级。这样做的核心收益是消歧:全国叫"朝阳"的区不止一个,但一旦锁定了省和市,候选就唯一了。

切分的锚点是行政后缀:省、市、区、县、旗、街道、镇、乡、开发区。先扫描这些后缀,把文本切成若干段,再从第一段开始往编码树里对。切不出来就直接走全串模糊匹配,作为兜底。

4.2 前缀树最大匹配的 Python 实现

把地址库里的名字全部挂到一棵树上,每个节点记录对应的编码和层级,匹配时从根往下走,能走多远走多远。

from collections import defaultdict class TrieNode: __slots__ = ("children", "code", "level") def __init__(self): self.children = defaultdict(TrieNode) self.code = None # 该节点对应的行政区划编码 self.level = 0 # 层级,用于逐级收敛 def build_trie(rows): """rows: [(code, name, level, parent_code), ...]""" root = TrieNode() for code, name, level, _ in rows: node = root for ch in name: node = node.children[ch] node.code, node.level = code, int(level) return root def match(text: str, root: TrieNode, max_level=4): """正向最大匹配,返回命中的编码列表(按优先级)""" hits, i, n = [], 0, len(text) while i < n: node, j, best = root, i, None # 从 i 开始尽量往后吃字符 while j < n and text[j] in node.children: node = node.children[text[j]] j += 1 if node.code and node.level <= max_level: best = (node.code, node.level, j) if best: hits.append(best) i = best[2] # 跳到匹配结尾,避免重叠命中 else: i += 1 return hits

__slots__是为了压内存,地址库街道层几万个名字,每个字一个节点,用普通对象会吃掉不少内存。best记录的是"当前起点能匹配到的最深节点",也就是最长匹配,这比最短匹配更准,因为"经济技术开发区"和"开发区"同时在树里时,前者应该优先。

i = best[2]这一句是最大匹配的关键:命中之后直接把游标跳到匹配串的末尾,避免"某某街道"里又匹配出"某某"这种重叠结果。

真正的逐级收敛要在外面再包一层:

def parse_address(text, trie, conn): hits = match(text, trie, max_level=4) if not hits: return None # 按层级排序,先省后市后区县再街道 hits.sort(key=lambda x: x[1]) parent = None for code, level, _ in hits: if parent is None: parent = code if level == 1 else None continue # 后一级的父编码必须等于前一级,否则丢弃这次命中 row = conn.execute( "SELECT parent_code FROM region WHERE code = %s", (code,) ).fetchone() if row and row[0] == parent: parent = code return parent

那个row[0] == parent的判断是整套逻辑的地基。没有它,文本里随便出现的"XX镇"和前面匹配到的区县不在一个父级下,就会被错误拼接,最后给出一个根本不存在的路径。这种错误比完全匹配不上更可怕,因为它看起来是对的。

4.3 命中率不够时的兜底策略

最大匹配跑完,通常会有一批地址落在区县一级就下不去了,或者干脆一条都没命中。这时候别急着上模型,先看兜底手段够不够。

第一种是别名表。街道改名、开发区合并、俗称替换,这些靠算法推不出来,只能靠人工维护一张alias表,映射"旧名/俗称 → 标准编码"。这张表是长期资产,每次线上出现解析失败,就往里加一条,几个月后命中率会明显爬升。

第二种是编辑距离。针对长度较短的候选名做模糊匹配,长度小于等于四的允许一次编辑,大于四的允许两次。阈值别再放宽,再宽就会出现"XX街道"匹配到"XX街"这种跨级别的错误结果。

import difflib def fuzzy_match(name, candidates, cutoff=0.8): """candidates: 同一父级下的兄弟节点名字列表""" return difflib.get_close_matches(name, candidates, n=3, cutoff=cutoff)

cutoff建议从 0.8 起调,往下调到 0.7 时会明显出现误匹配。get_close_matches内部用的是 Ratcliff-Obershelp 相似度,对中文短串的表现尚可,胜在零依赖。

第三种是置信度分级。解析结果别只给一个编码,同时给一个confidence:精确命中记exact,经过别名映射记normalized,走编辑距离的记fuzzy。后续业务按置信度分流,fuzzy的结果只用于统计,不用于发货地址,避免误改。

5. 避坑排查:地址库落地最常见的五类翻车

这部分是踩过的坑汇总,每一条都对应过一次线上排查。现象写得具体一点,方便你对号入座。

5.1 导入后中文全是问号

现象:SELECT出来中文正常,但前端展示成???,或者反过来数据库里存的就是问号,应用层读出来也是问号。

原因:字符集在三层不一致。CSV 文件本身是 UTF-8,客户端连接字符集是latin1,表是utf8mb4,中间任何一层对不上,中文就会在转换时被替换成问号,而且是不可逆的替换,存进去就回不来了。

解决:三处都要显式指定。命令行加--default-character-set=utf8mb4;LOAD DATA语句里写CHARACTER SET utf8mb4;JDBC 或 SQLAlchemy 的连接串里带charset=utf8mb4。建库时也要DEFAULT CHARSET=utf8mb4,别用utf8,那个是三字节的,遇到四字节字符(部分生僻字在地址名里真有)会直接报错。

5.2 直辖市层级断裂,下拉框选不出区县

现象:省选完之后,市的下拉是空的,或者市和区县重复出现两次。

原因:直辖市的行政层级和普通省份不一样,省下面是区县,中间没有地级市这一层。如果你的数据按"省-市-区县"三级硬铺,直辖市的市级节点要么是空的,要么被填成和省同名,前端按固定三级去取,自然取不到。

解决:两条路。一是在数据里给直辖市补一个虚拟市级节点,名字和省级同名,编码用省级前两位加0100这种规则补出来,保证层级连续;二是在前端做特判,判断当前省级节点下有没有市级子节点,没有就直接跳到区县。前者改动小,后者数据干净。我一般选前者,因为前端特判这种东西,换个页面就得重写一遍。

5.3 同名区县串数据,地址跑到别的省

现象:用户明明选的是南边的区,后台存下来的编码属于北边的同名区。

原因:查询时只按name匹配,没带父级条件。全国同名的区县、同名的街道非常多,只靠名字查必然串。

解决:所有按名字的查询强制带parent_code条件,前端级联的时候把已选的上级编码一起传下去。匹配逻辑里也一样,先锁定父级再匹配子级,别在全表范围内按名字LIKE。这条规则要在代码评审里当红线卡,因为它的错误在测试环境基本发现不了,只有在数据量大的时候才暴露。

5.4 六位码和九位码混存,JOIN 出来是空

现象:街道查区县,JOIN结果是空,但两边的数据明明都在表里。

原因:一边存的是六位区县码,另一边存的是九位街道码截断后的六位,看着一样,实际上因为补齐规则不同对不上。常见的是市级码存成了四位1101,而区县的父级存的是六位110100,字符串比较直接失败。

解决:入库前统一补齐到固定长度,省市区一律六位,街道九位。在表上加CHECK (CHAR_LENGTH(code) IN (6,9,12))做约束,脏数据根本进不来。已经混了的,写个脚本按第 2 章的parent_of重算一遍parent_code再回写。

5.5 用户写的"街道"其实是社区名

现象:解析结果一直停在区县一级,匹配不到街道,但人工看地址明明写了街道。

原因:用户写的是社区或村委会的名字,那属于十二位码,不在四级范围内。或者写的是某个小区名,压根不是行政区划。

解决:这种事没法靠四级库解决,得接受"街道不一定能落"这个现实。做法是解析到哪一级算哪一级,剩下的部分原样保留在详细地址字段里,别硬凑。真要往下落,得再引入一份社区级数据或者 POI 库,那是另一个量级的工作。

6. 增量更新与对账:让地址库半年后还对得上

地址数据不是一次性资产。行政区划会调整,街道会合并,开发区会新设,一份导进去就不管的库,半年后就会出现新地址查不到、旧编码对不上的情况。这一章讲怎么让更新这件事变得可控。

6.1 版本快照与 diff

最朴素也最有效的办法:每次拿到的全量数据都按批次存一份快照,表里加batch_no字段,主表只保留最新批次。新旧两批做 diff,产出一张变更表。

-- 新增:新批次有、旧批次没有 SELECT n.code, n.name, 'ADD' AS change_type FROM region n LEFT JOIN region_old o ON n.code = o.code WHERE o.code IS NULL; -- 删除:旧批次有、新批次没有 SELECT o.code, o.name, 'DEL' AS change_type FROM region_old o LEFT JOIN region n ON n.code = o.code WHERE n.code IS NULL; -- 改名:编码相同但名称不同 SELECT n.code, o.name AS old_name, n.name AS new_name, 'RENAME' AS change_type FROM region n JOIN region_old o ON n.code = o.code WHERE n.name <> o.name;

三条查询覆盖了绝大多数变更。RENAME这一类最需要人工确认,因为系统里的历史订单存的可能是旧名字,改名之后按名字反查就会失败,得靠别名表把旧名映射到新编码上去。这也是别名表除了纠错之外的第二个用途。

DEL这一类要谨慎处理。编码消失通常意味着合并或者撤销,直接删主表的行会让历史数据的外键失效。稳妥做法是加一个is_active标记,逻辑删除,物理行永久保留,查询时统一过滤is_active = 1。

6.2 回归用例

每次更新完,跑一遍固定的回归用例,比人眼核对靠谱得多。用例不用多,覆盖几条关键路径就行。

用例类型输入期望
正向级联某省级编码返回的市级节点数大于 0
逆序解析某街道编码路径深度为 4,且每级父码连续
别名映射已知旧街道名命中新编码,置信度为 normalized
边界直辖市的区县编码路径深度为 3,不报错

第一行那条用例是我一定会加的。曾经出现过一次数据包里某个省的市级节点整体缺失,级联下拉直接空白,而单看行数和总条数完全正常,人眼根本发现不了。有了这条用例,导入完跑一遍就知道。

从那以后,我每次拿到新批次的地址数据,都强制走一遍"快照 → 校验 → diff → 回归用例"这四步,哪怕只是多了一个街道,也不跳过。地址这种数据的特点是错得很安静,等到用户投诉包裹发错省,成本已经出去了。

如果你手上正好在做地址表单、订单归集或者物流分单,需要一份带街道层的四级库打底,这份数据可以直接拿去用,字段和编码都是现成的,按上面的表结构导进去就能跑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询