签入版本
This commit is contained in:
@@ -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 账号禁止修改/删除/禁用/重置密码
|
||||
```
|
||||
Reference in New Issue
Block a user