推荐单表存储省市区三级数据,字段含id、name、level、pid、code,pid加索引,接口按pid+level分层查询,避免关联与树形组装。

省市区数据表怎么设计才不翻车
直接用单表存三级数据最稳妥,别搞三张表关联。ThinkPHP 项目里常见错误是把 province、city、district 拆成三张表再加外键,结果前端请求 /api/area/city?pid=11 时要连表查,一并发就慢,而且 ThinkPHP 的 with() 或 join() 容易漏写条件或索引没建对。
推荐结构:一张 area 表,字段至少包含:id、name、level(1=省,2=市,3=区/县)、pid(父级 id,省的 pid 为 0)、code(国家标准行政区划代码,可选但建议留着)。
-
pid必须加索引,否则按父级查子集会全表扫描 -
level字段别用字符串(如 "province"),用整数更利于 where 查询和缓存分片 - 导入数据时注意:直辖市(北京、上海等)的市和区是平级的,它们的
pid都指向自己省级id,不是指向“地级市” - 不要在模型里写死“省-市-区”三层嵌套输出,接口只按需返回扁平列表
ThinkPHP 怎么写接口才能让前端联得稳
别用一个接口返回全部三级数据(比如 GET /api/area/all),体积大、缓存难、前端解析也重。应该拆成三个独立接口,靠 pid 参数驱动,后端只做单层查询 + 简单组装。
示例控制器方法(TP6+):
public function list($pid = 0, $level = 1)
{
$data = AreaModel::where('pid', $pid)
->where('level', $level)
->field('id,name,code')
->select();
return json(['code' => 0, 'data' => $data]);
}
- 路由建议用
GET /api/area/list?pid=110000&level=2,比/province/{id}/cities更通用,兼容省/市两级直辖区逻辑 - 必须校验
$level范围(1–3),防止恶意传level=999导致空结果或 SQL 报错 - 如果启用了 Redis 缓存,key 可设为
area:list:{$pid}:{$level},过期时间设 24 小时足够——行政区划极少变更 - 别在接口里调用
AreaModel::getSubList($id)这类封装方法,除非它内部已加缓存且明确只查一层
前端选中省之后,为什么 city 接口总返回空?
大概率是传错了 pid 值。常见坑有三个:
- 前端拿到的是省的
id(比如110000),但误传成了code字段值(其实一样),问题不大;但若数据库里省记录的id是自增主键(如1),而code才是110000,此时前端传pid=1查不到任何市——因为市记录的pid存的是父级code(标准做法) - 数据库里存在脏数据:某条市记录的
pid为空、为 null 或拼错(如11000少一位),查出来就是空 - ThinkPHP 查询时没加
->whereNotNull('pid'),而部分区县记录pid为 0 或空字符串,导致匹配失败
调试时直接在数据库跑这句:SELECT * FROM area WHERE pid = '110000' AND level = 2 LIMIT 5;,结果为空就说明数据或参数对不上。
要不要在 ThinkPHP 模型里加树形结构方法?
不要。所谓 getTree() 或 toTree() 方法,在三级联动场景下纯属负优化。一次查出全部数据再 PHP 递归组装,既浪费内存又无法利用数据库索引,还绕过缓存。
- 前端自己管理层级状态更可靠:选省 → 存
selectedProvince→ 请求市 → 存selectedCity→ 再请求区 - 如果真需要导出完整树(比如后台管理页),用数据库的
WITH RECURSIVE(MySQL 8.0+)或分三次查 + PHP 合并,别塞进模型公共方法里 - 注意:ThinkPHP 的
buildTree()辅助函数是静态工具,不走模型事件和自动转换,容易和datetime字段处理冲突,慎用
真正关键的是保证每层查询响应在 20ms 内,而不是让一个接口扛起所有层级。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











