签入版本

This commit is contained in:
2026-09-12 23:12:15 +08:00
commit 91998056ca
1841 changed files with 341325 additions and 0 deletions
@@ -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 文件。代码生成器可根据数据库表自动生成全套代码。
+259
View File
@@ -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` 过滤已删除数据,批量删除采用逻辑删除方式。
+160
View File
@@ -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 层。