签入版本
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# 完整代码清单
|
||||
|
||||
本章汇总 Example 案例模块涉及的全部文件,便于开发时快速查阅和复制。
|
||||
|
||||
## 后端文件清单
|
||||
|
||||
### 1. 数据模型 `application/example/models.py`
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from application.models import BaseModel
|
||||
from utils.common import get_table_name
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
"""案例模型类"""
|
||||
name = models.CharField(null=False, max_length=100, db_index=True,
|
||||
verbose_name="案例名称", db_comment='案例名称')
|
||||
avatar = models.CharField(null=True, blank=True, max_length=255,
|
||||
verbose_name="案例图片", db_comment='案例图片')
|
||||
TYPE_CHOICES = ((1, "类型1"), (2, "类型2"), (3, "类型3"), (4, "类型4"),)
|
||||
type = models.IntegerField(null=False, choices=TYPE_CHOICES,
|
||||
verbose_name="案例类型", db_comment='案例类型')
|
||||
STATUS_CHOICES = ((1, "正常"), (2, "禁用"),)
|
||||
status = models.IntegerField(null=False, choices=STATUS_CHOICES,
|
||||
verbose_name="案例状态", db_comment='案例状态')
|
||||
sort = models.IntegerField(null=False,
|
||||
verbose_name="排序", db_comment='排序')
|
||||
|
||||
class Meta:
|
||||
db_table = get_table_name('example')
|
||||
db_table_comment = "案例表"
|
||||
verbose_name = "案例表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
```
|
||||
|
||||
### 2. 表单验证 `application/example/forms.py`
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from application.example import models
|
||||
|
||||
|
||||
class ExampleForm(forms.ModelForm):
|
||||
"""案例表单验证类"""
|
||||
name = forms.CharField(required=True, max_length=100, error_messages={
|
||||
'required': '案例名称不能为空', 'max_length': '案例名称长度不得超过100个字符'})
|
||||
avatar = forms.CharField(required=False, max_length=255, error_messages={
|
||||
'max_length': '案例图片长度不得超过255个字符'})
|
||||
type = forms.IntegerField(required=True, min_value=1, max_value=4, error_messages={
|
||||
'required': '案例类型不能为空', 'min_value': '案例类型值不能小于1', 'max_value': '案例类型值不能大于4'})
|
||||
status = forms.IntegerField(required=True, min_value=1, max_value=2, error_messages={
|
||||
'required': '案例状态不能为空', 'min_value': '案例状态值不能小于1', 'max_value': '案例状态值不能大于2'})
|
||||
sort = forms.IntegerField(required=True, error_messages={'required': '排序不能为空'})
|
||||
|
||||
class Meta:
|
||||
model = models.Example
|
||||
fields = ['name', 'avatar', 'type', 'status', 'sort']
|
||||
```
|
||||
|
||||
### 3. 业务逻辑层 `application/example/services.py`
|
||||
|
||||
```python
|
||||
import json
|
||||
import logging
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage, EmptyPage
|
||||
from application.example import forms
|
||||
from application.example.models import Example
|
||||
from config.env import DJANGO_FILE_URL
|
||||
from constant.constants import PAGE_SIZE
|
||||
from utils import R, regular
|
||||
from utils.common import get_file_url, format_datetime, parse_request_body, save_file
|
||||
from utils.security import get_username
|
||||
|
||||
|
||||
def get_example_page(request):
|
||||
"""查询案例分页数据"""
|
||||
try:
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
query = _apply_filters(query, request)
|
||||
query = query.order_by("sort")
|
||||
paginator = Paginator(query, page_size)
|
||||
try:
|
||||
page_list = paginator.page(page_no)
|
||||
except (PageNotAnInteger, InvalidPage, EmptyPage):
|
||||
page_list = paginator.page(1)
|
||||
page_no = 1
|
||||
page_data = _build_page_data(page_list, paginator, page_no, page_size)
|
||||
return R.ok(data=page_data)
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例分页数据异常: {str(e)}")
|
||||
return R.failed(msg="查询失败,请稍后重试")
|
||||
|
||||
|
||||
def add_example(request):
|
||||
"""添加案例"""
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
try:
|
||||
cleaned_data = form.cleaned_data
|
||||
avatar = cleaned_data.get('avatar')
|
||||
if avatar:
|
||||
avatar = save_file(avatar, "example")
|
||||
Example.objects.create(
|
||||
name=cleaned_data.get('name'), avatar=avatar,
|
||||
type=cleaned_data.get('type'), status=cleaned_data.get('status'),
|
||||
sort=cleaned_data.get('sort'),
|
||||
create_user=get_username(request), update_user=get_username(request))
|
||||
return R.ok(msg="创建成功")
|
||||
except Exception as e:
|
||||
logging.error(f"添加案例异常: {str(e)}")
|
||||
return R.failed(msg="添加失败,请稍后重试")
|
||||
|
||||
|
||||
def update_example(request):
|
||||
"""更新案例"""
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
instance_id = data.get('id')
|
||||
if not instance_id or not str(instance_id).isdigit() or int(instance_id) <= 0:
|
||||
return R.failed("案例ID无效")
|
||||
instance_id = int(instance_id)
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
try:
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
cleaned_data = form.cleaned_data
|
||||
avatar = cleaned_data.get('avatar')
|
||||
if avatar and avatar != instance.avatar:
|
||||
avatar = save_file(avatar, "example")
|
||||
else:
|
||||
avatar = instance.avatar
|
||||
instance.name = cleaned_data.get('name')
|
||||
instance.avatar = avatar
|
||||
instance.type = cleaned_data.get('type')
|
||||
instance.status = cleaned_data.get('status')
|
||||
instance.sort = cleaned_data.get('sort')
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
return R.ok(msg="更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="更新失败,请稍后重试")
|
||||
|
||||
|
||||
def delete_examples(instance_ids):
|
||||
"""删除案例(批量删除)"""
|
||||
if not instance_ids:
|
||||
return R.failed("记录ID不存在")
|
||||
try:
|
||||
id_list = [int(id_str.strip()) for id_str in instance_ids.split(',') if id_str.strip()]
|
||||
if not id_list:
|
||||
return R.failed("无效的记录ID")
|
||||
updated_count = Example.objects.filter(id__in=id_list, is_delete=False).update(is_delete=True)
|
||||
return R.ok(msg=f"本次共删除{updated_count}条数据")
|
||||
except ValueError:
|
||||
return R.failed("记录ID格式错误")
|
||||
|
||||
|
||||
def update_example_status(request):
|
||||
"""更新案例状态"""
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
instance_id = data.get('id')
|
||||
if not instance_id or not str(instance_id).isdigit() or int(instance_id) <= 0:
|
||||
return R.failed("案例ID无效")
|
||||
instance_id = int(instance_id)
|
||||
status = data.get('status')
|
||||
if status is None:
|
||||
return R.failed("状态值不能为空")
|
||||
try:
|
||||
status = int(status)
|
||||
if status not in [1, 2]:
|
||||
return R.failed("状态值无效")
|
||||
except ValueError:
|
||||
return R.failed("状态值格式错误")
|
||||
try:
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
instance.status = status
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
return R.ok(msg="状态更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例状态异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="状态更新失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 4. 视图层 `application/example/views.py`
|
||||
|
||||
```python
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from application.example import services
|
||||
from config.env import DJANGO_DEMO
|
||||
from middleware.login_middleware import check_login
|
||||
from middleware.permission_middleware import PermissionRequired
|
||||
from utils import R
|
||||
from application.operation_log.constants import LogType
|
||||
from application.operation_log.decorators import operation_log
|
||||
|
||||
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
"""案例分页数据查询视图"""
|
||||
permission_required = ("sys:example:page",)
|
||||
|
||||
@operation_log(title="案例管理-查询分页列表", log_type=LogType.QUERY)
|
||||
def get(self, request):
|
||||
return services.get_example_page(request)
|
||||
|
||||
|
||||
@method_decorator(check_login, name="post")
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
"""添加案例视图"""
|
||||
permission_required = ("sys:example:add",)
|
||||
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
def post(self, request):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
return services.add_example(request)
|
||||
|
||||
|
||||
@method_decorator(check_login, name="put")
|
||||
class ExampleUpdateView(PermissionRequired, View):
|
||||
"""更新案例视图"""
|
||||
permission_required = ('sys:example:update',)
|
||||
|
||||
@operation_log(title="案例管理-更新记录", log_type=LogType.UPDATE)
|
||||
def put(self, request):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
return services.update_example(request)
|
||||
|
||||
|
||||
@method_decorator(check_login, name="delete")
|
||||
class ExampleDeleteView(PermissionRequired, View):
|
||||
"""删除案例视图"""
|
||||
permission_required = ('sys:example:delete',)
|
||||
|
||||
@operation_log(title="案例管理-删除记录", log_type=LogType.DELETE)
|
||||
def delete(self, request, id):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
return services.delete_examples(id)
|
||||
```
|
||||
|
||||
### 5. 路由配置 `application/example/urls.py`
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
from application.example import views
|
||||
|
||||
urlpatterns = [
|
||||
path('page', views.ExamplePageView.as_view()),
|
||||
path('list', views.ExampleListView.as_view()),
|
||||
path('detail/<int:id>', views.ExampleDetailView.as_view()),
|
||||
path('add', views.ExampleAddView.as_view()),
|
||||
path('update', views.ExampleUpdateView.as_view()),
|
||||
path('delete/<str:id>', views.ExampleDeleteView.as_view()),
|
||||
path('status', views.ExampleStatusView.as_view()),
|
||||
path('list', views.ExampleDataView.as_view()),
|
||||
]
|
||||
```
|
||||
|
||||
### 6. 应用配置 `application/example/apps.py`
|
||||
|
||||
```python
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExampleConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'application.example'
|
||||
```
|
||||
|
||||
### 7. 主路由注册(追加到 `application/urls.py`)
|
||||
|
||||
```python
|
||||
# 案例模块路由
|
||||
path('example/', include('application.example.urls')),
|
||||
```
|
||||
|
||||
### 8. settings.py 注册应用
|
||||
|
||||
在 `INSTALLED_APPS` 中添加:
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'application.example',
|
||||
]
|
||||
```
|
||||
|
||||
## 前端文件清单
|
||||
|
||||
### 9. API 请求 `ui/src/api/tool/example.ts`
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function getExamplePage(params?) {
|
||||
return http.request({ url: '/example/page', method: 'GET', params });
|
||||
}
|
||||
|
||||
export function getExampleList(params?) {
|
||||
return http.request({ url: '/example/list', method: 'GET', params });
|
||||
}
|
||||
|
||||
export function getExampleDetail(id) {
|
||||
return http.request({ url: '/example/detail/' + id, method: 'get' });
|
||||
}
|
||||
|
||||
export function exampleAdd(data: any) {
|
||||
return http.request({ url: '/example/add', method: 'POST', data });
|
||||
}
|
||||
|
||||
export function exampleUpdate(data: any) {
|
||||
return http.request({ url: '/example/update', method: 'PUT', data });
|
||||
}
|
||||
|
||||
export function exampleDelete(id) {
|
||||
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 页面视图(5 个文件)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|:----|:----|
|
||||
| `ui/src/views/tool/example/index.vue` | 主页面:搜索 + 表格 + 操作 |
|
||||
| `ui/src/views/tool/example/edit.vue` | 编辑弹窗:新增/编辑表单 |
|
||||
| `ui/src/views/tool/example/detail.vue` | 详情弹窗:查看记录详情 |
|
||||
| `ui/src/views/tool/example/columns.ts` | 表格列定义 |
|
||||
| `ui/src/views/tool/example/querySchemas.ts` | 搜索表单 Schema |
|
||||
|
||||
参考 [前端页面视图](/zh/volume2/fe-view) 中的完整代码。
|
||||
|
||||
## 文件总览
|
||||
|
||||
```
|
||||
后端(application/example/)
|
||||
├── models.py # 数据模型
|
||||
├── forms.py # 表单验证
|
||||
├── services.py # 业务逻辑层
|
||||
├── views.py # 视图层
|
||||
├── urls.py # 路由配置
|
||||
├── apps.py # 应用配置
|
||||
├── admin.py # Django Admin 注册
|
||||
└── migrations/ # 数据库迁移文件
|
||||
|
||||
前端(ui/src/)
|
||||
├── api/tool/
|
||||
│ └── example.ts # API 请求封装
|
||||
└── views/tool/example/
|
||||
├── index.vue # 主页面
|
||||
├── edit.vue # 编辑弹窗
|
||||
├── detail.vue # 详情弹窗
|
||||
├── columns.ts # 表格列定义
|
||||
└── querySchemas.ts # 搜索表单 Schema
|
||||
|
||||
注册文件
|
||||
├── application/urls.py # 主路由注册
|
||||
├── application/settings.py # INSTALLED_APPS 注册
|
||||
└── 数据库菜单表 # 菜单和权限节点
|
||||
```
|
||||
|
||||
## 开发检查清单
|
||||
|
||||
完成后端开发后,按顺序检查:
|
||||
|
||||
- [ ] `models.py` 已定义模型,表名使用 `get_table_name()` 加前缀
|
||||
- [ ] `forms.py` 已定义表单类,中文错误提示
|
||||
- [ ] `services.py` 已实现所有 CRUD 函数
|
||||
- [ ] `views.py` 已定义视图类 + 权限装饰器 + 操作日志
|
||||
- [ ] `urls.py` 已配置路由
|
||||
- [ ] `application/urls.py` 已注册模块路由
|
||||
- [ ] `application/settings.py` 已注册应用
|
||||
- [ ] 菜单管理中已添加权限节点
|
||||
|
||||
完成前端开发后,按顺序检查:
|
||||
|
||||
- [ ] `api/tool/example.ts` 已封装所有接口
|
||||
- [ ] `views/tool/example/index.vue` 主页面已实现
|
||||
- [ ] `views/tool/example/edit.vue` 编辑弹窗已实现
|
||||
- [ ] `views/tool/example/detail.vue` 详情弹窗已实现
|
||||
- [ ] `views/tool/example/columns.ts` 列定义已配置
|
||||
- [ ] `views/tool/example/querySchemas.ts` 搜索 Schema 已配置
|
||||
- [ ] 后端菜单已配置组件路径
|
||||
- [ ] 页面可正常访问和操作
|
||||
|
||||
## 总结
|
||||
|
||||
完整模块涉及后端 6 个文件(models/forms/services/views/urls/apps)+ 路由注册 + 应用注册 + 权限配置,前端 5 个文件(index.vue/edit.vue/detail.vue/columns.ts/querySchemas.ts)+ API 文件。代码生成器可根据数据库表自动生成全套代码。
|
||||
@@ -0,0 +1,259 @@
|
||||
# 常见问题与排错
|
||||
|
||||
本章汇总模块开发过程中的常见问题和解决方案。
|
||||
|
||||
## 后端问题
|
||||
|
||||
### Q1: 启动报错 `Table 'xxx.django_example' doesn't exist`
|
||||
|
||||
**原因**:模型定义后未执行数据库迁移。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 生成迁移文件
|
||||
python manage.py makemigrations example
|
||||
|
||||
# 执行迁移
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Q2: 接口返回 `{"code": 1, "msg": "暂无操作权限"}`
|
||||
|
||||
**原因**:权限节点未配置或未分配给当前用户角色。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认菜单管理中已添加权限节点,权限标识与代码中 `permission_required` 完全一致
|
||||
2. 确认权限节点已分配给当前用户的角色
|
||||
3. 使用 admin 账号(ID=1)测试,admin 自动跳过权限校验
|
||||
|
||||
```python
|
||||
# 权限标识必须完全一致
|
||||
permission_required = ("sys:example:page",) # 代码中
|
||||
sys:example:page # 菜单管理中
|
||||
```
|
||||
|
||||
### Q3: 接口返回 `{"code": 1, "msg": "演示环境,暂无操作权限"}`
|
||||
|
||||
**原因**:`.env` 中设置了 `DJANGO_DEMO=True`。
|
||||
|
||||
**解决**:将 `.env` 中的 `DJANGO_DEMO` 设为 `False`:
|
||||
|
||||
```bash
|
||||
DJANGO_DEMO=False
|
||||
```
|
||||
|
||||
### Q4: 分页查询返回空数据
|
||||
|
||||
**原因**:分页参数名不匹配或字段配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 前端传参名应为 `pageNo` 和 `pageSize`(驼峰)
|
||||
2. 检查数据库中是否有数据(`is_delete=0` 的记录)
|
||||
3. 检查筛选条件是否正确
|
||||
|
||||
```python
|
||||
# 正确的参数获取方式
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
```
|
||||
|
||||
### Q5: 表单验证错误信息不显示中文
|
||||
|
||||
**原因**:`error_messages` 配置不完整。
|
||||
|
||||
**解决**:确保每个验证规则都配置了中文错误提示:
|
||||
|
||||
```python
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空',
|
||||
'max_length': '案例名称长度不得超过100个字符',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Q6: 删除操作不生效
|
||||
|
||||
**原因**:删除使用逻辑删除,查询时需要加 `is_delete=False` 条件。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认删除函数使用 `update(is_delete=True)` 而非 `delete()`
|
||||
2. 确认所有查询都加了 `is_delete=False` 条件
|
||||
3. 检查数据库中 `is_delete` 字段的值
|
||||
|
||||
```python
|
||||
# 正确的逻辑删除
|
||||
Example.objects.filter(id__in=id_list, is_delete=False).update(is_delete=True)
|
||||
|
||||
# 正确的查询
|
||||
Example.objects.filter(is_delete=False)
|
||||
```
|
||||
|
||||
### Q7: 文件上传后路径不正确
|
||||
|
||||
**原因**:`save_file()` 函数使用不正确。
|
||||
|
||||
**解决**:确保传入的是完整的文件 URL,并指定正确的目录名:
|
||||
|
||||
```python
|
||||
# 正确用法
|
||||
avatar = save_file(avatar_url, "example")
|
||||
|
||||
# avatar_url 应该是类似 http://xxx/uploads/temp/xxx.jpg 的完整URL
|
||||
```
|
||||
|
||||
### Q8: `get_username(request)` 返回空字符串
|
||||
|
||||
**原因**:JWT Token 中没有 `realname` 字段,或 Token 已过期。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认请求头包含 `Authorization: Bearer <token>`
|
||||
2. 确认 Token 未过期
|
||||
3. 确认用户信息中包含 `realname` 字段
|
||||
|
||||
## 前端问题
|
||||
|
||||
### Q9: 页面白屏,控制台报 `Failed to fetch dynamically imported module`
|
||||
|
||||
**原因**:路由组件路径配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认后端菜单的 `component` 字段值正确(如 `tool/example/index`)
|
||||
2. 确认前端文件路径与 `component` 对应(`ui/src/views/tool/example/index.vue`)
|
||||
3. 检查文件名大小写是否匹配
|
||||
|
||||
### Q10: 搜索功能不生效
|
||||
|
||||
**原因**:搜索表单的 `submit` 事件未正确绑定。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认 `BasicForm` 绑定了 `@submit="handleSubmit"` 事件
|
||||
2. 确认 `handleSubmit` 中更新了 `formParams` 并调用了 `reloadTable()`
|
||||
3. 确认后端 `_apply_filters` 函数正确处理了筛选参数
|
||||
|
||||
### Q11: 编辑弹窗不回填数据
|
||||
|
||||
**原因**:`setFormData` 未正确调用或 API 返回数据格式不对。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认 `props.id` 不为 0(编辑模式)
|
||||
2. 确认 `getExampleDetail` API 返回了正确的数据
|
||||
3. 确认 `formData` 的字段名与 API 返回的字段名一致
|
||||
|
||||
### Q12: 操作按钮不显示
|
||||
|
||||
**原因**:`v-perm` 权限标识与后端不一致。
|
||||
|
||||
**排查步骤**:
|
||||
|
||||
1. 确认 `v-perm` 中的权限字符串与后端 `permission_required` 完全一致
|
||||
2. 确认当前用户拥有对应权限
|
||||
3. 检查 `TableAction` 中的 `auth` 字段是否正确
|
||||
|
||||
```vue
|
||||
<!-- 权限标识必须与后端一致 -->
|
||||
<el-button v-perm="['sys:example:add']">添加案例</el-button>
|
||||
```
|
||||
|
||||
### Q13: 表格数据不刷新
|
||||
|
||||
**原因**:操作成功后未调用 `reloadTable()`。
|
||||
|
||||
**解决**:在弹窗的 `success` 事件中刷新表格:
|
||||
|
||||
```vue
|
||||
<editDialog @success="reloadTable('noRefresh')" />
|
||||
```
|
||||
|
||||
## 通用问题
|
||||
|
||||
### Q14: 接口返回 401 Unauthorized
|
||||
|
||||
**原因**:JWT Token 过期或未携带。
|
||||
|
||||
**解决**:
|
||||
|
||||
1. 确认请求头包含 `Authorization: Bearer <token>`
|
||||
2. Token 过期后需重新登录
|
||||
3. 前端 `http` 自动处理 Token 注入,检查是否正确引入
|
||||
|
||||
### Q15: 数据库字段中文乱码
|
||||
|
||||
**原因**:数据库或表的字符集不是 UTF-8。
|
||||
|
||||
**解决**:确保 MySQL 数据库和表使用 `utf8mb4` 字符集:
|
||||
|
||||
```sql
|
||||
ALTER DATABASE dbname CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE tablename CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
```
|
||||
|
||||
### Q16: `DJANGO_DEMO` 环境变量不生效
|
||||
|
||||
**原因**:`.env` 文件修改后未重启服务。
|
||||
|
||||
**解决**:修改 `.env` 后必须重启 Django 服务:
|
||||
|
||||
```bash
|
||||
# 停止服务后重新启动
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## 开发调试技巧
|
||||
|
||||
### 查看 SQL 日志
|
||||
|
||||
在 `settings.py` 中开启 SQL 调试:
|
||||
|
||||
```python
|
||||
LOGGING = {
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.db.backends': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 Django Admin 调试
|
||||
|
||||
访问 `http://127.0.0.1:8000/admin/`,可以直接在浏览器中查看和编辑数据库数据。
|
||||
|
||||
### 检查权限配置
|
||||
|
||||
如果不确定权限是否配置正确,可以用 admin 账号测试。admin(ID=1)自动跳过所有权限校验。
|
||||
|
||||
### 测试 API 接口
|
||||
|
||||
使用 curl 或 Postman 测试 API 接口:
|
||||
|
||||
```bash
|
||||
# 登录获取 Token
|
||||
curl -X POST http://127.0.0.1:8000/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "123456"}'
|
||||
|
||||
# 使用 Token 访问接口
|
||||
curl -X GET http://127.0.0.1:8000/example/page?pageNo=1&pageSize=10 \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
模块开发常见问题主要集中在:数据库迁移未执行、权限节点未配置、软删除过滤缺失、分页参数命名不一致、前端组件路径配置错误等。遇到问题时优先检查后端日志和网络请求,确认接口入参和返回值是否符合预期。
|
||||
@@ -0,0 +1,191 @@
|
||||
# 前端 API 层
|
||||
|
||||
前端 API 层负责封装后端接口的 HTTP 请求。每个业务模块对应一个 API 文件,使用项目封装的 Axios 工具发送请求。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
ui/src/api/tool/example.ts
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* @description: 案例分页列表
|
||||
*/
|
||||
export function getExamplePage(params?) {
|
||||
return http.request({
|
||||
url: '/example/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部案例列表
|
||||
* @param params 参数
|
||||
* @returns 返回结果
|
||||
*/
|
||||
export function getExampleList(params?) {
|
||||
return http.request({
|
||||
url: '/example/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 根据ID获取案例详情
|
||||
* @param id 案例ID
|
||||
*/
|
||||
export function getExampleDetail(id) {
|
||||
return http.request({
|
||||
url: '/example/detail/' + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 添加案例
|
||||
* @param data 案例数据
|
||||
*/
|
||||
export function exampleAdd(data: any) {
|
||||
return http.request({
|
||||
url: '/example/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 更新案例
|
||||
* @param data 案例数据
|
||||
*/
|
||||
export function exampleUpdate(data: any) {
|
||||
return http.request({
|
||||
url: '/example/update',
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 删除案例
|
||||
* @param id 案例ID
|
||||
*/
|
||||
export function exampleDelete(id) {
|
||||
return http.request({
|
||||
url: '/example/delete/' + id,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 批量删除案例
|
||||
* @param data 案例ID数组
|
||||
*/
|
||||
export function exampleBatchDelete(data: any) {
|
||||
return http.request({
|
||||
url: '/example/batchDelete',
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### HTTP 客户端
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
```
|
||||
|
||||
`http` 是项目封装的 Axios 实例,自动处理:
|
||||
|
||||
- **请求拦截**:添加 JWT Token(`Authorization: Bearer xxx`)
|
||||
- **响应拦截**:统一处理错误码、弹出提示
|
||||
- **基础路径**:自动拼接 `/api` 前缀(通过 Vite 代理转发到后端)
|
||||
|
||||
### 请求方式
|
||||
|
||||
```typescript
|
||||
http.request({
|
||||
url: '/example/page', // 接口路径(不含 /api 前缀)
|
||||
method: 'GET', // HTTP 方法
|
||||
params, // GET 参数(拼接到 URL 查询字符串)
|
||||
})
|
||||
```
|
||||
|
||||
| 参数 | 用途 |
|
||||
|:----|:----|
|
||||
| `url` | 接口路径,与后端 urls.py 定义的 path 对应 |
|
||||
| `method` | HTTP 方法:GET / POST / PUT / DELETE |
|
||||
| `params` | GET 请求参数(拼接到 URL) |
|
||||
| `data` | POST/PUT/DELETE 请求体(JSON) |
|
||||
|
||||
### 函数命名规范
|
||||
|
||||
| 模式 | 示例 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `get{Module}Page` | `getExamplePage` | 分页列表查询 |
|
||||
| `get{Module}List` | `getExampleList` | 全量列表查询 |
|
||||
| `get{Module}Detail` | `getExampleDetail` | 详情查询 |
|
||||
| `{module}Add` | `exampleAdd` | 新增 |
|
||||
| `{module}Update` | `exampleUpdate` | 更新 |
|
||||
| `{module}Delete` | `exampleDelete` | 单条删除 |
|
||||
| `{module}BatchDelete` | `exampleBatchDelete` | 批量删除 |
|
||||
|
||||
### 参数传递区别
|
||||
|
||||
```typescript
|
||||
// GET:参数通过 params 传递(拼接到 URL 查询字符串)
|
||||
http.request({ url: '/example/page', method: 'GET', params })
|
||||
|
||||
// POST/PUT/DELETE:参数通过 data 传递(放入请求体 JSON)
|
||||
http.request({ url: '/example/add', method: 'POST', data })
|
||||
|
||||
// 路径参数:直接拼接到 URL
|
||||
http.request({ url: '/example/delete/' + id, method: 'DELETE' })
|
||||
```
|
||||
|
||||
## 页面中的调用方式
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
|
||||
// 分页查询
|
||||
const result = await getExamplePage({ pageNo: 1, pageSize: 10, name: '测试' });
|
||||
|
||||
// 详情查询
|
||||
const detail = await getExampleDetail(1);
|
||||
|
||||
// 新增
|
||||
await exampleAdd({ name: '新案例', type: 1, status: 1, sort: 0 });
|
||||
|
||||
// 更新
|
||||
await exampleUpdate({ id: 1, name: '修改后', type: 1, status: 1, sort: 1 });
|
||||
|
||||
// 单条删除
|
||||
await exampleDelete(1);
|
||||
|
||||
// 批量删除
|
||||
await exampleBatchDelete([1, 2, 3]);
|
||||
</script>
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **一个模块一个文件**:放在 `ui/src/api/{group}/` 目录下
|
||||
2. **URL 不含 `/api` 前缀**:Vite 代理自动拼接
|
||||
3. **GET 用 `params`,POST/PUT/DELETE 用 `data`**:参数传递方式不同
|
||||
4. **路径参数直接拼接到 URL**:如 `/example/delete/` + id
|
||||
5. **批量删除的参数是数组**:`data: [1, 2, 3]`
|
||||
|
||||
## 总结
|
||||
|
||||
前端 API 层使用封装后的 Axios 发送请求,URL 使用相对路径(不含 `/api` 前缀),函数命名采用 `get{Module}Page` / `{module}Add` 格式。GET 请求用 `params`,POST/PUT/DELETE 用 `data`,响应拦截器自动处理错误。
|
||||
@@ -0,0 +1,149 @@
|
||||
# 前端路由与菜单
|
||||
|
||||
前端路由定义页面的访问路径,菜单是路由的可视化展示。项目的路由和菜单由后端动态返回,前端只需注册路由组件映射。
|
||||
|
||||
## 动态路由机制
|
||||
|
||||
项目的路由和菜单是动态的,由后端驱动:
|
||||
|
||||
```
|
||||
登录成功 → 请求 /index/getMenus → 生成动态路由 → 注册到 Vue Router → 渲染菜单
|
||||
```
|
||||
|
||||
### 路由生成流程
|
||||
|
||||
1. 用户登录成功,获取 JWT Token
|
||||
2. 前端路由守卫(`router-guards.ts`)检测到已登录
|
||||
3. 调用 `GET /index/getMenus` 获取菜单树
|
||||
4. 将菜单树转换为 Vue Router 路由记录
|
||||
5. 使用 `router.addRoute()` 动态注册路由
|
||||
6. 渲染侧边栏菜单
|
||||
|
||||
### 关键代码
|
||||
|
||||
```typescript
|
||||
// src/router/generator-routers.ts
|
||||
// 将后端菜单转换为前端路由
|
||||
function generateRoutes(menus) {
|
||||
const routes = [];
|
||||
for (const menu of menus) {
|
||||
const route = {
|
||||
path: menu.path,
|
||||
name: menu.name,
|
||||
component: loadComponent(menu.component), // 动态加载组件
|
||||
meta: {
|
||||
title: menu.name,
|
||||
icon: menu.icon,
|
||||
},
|
||||
};
|
||||
if (menu.children) {
|
||||
route.children = generateRoutes(menu.children);
|
||||
}
|
||||
routes.push(route);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
```
|
||||
|
||||
## 菜单 component 字段
|
||||
|
||||
后端菜单的 `component` 字段必须与前端 `.vue` 文件路径对应:
|
||||
|
||||
| 菜单 component | 前端文件路径 |
|
||||
|:----|:----|
|
||||
| `tool/example/index` | `ui/src/views/tool/example/index.vue` |
|
||||
| `system/user/index` | `ui/src/views/system/user/index.vue` |
|
||||
| `content/article/index` | `ui/src/views/content/article/index.vue` |
|
||||
|
||||
### 组件加载方式
|
||||
|
||||
前端使用 `import.meta.glob` 自动扫描所有 `.vue` 文件:
|
||||
|
||||
```typescript
|
||||
// 扫描 src/views/ 下所有 .vue 文件
|
||||
const modules = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
function loadComponent(component) {
|
||||
const path = `../views/${component}.vue`;
|
||||
return modules[path];
|
||||
}
|
||||
```
|
||||
|
||||
## 后端菜单配置
|
||||
|
||||
在「系统管理 -> 菜单管理」中添加菜单节点:
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 菜单名称 | 案例管理 | 菜单显示名称 |
|
||||
| 菜单类型 | 菜单(type=0) | 目录/菜单/权限 |
|
||||
| 路由路径 | `/tool/example` | 前端路由路径 |
|
||||
| 组件路径 | `tool/example/index` | 对应 .vue 文件路径 |
|
||||
| 菜单图标 | 选择合适图标 | 菜单图标 |
|
||||
| 排序号 | 设置显示顺序 | 菜单排序 |
|
||||
| 状态 | 启用 | 是否启用 |
|
||||
|
||||
### 菜单数据返回格式
|
||||
|
||||
后端接口 `/index/getMenus` 返回的菜单数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 10,
|
||||
"name": "系统工具",
|
||||
"parent_id": 0,
|
||||
"path": "/tool",
|
||||
"component": "Layout",
|
||||
"type": 0,
|
||||
"icon": "tool",
|
||||
"children": [
|
||||
{
|
||||
"id": 101,
|
||||
"name": "案例管理",
|
||||
"parent_id": 10,
|
||||
"path": "/tool/example",
|
||||
"component": "tool/example/index",
|
||||
"type": 0,
|
||||
"icon": "example"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 菜单与权限的关系
|
||||
|
||||
```
|
||||
菜单管理
|
||||
├── 系统工具(目录,type=0)
|
||||
│ ├── 案例管理(菜单,type=0)
|
||||
│ │ ├── 案例查询(权限,type=1)→ sys:example:page
|
||||
│ │ ├── 案例新增(权限,type=1)→ sys:example:add
|
||||
│ │ ├── 案例编辑(权限,type=1)→ sys:example:update
|
||||
│ │ ├── 案例删除(权限,type=1)→ sys:example:delete
|
||||
│ │ └── 案例状态(权限,type=1)→ sys:example:status
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
| 节点类型 | type | 作用 |
|
||||
|:----|:----|:----|
|
||||
| 目录 | 0 | 菜单分组,不对应页面 |
|
||||
| 菜单 | 0 | 对应一个页面路由 |
|
||||
| 权限 | 1 | 对应一个操作权限,不在菜单中显示 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **路由和菜单由后端动态返回**:前端不需要手动编辑路由文件
|
||||
2. **component 字段必须与 .vue 文件路径对应**:否则页面白屏
|
||||
3. **使用 `import.meta.glob` 自动扫描组件**:无需手动注册
|
||||
4. **菜单类型区分目录和权限**:目录用于分组,权限用于按钮控制
|
||||
5. **新增模块只需在菜单管理中配置**:自动出现在前端菜单中
|
||||
|
||||
## 总结
|
||||
|
||||
前端路由使用 Vue Router 4,菜单路由由后端动态返回,前端通过 `addRoute` 动态注册。路由守卫负责权限校验和登录状态检查,未登录自动跳转登录页。新增模块只需在菜单管理中配置,无需编辑前端路由文件。
|
||||
@@ -0,0 +1,430 @@
|
||||
# 前端页面视图
|
||||
|
||||
前端页面视图是用户直接交互的界面。项目使用 Vue3 + ElementPlus + Vite 构建。一个完整的 CRUD 页面由 5 个文件组成:
|
||||
|
||||
```
|
||||
ui/src/views/tool/example/
|
||||
├── index.vue # 主页面:搜索 + 表格 + 操作
|
||||
├── edit.vue # 编辑弹窗:新增/编辑表单
|
||||
├── detail.vue # 详情弹窗:查看记录详情
|
||||
├── columns.ts # 表格列定义
|
||||
└── querySchemas.ts # 搜索表单 Schema
|
||||
```
|
||||
|
||||
## 1. 主页面 index.vue
|
||||
|
||||
主页面是模块的入口页面,包含搜索表单、数据表格和操作按钮。
|
||||
|
||||
### 核心结构
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<!-- 搜索表单卡片 -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格卡片 -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
|
||||
添加案例
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length">
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<editDialog v-if="editVisible" :id="currentId" v-model:visible="editVisible" @success="reloadTable('noRefresh')" />
|
||||
<!-- 详情对话框 -->
|
||||
<detailDialog v-if="detailVisible" :id="currentId" v-model:visible="detailVisible" @success="reloadTable('noRefresh')" />
|
||||
</PageWrapper>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 核心逻辑
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
import { columns } from './columns';
|
||||
|
||||
// 动态导入弹窗组件
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
|
||||
|
||||
// 响应式状态
|
||||
const currentId = ref(0);
|
||||
const editVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const selectionData = ref([]);
|
||||
const tableRef = ref();
|
||||
|
||||
// 查询参数
|
||||
const formParams = reactive({ name: '', type: '', status: '' });
|
||||
|
||||
// 操作栏配置
|
||||
const actionColumn = reactive({
|
||||
width: 300,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{ label: '编辑', icon: 'Edit', type: 'warning', onClick: handleEdit.bind(null, record), auth: ['sys:example:update'] },
|
||||
{ label: '详情', icon: 'View', type: '', onClick: handleDetail.bind(null, record), auth: ['sys:example:detail'] },
|
||||
{ label: '删除', icon: 'Delete', type: 'danger', onClick: handleDelete.bind(null, record), auth: ['sys:example:delete'] },
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadDataTable = async (params: any) => {
|
||||
return await getExamplePage({ ...formParams, ...params });
|
||||
};
|
||||
|
||||
// 注册搜索表单
|
||||
const [register, {}] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 },
|
||||
submitOnReset: true,
|
||||
schemas,
|
||||
});
|
||||
|
||||
// 提交搜索
|
||||
function handleSubmit(values: Recordable) {
|
||||
handleReset();
|
||||
for (const key in values) { formParams[key] = values[key]; }
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
function handleReset() {
|
||||
for (const key in formParams) { formParams[key] = ''; }
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = async () => { currentId.value = 0; await nextTick(); editVisible.value = true; };
|
||||
|
||||
// 编辑
|
||||
const handleEdit = async (record: Recordable) => { currentId.value = record.row.id; await nextTick(); editVisible.value = true; };
|
||||
|
||||
// 详情
|
||||
const handleDetail = async (record: Recordable) => { currentId.value = record.row.id; await nextTick(); detailVisible.value = true; };
|
||||
|
||||
// 删除
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
if (!ids.length) { message('请选择要删除的数据', 'warning'); return; }
|
||||
}
|
||||
await confirm('确定要删除选中的案例吗?');
|
||||
record ? await exampleDelete(record.row.id) : await exampleBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **PageWrapper**:页面容器,提供统一的页面布局
|
||||
- **BasicForm**:搜索表单组件,通过 `useForm` 注册
|
||||
- **BasicTable**:数据表格组件,内置分页功能
|
||||
- **TableAction**:操作栏组件,支持权限控制
|
||||
- **defineAsyncComponent**:懒加载弹窗组件,优化首屏加载
|
||||
- **v-perm**:权限指令,控制按钮显示
|
||||
|
||||
## 2. 编辑弹窗 edit.vue
|
||||
|
||||
编辑弹窗是独立组件,负责新增和编辑操作的表单展示与提交。
|
||||
|
||||
### 核心结构
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.visible"
|
||||
:title="props.id ? '编辑案例' : '新增案例'"
|
||||
width="500"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="dialogClose"
|
||||
>
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
<el-form-item label="案例名称" prop="name" :rules="{ required: true, message: '请输入案例名称', trigger: 'blur' }">
|
||||
<el-input v-model="formData.name" placeholder="请输入案例名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="案例图片" prop="avatar">
|
||||
<UploadImg v-model:image-url="formData.avatar" name="example" />
|
||||
</el-form-item>
|
||||
<el-form-item label="案例类型" prop="type" :rules="{ required: true, message: '请选择案例类型', trigger: 'change' }">
|
||||
<el-select v-model="formData.type" placeholder="请选择" clearable filterable>
|
||||
<el-option v-for="item in optionsData.typeList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="案例状态" prop="status" :rules="{ required: true, message: '请选择状态', trigger: 'change' }">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="2">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort" :rules="{ required: true, message: '请输入排序', trigger: 'blur' }">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="99999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogClose">取消</el-button>
|
||||
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 核心逻辑
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { getExampleDetail, exampleAdd, exampleUpdate } from '@/api/tool/example';
|
||||
import { onMounted, reactive, shallowRef, watch } from 'vue';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
const formRef = shallowRef<FormInstance>();
|
||||
|
||||
const formData = reactive({
|
||||
id: '', name: '', avatar: '', type: 0, status: 0, sort: 0,
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, required: true, default: false },
|
||||
id: { type: Number, required: true, default: 0 },
|
||||
});
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
props.id ? await exampleUpdate(formData) : await exampleAdd(formData);
|
||||
message('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
|
||||
// 防重复提交
|
||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||||
|
||||
// 编辑时加载数据
|
||||
const setFormData = async () => {
|
||||
const data = await getExampleDetail(props.id);
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) { formData[key] = data[key]; }
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.id) { setFormData(); } else { resetForm(); }
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **Props 接收**:`visible` 控制弹窗显示,`id` 区分新增/编辑模式
|
||||
- **新增/编辑判断**:`props.id ? exampleUpdate : exampleAdd`
|
||||
- **防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态
|
||||
- **数据回填**:编辑模式下调用 `getExampleDetail` 获取数据并填充表单
|
||||
- **事件通信**:`emit('update:visible')` 关闭弹窗,`emit('success')` 通知刷新
|
||||
|
||||
## 3. 详情弹窗 detail.vue
|
||||
|
||||
详情弹窗用于展示记录的详细信息,使用 ElementPlus 的 `el-descriptions` 组件。
|
||||
|
||||
### 核心结构
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog v-model="props.visible" title="案例详情" width="750" :close-on-click-modal="false">
|
||||
<el-descriptions column="2" border>
|
||||
<el-descriptions-item label="案例名称:">{{ formData.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="案例图片:">
|
||||
<el-image v-if="formData.avatar" :src="formData.avatar" :preview-src-list="[formData.avatar]" fit="cover" style="width: 80px; height: 80px" />
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="案例类型:">{{ getChoiceLabel('type', formData.type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="案例状态:">{{ getChoiceLabel('status', formData.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序:">{{ formData.sort || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="dialogClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **el-descriptions**:ElementPlus 的描述列表组件,用于展示详情
|
||||
- **getChoiceLabel**:将枚举值转换为中文标签
|
||||
- **图片预览**:使用 `el-image` 的 `preview-src-list` 实现图片预览
|
||||
|
||||
## 4. 表格列定义 columns.ts
|
||||
|
||||
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```typescript
|
||||
import { h } from 'vue';
|
||||
import { ElAvatar, ElTag } from 'element-plus';
|
||||
|
||||
export const columns = [
|
||||
{ type: 'selection' },
|
||||
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
|
||||
{ label: '案例名称', prop: 'name', minWidth: 120 },
|
||||
{
|
||||
label: '案例图片', prop: 'avatar', minWidth: 120,
|
||||
render(record) {
|
||||
return h(ElAvatar, { size: 48, src: record.row.avatar, shape: 'square', fit: 'fill' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '案例类型', prop: 'type', minWidth: 100,
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
1: { text: '类型1', type: 'success' },
|
||||
2: { text: '类型2', type: 'danger' },
|
||||
3: { text: '类型3', type: 'warning' },
|
||||
4: { text: '类型4', type: 'info' },
|
||||
};
|
||||
const status = statusMap[record.row.type];
|
||||
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.type });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '案例状态', prop: 'status', minWidth: 100,
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
1: { text: '正常', type: 'success' },
|
||||
2: { text: '禁用', type: 'danger' },
|
||||
};
|
||||
const status = statusMap[record.row.status];
|
||||
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.status });
|
||||
},
|
||||
},
|
||||
{ label: '排序', prop: 'sort', minWidth: 100 },
|
||||
{ label: '创建人', prop: 'createUser', minWidth: 100 },
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
];
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **type: 'selection'**:自动添加复选框列
|
||||
- **render 函数**:使用 Vue 的 `h()` 函数创建虚拟 DOM
|
||||
- **ElTag 组件**:将状态值渲染为彩色标签
|
||||
- **ElAvatar 组件**:将图片 URL 渲染为缩略图
|
||||
|
||||
## 5. 搜索表单 querySchemas.ts
|
||||
|
||||
定义搜索表单的字段配置,使用项目封装的 `FormSchema` 类型。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```typescript
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
label: '案例名称',
|
||||
componentProps: { placeholder: '请输入案例名称', clearable: true },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
component: 'Select',
|
||||
label: '案例类型',
|
||||
componentProps: {
|
||||
placeholder: '请输入案例类型',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '类型1', value: 1 },
|
||||
{ label: '类型2', value: 2 },
|
||||
{ label: '类型3', value: 3 },
|
||||
{ label: '类型4', value: 4 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'Select',
|
||||
label: '案例状态',
|
||||
componentProps: {
|
||||
placeholder: '请输入案例状态',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **field**:字段名,与后端查询参数对应
|
||||
- **component**:表单组件类型:`Input` / `Select` / `DatePicker` 等
|
||||
- **componentProps**:组件属性,透传给 ElementPlus 组件
|
||||
- **options**:Select 组件的下拉选项
|
||||
|
||||
## 文件关系图
|
||||
|
||||
```
|
||||
index.vue(主页面)
|
||||
├── import schemas from './querySchemas' ← 搜索表单配置
|
||||
├── import columns from './columns' ← 表格列配置
|
||||
├── import editDialog from './edit.vue' ← 编辑弹窗组件
|
||||
├── import detailDialog from './detail.vue' ← 详情弹窗组件
|
||||
└── import API from '@/api/tool/example' ← 接口请求
|
||||
|
||||
edit.vue(编辑弹窗)
|
||||
└── import API from '@/api/tool/example' ← 接口请求
|
||||
|
||||
detail.vue(详情弹窗)
|
||||
└── import API from '@/api/tool/example' ← 接口请求
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
|
||||
2. **5 个文件各司其职**:主页面、编辑弹窗、详情弹窗、列定义、搜索 Schema
|
||||
3. **弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面
|
||||
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、图片缩略图
|
||||
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段
|
||||
6. **新增/编辑通过 `id` 区分**:0 为新增,非 0 为编辑
|
||||
7. **防重复提交使用 `useLockFn`**:提交时按钮 loading
|
||||
|
||||
## 总结
|
||||
|
||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`detail.vue`(详情弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)五个文件。
|
||||
@@ -0,0 +1,243 @@
|
||||
# 表单验证(Form)
|
||||
|
||||
表单验证是模块开发的第二步,用于对前端提交的数据进行验证。项目使用 Django 的 `ModelForm` 实现表单验证,所有错误信息使用中文提示。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
application/example/forms.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
|
||||
from application.example import models
|
||||
|
||||
|
||||
class ExampleForm(forms.ModelForm):
|
||||
"""
|
||||
案例表单验证类
|
||||
|
||||
用于创建和更新案例信息时的表单验证。
|
||||
验证字段包括:name, avatar, type, status, sort
|
||||
"""
|
||||
|
||||
# 案例名称
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空',
|
||||
'max_length': '案例名称长度不得超过100个字符',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例图片
|
||||
avatar = forms.CharField(
|
||||
required=False,
|
||||
max_length=255,
|
||||
error_messages={
|
||||
'max_length': '案例图片长度不得超过255个字符',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例类型
|
||||
type = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
error_messages={
|
||||
'required': '案例类型不能为空',
|
||||
'min_value': '案例类型值不能小于1',
|
||||
'max_value': '案例类型值不能大于4',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例状态
|
||||
status = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=2,
|
||||
error_messages={
|
||||
'required': '案例状态不能为空',
|
||||
'min_value': '案例状态值不能小于1',
|
||||
'max_value': '案例状态值不能大于2',
|
||||
}
|
||||
)
|
||||
|
||||
# 排序
|
||||
sort = forms.IntegerField(
|
||||
required=True,
|
||||
error_messages={
|
||||
'required': '排序不能为空',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Example
|
||||
fields = [
|
||||
'name',
|
||||
'avatar',
|
||||
'type',
|
||||
'status',
|
||||
'sort',
|
||||
]
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### Form 类继承
|
||||
|
||||
```python
|
||||
class ExampleForm(forms.ModelForm):
|
||||
```
|
||||
|
||||
| 基类 | 来源 | 提供能力 |
|
||||
|:----|:----|:----|
|
||||
| `forms.ModelForm` | Django 内置 | 自动绑定模型、字段验证、cleaned_data |
|
||||
|
||||
`ModelForm` 会根据绑定的模型自动生成表单字段,但项目中通常手动重新定义每个字段以设置中文错误提示。
|
||||
|
||||
### 字段定义
|
||||
|
||||
#### 字符串字段
|
||||
|
||||
```python
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空',
|
||||
'max_length': '案例名称长度不得超过100个字符',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `required=True` | 必填字段 |
|
||||
| `max_length=100` | 最大长度 100 字符 |
|
||||
| `error_messages` | 自定义错误提示(中文) |
|
||||
|
||||
#### 整数字段
|
||||
|
||||
```python
|
||||
type = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
error_messages={
|
||||
'required': '案例类型不能为空',
|
||||
'min_value': '案例类型值不能小于1',
|
||||
'max_value': '案例类型值不能大于4',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `required=True` | 必填字段 |
|
||||
| `min_value=1` | 最小值 |
|
||||
| `max_value=4` | 最大值 |
|
||||
| `error_messages` | 自定义错误提示(中文) |
|
||||
|
||||
#### 可选字段
|
||||
|
||||
```python
|
||||
avatar = forms.CharField(
|
||||
required=False,
|
||||
max_length=255,
|
||||
error_messages={
|
||||
'max_length': '案例图片长度不得超过255个字符',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
`required=False` 表示该字段为可选字段,不填时不会报错。
|
||||
|
||||
### Meta 类配置
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
model = models.Example
|
||||
fields = ['name', 'avatar', 'type', 'status', 'sort']
|
||||
```
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|:----|:----|
|
||||
| `model` | 绑定的数据模型 |
|
||||
| `fields` | 需要验证的字段列表 |
|
||||
|
||||
## 错误信息提取
|
||||
|
||||
表单验证失败时,使用 `utils/regular.py` 中的 `get_err()` 函数提取错误信息:
|
||||
|
||||
```python
|
||||
from utils import regular
|
||||
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
```
|
||||
|
||||
`get_err()` 函数的实现:
|
||||
|
||||
```python
|
||||
def get_err(form):
|
||||
"""获取表单错误文本,多个错误用'/'分隔"""
|
||||
error_list = []
|
||||
for item in form.errors.get_json_data().values():
|
||||
error_list.append(item[0].get('message'))
|
||||
err_str = '/'.join(error_list)
|
||||
return err_str
|
||||
```
|
||||
|
||||
例如,如果 `name` 和 `type` 都验证失败,返回的错误信息为:
|
||||
|
||||
```
|
||||
案例名称不能为空/案例类型不能为空
|
||||
```
|
||||
|
||||
## 在 Service 中使用
|
||||
|
||||
表单验证通常在 Service 层的添加和更新函数中调用:
|
||||
|
||||
```python
|
||||
def add_example(request):
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 表单验证
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
# 获取验证后的数据
|
||||
cleaned_data = form.cleaned_data
|
||||
# ... 创建记录
|
||||
```
|
||||
|
||||
## 常用验证规则
|
||||
|
||||
| 字段类型 | 验证参数 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| CharField | `required`, `max_length`, `min_length` | 字符串长度 |
|
||||
| IntegerField | `required`, `min_value`, `max_value` | 整数范围 |
|
||||
| EmailField | `required` | 邮箱格式 |
|
||||
| URLField | `required` | URL 格式 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **所有错误信息必须使用中文**:便于前端直接展示给用户
|
||||
2. **error_messages 覆盖所有验证规则**:required、max_length、min_value 等
|
||||
3. **可选字段设置 `required=False`**:避免不必要的验证错误
|
||||
4. **Meta.fields 列出所有需要验证的字段**:未列出的字段不进行验证
|
||||
5. **使用 `regular.get_err()` 提取错误信息**:统一的错误信息格式
|
||||
|
||||
## 总结
|
||||
|
||||
表单验证使用 Django ModelForm,手动重新定义每个字段以设置中文错误提示。Meta 类绑定数据模型并指定需要验证的字段。验证失败时使用 `regular.get_err()` 提取错误信息,多个错误用 `/` 分隔。
|
||||
@@ -0,0 +1,287 @@
|
||||
# 数据模型(Model)
|
||||
|
||||
数据模型是模块开发的第一步,用于定义数据库表结构。项目使用 Django ORM,模型类通过继承 `BaseModel` 自动获得通用字段。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
application/example/models.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
from application.models import BaseModel
|
||||
from utils.common import get_table_name
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
"""
|
||||
案例模型类
|
||||
|
||||
用于存储系统中的案例信息。
|
||||
继承自 BaseModel,自动包含:
|
||||
- id: 主键
|
||||
- is_delete: 逻辑删除标识
|
||||
- create_user: 创建人
|
||||
- create_time: 创建时间
|
||||
- update_user: 更新人
|
||||
- update_time: 更新时间
|
||||
"""
|
||||
|
||||
# 案例名称
|
||||
name = models.CharField(
|
||||
null=False,
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
verbose_name="案例名称",
|
||||
help_text="案例名称",
|
||||
db_comment='案例名称'
|
||||
)
|
||||
|
||||
# 案例图片
|
||||
avatar = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=255,
|
||||
verbose_name="案例图片",
|
||||
help_text="案例图片",
|
||||
db_comment='案例图片'
|
||||
)
|
||||
|
||||
# 案例类型
|
||||
TYPE_CHOICES = (
|
||||
(1, "类型1"),
|
||||
(2, "类型2"),
|
||||
(3, "类型3"),
|
||||
(4, "类型4"),
|
||||
)
|
||||
|
||||
type = models.IntegerField(
|
||||
null=False,
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
help_text="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
db_comment='案例类型:1-类型1 2-类型2 3-类型3 4-类型4'
|
||||
)
|
||||
|
||||
# 案例状态
|
||||
STATUS_CHOICES = (
|
||||
(1, "正常"),
|
||||
(2, "禁用"),
|
||||
)
|
||||
|
||||
status = models.IntegerField(
|
||||
null=False,
|
||||
choices=STATUS_CHOICES,
|
||||
verbose_name="案例状态:1-正常 2-禁用",
|
||||
help_text="案例状态:1-正常 2-禁用",
|
||||
db_comment='案例状态:1-正常 2-禁用'
|
||||
)
|
||||
|
||||
# 排序
|
||||
sort = models.IntegerField(
|
||||
null=False,
|
||||
verbose_name="排序",
|
||||
help_text="排序",
|
||||
db_comment='排序'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = get_table_name('example')
|
||||
db_table_comment = "案例表"
|
||||
verbose_name = "案例表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
def __str__(self):
|
||||
return f"案例{self.name}"
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类继承
|
||||
|
||||
```python
|
||||
class Example(BaseModel):
|
||||
```
|
||||
|
||||
`BaseModel`(定义在 `application/models.py`)是一个抽象模型类,为所有业务模型提供以下 6 个公共字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `id` | AutoField | 自增主键 |
|
||||
| `create_user` | CharField(50) | 创建人 |
|
||||
| `create_time` | DateTimeField | 创建时间(auto_now_add) |
|
||||
| `update_user` | CharField(50) | 更新人 |
|
||||
| `update_time` | DateTimeField | 更新时间(auto_now) |
|
||||
| `is_delete` | BooleanField | 逻辑删除标识(0=正常,1=已删除) |
|
||||
|
||||
`BaseModel` 定义为 `abstract = True`,不会在数据库中创建对应的表。子类模型会继承这些字段,并在各自对应的表中创建这些列。
|
||||
|
||||
### 表名配置
|
||||
|
||||
```python
|
||||
db_table = get_table_name('example')
|
||||
```
|
||||
|
||||
`get_table_name()` 函数(来自 `utils/common.py`)会自动拼接数据库表前缀:
|
||||
|
||||
```python
|
||||
def get_table_name(base_name):
|
||||
prefix = getattr(settings, 'DATABASE_PREFIX', '')
|
||||
return f"{prefix}{base_name}"
|
||||
```
|
||||
|
||||
最终表名为 `django_example`(假设前缀为 `django_`)。所有业务表都使用此前缀,便于区分框架表与业务表。
|
||||
|
||||
### 字段定义
|
||||
|
||||
#### 字符串字段
|
||||
|
||||
```python
|
||||
name = models.CharField(
|
||||
null=False,
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
verbose_name="案例名称",
|
||||
help_text="案例名称",
|
||||
db_comment='案例名称'
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `null=False` | 数据库不允许为 NULL |
|
||||
| `max_length=100` | 最大长度 100 字符 |
|
||||
| `db_index=True` | 创建索引,加速查询 |
|
||||
| `verbose_name` | Django Admin 显示名称 |
|
||||
| `help_text` | 帮助文本 |
|
||||
| `db_comment` | 数据库字段注释 |
|
||||
|
||||
#### 枚举字段
|
||||
|
||||
```python
|
||||
TYPE_CHOICES = (
|
||||
(1, "类型1"),
|
||||
(2, "类型2"),
|
||||
(3, "类型3"),
|
||||
(4, "类型4"),
|
||||
)
|
||||
|
||||
type = models.IntegerField(
|
||||
null=False,
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
help_text="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
db_comment='案例类型:1-类型1 2-类型2 3-类型3 4-类型4'
|
||||
)
|
||||
```
|
||||
|
||||
`choices` 参数定义了字段的可选值,在 Django Admin 中会显示为下拉选择框。
|
||||
|
||||
#### 可选字段
|
||||
|
||||
```python
|
||||
avatar = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=255,
|
||||
verbose_name="案例图片",
|
||||
help_text="案例图片",
|
||||
db_comment='案例图片'
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `null=True` | 数据库允许为 NULL |
|
||||
| `blank=True` | 表单验证允许为空 |
|
||||
|
||||
### Meta 类配置
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
db_table = get_table_name('example') # 数据表名
|
||||
db_table_comment = "案例表" # 表注释
|
||||
verbose_name = "案例表" # Admin 显示名称(单数)
|
||||
verbose_name_plural = verbose_name # Admin 显示名称(复数)
|
||||
ordering = ("sort",) # 默认排序
|
||||
```
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|:----|:----|
|
||||
| `db_table` | 数据库表名,使用 `get_table_name()` 自动加前缀 |
|
||||
| `db_table_comment` | 表注释,在数据库中生成 COMMENT 语句 |
|
||||
| `verbose_name` | Django Admin 中显示的友好名称 |
|
||||
| `ordering` | 默认排序规则,按 sort 字段升序排列 |
|
||||
|
||||
### 枚举值约定
|
||||
|
||||
项目中状态字段统一使用整数枚举:
|
||||
|
||||
| 值 | 含义 |
|
||||
|:----|:----|
|
||||
| 1 | 正常 / 启用 |
|
||||
| 2 | 禁用 / 停用 |
|
||||
|
||||
软删除字段 `is_delete` 由基类提供:`0` = 正常,`1` = 已删除。
|
||||
|
||||
## BaseModel 源码参考
|
||||
|
||||
```python
|
||||
class BaseModel(models.Model):
|
||||
"""基础模型抽象类"""
|
||||
|
||||
id = models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='主键ID',
|
||||
db_comment='主键ID'
|
||||
)
|
||||
|
||||
create_user = models.CharField(
|
||||
null=True, max_length=50, default=None,
|
||||
verbose_name='创建人', db_comment='创建人'
|
||||
)
|
||||
|
||||
create_time = models.DateTimeField(
|
||||
null=True, auto_now_add=True,
|
||||
verbose_name="创建时间", db_comment='创建时间'
|
||||
)
|
||||
|
||||
update_user = models.CharField(
|
||||
null=True, max_length=50, default=None,
|
||||
verbose_name='更新人', db_comment='更新人'
|
||||
)
|
||||
|
||||
update_time = models.DateTimeField(
|
||||
null=True, auto_now=True,
|
||||
verbose_name="更新时间", db_comment='更新时间'
|
||||
)
|
||||
|
||||
is_delete = models.BooleanField(
|
||||
default=0, db_default=0,
|
||||
verbose_name="逻辑删除",
|
||||
db_comment='逻辑删除:0-正常 1-已删除'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **业务字段仅定义差异部分**:通用字段(id、时间、软删除等)由基类 BaseModel 自动提供
|
||||
2. **表名必须使用 `get_table_name()` 加前缀**:保持命名一致性
|
||||
3. **合理使用索引**:查询频繁的字段(如 name、status)设置 `db_index=True`
|
||||
4. **每个字段都要设置 `db_comment`**:便于数据库管理和维护
|
||||
5. **枚举字段使用 `choices`**:在 Django Admin 中显示为下拉选择框
|
||||
6. **所有查询都要加 `is_delete=False` 条件**:过滤已删除的数据
|
||||
|
||||
## 总结
|
||||
|
||||
数据模型继承 `BaseModel`(公共字段),表名使用 `get_table_name()` 自动加前缀。查询频繁的字段设置索引,每个字段都要设置 `db_comment` 数据库注释。所有查询都必须加 `is_delete=False` 过滤已删除数据。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 模块开发实战概述
|
||||
|
||||
本章以「案例管理(Example)」模块为完整案例,手把手演示从零开发一个业务模块的全流程。学完本章后,你将掌握项目中任意模块的开发方式。
|
||||
|
||||
## 你将学到什么
|
||||
|
||||
通过本章的实战演练,你将完整经历以下开发环节:
|
||||
|
||||
| 步骤 | 文件 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 1. 数据模型 | `models.py` | 定义数据库表结构,继承 BaseModel |
|
||||
| 2. 表单验证 | `forms.py` | Django ModelForm 验证,中文错误提示 |
|
||||
| 3. 业务逻辑 | `services.py` | 实现增删改查、分页、状态管理 |
|
||||
| 4. 视图层 | `views.py` | 类视图,登录校验 + 权限控制 + 操作日志 |
|
||||
| 5. 路由注册 | `urls.py` | 模块路由 + 主路由注册 |
|
||||
| 6. 权限配置 | 菜单管理 | 配置权限节点 sys:module:action |
|
||||
| 7. 前端 API | `api/tool/example.ts` | 封装 HTTP 请求 |
|
||||
| 8. 前端页面 | `views/tool/example/` | 列表页 + 编辑弹窗 + 详情弹窗 |
|
||||
|
||||
## 案例模块:案例管理
|
||||
|
||||
选择「案例管理(Example)」作为案例的原因:
|
||||
|
||||
1. **结构典型**:包含完整的 CRUD + 状态管理 + 批量删除,覆盖大多数业务场景
|
||||
2. **字段丰富**:涵盖字符串、整数、图片、状态枚举、排序等常见字段类型
|
||||
3. **代码规范**:由代码生成器生成,遵循项目统一的开发规范
|
||||
4. **前后端完整**:后端 5 个文件 + 前端 5 个文件,总计约 1500 行代码
|
||||
|
||||
## 模块文件总览
|
||||
|
||||
```
|
||||
后端(application/example/)
|
||||
├── models.py # 数据模型(继承 BaseModel)
|
||||
├── forms.py # 表单验证(Django ModelForm)
|
||||
├── services.py # 业务逻辑层(CRUD 函数)
|
||||
├── views.py # 视图层(类视图 + 装饰器)
|
||||
├── urls.py # 路由配置
|
||||
├── apps.py # 应用配置
|
||||
├── admin.py # Django Admin 注册
|
||||
└── migrations/ # 数据库迁移文件
|
||||
|
||||
前端(ui/src/)
|
||||
├── api/tool/
|
||||
│ └── example.ts # API 请求封装
|
||||
└── views/tool/example/
|
||||
├── index.vue # 主页面:搜索 + 表格 + 操作
|
||||
├── edit.vue # 编辑弹窗:新增/编辑表单
|
||||
├── detail.vue # 详情弹窗:查看记录详情
|
||||
├── columns.ts # 表格列定义
|
||||
└── querySchemas.ts # 搜索表单 Schema
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
案例表 `django_example` 结构如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| id | int | 主键,自增 |
|
||||
| name | varchar(100) | 案例名称 |
|
||||
| avatar | varchar(255) | 案例图片 |
|
||||
| type | int | 案例类型:1-类型1 2-类型2 3-类型3 4-类型4 |
|
||||
| status | int | 案例状态:1-正常 2-禁用 |
|
||||
| sort | int | 排序权重 |
|
||||
| create_user | varchar(50) | 创建人 |
|
||||
| create_time | datetime | 创建时间 |
|
||||
| update_user | varchar(50) | 更新人 |
|
||||
| update_time | datetime | 更新时间 |
|
||||
| is_delete | tinyint | 软删除标记:0-正常 1-已删除 |
|
||||
|
||||
其中 `id`、`create_user`、`create_time`、`update_user`、`update_time`、`is_delete` 由基类 `BaseModel` 自动提供,业务字段仅需定义 `name`、`avatar`、`type`、`status`、`sort`。
|
||||
|
||||
## 开发顺序
|
||||
|
||||
建议按以下顺序逐步开发,每完成一步即可验证:
|
||||
|
||||
```
|
||||
1. models.py → 建表,确认数据库中有 django_example 表
|
||||
2. forms.py → 定义表单验证规则
|
||||
3. services.py → 业务逻辑层(CRUD 函数)
|
||||
4. views.py → API 视图(类视图 + 装饰器)
|
||||
5. urls.py → 注册路由
|
||||
6. 权限配置 → 在菜单管理中添加权限节点
|
||||
7. 前端 API → 封装 HTTP 请求
|
||||
8. 前端页面 → 列表页 + 编辑弹窗 + 详情弹窗
|
||||
```
|
||||
|
||||
本章每一步都附有完整代码和详细注释。建议对照项目中 `application/example/` 目录下的实际源码一起阅读,加深理解。
|
||||
|
||||
## 模块开发五步模式
|
||||
|
||||
模块开发遵循固定的五步模式:
|
||||
|
||||
```
|
||||
Model → Form → Service → View → URL
|
||||
```
|
||||
|
||||
| 步骤 | 文件 | 职责 |
|
||||
|:----|:----|:----|
|
||||
| Model | `models.py` | 定义数据模型,继承 BaseModel,配置表名和字段 |
|
||||
| Form | `forms.py` | 定义表单验证,绑定模型,设置中文错误提示 |
|
||||
| Service | `services.py` | 实现业务逻辑,所有 CRUD 操作封装为独立函数 |
|
||||
| View | `views.py` | 定义 HTTP 视图,组合登录校验、权限控制、操作日志 |
|
||||
| URL | `urls.py` | 注册路由,将 URL 路径映射到视图类 |
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
所有 API 接口使用 `utils/R.py` 提供的统一响应格式:
|
||||
|
||||
```python
|
||||
# 成功响应
|
||||
{"code": 0, "data": {...}, "msg": "操作成功", "ok": true}
|
||||
|
||||
# 失败响应
|
||||
{"code": 1, "data": null, "msg": "操作失败", "ok": false}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
模块开发实战以案例管理(Example)为例,完整演示从 Model → Form → Service → View → URL → 权限配置 → 前端 API → 前端页面的全流程。每一步都附有完整代码和详细注释,建议对照实际源码一起阅读。
|
||||
@@ -0,0 +1,148 @@
|
||||
# 权限节点配置
|
||||
|
||||
后端代码中的 `permission_required = ("sys:example:page",)` 定义了接口所需的权限字符串,但权限节点本身需要在数据库中配置,才能在「菜单管理」中分配给角色。
|
||||
|
||||
## 权限字符串格式
|
||||
|
||||
```
|
||||
sys:{module}:{action}
|
||||
```
|
||||
|
||||
| 部分 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `sys` | 系统前缀(固定) | `sys` |
|
||||
| `module` | 模块名(小写) | `example` |
|
||||
| `action` | 操作名 | `page` / `add` / `update` / `delete` |
|
||||
|
||||
## Example 模块的权限节点
|
||||
|
||||
| 权限字符串 | 说明 | HTTP 方法 | 路径 |
|
||||
|:----|:----|:----|:----|
|
||||
| `sys:example:page` | 分页查询 | GET | `/example/page` |
|
||||
| `sys:example:detail` | 详情查询 | GET | `/example/detail/{id}` |
|
||||
| `sys:example:list` | 列表查询 | GET | `/example/list` |
|
||||
| `sys:example:add` | 新增案例 | POST | `/example/add` |
|
||||
| `sys:example:update` | 更新案例 | PUT | `/example/update` |
|
||||
| `sys:example:delete` | 删除案例 | DELETE | `/example/delete/{id}` |
|
||||
| `sys:example:status` | 状态变更 | PUT | `/example/status` |
|
||||
|
||||
## 权限校验流程
|
||||
|
||||
```
|
||||
用户请求 → check_login(JWT 校验) → PermissionRequired(权限校验)
|
||||
├── 用户 ID = 1(admin)→ 跳过权限校验,直接放行
|
||||
└── 其他用户 → 查询角色权限列表 → 匹配权限字符串 → 放行 / 拒绝
|
||||
```
|
||||
|
||||
### PermissionRequired 源码
|
||||
|
||||
```python
|
||||
class PermissionRequired(PermissionRequiredMixin):
|
||||
def has_permission(self):
|
||||
permissions = self.get_permission_required()
|
||||
user_id = get_user_id(self.request)
|
||||
|
||||
# 超级管理员自动放行
|
||||
if user_id and user_id != 1:
|
||||
from application.menu import services
|
||||
permission_list = services.get_user_permissions(user_id)
|
||||
for permission in permissions:
|
||||
if permission not in permission_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
def handle_no_permission(self):
|
||||
return R.failed("暂无操作权限", 401)
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
- **用户 ID 为 1 是超级管理员**:自动跳过所有权限校验
|
||||
- **权限校验失败返回 401**:前端收到 401 后提示"暂无操作权限"
|
||||
- **权限节点从菜单表查询**:`get_user_permissions(user_id)` 返回用户拥有的所有权限节点
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 第一步:添加菜单节点
|
||||
|
||||
登录管理后台,进入「系统管理 -> 菜单管理」:
|
||||
|
||||
1. 找到或创建「案例管理」菜单项
|
||||
2. 在该菜单下添加「权限」类型子节点
|
||||
3. 填写以下信息:
|
||||
|
||||
| 字段 | 值 |
|
||||
|:----|:----|
|
||||
| 菜单类型 | 权限 |
|
||||
| 权限标识 | `sys:example:page` |
|
||||
| 菜单名称 | 案例查询 |
|
||||
|
||||
### 第二步:批量添加权限节点
|
||||
|
||||
为每个接口重复上述步骤,添加全部权限节点。
|
||||
|
||||
查询类权限(page、detail、list)通常合并为一个节点 `sys:example:list`,减少配置工作量。写操作(add、update、delete、status)建议独立配置,便于细粒度控制。
|
||||
|
||||
### 第三步:分配给角色
|
||||
|
||||
权限节点创建并启用后,需将其分配给角色,用户才能通过角色继承相应权限。
|
||||
|
||||
操作路径:「系统管理 -> 角色管理」
|
||||
|
||||
1. 在角色列表中找到目标角色,点击「编辑」进入权限配置页面
|
||||
2. 在权限树中展开「案例管理」模块,勾选需要分配的权限节点
|
||||
3. 点击「保存」提交配置,权限即生效
|
||||
|
||||
注意:权限分配后,拥有该角色的用户需重新登录或刷新页面才能获取最新权限。
|
||||
|
||||
## 前端权限控制
|
||||
|
||||
前端使用 `v-perm` 指令控制按钮显示:
|
||||
|
||||
```vue
|
||||
<!-- 新增按钮 -->
|
||||
<el-button v-perm="['sys:example:add']">添加案例</el-button>
|
||||
|
||||
<!-- 批量删除按钮 -->
|
||||
<el-button v-perm="['sys:example:batchDelete']">批量删除</el-button>
|
||||
```
|
||||
|
||||
操作栏按钮使用 `auth` 属性:
|
||||
|
||||
```javascript
|
||||
{
|
||||
label: '编辑',
|
||||
icon: 'Edit',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['sys:example:update'],
|
||||
}
|
||||
```
|
||||
|
||||
## SQL 直接插入权限节点
|
||||
|
||||
如果需要通过 SQL 直接插入权限节点(如初始化脚本),参考以下语句:
|
||||
|
||||
```sql
|
||||
-- 假设案例管理菜单 ID 为 200
|
||||
INSERT INTO django_menu (name, parent_id, path, component, perms, type, icon, sort, create_time, is_delete)
|
||||
VALUES
|
||||
('案例查询', 200, NULL, NULL, 'sys:example:page', '1', NULL, 0, NOW(), 0),
|
||||
('案例新增', 200, NULL, NULL, 'sys:example:add', '1', NULL, 0, NOW(), 0),
|
||||
('案例编辑', 200, NULL, NULL, 'sys:example:update', '1', NULL, 0, NOW(), 0),
|
||||
('案例删除', 200, NULL, NULL, 'sys:example:delete', '1', NULL, 0, NOW(), 0),
|
||||
('案例状态', 200, NULL, NULL, 'sys:example:status', '1', NULL, 0, NOW(), 0);
|
||||
```
|
||||
|
||||
其中 `type='1'` 表示权限类型节点(非菜单)。
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **权限字符串必须与代码中的 `permission_required` 完全一致**
|
||||
2. **admin 用户(ID=1)自动跳过权限校验**:开发阶段可用 admin 测试
|
||||
3. **权限节点是菜单的子节点**:type 为 `1`(权限),不是 `0`(菜单)
|
||||
4. **查询权限可适当合并**:减少配置复杂度
|
||||
5. **写操作权限建议独立**:便于细粒度控制
|
||||
|
||||
## 总结
|
||||
|
||||
权限节点是菜单的子节点(type=1),命名规范为 `sys:{模块}:{操作}`。后端通过 `PermissionRequired` 混入类校验,前端通过 `v-perm` 指令控制按钮显示。admin 用户(ID=1)自动跳过权限校验,查询权限可适当合并,写操作权限建议独立以实现细粒度控制。
|
||||
@@ -0,0 +1,440 @@
|
||||
# 业务逻辑层(Service)
|
||||
|
||||
业务逻辑层是模块开发的第三步,也是最核心的部分。所有 CRUD 操作、业务规则、数据处理都封装在 Service 层的独立函数中。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
application/example/services.py
|
||||
```
|
||||
|
||||
## 命名规范
|
||||
|
||||
| 函数类型 | 命名格式 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| 分页查询 | `get_<m>_page` | `get_example_page` |
|
||||
| 列表查询 | `get_<m>_list` | `get_example_list` |
|
||||
| 详情查询 | `get_<m>_detail` | `get_example_detail` |
|
||||
| 添加 | `add_<m>` | `add_example` |
|
||||
| 更新 | `update_<m>` | `update_example` |
|
||||
| 删除 | `delete_<m>s` | `delete_examples` |
|
||||
| 状态更新 | `update_<m>_status` | `update_example_status` |
|
||||
| 数据列表 | `get_<m>_data` | `get_example_data` |
|
||||
|
||||
## 分页查询
|
||||
|
||||
```python
|
||||
def get_example_page(request):
|
||||
"""
|
||||
查询案例分页数据
|
||||
|
||||
Args:
|
||||
request: HttpRequest对象
|
||||
- pageNo: 当前页码(可选,默认为1)
|
||||
- pageSize: 每页记录数(可选,默认为PAGE_SIZE)
|
||||
- name: 案例名称(可选,模糊查询)
|
||||
- type: 案例类型(可选,精确匹配)
|
||||
- status: 案例状态(可选,精确匹配)
|
||||
|
||||
Returns:
|
||||
R: 包含分页数据的JSON响应
|
||||
"""
|
||||
try:
|
||||
# 获取分页参数
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
|
||||
# 构建基础查询(只查询未删除的记录)
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
|
||||
# 应用筛选条件
|
||||
query = _apply_filters(query, request)
|
||||
|
||||
# 按排序字段升序排列
|
||||
query = query.order_by("sort")
|
||||
|
||||
# 执行分页查询
|
||||
paginator = Paginator(query, page_size)
|
||||
try:
|
||||
page_list = paginator.page(page_no)
|
||||
except (PageNotAnInteger, InvalidPage, EmptyPage):
|
||||
page_list = paginator.page(1)
|
||||
page_no = 1
|
||||
|
||||
# 构建并返回分页数据
|
||||
page_data = _build_page_data(page_list, paginator, page_no, page_size)
|
||||
return R.ok(data=page_data)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例分页数据异常: {str(e)}")
|
||||
return R.failed(msg="查询失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 筛选条件应用
|
||||
|
||||
```python
|
||||
def _apply_filters(queryset, request):
|
||||
"""应用查询筛选条件(私有辅助函数)"""
|
||||
# 案例名称模糊筛选
|
||||
name = request.GET.get('name')
|
||||
if name:
|
||||
queryset = queryset.filter(name__contains=name)
|
||||
|
||||
# 案例类型筛选(精确匹配)
|
||||
type = request.GET.get('type')
|
||||
if type:
|
||||
queryset = queryset.filter(type=type)
|
||||
|
||||
# 案例状态筛选(精确匹配)
|
||||
status = request.GET.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
return queryset
|
||||
```
|
||||
|
||||
### 分页数据构建
|
||||
|
||||
```python
|
||||
def _build_page_data(page_list, paginator, page_no, page_size):
|
||||
"""构建分页返回数据(私有辅助函数)"""
|
||||
records = []
|
||||
for item in page_list:
|
||||
record = {
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'avatar': get_file_url(item.avatar) if item.avatar else "",
|
||||
'type': item.type,
|
||||
'status': item.status,
|
||||
'sort': item.sort,
|
||||
'createUser': item.create_user,
|
||||
'createTime': format_datetime(item.create_time),
|
||||
'updateUser': item.update_user,
|
||||
'updateTime': format_datetime(item.update_time),
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
return {
|
||||
'records': records,
|
||||
'total': paginator.count,
|
||||
'size': page_size,
|
||||
'current': page_no,
|
||||
'pages': paginator.num_pages
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"records": [...],
|
||||
"total": 100,
|
||||
"size": 10,
|
||||
"current": 1,
|
||||
"pages": 10
|
||||
},
|
||||
"msg": "操作成功",
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
## 详情查询
|
||||
|
||||
```python
|
||||
def get_example_detail(id):
|
||||
"""
|
||||
根据ID查询案例详情
|
||||
|
||||
Args:
|
||||
id: 案例ID
|
||||
|
||||
Returns:
|
||||
dict/None: 案例详情字典,不存在时返回None
|
||||
"""
|
||||
try:
|
||||
instance = Example.objects.filter(is_delete=False, id=id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'avatar': get_file_url(instance.avatar) if instance.avatar else "",
|
||||
'type': instance.type,
|
||||
'status': instance.status,
|
||||
'sort': instance.sort,
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例详情异常,ID: {id}, 错误: {str(e)}")
|
||||
return None
|
||||
```
|
||||
|
||||
## 添加记录
|
||||
|
||||
```python
|
||||
def add_example(request):
|
||||
"""
|
||||
添加案例
|
||||
|
||||
处理流程:
|
||||
1. 解析请求体中的JSON数据
|
||||
2. 表单验证
|
||||
3. 处理头像图片(保存到 example 目录)
|
||||
4. 创建数据库记录
|
||||
"""
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 表单验证
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
try:
|
||||
cleaned_data = form.cleaned_data
|
||||
|
||||
# 处理头像图片
|
||||
avatar = cleaned_data.get('avatar')
|
||||
if avatar:
|
||||
avatar = save_file(avatar, "example")
|
||||
|
||||
# 创建案例记录
|
||||
Example.objects.create(
|
||||
name=cleaned_data.get('name'),
|
||||
avatar=avatar,
|
||||
type=cleaned_data.get('type'),
|
||||
status=cleaned_data.get('status'),
|
||||
sort=cleaned_data.get('sort'),
|
||||
create_user=get_username(request),
|
||||
update_user=get_username(request)
|
||||
)
|
||||
return R.ok(msg="创建成功")
|
||||
except Exception as e:
|
||||
logging.error(f"添加案例异常: {str(e)}")
|
||||
return R.failed(msg="添加失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 添加流程说明
|
||||
|
||||
```
|
||||
请求 → parse_request_body → 表单验证 → 处理文件 → ORM.create → 返回结果
|
||||
```
|
||||
|
||||
| 步骤 | 函数 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 解析请求体 | `parse_request_body(request)` | 解析 JSON 请求体 |
|
||||
| 表单验证 | `forms.ExampleForm(data)` | 验证数据合法性 |
|
||||
| 处理文件 | `save_file(url, dir)` | 临时文件迁移到正式目录 |
|
||||
| 创建记录 | `Example.objects.create(...)` | ORM 创建数据库记录 |
|
||||
| 获取用户名 | `get_username(request)` | 从 JWT token 解析当前用户 |
|
||||
|
||||
## 更新记录
|
||||
|
||||
```python
|
||||
def update_example(request):
|
||||
"""
|
||||
更新案例
|
||||
|
||||
处理流程:
|
||||
1. 解析请求体中的JSON数据
|
||||
2. 验证案例ID
|
||||
3. 表单验证
|
||||
4. 查询现有记录
|
||||
5. 处理头像图片(如有变化则保存新图片)
|
||||
6. 更新各字段
|
||||
7. 保存更新
|
||||
"""
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 验证案例ID
|
||||
instance_id = data.get('id')
|
||||
if not instance_id or not str(instance_id).isdigit() or int(instance_id) <= 0:
|
||||
return R.failed("案例ID无效")
|
||||
instance_id = int(instance_id)
|
||||
|
||||
# 表单验证
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
try:
|
||||
# 查询案例记录
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
|
||||
cleaned_data = form.cleaned_data
|
||||
|
||||
# 处理头像图片(仅当有新图片且与当前不同时保存)
|
||||
avatar = cleaned_data.get('avatar')
|
||||
if avatar and avatar != instance.avatar:
|
||||
avatar = save_file(avatar, "example")
|
||||
else:
|
||||
avatar = instance.avatar
|
||||
|
||||
# 更新各字段
|
||||
instance.name = cleaned_data.get('name')
|
||||
instance.avatar = avatar
|
||||
instance.type = cleaned_data.get('type')
|
||||
instance.status = cleaned_data.get('status')
|
||||
instance.sort = cleaned_data.get('sort')
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
|
||||
return R.ok(msg="更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="更新失败,请稍后重试")
|
||||
```
|
||||
|
||||
## 删除记录(批量)
|
||||
|
||||
```python
|
||||
def delete_examples(instance_ids):
|
||||
"""
|
||||
删除案例(批量删除)
|
||||
|
||||
采用逻辑删除方式(is_delete=True),而非物理删除。
|
||||
支持逗号分隔的多个ID。
|
||||
|
||||
Args:
|
||||
instance_ids: 案例ID字符串,多个ID用逗号分隔
|
||||
例如: "1" 或 "1,2,3"
|
||||
"""
|
||||
if not instance_ids:
|
||||
return R.failed("记录ID不存在")
|
||||
|
||||
try:
|
||||
# 解析ID列表
|
||||
id_list = [int(id_str.strip()) for id_str in instance_ids.split(',') if id_str.strip()]
|
||||
if not id_list:
|
||||
return R.failed("无效的记录ID")
|
||||
|
||||
# 批量更新删除标识(逻辑删除)
|
||||
updated_count = Example.objects.filter(
|
||||
id__in=id_list,
|
||||
is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
return R.ok(msg=f"本次共删除{updated_count}条数据")
|
||||
except ValueError:
|
||||
return R.failed("记录ID格式错误")
|
||||
except Exception as e:
|
||||
logging.error(f"删除案例异常,IDs: {instance_ids}, 错误: {str(e)}")
|
||||
return R.failed(msg="删除失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 逻辑删除说明
|
||||
|
||||
项目采用逻辑删除(软删除),而非物理删除:
|
||||
|
||||
```python
|
||||
# 逻辑删除:设置 is_delete=True
|
||||
Example.objects.filter(id__in=id_list).update(is_delete=True)
|
||||
|
||||
# 物理删除(不推荐):真正删除数据库记录
|
||||
Example.objects.filter(id__in=id_list).delete()
|
||||
```
|
||||
|
||||
所有查询都必须加 `is_delete=False` 条件过滤已删除的数据。
|
||||
|
||||
## 状态更新
|
||||
|
||||
```python
|
||||
def update_example_status(request):
|
||||
"""
|
||||
更新案例状态
|
||||
|
||||
单独更新案例的启用/禁用状态,不涉及其他字段。
|
||||
"""
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 验证案例ID
|
||||
instance_id = data.get('id')
|
||||
if not instance_id or not str(instance_id).isdigit() or int(instance_id) <= 0:
|
||||
return R.failed("案例ID无效")
|
||||
instance_id = int(instance_id)
|
||||
|
||||
# 验证状态值(1=启用, 2=禁用)
|
||||
status = data.get('status')
|
||||
if status is None:
|
||||
return R.failed("状态值不能为空")
|
||||
try:
|
||||
status = int(status)
|
||||
if status not in [1, 2]:
|
||||
return R.failed("状态值无效")
|
||||
except ValueError:
|
||||
return R.failed("状态值格式错误")
|
||||
|
||||
try:
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
|
||||
instance.status = status
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
|
||||
return R.ok(msg="状态更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例状态异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="状态更新失败,请稍后重试")
|
||||
```
|
||||
|
||||
## 数据列表(下拉选择)
|
||||
|
||||
```python
|
||||
def get_example_data():
|
||||
"""
|
||||
获取所有启用的案例列表
|
||||
|
||||
通常用于前端下拉选择框等场景。
|
||||
只返回 status=1(启用)且未删除的记录。
|
||||
"""
|
||||
try:
|
||||
queryset = Example.objects.filter(
|
||||
is_delete=False,
|
||||
status=1
|
||||
).order_by("sort").values()
|
||||
|
||||
return list(queryset)
|
||||
except Exception as e:
|
||||
logging.error(f"获取案例列表异常: {str(e)}")
|
||||
return []
|
||||
```
|
||||
|
||||
## 常用工具函数
|
||||
|
||||
| 函数 | 来源 | 用途 |
|
||||
|:----|:----|:----|
|
||||
| `parse_request_body(request)` | `utils/common.py` | 解析 JSON 请求体 |
|
||||
| `regular.get_err(form)` | `utils/regular.py` | 提取表单错误信息 |
|
||||
| `get_username(request)` | `utils/security.py` | 从 JWT 获取当前用户名 |
|
||||
| `get_file_url(path)` | `utils/common.py` | 拼接文件完整 URL |
|
||||
| `save_file(url, dir)` | `utils/common.py` | 保存文件到正式目录 |
|
||||
| `format_datetime(dt)` | `utils/common.py` | 格式化日期时间 |
|
||||
| `R.ok()` / `R.failed()` | `utils/R.py` | 统一响应格式 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **所有查询都要加 `is_delete=False`**:过滤已删除的数据
|
||||
2. **使用 `parse_request_body()` 解析请求体**:统一的 JSON 解析方式
|
||||
3. **表单验证失败时使用 `regular.get_err()` 提取错误**:统一的错误信息格式
|
||||
4. **使用 `get_username(request)` 获取当前用户**:从 JWT token 解析
|
||||
5. **文件处理使用 `save_file()`**:临时文件迁移到正式目录
|
||||
6. **批量删除支持逗号分隔的多个 ID**:如 "1,2,3"
|
||||
7. **所有异常都要捕获并记录日志**:使用 `logging.error()`
|
||||
|
||||
## 总结
|
||||
|
||||
Service 层封装所有业务逻辑,每个 CRUD 操作都是独立的函数。使用 `parse_request_body` 解析请求体,`forms.ModelForm` 验证数据,`R.ok()` / `R.failed()` 返回统一响应。所有查询都必须加 `is_delete=False` 过滤已删除数据,批量删除采用逻辑删除方式。
|
||||
@@ -0,0 +1,160 @@
|
||||
# URL 路由配置
|
||||
|
||||
URL 路由是模块开发的最后一步,负责将 URL 路径映射到视图类。项目使用 Django 的 `path()` 函数定义路由,通过 `include()` 将模块路由注册到主路由。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
application/example/urls.py # 模块路由
|
||||
application/urls.py # 主路由
|
||||
```
|
||||
|
||||
## 模块路由配置
|
||||
|
||||
### 完整代码
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
from application.example import views
|
||||
|
||||
urlpatterns = [
|
||||
# 查询案例分页列表
|
||||
path('page', views.ExamplePageView.as_view()),
|
||||
|
||||
# 查询案例数据列表(支持筛选)
|
||||
path('list', views.ExampleListView.as_view()),
|
||||
|
||||
# 查询案例详情
|
||||
path('detail/<int:id>', views.ExampleDetailView.as_view()),
|
||||
|
||||
# 添加案例
|
||||
path('add', views.ExampleAddView.as_view()),
|
||||
|
||||
# 更新案例
|
||||
path('update', views.ExampleUpdateView.as_view()),
|
||||
|
||||
# 删除案例
|
||||
path('delete/<str:id>', views.ExampleDeleteView.as_view()),
|
||||
|
||||
# 设置案例状态
|
||||
path('status', views.ExampleStatusView.as_view()),
|
||||
|
||||
# 查询案例数据列表
|
||||
path('list', views.ExampleDataView.as_view()),
|
||||
]
|
||||
```
|
||||
|
||||
### 路由说明
|
||||
|
||||
| URL 路径 | HTTP 方法 | 视图类 | 说明 |
|
||||
|:----|:----|:----|:----|
|
||||
| `page` | GET | `ExamplePageView` | 分页查询 |
|
||||
| `list` | GET | `ExampleListView` | 列表查询(不分页) |
|
||||
| `detail/<int:id>` | GET | `ExampleDetailView` | 详情查询 |
|
||||
| `add` | POST | `ExampleAddView` | 添加记录 |
|
||||
| `update` | PUT | `ExampleUpdateView` | 更新记录 |
|
||||
| `delete/<str:id>` | DELETE | `ExampleDeleteView` | 删除记录 |
|
||||
| `status` | PUT | `ExampleStatusView` | 状态更新 |
|
||||
| `list` | GET | `ExampleDataView` | 数据列表(下拉选择) |
|
||||
|
||||
### URL 参数类型
|
||||
|
||||
```python
|
||||
# 整数参数
|
||||
path('detail/<int:id>', views.ExampleDetailView.as_view())
|
||||
|
||||
# 字符串参数(支持逗号分隔的多个ID)
|
||||
path('delete/<str:id>', views.ExampleDeleteView.as_view())
|
||||
```
|
||||
|
||||
| 参数类型 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `<int:id>` | 整数参数 | `/example/detail/5` |
|
||||
| `<str:id>` | 字符串参数 | `/example/delete/1,2,3` |
|
||||
|
||||
### 类视图转换
|
||||
|
||||
```python
|
||||
path('page', views.ExamplePageView.as_view())
|
||||
```
|
||||
|
||||
Django 的类视图需要调用 `.as_view()` 方法将其转换为视图函数,这样才能被 `path()` 识别。
|
||||
|
||||
## 主路由注册
|
||||
|
||||
### 完整代码
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# Django 自带的后台管理系统路由
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
# 登录模块路由
|
||||
path('', include('application.login.urls')),
|
||||
|
||||
# 系统主页模块路由
|
||||
path('index/', include('application.index.urls')),
|
||||
|
||||
# 文件上传模块路由
|
||||
path('upload/', include('application.upload.urls')),
|
||||
|
||||
# 职级管理模块路由
|
||||
path('level/', include('application.level.urls')),
|
||||
|
||||
# 岗位管理模块路由
|
||||
path('position/', include('application.position.urls')),
|
||||
|
||||
# 案例模块路由
|
||||
path('example/', include('application.example.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
### include() 函数
|
||||
|
||||
```python
|
||||
path('example/', include('application.example.urls'))
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `'example/'` | URL 前缀,所有以 `/example/` 开头的请求都会被转发 |
|
||||
| `include('application.example.urls')` | 包含模块的路由配置 |
|
||||
|
||||
`include()` 函数会自动去掉前缀,将剩余部分传递给模块路由。例如:
|
||||
|
||||
- 请求 URL:`/example/page`
|
||||
- 去掉前缀 `/example/` 后:`/page`
|
||||
- 匹配模块路由:`path('page', views.ExamplePageView.as_view())`
|
||||
|
||||
## 完整 URL 路径
|
||||
|
||||
注册到主路由后,完整的 API 路径如下:
|
||||
|
||||
| 功能 | HTTP 方法 | 完整 URL |
|
||||
|:----|:----|:----|
|
||||
| 分页查询 | GET | `/api/example/page?pageNo=1&pageSize=10` |
|
||||
| 列表查询 | GET | `/api/example/list` |
|
||||
| 详情查询 | GET | `/api/example/detail/5` |
|
||||
| 添加记录 | POST | `/api/example/add` |
|
||||
| 更新记录 | PUT | `/api/example/update` |
|
||||
| 删除记录 | DELETE | `/api/example/delete/1,2,3` |
|
||||
| 状态更新 | PUT | `/api/example/status` |
|
||||
| 数据列表 | GET | `/api/example/list` |
|
||||
|
||||
注意:前端请求的 URL 以 `/api` 开头,这是因为 Vite 开发服务器配置了代理,将 `/api` 转发到 Django 后端。
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **模块路由文件只定义相对路径**:不包含模块前缀
|
||||
2. **主路由使用 `include()` 注册模块路由**:实现路由的模块化管理
|
||||
3. **类视图必须调用 `.as_view()`**:转换为视图函数
|
||||
4. **URL 参数使用尖括号语法**:`<int:id>` 或 `<str:id>`
|
||||
5. **删除接口使用字符串参数**:支持逗号分隔的多个 ID
|
||||
6. **路由注册后需要在 `application/urls.py` 中添加 include**:否则无法访问
|
||||
|
||||
## 总结
|
||||
|
||||
URL 路由使用 Django 的 `path()` 定义,通过 `include()` 将模块路由注册到主路由。模块路由文件只定义相对路径,主路由负责添加模块前缀。类视图需要调用 `.as_view()` 转换为视图函数,URL 参数使用尖括号语法定义。
|
||||
@@ -0,0 +1,269 @@
|
||||
# 视图层(View)
|
||||
|
||||
视图层是模块开发的第四步,负责处理 HTTP 请求并调用 Service 层的业务逻辑。项目使用 Django 的类视图(Class-Based Views),每个视图类对应一个 API 端点。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
application/example/views.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from application.example import services
|
||||
from config.env import DJANGO_DEMO
|
||||
from middleware.login_middleware import check_login
|
||||
from middleware.permission_middleware import PermissionRequired
|
||||
from utils import R
|
||||
from application.operation_log.constants import LogType
|
||||
from application.operation_log.decorators import operation_log
|
||||
|
||||
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
"""案例分页数据查询视图"""
|
||||
permission_required = ("sys:example:page",)
|
||||
|
||||
@operation_log(title="案例管理-查询分页列表", log_type=LogType.QUERY)
|
||||
def get(self, request):
|
||||
result = services.get_example_page(request)
|
||||
return result
|
||||
|
||||
|
||||
@method_decorator(check_login, name='get')
|
||||
class ExampleListView(View):
|
||||
"""案例列表获取视图(支持筛选,不分页)"""
|
||||
|
||||
def get(self, request):
|
||||
result = services.get_example_list(request)
|
||||
return R.ok(data=result)
|
||||
|
||||
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExampleDetailView(PermissionRequired, View):
|
||||
"""案例详情查询视图"""
|
||||
permission_required = ("sys:example:detail",)
|
||||
|
||||
@operation_log(title="案例管理-查询记录详情", log_type=LogType.QUERY)
|
||||
def get(self, request, id):
|
||||
data = services.get_example_detail(id)
|
||||
return R.ok(data=data)
|
||||
|
||||
|
||||
@method_decorator(check_login, name="post")
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
"""添加案例视图"""
|
||||
permission_required = ("sys:example:add",)
|
||||
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
def post(self, request):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
result = services.add_example(request)
|
||||
return result
|
||||
|
||||
|
||||
@method_decorator(check_login, name="put")
|
||||
class ExampleUpdateView(PermissionRequired, View):
|
||||
"""更新案例视图"""
|
||||
permission_required = ('sys:example:update',)
|
||||
|
||||
@operation_log(title="案例管理-更新记录", log_type=LogType.UPDATE)
|
||||
def put(self, request):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
result = services.update_example(request)
|
||||
return result
|
||||
|
||||
|
||||
@method_decorator(check_login, name="delete")
|
||||
class ExampleDeleteView(PermissionRequired, View):
|
||||
"""删除案例视图"""
|
||||
permission_required = ('sys:example:delete',)
|
||||
|
||||
@operation_log(title="案例管理-删除记录", log_type=LogType.DELETE)
|
||||
def delete(self, request, id):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
result = services.delete_examples(id)
|
||||
return result
|
||||
|
||||
|
||||
@method_decorator(check_login, name="put")
|
||||
class ExampleStatusView(PermissionRequired, View):
|
||||
"""设置案例状态视图"""
|
||||
permission_required = ('sys:example:status',)
|
||||
|
||||
def put(self, request):
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
result = services.update_example_status(request)
|
||||
return result
|
||||
|
||||
|
||||
@method_decorator(check_login, name='get')
|
||||
class ExampleDataView(View):
|
||||
"""案例列表获取视图"""
|
||||
|
||||
def get(self, request):
|
||||
result = services.get_example_data()
|
||||
return R.ok(data=result)
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 视图类继承
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
```
|
||||
|
||||
| 基类/装饰器 | 来源 | 提供能力 |
|
||||
|:----|:----|:----|
|
||||
| `View` | Django 内置 | 类视图基类,提供 get/post/put/delete 方法分发 |
|
||||
| `PermissionRequired` | `middleware/permission_middleware.py` | 权限控制混入类 |
|
||||
| `@method_decorator(check_login, name="get")` | `middleware/login_middleware.py` | JWT 登录认证 |
|
||||
|
||||
### 三层防护机制
|
||||
|
||||
```
|
||||
请求 → check_login(登录校验) → PermissionRequired(权限校验) → operation_log(操作日志) → 业务逻辑
|
||||
```
|
||||
|
||||
#### 1. 登录校验(check_login)
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="get")
|
||||
```
|
||||
|
||||
`check_login` 装饰器从请求头 `Authorization: Bearer <token>` 中提取 JWT token,验证其有效性。验证失败返回 `401 Unauthorized`。
|
||||
|
||||
```python
|
||||
def check_login(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
ignoreURL = ['/login', '/captcha']
|
||||
if request.path not in ignoreURL:
|
||||
access_token = request.headers['Authorization']
|
||||
access_token = access_token.replace('Bearer ', "")
|
||||
result = parse_payload(access_token)
|
||||
code = result['code']
|
||||
if code != 0:
|
||||
return R.failed(code=401, msg=result['msg'])
|
||||
return func(request, *args, **kwargs)
|
||||
return wrapper
|
||||
```
|
||||
|
||||
#### 2. 权限校验(PermissionRequired)
|
||||
|
||||
```python
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
permission_required = ("sys:example:page",)
|
||||
```
|
||||
|
||||
`PermissionRequired` 混入类重写了 `has_permission()` 方法,检查当前用户是否拥有所需的权限节点。用户 ID 为 1 的超级管理员自动放行。
|
||||
|
||||
```python
|
||||
class PermissionRequired(PermissionRequiredMixin):
|
||||
def has_permission(self):
|
||||
permissions = self.get_permission_required()
|
||||
user_id = get_user_id(self.request)
|
||||
|
||||
# 超级管理员自动放行
|
||||
if user_id and user_id != 1:
|
||||
from application.menu import services
|
||||
permission_list = services.get_user_permissions(user_id)
|
||||
for permission in permissions:
|
||||
if permission not in permission_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
def handle_no_permission(self):
|
||||
return R.failed("暂无操作权限", 401)
|
||||
```
|
||||
|
||||
#### 3. 操作日志(operation_log)
|
||||
|
||||
```python
|
||||
@operation_log(title="案例管理-查询分页列表", log_type=LogType.QUERY)
|
||||
def get(self, request):
|
||||
...
|
||||
```
|
||||
|
||||
`@operation_log` 装饰器自动记录操作日志,包括操作标题、操作类型、请求参数、响应结果、耗时等信息。
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `title` | 操作标题,如"案例管理-添加记录" |
|
||||
| `log_type` | 操作类型,参考 `LogType` 常量 |
|
||||
| `source` | 操作来源,0=后台系统 |
|
||||
|
||||
### 操作类型常量
|
||||
|
||||
```python
|
||||
class LogType:
|
||||
OTHER = 0 # 其他操作
|
||||
QUERY = 1 # 查询操作
|
||||
ADD = 2 # 新增操作
|
||||
UPDATE = 3 # 修改操作
|
||||
SAVE = 4 # 保存操作
|
||||
DELETE = 5 # 删除操作
|
||||
BATCH_DELETE = 6 # 批量删除
|
||||
STATUS = 7 # 设置状态
|
||||
RESET_PWD = 8 # 重置密码
|
||||
IMPORT = 9 # 导入操作
|
||||
EXPORT = 10 # 导出操作
|
||||
UPLOAD = 11 # 上传操作
|
||||
```
|
||||
|
||||
### 演示环境检查
|
||||
|
||||
```python
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
```
|
||||
|
||||
写操作(add、update、delete、status)需要检查 `DJANGO_DEMO` 配置。当 `.env` 中设置 `DJANGO_DEMO=True` 时,所有写操作返回失败提示。
|
||||
|
||||
### 视图类分类
|
||||
|
||||
| 视图类 | HTTP 方法 | 权限 | 说明 |
|
||||
|:----|:----|:----|:----|
|
||||
| `ExamplePageView` | GET | `sys:example:page` | 分页查询 |
|
||||
| `ExampleListView` | GET | 无(仅登录) | 列表查询(不分页) |
|
||||
| `ExampleDetailView` | GET | `sys:example:detail` | 详情查询 |
|
||||
| `ExampleAddView` | POST | `sys:example:add` | 添加记录 |
|
||||
| `ExampleUpdateView` | PUT | `sys:example:update` | 更新记录 |
|
||||
| `ExampleDeleteView` | DELETE | `sys:example:delete` | 删除记录 |
|
||||
| `ExampleStatusView` | PUT | `sys:example:status` | 状态更新 |
|
||||
| `ExampleDataView` | GET | 无(仅登录) | 数据列表(下拉选择) |
|
||||
|
||||
### 权限节点格式
|
||||
|
||||
```
|
||||
sys:<module>:<action>
|
||||
```
|
||||
|
||||
| 部分 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `sys` | 固定前缀 | `sys` |
|
||||
| `<module>` | 模块名称 | `example` |
|
||||
| `<action>` | 操作类型 | `page`、`list`、`detail`、`add`、`update`、`delete`、`status` |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **每个视图类只处理一个 HTTP 方法**:单一职责原则
|
||||
2. **使用 `@method_decorator(check_login, name="方法名")` 进行登录校验**:确保用户已登录
|
||||
3. **需要权限控制的视图继承 `PermissionRequired`**:并设置 `permission_required`
|
||||
4. **写操作必须检查 `DJANGO_DEMO`**:演示环境禁止写操作
|
||||
5. **使用 `@operation_log` 记录操作日志**:便于审计和追踪
|
||||
6. **视图层只做请求分发**:业务逻辑全部委托给 Service 层
|
||||
7. **直接返回 Service 层的结果**:不要在视图层处理业务逻辑
|
||||
|
||||
## 总结
|
||||
|
||||
视图层使用 Django 类视图,组合三层防护机制:登录校验(check_login)、权限控制(PermissionRequired)、操作日志(operation_log)。每个视图类只处理一个 HTTP 方法,写操作需要检查 DJANGO_DEMO 配置。视图层只做请求分发,业务逻辑全部委托给 Service 层。
|
||||
Reference in New Issue
Block a user