198 lines
7.4 KiB
Markdown
198 lines
7.4 KiB
Markdown
:::tip 行政区划
|
||
行政区划模块管理全国省/市/区/街道四级区域数据,支持树形结构查询、级联选择器、拼音自动生成等功能。用户表通过 `province_code`/`city_code`/`district_code`/`street_code` 四个字段关联城市表的 `area_code`。
|
||
:::
|
||
|
||
## 模块结构
|
||
|
||
```
|
||
src/modules/city/
|
||
├── models.py # 城市模型
|
||
├── schema.py # 表单验证(WTForms)
|
||
├── repository.py # 数据访问层
|
||
├── service.py # 业务逻辑层(继承 BaseService)
|
||
└── router.py # 路由控制器
|
||
```
|
||
|
||
## 城市模型
|
||
|
||
```python
|
||
# src/modules/city/models.py
|
||
class City(BaseModel, BaseDB):
|
||
"""
|
||
城市区域模型
|
||
|
||
支持省、市、县区、街道四级区域结构。
|
||
"""
|
||
__tablename__ = DB_PREFIX + "city"
|
||
__table_comment__ = "城市表"
|
||
|
||
# 城市区号
|
||
city_code = db.Column(db.String(6), nullable=False, index=True, comment="城市区号")
|
||
# 行政编码(用于用户表关联)
|
||
area_code = db.Column(db.String(20), nullable=False, index=True, comment="行政编码")
|
||
# 上级行政编码
|
||
parent_code = db.Column(db.String(20), nullable=False, index=True, comment="上级行政编码")
|
||
# 邮政编码
|
||
zip_code = db.Column(db.String(6), nullable=False, comment="邮政编码")
|
||
# 城市级别:0-省份 1-城市 2-县区 3-街道 4-社区
|
||
level = db.Column(db.Integer, default=0, server_default=text('0'), index=True,
|
||
comment="城市级别:0-省份 1-城市 2-县区 3-街道 4-社区")
|
||
# 上级城市ID
|
||
pid = db.Column(db.Integer, default=0, server_default=text('0'), index=True, comment="上级城市ID")
|
||
# 城市名称
|
||
name = db.Column(db.String(150), nullable=False, comment="城市名称")
|
||
# 城市简称
|
||
short_name = db.Column(db.String(150), nullable=False, index=True, comment="城市简称")
|
||
# 城市全称(沿 pid 链拼接,如"台湾省,云林,元长,华鹿")
|
||
full_name = db.Column(db.String(150), nullable=True, comment="城市全称")
|
||
# 城市拼音(由简称自动生成,如"TaiWan")
|
||
pinyin = db.Column(db.String(150), nullable=True, comment="城市拼音")
|
||
# 经纬度
|
||
lng = db.Column(db.String(150), nullable=True, comment="城市经度")
|
||
lat = db.Column(db.String(150), nullable=True, comment="城市纬度")
|
||
```
|
||
|
||
## 服务层特色
|
||
|
||
城市服务继承 `BaseService`,并通过钩子实现自动派生字段:
|
||
|
||
```python
|
||
# src/modules/city/service.py
|
||
class CityService(BaseService):
|
||
repository = CityRepository
|
||
form = CityForm
|
||
model_name = "城市"
|
||
|
||
@classmethod
|
||
def before_add(cls, form):
|
||
"""新增前钩子:自动补全 full_name / pinyin"""
|
||
data = form.data
|
||
return {
|
||
"full_name": cls._build_full_name(data.get("pid") or 0, data.get("name")),
|
||
"pinyin": cls._build_pinyin(data.get("shortName") or data.get("name")),
|
||
}
|
||
|
||
@classmethod
|
||
def before_delete(cls, ids):
|
||
"""删除前校验:检查是否存在子级城市"""
|
||
for cid in ids:
|
||
if CityRepository.count_children(cid, parent_field='pid') > 0:
|
||
raise BizError(f"城市ID {cid} 存在子级城市,请先删除子级城市")
|
||
|
||
@classmethod
|
||
def _detail_dict(cls, obj):
|
||
"""详情序列化:追加 parentName(父级城市名称)"""
|
||
data = model_to_dict(obj)
|
||
parent_id = getattr(obj, 'pid', None)
|
||
if parent_id:
|
||
parent = CityRepository.get_by_id(parent_id)
|
||
data['parentName'] = parent.name if parent else None
|
||
else:
|
||
data['parentName'] = None
|
||
return data
|
||
```
|
||
|
||
### 自动生成字段
|
||
|
||
| 字段 | 生成规则 | 示例 |
|
||
|:---|:---|:---|
|
||
| `full_name` | 沿 pid 链向上拼接,逗号分隔 | `台湾省,云林,元长,华鹿` |
|
||
| `pinyin` | 由简称生成无音调拼音,每音节首字母大写 | `TaiWan`、`HuaLu` |
|
||
|
||
### 删除前校验
|
||
|
||
城市采用树形结构(`pid` 字段),删除前检查是否存在子级城市:
|
||
|
||
```
|
||
广东省 (pid=0)
|
||
├── 深圳市 (pid=广东省.id)
|
||
│ ├── 南山区 (pid=深圳市.id)
|
||
│ └── 福田区 (pid=深圳市.id)
|
||
└── 广州市 (pid=广东省.id)
|
||
```
|
||
|
||
删除「深圳市」前,会检查其下是否存在「南山区」「福田区」等子级城市。
|
||
|
||
## 树形数据查询
|
||
|
||
城市模块提供树形数据接口,用于前端级联选择器:
|
||
|
||
```python
|
||
def get_city_tree():
|
||
"""获取城市树形数据,构建省-市-区三级树形结构"""
|
||
all_cities = CityRepository.list_all()
|
||
city_dicts = models_to_list(all_cities)
|
||
return _build_city_tree(city_dicts)
|
||
|
||
|
||
def _build_city_tree(city_list, parent_id=0):
|
||
"""递归构建城市树形结构"""
|
||
tree = []
|
||
for city in city_list:
|
||
if city.get("pid") == parent_id:
|
||
node = {
|
||
"id": city.get("id"),
|
||
"name": city.get("name"),
|
||
"code": city.get("area_code"),
|
||
"level": city.get("level"),
|
||
"children": _build_city_tree(city_list, city.get("id")),
|
||
}
|
||
if not node["children"]:
|
||
del node["children"]
|
||
tree.append(node)
|
||
return tree
|
||
```
|
||
|
||
## 与用户模块的关联
|
||
|
||
用户表通过四个字段关联城市表:
|
||
|
||
```python
|
||
# src/modules/system/user/models.py
|
||
class User(BaseModel, BaseDB):
|
||
# 省份编码
|
||
province_code = db.Column(db.String(30), nullable=False, comment="省份编码")
|
||
# 城市编码
|
||
city_code = db.Column(db.String(30), nullable=False, comment="城市编码")
|
||
# 县区编码
|
||
district_code = db.Column(db.String(30), nullable=False, comment="县区编码")
|
||
# 街道编码
|
||
street_code = db.Column(db.String(30), nullable=False, comment="街道编码")
|
||
# 省市区信息(拼接后的可读文本,如"广东省深圳市南山区")
|
||
city_info = db.Column(db.String(255), nullable=True, comment="省市区信息")
|
||
```
|
||
|
||
用户新增/更新时,前端通过级联选择器传入 `[省编码, 市编码, 区编码, 街道编码]` 数组,Service 层拆分到独立字段:
|
||
|
||
```python
|
||
# src/modules/system/user/service.py
|
||
def _apply_city(create_data, city):
|
||
"""将行政区划数组拆分到对应的字段"""
|
||
create_data["province_code"] = city[0]
|
||
create_data["city_code"] = city[1]
|
||
create_data["district_code"] = city[2]
|
||
create_data["street_code"] = city[3]
|
||
```
|
||
|
||
## API 接口
|
||
|
||
| 方法 | 路径 | 权限节点 | 说明 |
|
||
|:---|:---|:---|:---|
|
||
| GET | `/api/v1/city/list` | `sys:city:list` | 城市列表(支持 pid/name 筛选) |
|
||
| GET | `/api/v1/city/detail/<id>` | `sys:city:detail` | 城市详情 |
|
||
| GET | `/api/v1/city/child/<code>` | - | 子级城市列表(级联选择器用) |
|
||
| GET | `/api/v1/city/tree` | - | 城市树形数据 |
|
||
| POST | `/api/v1/city/add` | `sys:city:add` | 新增城市 |
|
||
| PUT | `/api/v1/city/update` | `sys:city:update` | 编辑城市 |
|
||
| DELETE | `/api/v1/city/delete/<id>` | `sys:city:delete` | 删除城市 |
|
||
|
||
## 数据字典
|
||
|
||
| 字典编码 | 用途 | 字典项示例 |
|
||
|:---|:---|:---|
|
||
| `city_level` | 城市级别名称 | 0=省份, 1=城市, 2=县区, 3=街道, 4=社区 |
|
||
|
||
## 总结
|
||
|
||
行政区划模块采用树形结构(`pid` 字段),支持省/市/区/街道四级管理。`before_add` 钩子自动补全 `full_name`(沿 pid 链拼接)和 `pinyin`(由简称生成),`before_delete` 钩子校验子级城市引用。用户表通过 `area_code` 字段关联城市表,前端使用级联选择器。
|