签入版本

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,245 @@
:::tip 文章管理
文章管理模块支持富文本编辑、分类关联、封面图片上传、图集管理等功能。适用于新闻、公告、帮助文档等内容管理场景。
:::
## 模块结构
```
application/article/
├── models.py # 文章模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层(自定义函数)
├── views.py # 视图层
└── urls.py # 路由配置
```
:::warning 自定义 Service
文章模块因涉及文件处理、富文本清洗、图集解析等复杂逻辑,编写自定义 service 函数而非继承通用模式。
:::
## 文章模型
```python
# application/article/models.py
from application.models import BaseModel
class Article(BaseModel):
"""文章模型类"""
class Meta:
db_table = 'django_article'
category_id = models.IntegerField(null=True, blank=True, verbose_name='分类ID')
title = models.CharField(max_length=150, null=True, blank=True, verbose_name='文章标题')
cover = models.CharField(max_length=255, null=True, blank=True, verbose_name='文章封面')
images = models.TextField(null=True, blank=True, verbose_name='文章图集')
intro = models.CharField(max_length=255, null=True, blank=True, verbose_name='文章导读')
content = models.TextField(null=True, blank=True, verbose_name='文章内容')
author = models.CharField(max_length=100, null=True, blank=True, verbose_name='文章作者')
status = models.IntegerField(default=0, verbose_name='文章状态:0-正常 1-下架')
click = models.IntegerField(default=0, verbose_name='文章点击率')
sort = models.IntegerField(default=0, verbose_name='文章排序')
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `category_id` | int | 所属分类 ID(关联 category 表) |
| `title` | str | 文章标题 |
| `cover` | str | 封面图片路径 |
| `images` | str | 图集路径(逗号分隔) |
| `intro` | str | 文章导读/摘要 |
| `content` | str | 富文本内容(HTML) |
| `author` | str | 文章作者 |
| `status` | int | 状态:0-正常 1-下架 |
| `click` | int | 点击次数(自动统计) |
| `sort` | int | 排序权重 |
## 文章状态流转
```
草稿(0) → 发布(0) → 下架(1) → 重新发布(0)
```
| 状态 | 说明 | 可执行操作 |
|------|------|-----------|
| 0-正常 | 文章已发布,前台可见 | 编辑、下架、删除 |
| 1-下架 | 文章已下架,前台不可见 | 编辑、重新发布、删除 |
## 文件处理机制
文章模块涉及三类文件字段,处理方式各不相同:
### 1. 封面图片(cover)-- save_file
```python
# 新增时
cover = save_file(data.cover, "article") if data.cover else None
```
`save_file(url, directory)` 的处理逻辑:
1. 将前端传来的 URL 还原为磁盘绝对路径
2. 若文件在临时目录(`temp/`),移动到正式目录(`upload/article/日期/`)
3. 返回相对路径存入数据库
### 2. 图集(images)-- _build_images / _parse_images
图集在数据库中存储为逗号分隔的相对路径字符串:
```
/article/20260907/a.png,/article/20260907/b.png
```
前端传入数组,后端拼接为字符串存储:
```python
def _build_images(images_list):
"""将图片列表拼接为逗号分隔的字符串"""
if not images_list:
return None
return ",".join([save_file(img, "article") for img in images_list])
def _parse_images(images_str):
"""将逗号分隔的字符串解析为完整 URL 列表"""
if not images_str:
return []
return [get_file_url(img) for img in images_str.split(",")]
```
### 3. 富文本内容(content)-- save_content
```
原始 HTML → 提取图片 → 迁移文件 → 替换路径 → 设置 alt → 存库
```
```python
from utils.common import save_content
# 处理富文本内容
content = save_content(content, title, "article")
```
## 添加文章
```python
def add_article(request):
"""添加文章"""
data, error = parse_request_body(request)
if error:
return R.failed(msg=error)
# 表单验证
form = forms.ArticleForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
try:
cleaned_data = form.cleaned_data
# 处理封面图片
cover = save_file(cleaned_data.get('cover'), "article") if cleaned_data.get('cover') else None
# 处理图集
images = _build_images(cleaned_data.get('images'))
# 处理富文本内容
content = save_content(cleaned_data.get('content'), cleaned_data.get('title'), "article")
# 创建文章
Article.objects.create(
category_id=cleaned_data.get('categoryId'),
title=cleaned_data.get('title'),
cover=cover,
images=images,
intro=cleaned_data.get('intro'),
content=content,
author=cleaned_data.get('author'),
status=cleaned_data.get('status', 0),
sort=cleaned_data.get('sort', 0),
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="添加失败,请稍后重试")
```
## 查询分页
### 过滤条件
| 参数 | 类型 | 说明 |
|------|------|------|
| `title` | 模糊搜索 | 文章标题 |
| `category_id` | 精确匹配 | 分类 ID |
| `status` | 精确匹配 | 文章状态 |
### 排序规则
按 `sort` 降序 -> `id` 降序。
### 响应数据
```json
{
"id": 1,
"title": "DjangoAdmin 3.0 发布",
"cover": "http://domain/uploads/article/20260908/cover.png",
"images": ["http://domain/uploads/article/20260908/1.png"],
"intro": "DjangoAdmin 3.0 正式发布...",
"content": "<p>文章内容...</p>",
"author": "admin",
"status": 0,
"statusText": "正常",
"click": 100,
"sort": 1,
"categoryId": 1,
"categoryName": "产品动态",
"createUser": "admin",
"createTime": "2026-09-08 10:00:00"
}
```
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/article/page` | GET | `sys:article:page` | 分页查询 |
| `/article/detail/<int:article_id>` | GET | `sys:article:detail` | 详情查询 |
| `/article/add` | POST | `sys:article:add` | 新增文章 |
| `/article/update` | PUT | `sys:article:update` | 编辑文章 |
| `/article/status` | PUT | `sys:article:status` | 设置状态 |
| `/article/delete/<int:article_ids>` | DELETE | `sys:article:delete` | 删除文章(支持批量) |
## 前端实现
### 编辑弹窗
文章编辑弹窗包含:标题、分类选择、封面上传、图集上传、富文本编辑器、状态选择、排序。
```vue
<BasicModal @register="registerModal" @ok="handleSubmit" width="800px">
<BasicForm @register="registerForm" />
<!-- 富文本编辑器 -->
<Editor v-model="formData.content" :defaultConfig="editorConfig" />
</BasicModal>
```
### 文章列表
文章列表页支持按标题、分类、状态筛选,展示封面缩略图和状态标签。
## 总结
文章管理模块是项目中典型的「带文件处理」复杂业务模块,核心特点:
```
1. 自定义 Service:文件处理/富文本清洗/图集解析均为自定义逻辑
2. 三类文件字段:封面(save_file)、图集(逗号分隔)、富文本(save_content)
3. 临时文件迁移:前端上传到临时目录 -> 提交表单后后端迁移到正式目录
4. XSS 防护:富文本经 bleach 白名单清洗
5. 分类关联:通过 category_id 关联分类表
6. 状态管理:支持发布/下架状态切换
7. 点击统计:自动统计文章点击次数
```
@@ -0,0 +1,201 @@
:::tip 分类管理
分类管理模块用于管理树形分类数据,如文章分类、产品分类等。通过 `parent_id` 字段实现无限级分类。
:::
## 模块结构
```
application/category/
├── models.py # 分类模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
```
## 分类模型
```python
# application/category/models.py
from application.models import BaseModel
class Category(BaseModel):
"""文章分类模型类"""
class Meta:
db_table = 'django_category'
category_name = models.CharField(max_length=100, verbose_name='分类名称')
category_code = models.CharField(max_length=100, null=True, blank=True, verbose_name='分类编码')
parent_id = models.IntegerField(default=0, verbose_name='上级ID')
sort = models.IntegerField(default=0, verbose_name='排序')
note = models.CharField(max_length=100, null=True, blank=True, verbose_name='分类备注')
```
:::tip 字段说明
- `category_name`:分类名称,带索引用于模糊搜索
- `category_code`:分类编码,可选字段,用于程序化标识分类
- `parent_id`:上级分类 ID,0 表示顶级分类
:::
## 树形结构
分类通过 `parent_id` 字段形成无限级树形结构:
```
文章分类 (parent_id=0)
+-- 技术文章 (parent_id=1)
| +-- 前端技术 (parent_id=2)
| | +-- Vue (parent_id=5)
| | +-- React (parent_id=5)
| +-- 后端技术 (parent_id=2)
| +-- Django (parent_id=6)
| +-- Flask (parent_id=6)
+-- 产品动态 (parent_id=1)
+-- 公司新闻 (parent_id=1)
```
## 构建分类树
分类树构建采用非递归方案(`children_map` 分组),性能优于递归:
```python
def get_tree(data, parent_id):
"""获取树状结构(非递归,children_map 分组)"""
children_map = {}
for item in data:
pid = item["parentId"]
if pid not in children_map:
children_map[pid] = []
children_map[pid].append(item)
def build(pid):
result = []
for item in children_map.get(pid, []):
item["children"] = build(item["id"])
result.append(item)
return result
return build(parent_id)
```
### 构建流程
```
1. 查询所有分类数据
2. 按 parent_id 分组到 children_map
3. 从 parent_id=0 开始递归构建树
4. 每个节点添加 children 字段
```
## 删除分类(带子级校验)
删除分类前会检查是否存在子级分类:
```python
def delete_categories(category_ids):
"""删除分类(批量删除)"""
id_list = [int(id_str.strip()) for id_str in category_ids.split(',')]
# 检查是否有子级分类
has_children = Category.objects.filter(
parent_id__in=id_list, is_delete=False
).exists()
if has_children:
return R.failed("请先删除子级分类")
# 批量逻辑删除
updated_count = Category.objects.filter(
id__in=id_list, is_delete=False
).update(is_delete=True)
return R.ok(msg=f"本次共删除{updated_count}条数据")
```
:::warning 删除规则
分类采用**自下而上**的删除策略:必须先删除最底层的子分类,再删除父分类。这保证了数据的完整性。
:::
## 循环引用校验
更新分类时需要校验循环引用,避免 A→B→C→A 的死循环:
```python
def update_category(request):
"""更新分类(含循环引用校验)"""
# 校验:不能将分类设置为自己的子级
if parent_id == category_id:
return R.failed("上级分类不能是自己")
# 校验:不能将分类设置为自己子级的子级
if _is_descendant(category_id, parent_id):
return R.failed("上级分类不能是当前分类的子级")
```
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/category/list` | GET | `sys:category:list` | 分类列表(树形) |
| `/category/detail/<int:category_id>` | GET | `sys:category:detail` | 分类详情 |
| `/category/add` | POST | `sys:category:add` | 新增分类 |
| `/category/update` | PUT | `sys:category:update` | 编辑分类 |
| `/category/delete/<int:category_ids>` | DELETE | `sys:category:delete` | 删除分类(支持批量) |
## 前端使用
### 树形选择组件
分类选择通常使用 el-tree-select 组件:
```vue
<template>
<el-tree-select
v-model="form.category_id"
:data="categoryTree"
:props="{ label: 'categoryName', value: 'id' }"
placeholder="请选择分类"
check-strictly
/>
</template>
```
### 树形表格
分类列表页使用树形表格展示:
```vue
<template>
<BasicTable :data="categoryTree" row-key="id" default-expand-all>
<el-table-column prop="categoryName" label="分类名称" />
<el-table-column prop="categoryCode" label="分类编码" />
<el-table-column prop="sort" label="排序" />
</BasicTable>
</template>
```
### 拖拽排序
支持拖拽调整分类层级和顺序:
```vue
<el-tree
:data="categoryTree"
draggable
allow-drop="allowDrop"
@node-drop="handleDrop"
/>
```
## 总结
分类管理模块具备以下特点:
```
1. 树形结构:parent_id 实现无限级分类,非递归构建
2. 通用设计:适用于文章分类、产品分类等场景
3. 分类编码:category_code 字段支持程序化标识
4. 排序控制:sort 字段控制分类显示顺序
5. 删除校验:删除前检查子级,保证数据完整性
6. 循环引用校验:更新时防止 A→B→C→A 死循环
7. 前端集成:el-tree-select、树形表格、拖拽排序
```
@@ -0,0 +1,243 @@
:::tip 城市管理
城市管理模块用于管理行政区划数据,支持省/市/区/街道/社区五级行政区划。通过 `parent_code` 字段实现树形结构。
:::
## 城市模型
```python
# application/city/models.py
from application.models import BaseModel
class City(BaseModel):
"""城市/行政区划模型类"""
class Meta:
db_table = 'django_city'
city_code = models.CharField(max_length=6, verbose_name='城市区号')
area_code = models.CharField(max_length=20, verbose_name='行政编码')
parent_code = models.CharField(max_length=20, null=True, blank=True, verbose_name='上级行政编码')
zip_code = models.CharField(max_length=6, verbose_name='邮政编码')
level = models.IntegerField(default=0, verbose_name='城市级别:0-省份 1-城市 2-县区 3-街道 4-社区')
pid = models.IntegerField(default=0, verbose_name='上级城市ID')
name = models.CharField(max_length=150, verbose_name='城市名称')
short_name = models.CharField(max_length=150, verbose_name='城市简称')
full_name = models.CharField(max_length=150, null=True, blank=True, verbose_name='城市全称')
pinyin = models.CharField(max_length=150, null=True, blank=True, verbose_name='城市拼音')
lng = models.CharField(max_length=150, null=True, blank=True, verbose_name='城市经度')
lat = models.CharField(max_length=150, null=True, blank=True, verbose_name='城市纬度')
```
### 字段说明
| 字段 | 说明 | 示例 |
|------|------|------|
| `city_code` | 城市区号 | `0755` |
| `area_code` | 行政编码(唯一标识) | `440305` |
| `parent_code` | 上级行政编码 | `440300`(深圳市) |
| `zip_code` | 邮政编码 | `518000` |
| `level` | 层级:0-省 1-市 2-区 3-街道 4-社区 | `2` |
| `pid` | 上级城市 ID(关联本表 id) | `123` |
| `name` | 城市名称 | `南山区` |
| `short_name` | 城市简称 | `南山` |
| `full_name` | 城市全称 | `广东省深圳市南山区` |
| `pinyin` | 城市拼音 | `nanshanqu` |
| `lng` | 经度 | `113.9304` |
| `lat` | 纬度 | `22.5333` |
## 树形结构
城市通过 `parent_code` 和 `pid` 两个字段形成五级行政区划树形结构:
```
中国 (parent_code=null, level=0)
+-- 广东省 (parent_code=100000, level=0)
| +-- 深圳市 (parent_code=440000, level=1)
| | +-- 南山区 (parent_code=440300, level=2)
| | | +-- 粤海街道 (parent_code=440305, level=3)
| | +-- 福田区 (parent_code=440300, level=2)
| +-- 广州市 (parent_code=440000, level=1)
+-- 浙江省 (parent_code=100000, level=0)
+-- 杭州市 (parent_code=330000, level=1)
```
:::tip 行政区划与用户关联
用户表通过 `province_code`、`city_code`、`district_code`、`street_code` 四个字段关联城市表的 `area_code`,`city_info` 字段存储拼接后的城市名称(如"广东省深圳市南山区")。
:::
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/city/list` | GET | `sys:city:list` | 城市列表(支持按 pid 和 name 筛选) |
| `/city/detail/<int:city_id>` | GET | `sys:city:detail` | 城市详情 |
| `/city/add` | POST | `sys:city:add` | 新增城市 |
| `/city/update` | POST | `sys:city:update` | 编辑城市 |
| `/city/delete/<int:city_ids>` | POST | `sys:city:delete` | 删除城市(支持批量) |
## 列表查询
```
GET /city/list?pid=0&name=深圳
```
### 查询参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `pid` | int | 上级城市 ID(默认 0,查顶级) |
| `name` | str | 城市名称(模糊查询,不区分大小写) |
### 响应示例
```json
{
"code": 0,
"data": [
{
"id": 1,
"cityCode": "0755",
"areaCode": "440305",
"parentCode": "440300",
"zipCode": "518057",
"level": 2,
"pid": 123,
"name": "南山区",
"shortName": "南山",
"fullName": "广东省深圳市南山区",
"pinyin": "nanshanqu",
"lng": "113.9304",
"lat": "22.5333"
}
],
"msg": "操作成功",
"ok": true
}
```
## 子级城市查询
根据上级城市编码获取子级城市列表,用于前端级联选择器:
```python
# application/city/services.py
def get_child_cities(city_code):
"""根据上级城市编码获取子级城市列表"""
child_list = City.objects.filter(
is_delete=False,
parent_code=city_code
).values()
return list(child_list)
```
## 添加城市
```python
# application/city/services.py
def add_city(request):
"""添加城市"""
data, error = parse_request_body(request)
if error:
return R.failed(msg=error)
# 表单验证
form = forms.CityForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
cleaned_data = form.cleaned_data
City.objects.create(
city_code=cleaned_data.get('cityCode'),
area_code=cleaned_data.get('areaCode'),
parent_code=cleaned_data.get('parentCode'),
zip_code=cleaned_data.get('zipCode'),
level=int(cleaned_data.get('level')),
pid=int(cleaned_data.get('pid')),
name=cleaned_data.get('name'),
short_name=cleaned_data.get('shortName'),
lng=cleaned_data.get('lng'),
lat=cleaned_data.get('lat'),
create_user=get_username(request),
update_user=get_username(request)
)
return R.ok(msg="创建成功")
```
## 删除城市(带子级校验)
删除城市前会检查是否存在子级城市,如果有则阻止删除:
```python
def delete_cities(city_ids):
"""删除城市(批量删除)"""
id_list = [int(id_str.strip()) for id_str in city_ids.split(',') if id_str.strip()]
# 检查是否有子级城市
has_children = City.objects.filter(pid__in=id_list, is_delete=False).exists()
if has_children:
return R.failed("请先删除子级城市")
# 批量逻辑删除
updated_count = City.objects.filter(
id__in=id_list, is_delete=False
).update(is_delete=True)
return R.ok(msg=f"本次共删除{updated_count}条数据")
```
:::warning 删除规则
城市采用**自下而上**的删除策略:必须先删除最底层的社区/街道,再删除区县,最后删除城市和省份。这保证了数据的完整性。
:::
## 前端级联选择
前端使用 `el-cascader` 组件实现省市区三级联动选择:
```vue
<el-cascader
v-model="selectedRegion"
:props="cascaderProps"
placeholder="请选择省/市/区"
/>
<script setup>
const cascaderProps = {
lazy: true,
lazyLoad: async (node, resolve) => {
const { level, value } = node;
const res = await getCityList({ pid: value || 0 });
const nodes = res.map(item => ({
value: item.areaCode,
label: item.name,
leaf: level >= 2 // 区县级为叶子节点
}));
resolve(nodes);
}
};
</script>
```
## 数据导入
行政区划数据通常通过 SQL 文件批量导入,而非手动添加。项目内置了全国行政区划数据:
```bash
# 导入行政区划数据(首次部署时执行)
python manage.py migrate
```
:::tip 数据来源
行政区划数据来源于国家统计局公开数据,覆盖全国省/市/区/街道/社区五级行政区划。数据量约 70 万条记录。
:::
## 总结
城市管理模块具备以下特点:
```
1. 五级行政区划:省/市/区/街道/社区,通过 parent_code 形成树形结构
2. 丰富字段:含经纬度、拼音、邮编等信息
3. 用户关联:用户表通过四级编码关联城市表
4. 级联选择:前端通过 el-cascader 组件实现级联选择
5. 删除校验:删除前检查子级,保证数据完整性
6. 批量导入:内置全国行政区划数据
```
@@ -0,0 +1,230 @@
:::tip 公告/链接
公告和链接是两个简单 CRUD 内容模块,提供标准的增删改查接口。适用于系统公告、友情链接等场景。
:::
## 公告管理
### 公告模型
```python
# application/notice/models.py
from application.models import BaseModel
class Notice(BaseModel):
"""通知公告模型类"""
class Meta:
db_table = 'django_notice'
title = models.CharField(max_length=255, verbose_name='通知标题')
cover = models.CharField(max_length=255, null=True, blank=True, verbose_name='通知封面')
type = models.IntegerField(default=1, verbose_name='通知类型:1-通知 2-公告')
click_num = models.IntegerField(default=0, verbose_name='点击率')
status = models.IntegerField(default=1, verbose_name='通知状态:1-正常 2-停用')
content = models.TextField(null=True, blank=True, verbose_name='通知公告内容')
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | str | 通知标题 |
| `cover` | str | 封面图片路径 |
| `type` | int | 类型:1-通知 2-公告 |
| `click_num` | int | 点击次数(自动统计) |
| `status` | int | 状态:1-正常 2-停用 |
| `content` | str | 富文本内容(HTML) |
### API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/notice/page` | GET | `sys:notice:page` | 公告分页列表 |
| `/notice/detail/<int:id>` | GET | `sys:notice:detail` | 公告详情 |
| `/notice/add` | POST | `sys:notice:add` | 新增公告 |
| `/notice/update` | PUT | `sys:notice:update` | 编辑公告 |
| `/notice/delete/<int:id>` | DELETE | `sys:notice:delete` | 删除公告 |
### 分页查询参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `pageNo` | int | 页码 |
| `pageSize` | int | 每页条数 |
| `title` | str | 标题(模糊查询) |
| `type` | int | 类型:1-通知 2-公告 |
| `status` | int | 状态:1-正常 2-停用 |
### 富文本内容处理
公告内容使用富文本编辑器,支持 HTML 格式:
```python
# 添加公告时,content 字段接收 HTML 内容
Notice.objects.create(
title="系统升级通知",
type=1,
status=1,
content="<p>系统将于<strong>2026-09-10</strong>进行升级维护...</p>",
create_user=get_username(request)
)
```
:::warning XSS 防护
富文本内容存储前需进行 XSS 过滤,防止恶意脚本注入。项目内置了 `utils/common.py` 中的 `clean_html()` 函数用于过滤危险标签。
:::
## 链接管理
### 链接模型
```python
# application/link/models.py
from application.models import BaseModel
class Link(BaseModel):
"""友情链接模型类"""
class Meta:
db_table = 'django_link'
name = models.CharField(max_length=255, verbose_name='链接名称')
image = models.CharField(max_length=255, null=True, blank=True, verbose_name='链接图片')
url = models.CharField(max_length=255, null=True, blank=True, verbose_name='链接地址')
status = models.IntegerField(default=1, verbose_name='链接状态:1-正常 2-停用')
sort = models.IntegerField(default=0, verbose_name='排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='备注')
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | str | 链接名称 |
| `image` | str | 链接 Logo 图片路径 |
| `url` | str | 链接地址 |
| `status` | int | 状态:1-正常 2-停用 |
| `sort` | int | 排序权重(数字越小越靠前) |
| `note` | str | 备注信息 |
### API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/link/page` | GET | `sys:link:page` | 链接分页列表 |
| `/link/detail/<int:id>` | GET | `sys:link:detail` | 链接详情 |
| `/link/add` | POST | `sys:link:add` | 新增链接 |
| `/link/update` | PUT | `sys:link:update` | 编辑链接 |
| `/link/delete/<int:id>` | DELETE | `sys:link:delete` | 删除链接 |
| `/link/status` | PUT | `sys:link:status` | 设置状态 |
### 分页查询参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `pageNo` | int | 页码 |
| `pageSize` | int | 每页条数 |
| `name` | str | 链接名称(模糊查询) |
| `status` | int | 状态:1-正常 2-停用 |
### 响应示例
```json
{
"code": 0,
"data": {
"records": [
{
"id": 1,
"name": "Django 官方网站",
"image": "/uploads/link/django.png",
"url": "https://www.djangoproject.com",
"status": 1,
"statusText": "正常",
"sort": 1,
"note": "Python Web 框架",
"createUser": "admin",
"createTime": "2026-09-08 10:00:00"
}
],
"total": 10,
"size": 10,
"current": 1,
"pages": 1
},
"msg": "操作成功",
"ok": true
}
```
## 通用模式
两个模块均遵循相同的开发模式:
```
1. models.py:继承 BaseModel,定义业务字段
2. forms.py:继承 Django Form,定义字段约束和验证规则
3. services.py:实现 CRUD 业务逻辑函数
4. views.py:PermissionRequired + check_login 装饰器
5. urls.py:路由配置,映射 URL 到视图类
```
### 文件结构
```
application/notice/ application/link/
├── models.py ├── models.py
├── forms.py ├── forms.py
├── services.py ├── services.py
├── views.py ├── views.py
├── urls.py ├── urls.py
├── apps.py ├── apps.py
└── admin.py └── admin.py
```
## 前端实现
### 公告编辑弹窗
公告编辑使用富文本编辑器,支持图片上传:
```vue
<template>
<BasicModal @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
<!-- 富文本编辑器 -->
<div class="editor-container">
<Toolbar :editor="editorRef" />
<Editor v-model="content" :defaultConfig="editorConfig" />
</div>
</BasicModal>
</template>
```
### 链接列表页
链接列表页使用 BasicTable 组件,支持状态切换和排序:
```vue
<template>
<PageWrapper>
<BasicForm @register="registerSearch" />
<BasicTable @register="registerTable">
<template #tableTitle>
<el-button type="primary" @click="handleAdd">新增链接</el-button>
</template>
</BasicTable>
</PageWrapper>
</template>
```
## 总结
内容管理模块具备以下特点:
```
1. 简单 CRUD:标准增删改查接口,遵循五层架构
2. 公告管理:支持富文本内容、封面图片、类型区分(通知/公告)
3. 链接管理:友情链接,支持 Logo 图片、排序权重、状态管理
4. 统一模式:models/forms/services/views/urls 五层结构
5. XSS 防护:富文本内容自动过滤危险标签
6. 状态管理:支持启用/停用状态切换
```
@@ -0,0 +1,155 @@
:::tip 部门/职级/岗位
部门、职级、岗位是组织架构管理的三个核心模块。部门采用树形结构,职级和岗位为简单 CRUD 模块。用户表通过 `dept_id`、`position_id`、`level_id` 三个外键关联这三个模块。
:::
## 部门管理
### 部门模型
```python
# application/dept/models.py
from application.models import BaseModel
class Dept(BaseModel):
"""部门模型类"""
class Meta:
db_table = 'django_dept'
name = models.CharField(max_length=150, verbose_name='部门名称')
type = models.IntegerField(default=0, verbose_name='部门类型:1-公司 2-子公司 3-部门 4-小组')
parent_id = models.IntegerField(default=0, verbose_name='上级部门ID')
sort = models.IntegerField(default=0, verbose_name='部门排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='备注')
```
### 树形结构
部门通过 `parent_id` 字段形成树形结构:
```
公司总部 (parent_id=0)
+-- 技术部 (parent_id=1)
| +-- 前端组 (parent_id=2)
| +-- 后端组 (parent_id=2)
+-- 市场部 (parent_id=1)
```
### 删除前校验
部门删除时需检查是否存在下级部门或用户引用:
```python
# application/dept/services.py
def delete_dept(request):
data = parse_request_body(request)
dept_id = data.get('id')
# 检查是否存在下级部门
if Dept.objects.filter(parent_id=dept_id, is_delete=False).exists():
return R.failed("存在下级部门,请先删除下级部门")
# 检查是否存在用户引用
if User.objects.filter(dept_id=dept_id, is_delete=False).exists():
return R.failed("存在用户引用该部门,请先调整用户部门")
# 执行删除...
return R.ok(msg="删除成功")
```
## 职级管理
### 职级模型
```python
# application/level/models.py
from application.models import BaseModel
class Level(BaseModel):
"""职级模型类"""
class Meta:
db_table = 'django_level'
name = models.CharField(max_length=255, verbose_name='职级名称')
status = models.IntegerField(default=0, verbose_name='职级状态:1-在用 2-停用')
sort = models.IntegerField(default=0, verbose_name='职级排序')
```
### 表单验证
```python
# application/level/forms.py
from django import forms
from application.level.models import Level
class LevelForm(forms.ModelForm):
class Meta:
model = Level
fields = ['name', 'status', 'sort']
error_messages = {
'name': {
'required': '职级名称不能为空',
'max_length': '职级名称不能超过255个字符',
},
}
```
## 岗位管理
### 岗位模型
```python
# application/position/models.py
from application.models import BaseModel
class Position(BaseModel):
"""岗位模型类"""
class Meta:
db_table = 'django_position'
name = models.CharField(max_length=255, verbose_name='岗位名称')
status = models.IntegerField(default=0, verbose_name='岗位状态:1-在用 2-停用')
sort = models.IntegerField(default=0, verbose_name='岗位排序')
```
## API 接口
### 部门接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/dept/list` | GET | `sys:dept:list` | 部门列表 |
| `/dept/add` | POST | `sys:dept:add` | 新增部门 |
| `/dept/update` | POST | `sys:dept:update` | 编辑部门 |
| `/dept/delete/<int:dept_id>` | POST | `sys:dept:delete` | 删除部门 |
### 职级接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/level/page` | GET | `sys:level:page` | 职级分页列表 |
| `/level/add` | POST | `sys:level:add` | 新增职级 |
| `/level/update` | POST | `sys:level:update` | 编辑职级 |
| `/level/delete/<int:level_id>` | POST | `sys:level:delete` | 删除职级 |
| `/level/status` | POST | `sys:level:status` | 设置状态 |
### 岗位接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/position/page` | GET | `sys:position:page` | 岗位分页列表 |
| `/position/add` | POST | `sys:position:add` | 新增岗位 |
| `/position/update` | POST | `sys:position:update` | 编辑岗位 |
| `/position/delete/<int:position_id>` | POST | `sys:position:delete` | 删除岗位 |
| `/position/status` | POST | `sys:position:status` | 设置状态 |
## 总结
组织架构模块具备以下特点:
```
1. 部门树形:parent_id 形成多级部门结构,删除前校验下级部门和用户引用
2. 职级/岗位:简单 CRUD 模块
3. 表单验证:Django Form + error_messages 中文提示
4. 用户关联:用户表通过 dept_id、position_id、level_id 关联
5. 删除保护:删除前校验引用关系,防止数据悬空
```
@@ -0,0 +1,178 @@
:::tip 定时任务
定时任务模块基于 APScheduler 实现,支持 Cron 表达式调度、任务执行日志、动态启停等功能。包含 job(任务定义)、job_log(执行日志)两个子模块。
:::
## 模块结构
```
application/job/
├── models.py # 任务模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
application/job_log/
├── models.py # 执行日志模型
├── services.py # 日志业务逻辑
└── urls.py # 路由配置
```
## 任务模型
```python
# application/job/models.py
from application.models import BaseModel
class Job(BaseModel):
"""定时任务模型类"""
class Meta:
db_table = 'django_job'
job_name = models.CharField(max_length=255, verbose_name='任务名称')
job_alias = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务别名')
job_group = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务分组')
job_trigger = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务触发器')
status = models.IntegerField(default=0, verbose_name='任务状态:0-未发布 1-运行中 2-暂停 3-删除')
cron_expression = models.CharField(max_length=255, null=True, blank=True, verbose_name='Cron表达式')
execute_policy = models.IntegerField(default=1, verbose_name='执行策略:1-立即执行 2-执行一次 3-放弃执行')
is_sync = models.IntegerField(default=0, verbose_name='是否同步任务:0-否 1-是')
url = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务URL')
params = models.TextField(null=True, blank=True, verbose_name='任务参数')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务备注')
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `job_name` | str | 任务名称(唯一标识) |
| `job_alias` | str | 任务别名(显示用) |
| `job_group` | str | 任务分组(如"系统任务"、"业务任务") |
| `job_trigger` | str | 触发器类型(cron/interval/date) |
| `status` | int | 状态:0-未发布 1-运行中 2-暂停 3-删除 |
| `cron_expression` | str | Cron 表达式 |
| `execute_policy` | int | 执行策略:1-立即执行 2-执行一次 3-放弃执行 |
| `is_sync` | int | 是否同步:0-异步 1-同步 |
| `url` | str | 任务执行的 URL |
| `params` | str | 任务参数(JSON 格式) |
## 执行日志模型
```python
# application/job_log/models.py
from application.models import BaseModel
class JobLog(BaseModel):
"""定时任务日志模型类"""
class Meta:
db_table = 'django_job_log'
job_id = models.IntegerField(default=0, verbose_name='任务ID')
job_name = models.CharField(max_length=255, verbose_name='任务名称')
job_group = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务分组')
job_trigger = models.CharField(max_length=255, null=True, blank=True, verbose_name='任务触发器')
job_message = models.CharField(max_length=500, null=True, blank=True, verbose_name='任务日志信息')
cron_expression = models.CharField(max_length=255, null=True, blank=True, verbose_name='Cron表达式')
status = models.IntegerField(default=0, verbose_name='执行状态:0-正常 1-失败')
start_time = models.DateTimeField(null=True, blank=True, verbose_name='执行开始时间')
end_time = models.DateTimeField(null=True, blank=True, verbose_name='执行结束时间')
consume_time = models.BigIntegerField(default=0, verbose_name='执行耗时(毫秒)')
exception_info = models.TextField(null=True, blank=True, verbose_name='异常信息')
```
## Cron 表达式
| 字段 | 允许值 | 允许的特殊字符 |
|------|--------|---------------|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1-31 | , - * / |
| 月 | 1-12 | , - * / |
| 周 | 0-7 | , - * / |
### 常用示例
```
0 0/5 * * * ? 每5分钟执行
0 0 2 * * ? 每天凌晨2点执行
0 0 0 1 * ? 每月1号零点执行
0 0 * * * ? 每小时执行
0 0 9 * * 1-5 工作日上午9点执行
0 0/30 8-18 * * ? 工作时间每30分钟执行
```
### 特殊字符说明
| 字符 | 说明 | 示例 |
|------|------|------|
| `*` | 所有值 | `* * * * *` 每分钟 |
| `?` | 不指定值(日和周) | `0 0 12 * * ?` |
| `-` | 范围 | `1-5` 表示 1 到 5 |
| `,` | 列表 | `1,3,5` 表示 1、3、5 |
| `/` | 步长 | `0/5` 从 0 开始每 5 |
## 任务状态
```
未发布(0) → 运行中(1) → 暂停(2) → 运行中(1) → 删除(3)
↓
删除(3)
```
| 状态 | 说明 | 可执行操作 |
|------|------|-----------|
| 0-未发布 | 任务已创建但未启动 | 启动、编辑、删除 |
| 1-运行中 | 任务正在调度执行 | 暂停、立即执行、编辑 |
| 2-暂停 | 任务已暂停 | 恢复、编辑、删除 |
| 3-删除 | 任务已删除(逻辑删除) | 无 |
## 执行策略
| 策略 | 说明 | 场景 |
|------|------|------|
| 1-立即执行 | 错过执行时间后立即补执行 | 关键任务(如数据备份) |
| 2-执行一次 | 错过执行时间后只执行一次 | 普通任务 |
| 3-放弃执行 | 错过执行时间后放弃本次执行 | 非关键任务 |
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/job/page` | GET | `sys:job:page` | 任务分页列表 |
| `/job/detail/<int:id>` | GET | `sys:job:detail` | 任务详情 |
| `/job/add` | POST | `sys:job:add` | 新增任务 |
| `/job/update` | PUT | `sys:job:update` | 编辑任务 |
| `/job/delete/<int:id>` | DELETE | `sys:job:delete` | 删除任务 |
| `/job/status` | PUT | `sys:job:status` | 启停任务 |
| `/job/run` | POST | `sys:job:run` | 立即执行 |
| `/job/log/page` | GET | `sys:jobLog:page` | 执行日志 |
## 分布式锁
多实例部署时,定时任务可能重复执行。项目通过 Redis 分布式锁解决:
```
1. 任务触发时,先尝试获取 Redis 锁
2. 获取成功 → 执行任务 → 释放锁
3. 获取失败 → 跳过本次执行(其他实例正在执行)
```
:::tip 锁的 Key
分布式锁的 Key 格式为 `job:lock:{job_id}`,TTL 与任务执行超时时间一致。
:::
## 总结
定时任务模块具备以下特点:
```
1. APScheduler:基于成熟的调度框架,支持 Cron 表达式
2. 动态管理:通过接口动态添加、修改、启停任务
3. 执行日志:自动记录每次执行的开始时间、耗时、状态、异常
4. 并发控制:可配置是否允许并发执行
5. 错过策略:配置错过执行时的处理策略
6. 分布式锁:多实例部署时避免任务重复执行
7. 立即执行:支持手动触发任务立即执行
```
@@ -0,0 +1,120 @@
:::tip 菜单管理
菜单管理是 RBAC 权限系统的基础,负责管理系统的导航菜单和权限节点。支持目录/菜单(type=0)和权限按钮节点(type=1)两种类型,通过树形结构组织。
:::
## 菜单类型
| type | 名称 | 说明 | 用途 |
|------|------|------|------|
| `0` | 目录/菜单 | 页面路由 | 前端侧边栏分组 + 路由生成 |
| `1` | 节点 | 权限按钮 | 控制操作权限 |
## 菜单模型
```python
# application/menu/models.py
from application.models import BaseModel
class Menu(BaseModel):
"""菜单模型类"""
class Meta:
db_table = 'django_menu'
name = models.CharField(max_length=150, verbose_name='菜单名称')
icon = models.CharField(max_length=50, null=True, blank=True, verbose_name='菜单图标')
parent_id = models.IntegerField(default=0, verbose_name='上级ID')
path = models.CharField(max_length=255, null=True, blank=True, verbose_name='菜单路径')
component = models.CharField(max_length=255, null=True, blank=True, verbose_name='菜单组件')
permission = models.CharField(max_length=150, null=True, blank=True, verbose_name='权限节点')
target = models.IntegerField(default=1, verbose_name='打开方式:0-组件 1-内链 2-外链')
type = models.IntegerField(default=0, verbose_name='菜单类型:0-菜单 1-节点')
status = models.IntegerField(default=0, verbose_name='菜单状态:0-正常 1-停用')
hide = models.IntegerField(default=1, verbose_name='是否可见:0-可见 1-不可见')
sort = models.IntegerField(default=0, verbose_name='排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='备注')
```
## 权限节点格式
权限节点采用 `sys:module:action` 三段式命名:
```
1 -> 查询(sys:{module}:page)
5 -> 添加(sys:{module}:add)
10 -> 修改(sys:{module}:update)
15 -> 删除(sys:{module}:delete)
20 -> 详情(sys:{module}:detail)
25 -> 设置状态(sys:{module}:status)
30 -> 批量删除(sys:{module}:batchDelete)
```
## 菜单树构建
使用 children_map 字典实现非递归树构建:
```python
# application/menu/services.py
def get_tree(data, parent_id):
"""获取树状结构(非递归,children_map 分组)"""
children_map = {}
for item in data:
pid = item["parentId"]
if pid not in children_map:
children_map[pid] = []
children_map[pid].append(item)
def build(pid):
result = []
for item in children_map.get(pid, []):
item["children"] = build(item["id"])
result.append(item)
return result
return build(parent_id)
```
## 用户权限查询
权限查询通过 ORM 关联实现:
```python
# application/menu/services.py
def get_user_permissions(user_id):
"""获取用户的权限节点列表"""
if user_id == 1:
# 超管拥有全部权限
menu_list = Menu.objects.filter(type=1, is_delete=False)
else:
# 通过 user -> role -> menu 关联查询
menu_list = Menu.objects.filter(
type=1,
is_delete=False,
status=0,
rolemenu__role__userrole__user_id=user_id,
rolemenu__role__is_delete=False,
).distinct()
return [item.permission for item in menu_list]
```
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/menu/list` | GET | `sys:menu:list` | 菜单列表 |
| `/menu/detail/<int:menu_id>` | GET | `sys:menu:detail` | 菜单详情 |
| `/menu/add` | POST | `sys:menu:add` | 新增菜单 |
| `/menu/update` | POST | `sys:menu:update` | 编辑菜单 |
| `/menu/delete/<int:menu_id>` | POST | `sys:menu:delete` | 删除菜单 |
## 总结
菜单管理模块具备以下特点:
```
1. 树形结构:通过 parent_id 形成多级菜单树,非递归构建
2. 权限节点:type=1 按钮节点,sys:module:action 格式
3. 缓存联动:菜单增删改后清理权限缓存
4. 环路防护:编辑时检测父级不能是自身或下级
5. 级联删除:删除菜单时一并清理权限节点和 role_menu 关联
```
@@ -0,0 +1,138 @@
:::tip 参数管理
参数管理模块用于管理动态系统参数,支持按参数键查询、修改,无需重启服务即可生效。
:::
## 模块结构
```
application/param/
├── models.py # 参数模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
```
## 参数模型
```python
# application/param/models.py
from application.models import BaseModel
class Param(BaseModel):
"""参数模型类"""
class Meta:
db_table = 'django_param'
name = models.CharField(max_length=100, verbose_name='参数名称')
code = models.CharField(max_length=100, verbose_name='参数编码')
value = models.CharField(max_length=255, verbose_name='参数值')
type = models.IntegerField(verbose_name='参数类型:0-系统 1-业务')
status = models.IntegerField(verbose_name='参数状态:1-正常 2-禁用')
sort = models.IntegerField(verbose_name='参数排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='参数备注')
```
## 表单验证
```python
# application/param/forms.py
from django import forms
from application.param.models import Param
class ParamForm(forms.ModelForm):
class Meta:
model = Param
fields = ['name', 'code', 'value', 'type', 'status', 'sort', 'note']
error_messages = {
'name': {
'required': '参数名称不能为空',
},
'code': {
'required': '参数编码不能为空',
},
'value': {
'required': '参数值不能为空',
},
}
```
## 按编码取值
这是参数模块的核心自定义方法,供其他业务模块调用:
```python
# application/param/services.py
def get_value_by_code(code, default=None):
"""根据参数编码获取参数值"""
param = Param.objects.filter(
code=code, status=1, is_delete=False
).first()
return param.value if param else default
```
### 使用示例
```python
from application.param.services import get_value_by_code
# 获取参数值(存在且状态=正常时返回值,否则返回默认值)
default_pwd = get_value_by_code("USER_DEFAULT_PASSWORD", "123456")
upload_limit = get_value_by_code("UPLOAD_MAX_SIZE", "10")
maintenance = get_value_by_code("MAINTENANCE_MODE", "0")
```
:::tip 典型用法
在其他 service 中注入参数值,实现运行时可调的业务配置:
```python
from application.param.services import get_value_by_code
def reset_password(user_id):
# 从参数表读取默认密码,无需硬编码
default_pwd = get_value_by_code("USER_DEFAULT_PASSWORD", "123456")
# ... 重置密码逻辑
```
:::
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|:----|:----|:----|:----|
| `/param/page` | GET | `sys:param:page` | 分页查询 |
| `/param/detail/<int:param_id>` | GET | `sys:param:detail` | 详情查询 |
| `/param/add` | POST | `sys:param:add` | 新增参数 |
| `/param/update` | POST | `sys:param:update` | 编辑参数 |
| `/param/delete/<int:param_id>` | POST | `sys:param:delete` | 删除参数 |
### 查询参数
分页接口支持以下查询参数:
| 参数 | 类型 | 说明 |
|:----|:----|:----|
| `pageNo` | int | 页码,默认 1 |
| `pageSize` | int | 每页条数,默认 10 |
| `name` | string | 参数名称,模糊搜索 |
| `code` | string | 参数编码,模糊搜索 |
| `type` | int | 参数类型(0=系统 1=业务) |
| `status` | int | 参数状态(1=正常 2=禁用) |
## 与系统配置的区别
| 特性 | 参数管理(Param) | 系统配置(Config) |
|:----|:----|:----|
| 用途 | 单个系统参数 | 配置分组 + 配置项 |
| 结构 | 扁平(name+code+value) | 两级(config + config_item) |
| 典型场景 | 默认密码、功能开关 | SMTP 配置、站点信息 |
## 总结
参数管理模块是项目中典型的简单 CRUD 模块,核心特点:
```
1. 按编码取值:get_value_by_code() 供其他业务模块调用
2. 模糊搜索:name/code 模糊搜索,type/status 等值搜索
3. 唯一性校验:name 和 code 双重唯一性
4. 软删除 + 状态过滤:自动过滤已删除和已禁用的参数
```
@@ -0,0 +1,136 @@
:::tip 角色管理
角色是 RBAC 权限模型的核心,连接用户和菜单(权限)。通过为角色分配菜单权限,再将角色分配给用户,实现灵活的权限管理。
:::
## 模块结构
```
application/role/
├── models.py # 角色模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
application/role_menu/
├── models.py # 角色-菜单关联模型
├── services.py # 关联业务逻辑
└── urls.py # 路由配置
```
## 角色模型
```python
# application/role/models.py
from application.models import BaseModel
class Role(BaseModel):
"""角色模型类"""
class Meta:
db_table = 'django_role'
name = models.CharField(max_length=150, verbose_name='角色名称')
code = models.CharField(max_length=30, verbose_name='角色编码')
sort = models.IntegerField(default=0, verbose_name='角色排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='角色备注')
```
## 角色-菜单关联
角色与菜单通过 `django_role_menu` 关联表实现多对多关系:
```python
# application/role_menu/models.py
class RoleMenu(BaseModel):
"""角色菜单关联模型类"""
class Meta:
db_table = 'django_role_menu'
role_id = models.IntegerField(default=0, verbose_name='角色ID')
menu_id = models.IntegerField(default=0, verbose_name='菜单ID')
```
## 自定义 Service
角色模块使用自定义 service 函数,涉及唯一性校验、内置角色保护、关联数据清理等复杂逻辑:
```python
# application/role/services.py
def add_role(request):
data = parse_request_body(request)
form = RoleForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
# 唯一性校验
name = data.get('name')
code = data.get('code')
if Role.objects.filter(name=name, is_delete=False).exists():
return R.failed("角色名称不能重复")
if Role.objects.filter(code=code, is_delete=False).exists():
return R.failed("角色编码不能重复")
Role.objects.create(
name=name,
code=code,
sort=data.get('sort', 0),
note=data.get('note', ''),
create_user=get_realname(request)
)
return R.ok(msg="添加成功")
```
## 删除与缓存清理
角色删除需清理关联的用户角色和角色菜单数据:
```python
# application/role/services.py
def delete_role(request):
data = parse_request_body(request)
role_id = data.get('id')
role = Role.objects.filter(id=role_id, is_delete=False).first()
if not role:
return R.failed("记录不存在")
# 内置角色禁止删除
if role_id in BUILTIN_ROLE_IDS:
return R.failed("内置角色不允许删除")
# 软删除角色
role.is_delete = True
role.save()
# 清理关联数据
UserRole.objects.filter(role_id=role_id).delete()
RoleMenu.objects.filter(role_id=role_id).delete()
return R.ok(msg="删除成功")
```
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/role/page` | GET | `sys:role:page` | 角色分页列表 |
| `/role/list` | GET | `sys:role:list` | 角色列表(无分页) |
| `/role/detail/<int:role_id>` | GET | `sys:role:detail` | 角色详情 |
| `/role/add` | POST | `sys:role:add` | 新增角色 |
| `/role/update` | POST | `sys:role:update` | 编辑角色 |
| `/role/delete/<int:role_id>` | POST | `sys:role:delete` | 删除角色 |
| `/role/menu` | POST | `sys:role:auth` | 分配菜单权限 |
## 总结
角色管理模块具备以下特点:
```
1. RBAC 核心:连接用户和权限的桥梁
2. 双字段唯一性:name + code 均需唯一性校验
3. 内置角色保护:BUILTIN_ROLE_IDS 中的角色禁止删除
4. 级联清理:删除角色时清理 user_role / role_menu 关联
5. 灵活组合:一个角色可分配多个菜单,一个用户可拥有多个角色
```
@@ -0,0 +1,149 @@
:::tip 用户管理
用户管理是系统的核心模块,属于复杂业务模块。包含用户 CRUD、角色关联、密码加密、行政区划等功能。与简单 CRUD 模块不同,用户管理使用自定义 service 函数。
:::
## 模块结构
```
application/user/
├── models.py # 用户模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层(自定义函数)
├── views.py # 视图层
└── urls.py # 路由配置
application/user_role/
├── models.py # 用户-角色关联模型
├── services.py # 关联业务逻辑
└── urls.py # 路由配置
```
## 用户模型
```python
# application/user/models.py
from application.models import BaseModel
class User(BaseModel):
"""用户模型类"""
class Meta:
db_table = 'django_user'
code = models.CharField(max_length=100, default='', verbose_name='用户编码')
realname = models.CharField(max_length=150, verbose_name='用户姓名')
gender = models.IntegerField(default=1, verbose_name='性别:1-男 2-女 3-保密')
avatar = models.CharField(max_length=255, default='', verbose_name='用户头像')
mobile = models.CharField(max_length=30, default='', verbose_name='手机号')
email = models.CharField(max_length=30, default='', verbose_name='邮箱')
dept_id = models.IntegerField(default=0, verbose_name='部门ID')
level_id = models.IntegerField(default=0, verbose_name='职级ID')
position_id = models.IntegerField(default=0, verbose_name='岗位ID')
province_code = models.CharField(max_length=30, default='', verbose_name='省份编码')
city_code = models.CharField(max_length=30, default='', verbose_name='城市编码')
district_code = models.CharField(max_length=30, default='', verbose_name='县区编码')
street_code = models.CharField(max_length=30, default='', verbose_name='街道编码')
city_info = models.CharField(max_length=255, null=True, blank=True, verbose_name='省市区信息')
address = models.CharField(max_length=255, default='', verbose_name='详细地址')
username = models.CharField(max_length=30, null=True, blank=True, verbose_name='用户名')
password = models.CharField(max_length=255, null=True, blank=True, verbose_name='密码')
salt = models.CharField(max_length=30, null=True, blank=True, verbose_name='加密盐')
intro = models.CharField(max_length=255, null=True, blank=True, verbose_name='个人简介')
status = models.IntegerField(default=1, verbose_name='状态:1-正常 2-禁用')
sort = models.IntegerField(default=0, verbose_name='排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='个人备注')
```
## 密码加密
使用 bcrypt 双重加盐方案,由 `utils/password.py` 提供:
```python
# utils/password.py
import secrets
import bcrypt
BCRYPT_ROUNDS = 12
def generate_salt(length=10):
"""生成随机盐值"""
return secrets.token_urlsafe(length)[:length]
def encrypt_password(password, salt):
"""使用 bcrypt 加密密码(外部 salt 双重加盐)"""
combined = (password + salt).encode('utf-8')
hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS))
return hashed.decode('utf-8')
def verify_password(password, salt, hashed):
"""验证密码是否正确"""
combined = (password + salt).encode('utf-8')
return bcrypt.checkpw(combined, hashed.encode('utf-8'))
```
:::tip 双重加盐
密码加密采用双重加盐:外部 salt(存储在 user 表)+ bcrypt 内部 salt。即使数据库泄露,攻击者也无法通过彩虹表破解密码。工作因子 12,单次加密约 250ms。
:::
## 角色关联
用户与角色通过 `django_user_role` 关联表实现多对多关系:
```python
# application/user_role/models.py
class UserRole(BaseModel):
"""用户角色关联模型类"""
class Meta:
db_table = 'django_user_role'
user_id = models.IntegerField(default=0, verbose_name='用户ID')
role_id = models.IntegerField(default=0, verbose_name='角色ID')
```
## 关联数据富化
用户列表和详情需要富化部门、职级、岗位、角色等关联数据:
```python
# application/user/services.py
def _enrich_user_records(records):
"""为用户记录补充关联数据"""
dept_map = {d.id: d.name for d in Dept.objects.filter(is_delete=False)}
level_map = {l.id: l.name for l in Level.objects.filter(is_delete=False)}
position_map = {p.id: p.name for p in Position.objects.filter(is_delete=False)}
for data in records:
data["deptName"] = dept_map.get(data.get("deptId"))
data["levelName"] = level_map.get(data.get("levelId"))
data["positionName"] = position_map.get(data.get("positionId"))
return records
```
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/user/page` | GET | `sys:user:page` | 用户分页列表 |
| `/user/detail/<int:user_id>` | GET | `sys:user:detail` | 用户详情 |
| `/user/add` | POST | `sys:user:add` | 新增用户 |
| `/user/update` | POST | `sys:user:update` | 编辑用户 |
| `/user/delete/<int:user_id>` | POST | `sys:user:delete` | 删除用户 |
| `/user/status` | POST | `sys:user:status` | 设置状态 |
| `/user/resetPwd` | POST | `sys:user:resetPwd` | 重置密码 |
:::warning 内置超管保护
用户 ID 为 1 的内置超管账号禁止通过管理接口修改、删除、禁用、重置密码。这些操作会在 service 层提前拦截并返回失败提示。
:::
## 总结
用户管理模块具备以下特点:
```
1. 复杂业务:自定义 service 函数,处理关联数据
2. 密码安全:bcrypt 双重加盐,工作因子12
3. 角色关联:多对多关系,通过 user_role 关联表
4. 关联富化:列表/详情补充部门、职级、岗位、角色等展示名称
5. 行政区划:省/市/区/街道四级编码
6. 超管保护:ID=1 账号禁止修改/删除/禁用/重置密码
```