205 lines
6.7 KiB
Markdown
205 lines
6.7 KiB
Markdown
# 地区三级联动表实现计划
|
||
|
||
## 目标
|
||
创建省份/城市/地区三级联动数据库表,在个人中心"所在位置"字段中使用级联选择,替代当前的静态硬编码数据。
|
||
|
||
## 当前状态分析
|
||
|
||
### 现状
|
||
- `UserInfoCard.tsx` 中的 `locationOptions` 是静态硬编码的(只包含北京、上海、广东的部分区县)
|
||
- `FUser.location` 字段是 `CharField(max_length=100)`,存储格式为 `"省/市/区"` 字符串
|
||
- IP定位API (`GetIPDataView`) 返回 `regionName` 和 `city`,但没有结构化存储
|
||
- 无地区数据库表
|
||
|
||
### 问题
|
||
- 静态数据覆盖范围有限
|
||
- 无法与IP定位数据联动
|
||
- 用户体验差(需要手动输入)
|
||
|
||
## 实现方案
|
||
|
||
### 1. 创建 Region 模型
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\models.py`
|
||
|
||
```python
|
||
class Region(models.Model):
|
||
"""地区三级联动表 - 省/市/区"""
|
||
REGION_LEVEL_CHOICES = [
|
||
(1, '省份'),
|
||
(2, '城市'),
|
||
(3, '区县'),
|
||
]
|
||
|
||
name = models.CharField(max_length=100, verbose_name='地区名称')
|
||
code = models.CharField(max_length=20, unique=True, verbose_name='地区编码')
|
||
level = models.IntegerField(choices=REGION_LEVEL_CHOICES, verbose_name='级别')
|
||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name='上级地区')
|
||
pinyin = models.CharField(max_length=200, blank=True, verbose_name='拼音')
|
||
sort_order = models.IntegerField(default=0, verbose_name='排序')
|
||
|
||
class Meta:
|
||
verbose_name = '地区'
|
||
verbose_name_plural = '地区'
|
||
ordering = ['level', 'sort_order', 'code']
|
||
indexes = [
|
||
models.Index(fields=['parent', 'level']),
|
||
models.Index(fields=['code']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
```
|
||
|
||
### 2. 创建数据迁移文件
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\migrations\0010_region.py`
|
||
|
||
迁移文件将:
|
||
- 创建 Region 表
|
||
- 包含初始数据种子命令的引用
|
||
|
||
### 3. 创建数据种子命令
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\management\commands\seed_regions.py`
|
||
|
||
功能:
|
||
- 从JSON文件或内置数据导入全国省市区数据
|
||
- 支持增量更新(已存在则跳过)
|
||
- 数据来源:国家统计局行政区划代码
|
||
|
||
### 4. 创建 API 序列化器
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\serializers\region_serializers.py`
|
||
|
||
```python
|
||
class RegionSerializer(serializers.ModelSerializer):
|
||
children = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Region
|
||
fields = ['id', 'name', 'code', 'level', 'parent_id', 'children']
|
||
|
||
def get_children(self, obj):
|
||
children = obj.children.all()
|
||
if children.exists():
|
||
return RegionSerializer(children, many=True).data
|
||
return None
|
||
```
|
||
|
||
### 5. 创建 API 视图
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\views\region.py`
|
||
|
||
```python
|
||
class RegionListView(APIView):
|
||
"""获取地区级联数据"""
|
||
permission_classes = [AllowAny]
|
||
|
||
def get(self, request):
|
||
# 只返回省级数据,前端按需加载子级
|
||
level = request.GET.get('level', 1)
|
||
parent_code = request.GET.get('parent_code')
|
||
|
||
if parent_code:
|
||
regions = Region.objects.filter(parent__code=parent_code)
|
||
else:
|
||
regions = Region.objects.filter(level=level)
|
||
|
||
serializer = RegionSerializer(regions, many=True)
|
||
return Response({'code': 200, 'data': serializer.data})
|
||
```
|
||
|
||
### 6. 注册路由
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project\user\urls.py`
|
||
|
||
添加:
|
||
```python
|
||
path('regions/', region_views.RegionListView.as_view(), name='region-list'),
|
||
```
|
||
|
||
### 7. 前端 API 请求
|
||
|
||
**文件**: `c:\Users\12914\Desktop\vscode\chunyu_project_react\src\utils\request.ts`
|
||
|
||
添加:
|
||
```typescript
|
||
const region = {
|
||
getList: (params?: { level?: number; parent_code?: string }) =>
|
||
request.public.get('/user/regions/', params),
|
||
};
|
||
```
|
||
|
||
### 8. 前端组件改造
|
||
|
||
**文件**: `C:\Users\12914\Desktop\vscode\chunyu_project_react\src\pages\Profile\components\UserInfoCard.tsx`
|
||
|
||
修改内容:
|
||
1. 移除静态 `locationOptions`
|
||
2. 添加 `regionOptions` 状态,从API获取省级数据
|
||
3. 改造 `Cascader` 组件为动态加载模式
|
||
4. 使用 `loadData` 属性实现按需加载子级数据
|
||
5. 保存时将地区编码数组转换为 `"省/市/区"` 格式字符串
|
||
|
||
```typescript
|
||
// 新的地区级联选择器
|
||
<Cascader
|
||
options={regionOptions}
|
||
loadData={async (selectedOptions) => {
|
||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||
targetOption.loading = true;
|
||
const children = await region.getList({ parent_code: targetOption.code });
|
||
targetOption.children = children;
|
||
setRegionOptions([...regionOptions]);
|
||
}}
|
||
value={location}
|
||
onChange={(v) => setLocation(v as string[])}
|
||
placeholder="请选择地区"
|
||
expandTrigger="hover"
|
||
displayRender={(labels) => labels.join(" / ")}
|
||
style={{ width: "100%" }}
|
||
/>
|
||
```
|
||
|
||
### 9. IP定位联动(可选)
|
||
|
||
登录时自动填充位置:
|
||
- 在 `_create_login_record` 中调用IP API
|
||
- 将 `regionName` 和 `city` 存入 `LoginRecord.location`
|
||
- 尝试匹配 Region 表中的地区编码
|
||
|
||
## 文件清单
|
||
|
||
| 文件 | 操作 | 说明 |
|
||
|------|------|------|
|
||
| `chunyu_project/user/models.py` | 修改 | 添加 Region 模型 |
|
||
| `chunyu_project/user/migrations/0010_region.py` | 新建 | 创建 Region 表迁移 |
|
||
| `chunyu_project/user/management/commands/seed_regions.py` | 新建 | 数据种子命令 |
|
||
| `chunyu_project/user/serializers/region_serializers.py` | 新建 | 地区序列化器 |
|
||
| `chunyu_project/user/views/region.py` | 新建 | 地区API视图 |
|
||
| `chunyu_project/user/urls.py` | 修改 | 注册地区路由 |
|
||
| `chunyu_project_react/src/utils/request.ts` | 修改 | 添加 region API |
|
||
| `chunyu_project_react/src/pages/Profile/components/UserInfoCard.tsx` | 修改 | 使用动态地区数据 |
|
||
|
||
## 验证步骤
|
||
|
||
1. 运行 `python manage.py makemigrations` 生成迁移
|
||
2. 运行 `python manage.py migrate` 执行迁移
|
||
3. 运行 `python manage.py seed_regions` 导入初始数据
|
||
4. 访问 `/api/user/regions/` 验证API返回数据
|
||
5. 打开个人中心,验证地区级联选择器正常工作
|
||
6. 保存位置后验证数据库存储格式正确
|
||
|
||
## 假设与决策
|
||
|
||
### 假设
|
||
- 前端使用 Ant Design Cascader 组件的 `loadData` 属性实现懒加载
|
||
- 初始数据来源于国家统计局行政区划代码
|
||
- Region 表数据量约为 3000+ 条(省34个、市300+、区县2800+)
|
||
|
||
### 决策
|
||
- 使用 `parent` 自关联外键实现树形结构
|
||
- API 只返回当前级别数据,前端按需加载子级
|
||
- 存储格式保持兼容:`"省/市/区"` 字符串
|