签入版本
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# API响应规范
|
||||
|
||||
## 概述
|
||||
|
||||
统一使用 `R` 模块封装 API 响应,所有接口返回一致的 JSON 格式,便于前端统一处理。
|
||||
|
||||
:::tip 核心原则
|
||||
- 所有接口必须通过 `R.ok()` 或 `R.failed()` 返回,禁止直接返回 dict
|
||||
- 成功响应 `code=0`,失败响应 `code=1`
|
||||
- 响应体始终包含 `code`、`data`、`msg`、`ok` 四个字段
|
||||
:::
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": { ... },
|
||||
"msg": "操作成功",
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
### 失败响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": null,
|
||||
"msg": "错误信息",
|
||||
"ok": false
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"records": [ ... ],
|
||||
"total": 100,
|
||||
"size": 10,
|
||||
"current": 1,
|
||||
"pages": 10
|
||||
},
|
||||
"msg": "操作成功",
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
## R 模块使用方法
|
||||
|
||||
### R.ok() 成功响应
|
||||
|
||||
```python
|
||||
from utils import R
|
||||
|
||||
# 基础成功响应
|
||||
return R.ok()
|
||||
|
||||
# 带数据的成功响应
|
||||
return R.ok(data=user_info)
|
||||
|
||||
# 自定义提示消息
|
||||
return R.ok(msg='添加成功')
|
||||
|
||||
# 附加额外字段(如 count)
|
||||
return R.ok(data=result, count=total)
|
||||
```
|
||||
|
||||
### R.failed() 失败响应
|
||||
|
||||
```python
|
||||
# 基础失败响应
|
||||
return R.failed()
|
||||
|
||||
# 自定义错误消息
|
||||
return R.failed('岗位名称不能重复')
|
||||
|
||||
# 自定义错误码和消息
|
||||
return R.failed(msg='未授权', code=401)
|
||||
```
|
||||
|
||||
## 字段定义
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `code` | int | 状态码:0=成功,1=失败 |
|
||||
| `data` | any/null | 响应数据,失败时为 null |
|
||||
| `msg` | string | 提示消息 |
|
||||
| `ok` | boolean | 是否成功 |
|
||||
|
||||
## 状态码规范
|
||||
|
||||
| 状态码 | 含义 | 使用场景 |
|
||||
|--------|------|---------|
|
||||
| 0 | 成功 | 操作成功 |
|
||||
| 1 | 失败 | 业务逻辑失败(如参数校验、唯一性冲突) |
|
||||
|
||||
:::tip 错误码与 HTTP 状态码
|
||||
业务错误码(code 字段)始终为 0 或 1,HTTP 状态码始终为 200。401/403 等仅在中间件中使用,常规业务错误统一返回 `code=1`。
|
||||
:::
|
||||
|
||||
## 在 View 中使用
|
||||
|
||||
```python
|
||||
from utils import R
|
||||
|
||||
# 查询详情:service 返回 dict,view 包 R.ok
|
||||
class ExampleDetailView(PermissionRequired, View):
|
||||
def get(self, request, id):
|
||||
result = services.get_example_detail(id)
|
||||
return R.ok(data=result)
|
||||
|
||||
# 添加记录:service 直接返回 R 对象
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
def post(self, request):
|
||||
return services.add_example(request)
|
||||
|
||||
# 删除记录:service 直接返回 R 对象
|
||||
class ExampleDeleteView(PermissionRequired, View):
|
||||
def delete(self, request, id):
|
||||
return services.delete_examples(id)
|
||||
```
|
||||
|
||||
:::tip 响应封装层级
|
||||
- **分页/列表查询**:service 返回 `R.ok(data=page_data)`
|
||||
- **详情查询**:service 返回 dict/None,view 包 `R.ok(data=result)`
|
||||
- **写操作(增/删/改/状态)**:service 直接返回 `R` 对象
|
||||
:::
|
||||
|
||||
## Django 表单验证错误响应
|
||||
|
||||
Django 表单验证错误由 `regular.get_err(form)` 转换为标准格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": null,
|
||||
"msg": "案例名称不能为空",
|
||||
"ok": false
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
API 响应规范通过 `R` 模块统一封装:`R.ok()` 返回成功、`R.failed()` 返回失败。所有响应保持 `{code, data, msg, ok}` 四字段结构,HTTP 状态码始终为 200。业务层错误统一用 `code=1`,前端只需判断 `ok` 字段即可。
|
||||
@@ -0,0 +1,107 @@
|
||||
# 代码审查清单
|
||||
|
||||
## 概述
|
||||
|
||||
代码审查(Code Review)是保障代码质量的重要环节。本文提供标准化的审查清单,覆盖安全、性能、规范、边界等维度,帮助审查者系统性地检查代码质量。
|
||||
|
||||
:::tip 审查原则
|
||||
- 每次提交必须经过至少一人审查
|
||||
- 审查关注点:正确性 > 安全性 > 可维护性 > 性能
|
||||
- 审查意见应具体、可操作,避免主观评价
|
||||
:::
|
||||
|
||||
## 安全检查
|
||||
|
||||
- [ ] **DEBUG 关闭**:生产配置 `DJANGO_DEBUG=False`
|
||||
- [ ] **JWT 安全**:`JWT_SALT` 使用强密码,未硬编码在代码中
|
||||
- [ ] **CORS 配置**:未使用 `CORS_ALLOW_ORIGINS = ['*']`,仅列出信任域名
|
||||
- [ ] **SQL 注入**:使用 Django ORM 参数化查询,禁止字符串拼接 SQL
|
||||
- [ ] **权限校验**:写操作接口有 `PermissionRequired` mixin 和 `DJANGO_DEMO` 检查
|
||||
- [ ] **敏感信息**:日志中不记录密码、token、密钥等敏感信息
|
||||
- [ ] **文件上传**:有文件类型白名单验证,文件名已重命名
|
||||
- [ ] **输入校验**:所有用户输入通过 Django Form 校验
|
||||
- [ ] **.env 安全**:`.env` 文件未提交到 Git(在 `.gitignore` 中)
|
||||
- [ ] **密码存储**:使用 `make_password`/`check_password` 哈希存储,禁止明文
|
||||
|
||||
## 性能检查
|
||||
|
||||
- [ ] **N+1 查询**:关联查询使用 `select_related`/`prefetch_related`,避免循环查询
|
||||
- [ ] **索引覆盖**:WHERE/ORDER BY 字段已添加索引(`db_index=True`)
|
||||
- [ ] **分页查询**:大列表接口使用 Django Paginator,禁止全表查询返回
|
||||
- [ ] **SELECT ***:只查询需要的字段,避免 `SELECT *`
|
||||
- [ ] **缓存使用**:热点数据使用 Redis 缓存(`django_redis`)
|
||||
- [ ] **批量操作**:批量插入/更新使用 `bulk_create`/`update`,非逐条执行
|
||||
- [ ] **连接池**:数据库连接池大小合理,不超过数据库 max_connections
|
||||
|
||||
## 代码规范检查
|
||||
|
||||
- [ ] **装饰器顺序**:`@method_decorator(check_login)` → `PermissionRequired` → `@operation_log`
|
||||
- [ ] **命名规范**:文件名 snake_case,类名 PascalCase,函数名 snake_case
|
||||
- [ ] **注释规范**:关键逻辑有注释说明,docstring 描述函数用途
|
||||
- [ ] **导入规范**:导入分组(标准库 → 第三方 → 本地模块),无未使用的导入
|
||||
- [ ] **R 封装**:所有接口通过 `R.ok()`/`R.failed()` 返回,禁止直接返回 dict
|
||||
- [ ] **删除规范**:使用 `is_delete=True` 软删除,禁止物理删除
|
||||
- [ ] **查询过滤**:所有查询加 `is_delete=False` 条件
|
||||
|
||||
## 边界条件检查
|
||||
|
||||
- [ ] **空值处理**:可选字段支持 None 值,不抛异常
|
||||
- [ ] **空列表**:批量操作传入空列表时安全返回
|
||||
- [ ] **超大输入**:字符串长度限制合理(`max_length`)
|
||||
- [ ] **数值范围**:数值字段有 `min_value`/`max_value` 约束
|
||||
- [ ] **ID 有效性**:ID 参数校验 > 0,兼容字符串 ID
|
||||
- [ ] **重复操作**:重复添加/删除同一数据的幂等性
|
||||
- [ ] **并发安全**:并发修改同一记录的处理(乐观锁/唯一性校验)
|
||||
- [ ] **软删除过滤**:查询方法自动过滤 `is_delete=False`
|
||||
|
||||
## 数据库检查
|
||||
|
||||
- [ ] **模型定义**:继承 `BaseModel`(`application/models.py`)
|
||||
- [ ] **表名前缀**:`db_table = get_table_name('表名')`
|
||||
- [ ] **字段注释**:`db_comment` 参数描述字段用途
|
||||
- [ ] **索引定义**:常用查询字段有索引(`db_index=True`)
|
||||
- [ ] **默认值**:`db_default` 与数据库默认值一致
|
||||
- [ ] **软删除**:不使用物理删除,统一软删除(`is_delete`)
|
||||
|
||||
## API 接口检查
|
||||
|
||||
- [ ] **HTTP 方法**:查询用 GET,新增用 POST,编辑用 PUT,删除用 DELETE
|
||||
- [ ] **权限标识**:格式 `sys:{module}:{action}`
|
||||
- [ ] **请求参数**:Django Form 定义完整约束和中文错误提示
|
||||
- [ ] **响应格式**:统一使用 `R.ok()`/`R.failed()`
|
||||
- [ ] **错误提示**:错误消息清晰、用户友好(中文)
|
||||
- [ ] **路由注册**:新模块已在 `application/urls.py` 中注册
|
||||
|
||||
## 前端检查
|
||||
|
||||
- [ ] **API 函数命名**:`getXxxPage`/`addXxx`/`updateXxx`/`deleteXxx`
|
||||
- [ ] **错误处理**:响应拦截器统一处理错误
|
||||
- [ ] **loading 状态**:数据加载时显示 loading
|
||||
- [ ] **确认弹窗**:删除操作有二次确认
|
||||
- [ ] **表单校验**:提交前前端校验必填项
|
||||
|
||||
## 提交检查
|
||||
|
||||
- [ ] **提交信息**:遵循 Conventional Commits 格式
|
||||
- [ ] **变更范围**:每次提交只做一件事
|
||||
- [ ] **测试覆盖**:新功能/修复有对应测试
|
||||
- [ ] **文档更新**:接口变更同步更新文档
|
||||
|
||||
## 审查意见模板
|
||||
|
||||
```
|
||||
[严重] 安全问题:SQL 拼接存在注入风险,应使用 Django ORM 参数化查询
|
||||
[建议] 性能优化:此处可用 select_related 避免 N+1 查询
|
||||
[疑问] 这里的 try-except 吞掉了异常,是否需要记录日志?
|
||||
[肯定] 使用 R.ok() 统一封装响应,符合规范
|
||||
```
|
||||
|
||||
:::tip 审查态度
|
||||
- 区分「必须修改」和「建议优化」
|
||||
- 给出具体的修改方案,而非只指出问题
|
||||
- 对好的实践给予肯定
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
代码审查清单覆盖安全、性能、规范、边界、数据库、API、前端、提交等 8 个维度。审查时按清单逐项检查,区分严重问题和建议优化。核心关注点:权限校验是否完整、SQL 是否安全、响应格式是否统一、软删除是否正确。审查意见应具体可操作,帮助团队持续提升代码质量。
|
||||
@@ -0,0 +1,258 @@
|
||||
# 代码风格与 Lint 规范
|
||||
|
||||
:::tip 说明
|
||||
统一的代码风格是团队协作的基础。本文档介绍项目的代码风格规范和 Lint 工具配置。
|
||||
:::
|
||||
|
||||
## Python 代码风格
|
||||
|
||||
### 基本规范(PEP 8)
|
||||
|
||||
```python
|
||||
# ✅ 正确
|
||||
def get_user_page(request):
|
||||
"""查询用户分页数据"""
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
|
||||
query = User.objects.filter(is_delete=False)
|
||||
query = query.order_by('-create_time')
|
||||
|
||||
return R.ok(data=page_data)
|
||||
|
||||
# ❌ 错误
|
||||
def GetUserPage(request):
|
||||
pageNo=int(request.GET.get('pageNo',1))
|
||||
query=User.objects.filter(is_delete=False)
|
||||
return R.ok(data=page_data)
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 文件名 | snake_case | `user_service.py` |
|
||||
| 类名 | PascalCase | `UserAddView` |
|
||||
| 函数名 | snake_case | `get_user_page` |
|
||||
| 变量名 | snake_case | `page_no` |
|
||||
| 常量名 | UPPER_SNAKE_CASE | `PAGE_SIZE` |
|
||||
| 私有函数 | _前缀 | `_apply_filters` |
|
||||
|
||||
### 导入规范
|
||||
|
||||
```python
|
||||
# 1. 标准库
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
# 2. 第三方库
|
||||
from django.db import models
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
# 3. 本地模块
|
||||
from application.user import forms
|
||||
from utils import R
|
||||
from utils.common import parse_request_body
|
||||
```
|
||||
|
||||
### 注释规范
|
||||
|
||||
```python
|
||||
def add_user(request):
|
||||
"""
|
||||
添加用户
|
||||
|
||||
处理流程:
|
||||
1. 解析请求体中的JSON数据
|
||||
2. 表单验证
|
||||
3. 密码加密
|
||||
4. 创建数据库记录
|
||||
|
||||
Args:
|
||||
request: HttpRequest对象
|
||||
|
||||
Returns:
|
||||
R: 操作结果
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Ruff 配置
|
||||
|
||||
项目推荐使用 [Ruff](https://docs.astral.sh/ruff/) 进行 Python 代码检查和格式化:
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["application", "utils", "config", "middleware"]
|
||||
```
|
||||
|
||||
### 使用命令
|
||||
|
||||
```bash
|
||||
# 检查代码
|
||||
ruff check .
|
||||
|
||||
# 自动修复
|
||||
ruff check --fix .
|
||||
|
||||
# 格式化
|
||||
ruff format .
|
||||
```
|
||||
|
||||
## 前端代码风格
|
||||
|
||||
### ESLint 配置
|
||||
|
||||
项目使用 ESLint 进行 TypeScript/Vue 代码检查:
|
||||
|
||||
```javascript
|
||||
// .eslintrc.cjs
|
||||
module.exports = {
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier',
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Prettier 配置
|
||||
|
||||
```json
|
||||
// .prettierrc
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
```
|
||||
|
||||
### Stylelint 配置
|
||||
|
||||
```javascript
|
||||
// .stylelintrc.cjs
|
||||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard-scss',
|
||||
'stylelint-config-recommended-vue/scss',
|
||||
],
|
||||
rules: {
|
||||
'selector-class-pattern': null,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 使用命令
|
||||
|
||||
```bash
|
||||
# ESLint 检查
|
||||
pnpm lint:eslint
|
||||
|
||||
# ESLint 自动修复
|
||||
pnpm lint:eslint --fix
|
||||
|
||||
# Prettier 格式化
|
||||
pnpm lint:prettier
|
||||
|
||||
# Stylelint 检查
|
||||
pnpm lint:stylelint
|
||||
|
||||
# 全部检查
|
||||
pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint
|
||||
```
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
使用 pre-commit 在提交前自动检查代码:
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v9.0.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: \.(js|ts|vue)$
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: \.(js|ts|vue|css|scss)$
|
||||
```
|
||||
|
||||
### 安装 pre-commit
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## IDE 配置推荐
|
||||
|
||||
### VS Code
|
||||
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 推荐插件
|
||||
|
||||
```
|
||||
Python:
|
||||
- Ruff (Python linter/formatter)
|
||||
- Python (Microsoft)
|
||||
|
||||
前端:
|
||||
- ESLint
|
||||
- Prettier
|
||||
- Vue - Official
|
||||
- TypeScript Vue Plugin (Volar)
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
代码风格规范涵盖 Python(PEP 8 + Ruff)和前端(ESLint + Prettier + Stylelint)两个维度。通过 pre-commit hooks 在提交前自动检查,确保代码风格一致性。IDE 配置推荐实现保存时自动格式化。
|
||||
@@ -0,0 +1,222 @@
|
||||
# 删除规范
|
||||
|
||||
## 概述
|
||||
|
||||
统一使用软删除机制,通过 `is_delete` 字段标记记录状态,不物理删除数据。删除操作支持逗号分隔的多 ID 批量删除。
|
||||
|
||||
:::tip 软删除约定
|
||||
- `is_delete = False`(0):正常记录(默认值)
|
||||
- `is_delete = True`(1):已删除记录
|
||||
- 所有查询方法自动过滤 `is_delete=False`,业务层无需关心
|
||||
:::
|
||||
|
||||
## 软删除 vs 硬删除
|
||||
|
||||
| 维度 | 软删除 | 硬删除 |
|
||||
|------|--------|--------|
|
||||
| 实现方式 | 更新 `is_delete=True` | `DELETE FROM table` |
|
||||
| 数据可恢复 | ✅ 可恢复 | ❌ 不可恢复 |
|
||||
| 查询影响 | 需加 `is_delete=False` | 数据已不存在 |
|
||||
| 存储空间 | 占用(数据仍在表中) | 释放 |
|
||||
| 适用场景 | 业务数据(用户、订单等) | 日志、临时数据 |
|
||||
| 项目约定 | ✅ 统一使用 | ❌ 禁止使用 |
|
||||
|
||||
## BaseModel 公共字段
|
||||
|
||||
所有业务模型继承 `BaseModel`,自动包含 `is_delete` 字段:
|
||||
|
||||
```python
|
||||
# application/models.py
|
||||
class BaseModel(models.Model):
|
||||
"""基础模型类"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
create_user = models.CharField(max_length=50, null=True, blank=True)
|
||||
create_time = models.DateTimeField(auto_now_add=True)
|
||||
update_user = models.CharField(max_length=50, null=True, blank=True)
|
||||
update_time = models.DateTimeField(auto_now=True)
|
||||
is_delete = models.IntegerField(default=0, db_comment='是否删除:0-正常 1-已删除')
|
||||
```
|
||||
|
||||
## 删除接口
|
||||
|
||||
### URL 格式
|
||||
|
||||
```
|
||||
DELETE /{module}/delete/{id}
|
||||
```
|
||||
|
||||
支持逗号分隔的多 ID 删除:
|
||||
|
||||
```
|
||||
DELETE /example/delete/1,2,3
|
||||
```
|
||||
|
||||
### View 示例
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="delete")
|
||||
class ExampleDeleteView(PermissionRequired, View):
|
||||
"""删除案例视图"""
|
||||
permission_required = ('sys:example:delete',)
|
||||
|
||||
@operation_log(title="案例管理-删除记录", log_type=LogType.DELETE)
|
||||
def delete(self, request, id):
|
||||
# 演示环境禁止操作
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
|
||||
result = services.delete_examples(id)
|
||||
return result
|
||||
```
|
||||
|
||||
## Service 实现
|
||||
|
||||
```python
|
||||
def delete_examples(instance_ids):
|
||||
"""删除案例(批量删除)"""
|
||||
if not instance_ids:
|
||||
return R.failed("记录ID不存在")
|
||||
|
||||
try:
|
||||
# 解析ID列表
|
||||
id_list = [int(id_str.strip()) for id_str in instance_ids.split(',') if id_str.strip()]
|
||||
if not id_list:
|
||||
return R.failed("无效的记录ID")
|
||||
|
||||
# 批量更新删除标识(逻辑删除)
|
||||
updated_count = Example.objects.filter(
|
||||
id__in=id_list,
|
||||
is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
return R.ok(msg=f"本次共删除{updated_count}条数据")
|
||||
except ValueError:
|
||||
return R.failed("记录ID格式错误")
|
||||
except Exception as e:
|
||||
logging.error(f"删除案例异常,IDs: {instance_ids}, 错误: {str(e)}")
|
||||
return R.failed(msg="删除失败,请稍后重试")
|
||||
```
|
||||
|
||||
:::tip 返回值说明
|
||||
`delete` 方法返回实际软删除的条数。如果传入的 ID 中包含已删除或不存在的记录,返回值会小于入参数量。
|
||||
:::
|
||||
|
||||
## 删除流程
|
||||
|
||||
```
|
||||
1. View 接收请求(id 参数,逗号分隔)
|
||||
↓
|
||||
2. 演示模式检查(DJANGO_DEMO)
|
||||
↓(拦截则返回 R.failed)
|
||||
3. Service.delete_examples(id)
|
||||
↓
|
||||
4. 解析 ID 列表(逗号分隔转 list)
|
||||
↓
|
||||
5. ORM 批量更新 is_delete=True
|
||||
↓
|
||||
6. 返回 R.ok(msg="本次共删除N条数据")
|
||||
```
|
||||
|
||||
## 查询过滤
|
||||
|
||||
所有查询方法必须过滤 `is_delete=False`:
|
||||
|
||||
```python
|
||||
# 分页查询
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
|
||||
# 详情查询
|
||||
instance = Example.objects.filter(is_delete=False, id=id).first()
|
||||
|
||||
# 列表查询
|
||||
queryset = Example.objects.filter(is_delete=False, status=1)
|
||||
|
||||
# 带筛选条件的查询
|
||||
query = Example.objects.filter(is_delete=False, name__contains=name)
|
||||
```
|
||||
|
||||
:::warning 常见错误
|
||||
忘记加 `is_delete=False` 是最常见的 bug,会导致已删除的数据重新出现在列表中。建议在代码审查时重点检查。
|
||||
:::
|
||||
|
||||
## 级联删除处理
|
||||
|
||||
当模块之间存在关联关系时,删除父记录需要同步处理子记录:
|
||||
|
||||
```python
|
||||
def delete_role(role_ids):
|
||||
"""删除角色(同步清理关联数据)"""
|
||||
id_list = [int(id_str.strip()) for id_str in role_ids.split(',')]
|
||||
|
||||
# 1. 软删除角色
|
||||
updated_count = Role.objects.filter(
|
||||
id__in=id_list, is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
# 2. 同步软删除角色菜单关联
|
||||
RoleMenu.objects.filter(
|
||||
role_id__in=id_list, is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
# 3. 同步软删除用户角色关联
|
||||
UserRole.objects.filter(
|
||||
role_id__in=id_list, is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
return R.ok(msg=f"本次共删除{updated_count}条数据")
|
||||
```
|
||||
|
||||
## 数据恢复
|
||||
|
||||
软删除的数据可以通过恢复接口重新启用:
|
||||
|
||||
```python
|
||||
def restore_examples(instance_ids):
|
||||
"""恢复已删除的案例"""
|
||||
id_list = [int(id_str.strip()) for id_str in instance_ids.split(',')]
|
||||
|
||||
updated_count = Example.objects.filter(
|
||||
id__in=id_list, is_delete=True
|
||||
).update(is_delete=False)
|
||||
|
||||
return R.ok(msg=f"本次共恢复{updated_count}条数据")
|
||||
```
|
||||
|
||||
:::tip 恢复场景
|
||||
数据恢复通常用于:
|
||||
1. 误删除后的数据恢复
|
||||
2. 回收站功能
|
||||
3. 数据审计
|
||||
:::
|
||||
|
||||
## 权限标识
|
||||
|
||||
| 操作 | 权限标识 | 说明 |
|
||||
|------|---------|------|
|
||||
| 删除 | `sys:{module}:delete` | 删除记录(支持批量) |
|
||||
|
||||
## 常见错误提示
|
||||
|
||||
| 提示 | 原因 |
|
||||
|------|------|
|
||||
| "记录ID不存在" | 入参为空 |
|
||||
| "无效的记录ID" | ID 格式错误或全部无效 |
|
||||
| "记录ID格式错误" | ID 包含非数字字符 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
```
|
||||
1. 统一软删除:所有业务数据使用 is_delete 字段,禁止物理删除
|
||||
2. 查询过滤:所有查询加 is_delete=False,避免遗漏
|
||||
3. 级联处理:删除父记录时同步处理关联的子记录
|
||||
4. 批量删除:支持逗号分隔的多 ID,提高操作效率
|
||||
5. 错误提示:返回明确的错误信息,便于排查
|
||||
6. 操作日志:删除操作记录操作日志,便于审计
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
删除规范统一使用软删除(`is_delete` 标记),支持逗号分隔的多 ID 批量删除。Service 层解析 ID 列表后通过 ORM 批量更新 `is_delete=True`。所有查询方法自动过滤 `is_delete=False`,业务层无需关心软删除逻辑。级联删除时需同步处理关联数据,保证数据一致性。
|
||||
@@ -0,0 +1,181 @@
|
||||
# 文档规范
|
||||
|
||||
## 概述
|
||||
|
||||
良好的文档是项目可维护性的基石。本文规范 API 文档、Form 文档、代码注释等文档的编写标准。
|
||||
|
||||
:::tip 文档原则
|
||||
- 文档与代码同步更新
|
||||
- 使用中文编写(项目约定)
|
||||
- 示例代码可直接运行
|
||||
:::
|
||||
|
||||
## API 接口文档
|
||||
|
||||
Django 项目通过代码注释和文档站点提供 API 文档。所有接口定义在各模块的 `urls.py` 中,视图类的 docstring 描述接口用途。
|
||||
|
||||
### View 文档注释
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="post")
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
"""
|
||||
添加案例视图
|
||||
|
||||
处理POST请求,创建新的案例记录。
|
||||
|
||||
URL: /example/add
|
||||
请求方法: POST
|
||||
权限要求:sys:example:add
|
||||
|
||||
请求参数(JSON Body):
|
||||
- name: 案例名称(必填,1-100字符)
|
||||
- avatar: 案例图片(可选)
|
||||
- type: 案例类型(必填,1-4)
|
||||
- status: 案例状态(必填,1-正常 2-禁用)
|
||||
- sort: 排序权重(必填)
|
||||
"""
|
||||
permission_required = ("sys:example:add",)
|
||||
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
def post(self, request):
|
||||
result = services.add_example(request)
|
||||
return result
|
||||
```
|
||||
|
||||
:::tip 文档层级
|
||||
- 类 docstring:接口说明(URL、方法、权限、参数)
|
||||
- 方法 docstring:处理流程说明
|
||||
- 行内注释:关键逻辑说明
|
||||
:::
|
||||
|
||||
## Form 文档
|
||||
|
||||
Django ModelForm 通过 `error_messages` 参数定义中文错误提示,通过 `help_text` 和 `verbose_name` 提供字段说明:
|
||||
|
||||
```python
|
||||
class ExampleForm(forms.ModelForm):
|
||||
"""案例表单验证类"""
|
||||
|
||||
# 案例名称(必填,1-100字符)
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空',
|
||||
'max_length': '案例名称长度不得超过100个字符',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例类型(1-类型1 2-类型2 3-类型3 4-类型4)
|
||||
type = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
error_messages={
|
||||
'required': '案例类型不能为空',
|
||||
'min_value': '案例类型值不能小于1',
|
||||
'max_value': '案例类型值不能大于4',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Example
|
||||
fields = ['name', 'avatar', 'type', 'status', 'sort']
|
||||
```
|
||||
|
||||
:::tip error_messages 规范
|
||||
- 必须填写所有验证规则的中文错误提示
|
||||
- 枚举值说明使用 `值-含义` 格式(如 `1-正常 2-禁用`)
|
||||
- 有约束条件时在描述中说明(如 `1-100字符`)
|
||||
:::
|
||||
|
||||
## 代码注释规范
|
||||
|
||||
### 模块注释
|
||||
|
||||
```python
|
||||
"""
|
||||
案例管理服务模块
|
||||
=============================================================
|
||||
|
||||
该模块实现了案例管理相关的所有业务逻辑。
|
||||
包括案例的分页查询、详情查询、添加、更新、删除等功能。
|
||||
"""
|
||||
```
|
||||
|
||||
### 方法注释
|
||||
|
||||
```python
|
||||
def get_example_page(request):
|
||||
"""
|
||||
查询案例分页数据
|
||||
|
||||
Args:
|
||||
request: HttpRequest对象,包含查询参数
|
||||
- pageNo: 当前页码(可选,默认为1)
|
||||
- pageSize: 每页记录数(可选,默认为PAGE_SIZE)
|
||||
- name: 案例名称(可选,模糊查询)
|
||||
|
||||
Returns:
|
||||
R: 包含分页数据的JSON响应
|
||||
"""
|
||||
```
|
||||
|
||||
### 行内注释
|
||||
|
||||
```python
|
||||
# 构建基础查询(只查询未删除的记录)
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
```
|
||||
|
||||
:::tip 注释原则
|
||||
- 解释"为什么"而非"是什么"
|
||||
- 复杂逻辑必须注释
|
||||
- 简单代码无需注释
|
||||
- 注释与代码同步更新
|
||||
:::
|
||||
|
||||
## 文档目录结构
|
||||
|
||||
```
|
||||
wiki/ # VitePress 文档站
|
||||
├── zh/ # 中文文档
|
||||
│ ├── 1. 了解项目/ # introduction.md, why.md, struct.md, course.md
|
||||
│ ├── 2. 快速入门/ # volume1/(环境、启动、数据库、Docker)
|
||||
│ ├── 3. 模块开发实战/ # volume2/(Model→Form→Service→View→URL→前端)
|
||||
│ ├── 4. 架构设计/ # volume3/(分层、基类、缓存、安全)
|
||||
│ ├── 5. 开发指南/ # volume4/
|
||||
│ │ ├── 5.1 后端核心功能/ # core/(JWT、RBAC、字典、日志、限流)
|
||||
│ │ ├── 5.2 后端业务模块/ # business/(用户、角色、菜单、文章、任务)
|
||||
│ │ ├── 5.3 后端通用工具/ # tools/(上传、Excel、IP/UA、密码、富文本)
|
||||
│ │ ├── 5.4 前端开发/ # frontend/(组件、路由、Store、构建)
|
||||
│ │ └── 5.5 前后端联调/ # integration/(代理、权限、上传、字典)
|
||||
│ ├── 6. 代码生成器/ # volume5/(CLI、Web UI、模板)
|
||||
│ ├── 7. 运维部署/ # volume6/(Docker、Nginx、Supervisor、监控)
|
||||
│ └── 8. 规范标准/ # volume7/(API 响应、分页、Git、测试、文档)
|
||||
```
|
||||
|
||||
## 前端文档结构
|
||||
|
||||
前端相关文档分布在以下位置:
|
||||
|
||||
| 文档 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 前端页面规范 | `volume7/fe-component.md` | 页面结构、组件使用、命名规范 |
|
||||
| 前端 API 规范 | `volume7/fe-api.md` | API 文件组织、函数命名、请求规范 |
|
||||
| 公共组件库 | `volume4/frontend/components.md` | 组件列表、Props、Events、用法 |
|
||||
| 前端构建 | `volume4/frontend/build.md` | Vite 配置、环境变量、Nginx 部署 |
|
||||
| API 层开发 | `volume4/frontend/api.md` | API 层开发指南 |
|
||||
| 页面视图开发 | `volume4/frontend/view.md` | 页面视图开发指南 |
|
||||
|
||||
## 文档更新检查清单
|
||||
|
||||
- [ ] 新增/修改 API 接口时,同步更新 View 类的 docstring
|
||||
- [ ] 新增/修改 Form 字段时,填写 `error_messages` 中文提示
|
||||
- [ ] 复杂逻辑添加代码注释
|
||||
- [ ] 重大变更更新 wiki 文档
|
||||
|
||||
## 总结
|
||||
|
||||
文档规范覆盖 API 文档(View docstring)、Form 文档(error_messages)、代码注释(文件头/模块/方法/行内)。核心原则:文档与代码同步更新,使用中文编写,示例可直接运行。重大变更同步更新 wiki 文档。
|
||||
@@ -0,0 +1,305 @@
|
||||
# 前端 API 规范
|
||||
|
||||
## 概述
|
||||
|
||||
前端 API 层封装所有 HTTP 请求,与后端接口一一对应。API 文件位于 `ui/src/api/` 目录,按模块分组,使用封装后的 `VAxios` 发送请求。
|
||||
|
||||
:::tip 设计原则
|
||||
- 每个后端模块对应一个 API 文件
|
||||
- 函数命名统一使用 `名词 + 动词` 格式(与后端保持一致)
|
||||
- 请求路径使用相对路径(`/position/page`),不含 `/api/v1` 前缀(由 Axios baseURL 统一拼接)
|
||||
- 统一通过 `http.request()` 发送请求
|
||||
:::
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ui/src/api/
|
||||
├── common/ # 公共 API(上传、字典按编码查询、城市、模板下载)
|
||||
│ ├── index.ts
|
||||
│ ├── menu.ts
|
||||
│ ├── user.ts
|
||||
│ └── city.ts
|
||||
├── system/ # 系统模块
|
||||
│ ├── user.ts
|
||||
│ ├── role.ts
|
||||
│ ├── menu.ts
|
||||
│ ├── dept.ts
|
||||
│ ├── position.ts
|
||||
│ ├── level.ts
|
||||
│ ├── dictionary.ts
|
||||
│ ├── loginLog.ts
|
||||
│ ├── operLog.ts
|
||||
│ └── tenant.ts
|
||||
├── data/ # 数据模块
|
||||
│ ├── param.ts
|
||||
│ ├── config.ts
|
||||
│ ├── dictionary.ts
|
||||
│ ├── notice.ts
|
||||
│ └── city.ts
|
||||
├── content/ # 内容模块
|
||||
│ ├── article.ts
|
||||
│ ├── category.ts
|
||||
│ └── link.ts
|
||||
├── monitor/ # 监控模块
|
||||
│ └── job.ts
|
||||
├── file/ # 文件模块
|
||||
│ └── fileTemplate.ts
|
||||
├── tool/ # 工具模块
|
||||
│ ├── generator.ts
|
||||
│ └── example.ts
|
||||
├── setting/ # 设置模块
|
||||
│ ├── profile.ts
|
||||
│ └── web.ts
|
||||
├── region/ # 区域模块
|
||||
│ └── region.ts
|
||||
└── dashboard/ # 仪表盘
|
||||
├── console.ts
|
||||
└── message.ts
|
||||
```
|
||||
|
||||
## 函数命名规范
|
||||
|
||||
| 操作 | 命名格式 | 示例 | HTTP 方法 |
|
||||
|------|---------|------|----------|
|
||||
| 分页查询 | `getXxxList` 或 `getXxxPage` | `getPositionList` | GET |
|
||||
| 全量列表 | `getXxxAllList` | `getPositionAllList` | GET |
|
||||
| 详情查询 | `getXxxDetail` | `getPositionDetail` | GET |
|
||||
| 添加 | `xxxAdd` | `positionAdd` | POST |
|
||||
| 编辑 | `xxxUpdate` | `positionUpdate` | PUT |
|
||||
| 删除 | `xxxDelete` | `positionDelete` | DELETE |
|
||||
| 批量删除 | `xxxBatchDelete` | `positionBatchDelete` | DELETE |
|
||||
| 导入 | `xxxImport` | `levelImport` | POST |
|
||||
| 导出 | `xxxExport` | `levelExport` | GET |
|
||||
|
||||
:::warning 命名风格
|
||||
实际代码中采用 `positionAdd` 而非 `addPosition`(名词在前、动词在后),与后端 Python 函数命名风格一致。
|
||||
:::
|
||||
|
||||
## API 文件标准模板
|
||||
|
||||
以岗位管理(`api/system/position.ts`)为标准:
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* @description: 岗位列表(分页)
|
||||
*/
|
||||
export function getPositionList(params?) {
|
||||
return http.request({
|
||||
url: '/position/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部岗位列表(不分页)
|
||||
* @param params 参数
|
||||
* @returns 返回结果
|
||||
*/
|
||||
export function getPositionAllList(params?) {
|
||||
return http.request({
|
||||
url: '/position/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 根据ID获取详情
|
||||
*/
|
||||
export function getPositionDetail(positionId) {
|
||||
return http.request({
|
||||
url: '/position/detail/' + positionId,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 添加岗位
|
||||
*/
|
||||
export function positionAdd(data: any) {
|
||||
return http.request({
|
||||
url: '/position/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 更新岗位
|
||||
*/
|
||||
export function positionUpdate(data: any) {
|
||||
return http.request({
|
||||
url: '/position/update',
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 删除岗位
|
||||
*/
|
||||
export function positionDelete(positionId) {
|
||||
return http.request({
|
||||
url: '/position/delete/' + positionId,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 批量删除岗位
|
||||
*/
|
||||
export function positionBatchDelete(data: any) {
|
||||
return http.request({
|
||||
url: '/position/batchDelete',
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:用户模块
|
||||
|
||||
```typescript
|
||||
// api/system/user.ts
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export interface BasicResponseModel<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface BasicPageParams {
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** @description: 获取用户列表 */
|
||||
export function getUserList(params) {
|
||||
return http.request({ url: '/user/page', method: 'get', params });
|
||||
}
|
||||
|
||||
/** @description: 根据ID获取详情 */
|
||||
export function getUserDetail(userId) {
|
||||
return http.request({ url: '/user/detail/' + userId, method: 'get' });
|
||||
}
|
||||
|
||||
/** @description: 添加用户 */
|
||||
export function userAdd(data: any) {
|
||||
return http.request({ url: '/user/add', method: 'POST', data });
|
||||
}
|
||||
|
||||
/** @description: 更新用户 */
|
||||
export function userUpdate(data: any) {
|
||||
return http.request({ url: '/user/update', method: 'PUT', data });
|
||||
}
|
||||
|
||||
/** @description: 删除用户 */
|
||||
export function userDelete(userId) {
|
||||
return http.request({ url: '/user/delete/' + userId, method: 'DELETE' });
|
||||
}
|
||||
|
||||
/** @description: 批量删除用户 */
|
||||
export function userBatchDelete(data: any) {
|
||||
return http.request({ url: '/user/batchDelete', method: 'DELETE', data });
|
||||
}
|
||||
|
||||
/** @description: 重置密码 */
|
||||
export function resetPwd(data: any) {
|
||||
return http.request({ url: '/user/resetPwd', method: 'PUT', data });
|
||||
}
|
||||
|
||||
/** @description: 导出用户 */
|
||||
export function userExport(params) {
|
||||
return http.request({ url: '/user/export', method: 'GET' });
|
||||
}
|
||||
```
|
||||
|
||||
## 请求路径规则
|
||||
|
||||
URL 使用相对路径,不含 `/api/v1` 前缀(由 Axios 实例的 `baseURL` 统一拼接):
|
||||
|
||||
| 后端路由 | 前端 URL | 说明 |
|
||||
|----------|----------|------|
|
||||
| `GET /api/v1/position/page` | `/position/page` | baseURL 已含 `/api/v1` |
|
||||
| `GET /api/v1/position/detail/{id}` | `/position/detail/` + id | 路径参数拼接 |
|
||||
| `POST /api/v1/position/add` | `/position/add` | - |
|
||||
| `PUT /api/v1/position/update` | `/position/update` | - |
|
||||
| `DELETE /api/v1/position/delete/{id}` | `/position/delete/` + id | - |
|
||||
| `DELETE /api/v1/position/batchDelete` | `/position/batchDelete` | body 传 ID 数组 |
|
||||
|
||||
## params 与 data 区别
|
||||
|
||||
| 参数 | 适用方法 | 说明 |
|
||||
|------|----------|------|
|
||||
| `params` | GET | query 参数,拼接到 URL 后面 |
|
||||
| `data` | POST / PUT / DELETE | body 参数,放在请求体中 |
|
||||
|
||||
```typescript
|
||||
// GET 请求:params 拼到 URL
|
||||
http.request({ url: '/position/page', method: 'GET', params: { pageNo: 1, pageSize: 10 } });
|
||||
// 实际请求: /api/v1/position/page?pageNo=1&pageSize=10
|
||||
|
||||
// POST 请求:data 放 body
|
||||
http.request({ url: '/position/add', method: 'POST', data: { name: '测试', status: 1 } });
|
||||
// 实际请求: /api/v1/position/add body: {"name":"测试","status":1}
|
||||
```
|
||||
|
||||
## 公共 API
|
||||
|
||||
公共 API 位于 `api/common/index.ts`,包含跨模块复用的接口:
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/** 本地文件上传 */
|
||||
export function upload(data) {
|
||||
return http.request({ url: '/upload/uploadFile', method: 'post', data });
|
||||
}
|
||||
|
||||
/** 根据字典编码查询字典项 */
|
||||
export function getDictItemByCode(code) {
|
||||
return http.request({ url: '/dict/item/getDictItemList/' + code, method: 'GET' });
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
export function getTemplateByCode(code: any) {
|
||||
return http.request({ url: '/file/template/getTemplateByCode/' + code, method: 'GET' });
|
||||
}
|
||||
```
|
||||
|
||||
## 响应格式
|
||||
|
||||
后端统一返回 `{code, data, msg, ok}` 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": { "records": [...], "total": 100 },
|
||||
"msg": "操作成功",
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
前端 Axios 拦截器自动处理:
|
||||
- `ok: true` → 返回 `res.data`
|
||||
- `ok: false` → 弹出 `res.msg` 错误提示
|
||||
- `code: 401` → token 失效,跳转登录页
|
||||
|
||||
页面中无需手动判断,直接使用返回值:
|
||||
|
||||
```typescript
|
||||
const loadDataTable = async (res: any) => {
|
||||
const result = await getPositionList({ ...formParams, ...res });
|
||||
return result; // BasicTable 自动处理分页数据
|
||||
};
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
前端 API 规范:每个后端模块对应一个 API 文件,函数命名使用 `名词+动词` 格式(`positionAdd` / `positionDelete`)。URL 使用相对路径(不含 `/api/v1` 前缀),GET 用 `params`,POST/PUT/DELETE 用 `data`。统一通过 `http.request()` 发送请求,响应拦截器自动处理错误。
|
||||
@@ -0,0 +1,391 @@
|
||||
# 前端页面规范
|
||||
|
||||
## 概述
|
||||
|
||||
前端基于 Vue3 + ElementPlus + 自定义组件库构建。页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理,无需单独引入分页组件。
|
||||
|
||||
:::tip 技术栈
|
||||
- Vue3(Composition API + `<script setup>`)
|
||||
- ElementPlus 2.x
|
||||
- 自定义组件(BasicTable / BasicForm / TableAction / PageWrapper)
|
||||
- Pinia 状态管理
|
||||
- Vue Router 路由管理
|
||||
:::
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ui/src/
|
||||
├── api/ # API 接口定义
|
||||
│ ├── data/ # 数据模块 API(param、dict、config、notice 等)
|
||||
│ │ ├── param.ts
|
||||
│ │ ├── dictionary.ts
|
||||
│ │ └── ...
|
||||
│ ├── system/ # 系统模块 API(user、role、dept、position、level)
|
||||
│ │ ├── user.ts
|
||||
│ │ ├── role.ts
|
||||
│ │ └── ...
|
||||
│ ├── content/ # 内容模块 API(article、category)
|
||||
│ ├── common/ # 公共 API(city、upload、dict 按编码查询)
|
||||
│ └── monitor/ # 监控模块 API(job、loginLog、operLog)
|
||||
├── components/ # 公共组件(详见组件库文档)
|
||||
├── views/ # 页面视图(按模块分目录)
|
||||
│ ├── data/
|
||||
│ │ ├── param/ # 参数管理
|
||||
│ │ │ ├── index.vue # 列表页
|
||||
│ │ │ ├── edit.vue # 编辑弹窗
|
||||
│ │ │ ├── columns.ts # 表格列定义
|
||||
│ │ │ └── querySchemas.ts # 搜索表单配置
|
||||
│ │ ├── dict/
|
||||
│ │ └── config/
|
||||
│ ├── system/
|
||||
│ │ ├── user/
|
||||
│ │ ├── role/
|
||||
│ │ ├── dept/
|
||||
│ │ ├── position/
|
||||
│ │ └── level/
|
||||
│ ├── content/
|
||||
│ │ ├── article/
|
||||
│ │ └── category/
|
||||
│ └── monitor/
|
||||
│ ├── job/
|
||||
│ ├── loginLog/
|
||||
│ └── operLog/
|
||||
├── store/ # Pinia 状态管理
|
||||
├── router/ # 路由配置
|
||||
├── utils/ # 工具函数
|
||||
├── hooks/ # 组合式函数
|
||||
├── styles/ # 全局样式
|
||||
├── plugins/ # 插件注册(ElementPlus、自定义组件、指令)
|
||||
└── main.ts # 入口文件
|
||||
```
|
||||
|
||||
## 文件组织规范
|
||||
|
||||
每个业务模块统一包含以下文件:
|
||||
|
||||
| 文件 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `index.vue` | 列表页(搜索 + 表格 + 操作) | `views/data/param/index.vue` |
|
||||
| `edit.vue` | 编辑弹窗(新增/编辑共用) | `views/data/param/edit.vue` |
|
||||
| `columns.ts` | 表格列定义 | `views/data/param/columns.ts` |
|
||||
| `querySchemas.ts` | 搜索表单配置 | `views/data/param/querySchemas.ts` |
|
||||
|
||||
## 页面标准结构
|
||||
|
||||
以参数管理(`views/data/param/index.vue`)为标准模板:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<!-- 搜索表单 -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:param:add']">
|
||||
<el-icon><PlusOutlined /></el-icon>
|
||||
添加参数
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['sys:param:batchDelete']"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑弹窗(懒加载) -->
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
:paramId="paramId"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
- 分页由 `BasicTable` 内置处理,无需引入 `<pagination>` 组件
|
||||
- `:request="loadDataTable"` 自动管理分页参数(pageNo / pageSize)和 loading 状态
|
||||
- `tableRef.value.reload({ pageNo: 1 })` 用于搜索/刷新时重置到第一页
|
||||
:::
|
||||
|
||||
## 表格列定义(columns.ts)
|
||||
|
||||
```typescript
|
||||
// views/data/param/columns.ts
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
export const columns = [
|
||||
{ type: 'selection' }, // 多选列
|
||||
{ label: 'ID', prop: 'id', width: 50 }, // 固定列
|
||||
{ label: '参数名称', prop: 'name', minWidth: 200 },
|
||||
{ label: '参数编码', prop: 'code', minWidth: 250 },
|
||||
{ label: '参数值', prop: 'value', minWidth: 200 },
|
||||
{
|
||||
label: '参数类型',
|
||||
prop: 'type',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.type == 0 ? 'primary' : 'warning' },
|
||||
{ default: () => (record.row.type == 0 ? '系统' : '业务') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => (record.row.status == 1 ? '正常' : '禁用') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
];
|
||||
```
|
||||
|
||||
### render 函数自定义渲染
|
||||
|
||||
```typescript
|
||||
// 状态标签
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => record.row.statusText } // 使用后端 serialize_maps 返回的 xxxText
|
||||
);
|
||||
}
|
||||
|
||||
// 链接
|
||||
render(record) {
|
||||
return h('a',
|
||||
{ href: record.row.url, target: '_blank', class: 'text-blue-500' },
|
||||
{ default: () => record.row.url }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 搜索表单配置(querySchemas.ts)
|
||||
|
||||
```typescript
|
||||
// views/data/param/querySchemas.ts
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
label: '参数名称',
|
||||
componentProps: { placeholder: '请输入参数名称' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
component: 'Select',
|
||||
label: '参数类型',
|
||||
componentProps: {
|
||||
placeholder: '请选择参数类型',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '系统', value: '0' },
|
||||
{ label: '业务', value: '1' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'Select',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '正常', value: '1' },
|
||||
{ label: '禁用', value: '2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## 操作栏(TableAction)
|
||||
|
||||
```typescript
|
||||
const actionColumn = reactive({
|
||||
width: 200,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
icon: 'Edit',
|
||||
type: 'warning',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['sys:param:update'],
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'Delete',
|
||||
type: 'danger',
|
||||
onClick: handleDelete.bind(null, record),
|
||||
auth: ['sys:param:delete'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API 接口定义
|
||||
|
||||
```typescript
|
||||
// api/data/param.ts
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function getParamList(params?) {
|
||||
return http.request({ url: '/param/page', method: 'GET', params });
|
||||
}
|
||||
|
||||
export function getParamDetail(id) {
|
||||
return http.request({ url: '/param/detail/' + id, method: 'get' });
|
||||
}
|
||||
|
||||
export function paramAdd(data: any) {
|
||||
return http.request({ url: '/param/add', method: 'POST', data });
|
||||
}
|
||||
|
||||
export function paramUpdate(data: any) {
|
||||
return http.request({ url: '/param/update', method: 'PUT', data });
|
||||
}
|
||||
|
||||
export function paramDelete(id) {
|
||||
return http.request({ url: '/param/delete/' + id, method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function paramBatchDelete(data: any) {
|
||||
return http.request({ url: '/param/batchDelete', method: 'DELETE', data });
|
||||
}
|
||||
```
|
||||
|
||||
## 数据加载与刷新
|
||||
|
||||
```typescript
|
||||
// 加载数据(BasicTable 的 :request 回调,自动传入分页参数)
|
||||
const loadDataTable = async (res: any) => {
|
||||
const result = await getParamList({ ...formParams, ...res });
|
||||
return result; // 返回 { items, total } 或 R.ok(data=列表, count=总数)
|
||||
};
|
||||
|
||||
// 刷新表格
|
||||
function reloadTable(noRefresh = '') {
|
||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
}
|
||||
|
||||
// 搜索提交
|
||||
function handleSubmit(values: Recordable) {
|
||||
handleReset();
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable(); // 重置到第一页
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
function handleReset() {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编辑弹窗(edit.vue)
|
||||
|
||||
编辑弹窗通过 `defineAsyncComponent` 懒加载,使用 `v-model:visible` 控制显示:
|
||||
|
||||
```vue
|
||||
<!-- index.vue 中引用 -->
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
:paramId="paramId"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 懒加载编辑组件
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
|
||||
// 新增
|
||||
const handleAdd = async () => {
|
||||
paramId.value = 0;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
paramId.value = record.row.id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
```
|
||||
|
||||
## 删除(单条 + 批量)
|
||||
|
||||
```typescript
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
// 批量删除:取多选行的 ID
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
await confirm('确定要删除?');
|
||||
record ? await paramDelete(record.row.id) : await paramBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
// 多选变化
|
||||
function onSelectionChange(value) {
|
||||
selectionData.value = value;
|
||||
}
|
||||
```
|
||||
|
||||
## 方法命名规范
|
||||
|
||||
| 前缀 | 用途 | 示例 |
|
||||
|------|------|------|
|
||||
| `handle` | 事件处理 | `handleAdd`、`handleEdit`、`handleDelete`、`handleSubmit`、`handleReset` |
|
||||
| `load` | 数据加载 | `loadDataTable` |
|
||||
| `reload` | 刷新数据 | `reloadTable` |
|
||||
| `on` | 组件事件回调 | `onSelectionChange`、`onSuccess`、`onError` |
|
||||
|
||||
## 总结
|
||||
|
||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套。分页由 `BasicTable` 内置处理,无需单独引入分页组件。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。API 接口按模块分文件定义在 `api/` 目录下。
|
||||
@@ -0,0 +1,226 @@
|
||||
# Form层规范
|
||||
|
||||
## 概述
|
||||
|
||||
Form 层负责数据验证,使用 Django `ModelForm` 实现。每个模块的 `forms.py` 定义表单验证类,包含字段验证规则和中文错误提示。
|
||||
|
||||
:::tip 设计原则
|
||||
- 继承 `forms.ModelForm`
|
||||
- 每个字段定义中文 `error_messages`
|
||||
- 使用 `Meta.fields` 指定验证字段
|
||||
- 验证在 Service 层 ORM 操作之前执行
|
||||
:::
|
||||
|
||||
## 完整示例
|
||||
|
||||
以案例模块为例,`application/example/forms.py`:
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from application.example import models
|
||||
|
||||
|
||||
class ExampleForm(forms.ModelForm):
|
||||
"""案例表单验证类"""
|
||||
|
||||
# 案例名称
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空',
|
||||
'max_length': '案例名称长度不得超过100个字符',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例图片
|
||||
avatar = forms.CharField(
|
||||
required=False,
|
||||
max_length=255,
|
||||
error_messages={
|
||||
'max_length': '案例图片长度不得超过255个字符',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例类型
|
||||
type = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
error_messages={
|
||||
'required': '案例类型不能为空',
|
||||
'min_value': '案例类型值不能小于1',
|
||||
'max_value': '案例类型值不能大于4',
|
||||
}
|
||||
)
|
||||
|
||||
# 案例状态
|
||||
status = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=2,
|
||||
error_messages={
|
||||
'required': '案例状态不能为空',
|
||||
'min_value': '案例状态值不能小于1',
|
||||
'max_value': '案例状态值不能大于2',
|
||||
}
|
||||
)
|
||||
|
||||
# 排序
|
||||
sort = forms.IntegerField(
|
||||
required=True,
|
||||
error_messages={
|
||||
'required': '排序不能为空',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Example
|
||||
fields = ['name', 'avatar', 'type', 'status', 'sort']
|
||||
```
|
||||
|
||||
## 字段类型
|
||||
|
||||
| Django 表单字段 | 说明 | 常用参数 |
|
||||
|:----|:----|:----|
|
||||
| `CharField` | 字符串 | `max_length`, `min_length` |
|
||||
| `IntegerField` | 整数 | `min_value`, `max_value` |
|
||||
| `FloatField` | 浮点数 | `min_value`, `max_value` |
|
||||
| `DecimalField` | 精确小数 | `max_digits`, `decimal_places` |
|
||||
| `BooleanField` | 布尔值 | - |
|
||||
| `DateTimeField` | 日期时间 | `input_formats` |
|
||||
| `DateField` | 日期 | `input_formats` |
|
||||
| `EmailField` | 邮箱 | `max_length` |
|
||||
| `URLField` | URL | `max_length` |
|
||||
|
||||
## 错误消息规范
|
||||
|
||||
每个字段必须定义中文 `error_messages`:
|
||||
|
||||
```python
|
||||
name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
error_messages={
|
||||
'required': '案例名称不能为空', # 必填验证
|
||||
'max_length': '案例名称长度不得超过100个字符', # 长度验证
|
||||
'min_length': '案例名称长度不能少于2个字符', # 最小长度
|
||||
}
|
||||
)
|
||||
|
||||
type = forms.IntegerField(
|
||||
required=True,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
error_messages={
|
||||
'required': '案例类型不能为空', # 必填验证
|
||||
'min_value': '案例类型值不能小于1', # 最小值验证
|
||||
'max_value': '案例类型值不能大于4', # 最大值验证
|
||||
'invalid': '案例类型格式不正确', # 格式验证
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 常用错误消息键
|
||||
|
||||
| 键 | 说明 | 适用字段 |
|
||||
|:----|:----|:----|
|
||||
| `required` | 必填验证 | 所有字段 |
|
||||
| `max_length` | 最大长度 | CharField |
|
||||
| `min_length` | 最小长度 | CharField |
|
||||
| `max_value` | 最大值 | IntegerField/FloatField |
|
||||
| `min_value` | 最小值 | IntegerField/FloatField |
|
||||
| `invalid` | 格式验证 | 所有字段 |
|
||||
| `unique` | 唯一性验证 | 需手动实现 |
|
||||
|
||||
## Meta 类配置
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
# 绑定模型
|
||||
model = models.Example
|
||||
|
||||
# 指定验证字段(只验证以下字段)
|
||||
fields = ['name', 'avatar', 'type', 'status', 'sort']
|
||||
|
||||
# 排除字段(不验证以下字段)
|
||||
# exclude = ['create_user', 'update_user']
|
||||
```
|
||||
|
||||
:::tip fields vs exclude
|
||||
- `fields`:白名单,只验证列出的字段
|
||||
- `exclude`:黑名单,排除列出的字段
|
||||
- 推荐使用 `fields`,更明确
|
||||
:::
|
||||
|
||||
## 在 Service 中使用
|
||||
|
||||
```python
|
||||
from application.example import forms
|
||||
from utils import R, regular
|
||||
|
||||
def add_example(request):
|
||||
"""添加案例"""
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 表单验证
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
try:
|
||||
cleaned_data = form.cleaned_data
|
||||
# 使用 cleaned_data 创建记录
|
||||
Example.objects.create(
|
||||
name=cleaned_data.get('name'),
|
||||
type=cleaned_data.get('type'),
|
||||
status=cleaned_data.get('status'),
|
||||
sort=cleaned_data.get('sort'),
|
||||
)
|
||||
return R.ok(msg="创建成功")
|
||||
except Exception as e:
|
||||
return R.failed(msg="添加失败")
|
||||
```
|
||||
|
||||
## 错误信息提取
|
||||
|
||||
使用 `regular.get_err(form)` 提取表单验证错误信息:
|
||||
|
||||
```python
|
||||
from utils import regular
|
||||
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
error_msg = regular.get_err(form) # 返回第一个错误的中文提示
|
||||
return R.failed(msg=error_msg)
|
||||
```
|
||||
|
||||
## 自定义验证方法
|
||||
|
||||
```python
|
||||
class ExampleForm(forms.ModelForm):
|
||||
name = forms.CharField(required=True, max_length=100)
|
||||
|
||||
def clean_name(self):
|
||||
"""自定义验证:检查名称是否包含敏感词"""
|
||||
name = self.cleaned_data.get('name')
|
||||
if '敏感词' in name:
|
||||
raise forms.ValidationError('名称包含敏感词')
|
||||
return name
|
||||
|
||||
def clean(self):
|
||||
"""全局验证:检查字段间的关系"""
|
||||
cleaned_data = super().clean()
|
||||
start_date = cleaned_data.get('start_date')
|
||||
end_date = cleaned_data.get('end_date')
|
||||
if start_date and end_date and start_date > end_date:
|
||||
raise forms.ValidationError('开始日期不能晚于结束日期')
|
||||
return cleaned_data
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Form 层使用 Django `ModelForm` 实现数据验证,每个字段定义中文 `error_messages`。在 Service 层 ORM 操作之前执行验证,使用 `regular.get_err(form)` 提取错误信息。支持自定义验证方法(`clean_{field}`)和全局验证(`clean`)。
|
||||
@@ -0,0 +1,192 @@
|
||||
# Git提交规范
|
||||
|
||||
## 概述
|
||||
|
||||
使用 Conventional Commits 规范管理提交信息,确保提交历史清晰可读,便于自动生成变更日志和语义化版本管理。
|
||||
|
||||
:::tip Conventional Commits 格式
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
:::
|
||||
|
||||
## 提交类型(type)
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `feat` | 新功能 | `feat(user): 添加用户导入功能` |
|
||||
| `fix` | 修复 bug | `fix(auth): 修复 token 过期后未刷新的问题` |
|
||||
| `docs` | 文档变更 | `docs: 更新部署文档` |
|
||||
| `style` | 代码格式(不影响逻辑) | `style: 格式化代码缩进` |
|
||||
| `refactor` | 重构(非新功能非修复) | `refactor(repo): 统一查询方法封装` |
|
||||
| `perf` | 性能优化 | `perf(query): 优化分页查询性能` |
|
||||
| `test` | 测试相关 | `test(unit): 添加岗位模块单元测试` |
|
||||
| `chore` | 构建/工具变更 | `chore: 更新依赖版本` |
|
||||
| `ci` | CI/CD 变更 | `ci: 添加 GitHub Actions 配置` |
|
||||
| `revert` | 回滚提交 | `revert: 回滚 feat(user) 提交` |
|
||||
|
||||
## 范围(scope)
|
||||
|
||||
可选,用于说明影响的模块或组件:
|
||||
|
||||
```
|
||||
feat(user): 用户模块
|
||||
fix(auth): 认证模块
|
||||
refactor(repo): Repository 层
|
||||
feat(api): API 层
|
||||
docs(deploy): 部署文档
|
||||
```
|
||||
|
||||
## 提交信息示例
|
||||
|
||||
### 简单提交
|
||||
|
||||
```
|
||||
feat(position): 添加岗位状态切换接口
|
||||
fix(captcha): 修复验证码过期时间计算错误
|
||||
docs: 更新 README 安装说明
|
||||
```
|
||||
|
||||
### 带正文的提交
|
||||
|
||||
```
|
||||
fix(auth): 修复 token 刷新后旧 token 仍有效的问题
|
||||
|
||||
旧 token 在刷新后未加入黑名单,导致短时间内可同时使用新旧两个 token。
|
||||
修复方案:刷新时将旧 token 加入 Redis 黑名单。
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
### 破坏性变更
|
||||
|
||||
```
|
||||
feat(api)!: 统一响应格式,移除 data.code 字段
|
||||
|
||||
BREAKING CHANGE: 响应格式从 {data: {code, data}} 变为 {code, data, msg, ok}
|
||||
前端需同步更新响应处理逻辑。
|
||||
```
|
||||
|
||||
:::warning 破坏性变更
|
||||
破坏性变更使用 `!` 后缀标记,并在正文或 footer 中说明 `BREAKING CHANGE`。此类变更需在版本号中体现(主版本号 +1)。
|
||||
:::
|
||||
|
||||
## 分支策略
|
||||
|
||||
| 分支 | 用途 | 命名格式 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| `main` | 主分支(生产) | `main` | 始终保持可发布状态 |
|
||||
| `master` | 开发主分支 | `master` | 日常开发合并目标 |
|
||||
| `feature/*` | 功能分支 | `feature/功能名` | 从 master 拉取 |
|
||||
| `fix/*` | 修复分支 | `fix/问题描述` | 从 master 拉取 |
|
||||
| `hotfix/*` | 紧急修复 | `hotfix/问题描述` | 从 main 拉取 |
|
||||
|
||||
### 分支工作流
|
||||
|
||||
```
|
||||
1. 从 master 创建功能分支
|
||||
git checkout -b feature/position-import master
|
||||
|
||||
2. 开发完成后提交
|
||||
git commit -m "feat(position): 添加岗位 Excel 导入功能"
|
||||
|
||||
3. 合并到 master
|
||||
git checkout master
|
||||
git merge feature/position-import
|
||||
|
||||
4. 推送到远程
|
||||
git push origin master
|
||||
|
||||
5. 生产发布时合并到 main
|
||||
git checkout main
|
||||
git merge master
|
||||
git tag v1.2.0
|
||||
```
|
||||
|
||||
## 版本标签
|
||||
|
||||
使用语义化版本号(Semantic Versioning):
|
||||
|
||||
```
|
||||
v主版本号.次版本号.修订号
|
||||
v1.2.3
|
||||
|
||||
主版本号:破坏性变更
|
||||
次版本号:新功能(向后兼容)
|
||||
修订号:Bug 修复
|
||||
```
|
||||
|
||||
```bash
|
||||
# 创建标签
|
||||
git tag -a v1.2.0 -m "feat: 添加岗位导入功能"
|
||||
|
||||
# 推送标签
|
||||
git push origin v1.2.0
|
||||
|
||||
# 查看标签
|
||||
git tag -l
|
||||
```
|
||||
|
||||
## .gitignore 配置
|
||||
|
||||
```gitignore
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
venv/
|
||||
|
||||
# 环境配置
|
||||
.env
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# 上传文件
|
||||
uploads/
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
|
||||
# 数据库
|
||||
*.sqlite3
|
||||
```
|
||||
|
||||
:::tip .env 安全
|
||||
`.env` 文件包含敏感信息(数据库密码、JWT 密钥等),必须在 `.gitignore` 中排除。各环境的 `.env` 独立管理,不提交到仓库。
|
||||
:::
|
||||
|
||||
## 常用 Git 命令
|
||||
|
||||
```bash
|
||||
# 查看提交历史
|
||||
git log --oneline -20
|
||||
|
||||
# 查看某次提交的变更
|
||||
git show <commit-hash>
|
||||
|
||||
# 撤销工作区修改
|
||||
git checkout -- <file>
|
||||
|
||||
# 撤销暂存
|
||||
git reset HEAD <file>
|
||||
|
||||
# 暂存当前工作
|
||||
git stash
|
||||
git stash pop
|
||||
|
||||
# 查看分支图
|
||||
git log --graph --oneline --all
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Git 提交规范采用 Conventional Commits 格式:`type(scope): subject`。常用类型包括 feat/fix/docs/refactor/test/chore。分支策略:main(生产)、master(开发)、feature/fix/hotfix(临时分支)。版本使用语义化标签(v1.2.0)。`.env` 等敏感文件必须排除在仓库之外。
|
||||
@@ -0,0 +1,252 @@
|
||||
# Model层规范
|
||||
|
||||
## 概述
|
||||
|
||||
Model 层负责定义数据库表结构,所有业务模型继承 `BaseModel` 抽象基类,自动获得公共字段(id、create_user、create_time、update_user、update_time、is_delete)。
|
||||
|
||||
:::tip 设计原则
|
||||
- 所有模型继承 `BaseModel`
|
||||
- 字段命名使用 snake_case
|
||||
- 每个字段必须有 `db_comment` 注释
|
||||
- 使用 `get_table_name()` 生成表名
|
||||
:::
|
||||
|
||||
## BaseModel 基类
|
||||
|
||||
`application/models.py` 定义了 `BaseModel` 抽象基类:
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
class BaseModel(models.Model):
|
||||
"""基础模型抽象类"""
|
||||
|
||||
# 主键ID字段
|
||||
id = models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='主键ID',
|
||||
db_comment='主键ID'
|
||||
)
|
||||
|
||||
# 创建人字段
|
||||
create_user = models.CharField(
|
||||
null=True, max_length=50, default=None,
|
||||
verbose_name='创建人', db_comment='创建人'
|
||||
)
|
||||
|
||||
# 创建时间字段
|
||||
create_time = models.DateTimeField(
|
||||
null=True, auto_now_add=True,
|
||||
verbose_name="创建时间", db_comment='创建时间'
|
||||
)
|
||||
|
||||
# 更新人字段
|
||||
update_user = models.CharField(
|
||||
null=True, max_length=50, default=None,
|
||||
verbose_name='更新人', db_comment='更新人'
|
||||
)
|
||||
|
||||
# 更新时间字段
|
||||
update_time = models.DateTimeField(
|
||||
null=True, auto_now=True,
|
||||
verbose_name="更新时间", db_comment='更新时间'
|
||||
)
|
||||
|
||||
# 逻辑删除标识
|
||||
is_delete = models.BooleanField(
|
||||
default=0, db_default=0,
|
||||
verbose_name="逻辑删除",
|
||||
db_comment='逻辑删除:0-正常 1-已删除'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
以案例模块为例,`application/example/models.py`:
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from application.models import BaseModel
|
||||
from utils.common import get_table_name
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
"""案例模型类"""
|
||||
|
||||
# 案例名称
|
||||
name = models.CharField(
|
||||
null=False, max_length=100, db_index=True,
|
||||
verbose_name="案例名称",
|
||||
help_text="案例名称",
|
||||
db_comment='案例名称'
|
||||
)
|
||||
|
||||
# 案例图片
|
||||
avatar = models.CharField(
|
||||
null=True, blank=True, max_length=255,
|
||||
verbose_name="案例图片",
|
||||
help_text="案例图片",
|
||||
db_comment='案例图片'
|
||||
)
|
||||
|
||||
# 案例类型
|
||||
TYPE_CHOICES = (
|
||||
(1, "类型1"),
|
||||
(2, "类型2"),
|
||||
(3, "类型3"),
|
||||
(4, "类型4"),
|
||||
)
|
||||
type = models.IntegerField(
|
||||
null=False, choices=TYPE_CHOICES,
|
||||
verbose_name="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
help_text="案例类型:1-类型1 2-类型2 3-类型3 4-类型4",
|
||||
db_comment='案例类型:1-类型1 2-类型2 3-类型3 4-类型4'
|
||||
)
|
||||
|
||||
# 案例状态
|
||||
STATUS_CHOICES = (
|
||||
(1, "正常"),
|
||||
(2, "禁用"),
|
||||
)
|
||||
status = models.IntegerField(
|
||||
null=False, choices=STATUS_CHOICES,
|
||||
verbose_name="案例状态:1-正常 2-禁用",
|
||||
help_text="案例状态:1-正常 2-禁用",
|
||||
db_comment='案例状态:1-正常 2-禁用'
|
||||
)
|
||||
|
||||
# 排序
|
||||
sort = models.IntegerField(
|
||||
null=False,
|
||||
verbose_name="排序",
|
||||
help_text="排序",
|
||||
db_comment='排序'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = get_table_name('example')
|
||||
db_table_comment = "案例表"
|
||||
verbose_name = "案例表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
def __str__(self):
|
||||
return f"案例{self.name}"
|
||||
```
|
||||
|
||||
## 字段定义规范
|
||||
|
||||
### 字段类型映射
|
||||
|
||||
| 数据库类型 | Django 字段类型 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| varchar | CharField | 短文本,需指定 max_length |
|
||||
| text | TextField | 长文本 |
|
||||
| int | IntegerField | 整数 |
|
||||
| datetime | DateTimeField | 日期时间 |
|
||||
| date | DateField | 日期 |
|
||||
| float | FloatField | 浮点数 |
|
||||
| decimal | DecimalField | 精确小数 |
|
||||
| boolean | BooleanField | 布尔值 |
|
||||
|
||||
### 字段属性
|
||||
|
||||
| 属性 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `null` | 数据库是否允许 NULL | `null=True` |
|
||||
| `blank` | 表单验证是否允许空 | `blank=True` |
|
||||
| `max_length` | 最大长度 | `max_length=100` |
|
||||
| `default` | 默认值 | `default=0` |
|
||||
| `db_index` | 是否创建索引 | `db_index=True` |
|
||||
| `choices` | 选项列表 | `choices=STATUS_CHOICES` |
|
||||
| `verbose_name` | 字段中文名 | `verbose_name="案例名称"` |
|
||||
| `help_text` | 帮助文本 | `help_text="案例名称"` |
|
||||
| `db_comment` | 数据库字段注释 | `db_comment='案例名称'` |
|
||||
|
||||
### 选项字段定义
|
||||
|
||||
```python
|
||||
# 状态字段
|
||||
STATUS_CHOICES = (
|
||||
(1, "正常"), # 正常状态
|
||||
(2, "禁用"), # 禁用状态
|
||||
)
|
||||
status = models.IntegerField(
|
||||
null=False, choices=STATUS_CHOICES,
|
||||
verbose_name="状态:1-正常 2-禁用",
|
||||
db_comment='状态:1-正常 2-禁用'
|
||||
)
|
||||
```
|
||||
|
||||
:::tip 选项定义约定
|
||||
- 选项定义为类属性,命名为 `{FIELD}_CHOICES`
|
||||
- 选项格式为 `(value, label)` 元组
|
||||
- 字段注释中包含选项说明(如 `1-正常 2-禁用`)
|
||||
:::
|
||||
|
||||
## Meta 类配置
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
# 数据表名(使用 get_table_name 自动生成)
|
||||
db_table = get_table_name('example')
|
||||
|
||||
# 表注释(Django 4.1+)
|
||||
db_table_comment = "案例表"
|
||||
|
||||
# Admin 后台显示名称
|
||||
verbose_name = "案例表"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
# 默认排序
|
||||
ordering = ("sort",)
|
||||
```
|
||||
|
||||
### get_table_name 函数
|
||||
|
||||
`utils/common.py` 中的 `get_table_name` 函数自动拼接表前缀:
|
||||
|
||||
```python
|
||||
from config.env import DATABASE_PREFIX
|
||||
|
||||
def get_table_name(table_name):
|
||||
"""获取带前缀的表名"""
|
||||
return f"{DATABASE_PREFIX}{table_name}"
|
||||
```
|
||||
|
||||
例如:`DATABASE_PREFIX = "django_"` 时,`get_table_name('example')` 返回 `"django_example"`。
|
||||
|
||||
## 索引定义
|
||||
|
||||
```python
|
||||
class Example(BaseModel):
|
||||
name = models.CharField(max_length=100, db_index=True) # 单列索引
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'type'], name='idx_status_type'), # 复合索引
|
||||
]
|
||||
```
|
||||
|
||||
## 信号机制
|
||||
|
||||
项目使用 `class_prepared` 信号自动重排字段顺序,将公共字段(id、create_user 等)移到业务字段之后:
|
||||
|
||||
```python
|
||||
from django.db.models.signals import class_prepared
|
||||
|
||||
def _reorder_fields(sender, **kwargs):
|
||||
"""重排模型字段顺序,将公共字段移到最后"""
|
||||
# ... 实现逻辑 ...
|
||||
|
||||
class_prepared.connect(_reorder_fields)
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Model 层通过继承 `BaseModel` 获得公共字段,使用 `get_table_name()` 生成带前缀的表名。字段定义需包含 `db_comment` 注释、`verbose_name` 中文名。选项字段使用 `choices` 参数定义,排序使用 `Meta.ordering` 配置。
|
||||
@@ -0,0 +1,186 @@
|
||||
# 分页规范
|
||||
|
||||
## 概述
|
||||
|
||||
统一使用 `pageNo`/`pageSize` 作为分页参数,Django `Paginator` 封装分页逻辑,返回标准分页结构。
|
||||
|
||||
:::tip 分页约定
|
||||
- 前端传递 `pageNo`(页码,从 1 开始)和 `pageSize`(每页条数)
|
||||
- 后端返回 `{records, total, size, current, pages}` 结构
|
||||
- 默认每页 10 条(`PAGE_SIZE = 10`)
|
||||
:::
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| `pageNo` | int | 否 | 1 | 页码,从 1 开始 |
|
||||
| `pageSize` | int | 否 | 10 | 每页条数 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
# GET 请求
|
||||
GET /example/page?pageNo=1&pageSize=10
|
||||
|
||||
# 带筛选条件
|
||||
GET /example/page?pageNo=1&pageSize=10&name=案例&type=1&status=1
|
||||
```
|
||||
|
||||
## 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "案例名称",
|
||||
"type": 1,
|
||||
"status": 1,
|
||||
"sort": 1,
|
||||
"createUser": "admin",
|
||||
"createTime": "2026-01-01 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 50,
|
||||
"size": 10,
|
||||
"current": 1,
|
||||
"pages": 5
|
||||
},
|
||||
"msg": "操作成功",
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `records` | array | 当前页数据列表 |
|
||||
| `total` | int | 总记录数 |
|
||||
| `size` | int | 每页条数 |
|
||||
| `current` | int | 当前页码 |
|
||||
| `pages` | int | 总页数 |
|
||||
|
||||
## Service 实现
|
||||
|
||||
### 分页查询方法
|
||||
|
||||
```python
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage, EmptyPage
|
||||
from constant.constants import PAGE_SIZE
|
||||
from utils import R
|
||||
|
||||
def get_example_page(request):
|
||||
"""查询案例分页数据"""
|
||||
try:
|
||||
# 获取分页参数
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
|
||||
# 构建基础查询(只查询未删除的记录)
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
|
||||
# 应用筛选条件
|
||||
query = _apply_filters(query, request)
|
||||
|
||||
# 按排序字段升序排列
|
||||
query = query.order_by("sort")
|
||||
|
||||
# 执行分页查询
|
||||
paginator = Paginator(query, page_size)
|
||||
try:
|
||||
page_list = paginator.page(page_no)
|
||||
except (PageNotAnInteger, InvalidPage, EmptyPage):
|
||||
# 页码无效时默认返回第一页
|
||||
page_list = paginator.page(1)
|
||||
page_no = 1
|
||||
|
||||
# 构建并返回分页数据
|
||||
page_data = _build_page_data(page_list, paginator, page_no, page_size)
|
||||
return R.ok(data=page_data)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例分页数据异常: {str(e)}")
|
||||
return R.failed(msg="查询失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 分页数据构建
|
||||
|
||||
```python
|
||||
def _build_page_data(page_list, paginator, page_no, page_size):
|
||||
"""构建分页返回数据"""
|
||||
records = []
|
||||
for item in page_list:
|
||||
record = {
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'type': item.type,
|
||||
'status': item.status,
|
||||
'sort': item.sort,
|
||||
'createUser': item.create_user,
|
||||
'createTime': format_datetime(item.create_time),
|
||||
'updateUser': item.update_user,
|
||||
'updateTime': format_datetime(item.update_time),
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
return {
|
||||
'records': records,
|
||||
'total': paginator.count, # 总记录数
|
||||
'size': page_size, # 每页大小
|
||||
'current': page_no, # 当前页码
|
||||
'pages': paginator.num_pages # 总页数
|
||||
}
|
||||
```
|
||||
|
||||
### 筛选条件应用
|
||||
|
||||
```python
|
||||
def _apply_filters(queryset, request):
|
||||
"""应用查询筛选条件"""
|
||||
# 名称模糊筛选
|
||||
name = request.GET.get('name')
|
||||
if name:
|
||||
queryset = queryset.filter(name__contains=name)
|
||||
|
||||
# 类型精确筛选
|
||||
type = request.GET.get('type')
|
||||
if type:
|
||||
queryset = queryset.filter(type=type)
|
||||
|
||||
# 状态精确筛选
|
||||
status = request.GET.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
return queryset
|
||||
```
|
||||
|
||||
:::tip 筛选字段说明
|
||||
- 字符串字段使用 `__contains` 实现模糊查询(LIKE)
|
||||
- 整数字段直接使用 `=` 实现精确匹配
|
||||
- 空值自动跳过,无需手动判断
|
||||
:::
|
||||
|
||||
## View 示例
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
"""案例分页数据查询视图"""
|
||||
permission_required = ("sys:example:page",)
|
||||
|
||||
@operation_log(title="案例管理-查询分页列表", log_type=LogType.QUERY)
|
||||
def get(self, request):
|
||||
result = services.get_example_page(request)
|
||||
return result
|
||||
```
|
||||
|
||||
:::tip 权限标识
|
||||
分页接口权限标识格式:`sys:{模块名}:page`
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
分页规范统一使用 `pageNo`/`pageSize` 参数,通过 Django `Paginator` 处理分页逻辑。Service 层构建查询、应用筛选、执行分页、构建返回数据。响应使用 `R.ok(data=page_data)` 封装标准分页结构,前端可直接使用 `records`/`total`/`pages` 渲染列表和分页组件。
|
||||
@@ -0,0 +1,312 @@
|
||||
# Service层规范
|
||||
|
||||
## 概述
|
||||
|
||||
Service 层是业务逻辑层,每个模块的 `services.py` 包含该模块的所有业务函数。采用函数式风格,每个函数对应一个业务操作。
|
||||
|
||||
:::tip 设计原则
|
||||
- 所有业务逻辑集中在 Service 层
|
||||
- View 层只负责接收请求和返回响应
|
||||
- Model 层只负责数据定义
|
||||
- Form 层只负责数据验证
|
||||
:::
|
||||
|
||||
## 函数命名规范
|
||||
|
||||
| 函数名 | 说明 | 参数 | 返回值 |
|
||||
|:----|:----|:----|:----|
|
||||
| `get_{m}_page` | 分页查询 | request | R.ok(data=page_data) |
|
||||
| `get_{m}_list` | 列表查询(不分页) | request | list |
|
||||
| `get_{m}_detail` | 详情查询 | id | dict/None |
|
||||
| `add_{m}` | 添加记录 | request | R.ok/R.failed |
|
||||
| `update_{m}` | 更新记录 | request | R.ok/R.failed |
|
||||
| `delete_{m}s` | 删除记录(支持批量) | instance_ids | R.ok/R.failed |
|
||||
| `update_{m}_status` | 更新状态 | request | R.ok/R.failed |
|
||||
| `get_{m}_data` | 获取数据列表(下拉选择) | - | list |
|
||||
|
||||
## 完整示例
|
||||
|
||||
以案例模块为例,`application/example/services.py` 的典型结构:
|
||||
|
||||
```python
|
||||
import logging
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage, EmptyPage
|
||||
from application.example import forms
|
||||
from application.example.models import Example
|
||||
from constant.constants import PAGE_SIZE
|
||||
from utils import R, regular
|
||||
from utils.common import format_datetime, parse_request_body, save_file
|
||||
from utils.security import get_username
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 分页查询服务
|
||||
# =============================================================
|
||||
|
||||
def get_example_page(request):
|
||||
"""查询案例分页数据"""
|
||||
try:
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
|
||||
query = Example.objects.filter(is_delete=False)
|
||||
query = _apply_filters(query, request)
|
||||
query = query.order_by("sort")
|
||||
|
||||
paginator = Paginator(query, page_size)
|
||||
try:
|
||||
page_list = paginator.page(page_no)
|
||||
except (PageNotAnInteger, InvalidPage, EmptyPage):
|
||||
page_list = paginator.page(1)
|
||||
page_no = 1
|
||||
|
||||
page_data = _build_page_data(page_list, paginator, page_no, page_size)
|
||||
return R.ok(data=page_data)
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例分页数据异常: {str(e)}")
|
||||
return R.failed(msg="查询失败,请稍后重试")
|
||||
|
||||
|
||||
def _apply_filters(queryset, request):
|
||||
"""应用查询筛选条件"""
|
||||
name = request.GET.get('name')
|
||||
if name:
|
||||
queryset = queryset.filter(name__contains=name)
|
||||
|
||||
type = request.GET.get('type')
|
||||
if type:
|
||||
queryset = queryset.filter(type=type)
|
||||
|
||||
status = request.GET.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def _build_page_data(page_list, paginator, page_no, page_size):
|
||||
"""构建分页返回数据"""
|
||||
records = []
|
||||
for item in page_list:
|
||||
record = {
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'type': item.type,
|
||||
'status': item.status,
|
||||
'sort': item.sort,
|
||||
'createUser': item.create_user,
|
||||
'createTime': format_datetime(item.create_time),
|
||||
'updateUser': item.update_user,
|
||||
'updateTime': format_datetime(item.update_time),
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
return {
|
||||
'records': records,
|
||||
'total': paginator.count,
|
||||
'size': page_size,
|
||||
'current': page_no,
|
||||
'pages': paginator.num_pages
|
||||
}
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 详情查询服务
|
||||
# =============================================================
|
||||
|
||||
def get_example_detail(id):
|
||||
"""根据ID查询案例详情"""
|
||||
try:
|
||||
instance = Example.objects.filter(is_delete=False, id=id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'type': instance.type,
|
||||
'status': instance.status,
|
||||
'sort': instance.sort,
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例详情异常,ID: {id}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 添加服务
|
||||
# =============================================================
|
||||
|
||||
def add_example(request):
|
||||
"""添加案例"""
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 表单验证
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
try:
|
||||
cleaned_data = form.cleaned_data
|
||||
Example.objects.create(
|
||||
name=cleaned_data.get('name'),
|
||||
type=cleaned_data.get('type'),
|
||||
status=cleaned_data.get('status'),
|
||||
sort=cleaned_data.get('sort'),
|
||||
create_user=get_username(request),
|
||||
update_user=get_username(request)
|
||||
)
|
||||
return R.ok(msg="创建成功")
|
||||
except Exception as e:
|
||||
logging.error(f"添加案例异常: {str(e)}")
|
||||
return R.failed(msg="添加失败,请稍后重试")
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 更新服务
|
||||
# =============================================================
|
||||
|
||||
def update_example(request):
|
||||
"""更新案例"""
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
instance_id = data.get('id')
|
||||
if not instance_id:
|
||||
return R.failed("案例ID无效")
|
||||
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
|
||||
try:
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
|
||||
cleaned_data = form.cleaned_data
|
||||
instance.name = cleaned_data.get('name')
|
||||
instance.type = cleaned_data.get('type')
|
||||
instance.status = cleaned_data.get('status')
|
||||
instance.sort = cleaned_data.get('sort')
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
|
||||
return R.ok(msg="更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="更新失败,请稍后重试")
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 删除服务(支持批量)
|
||||
# =============================================================
|
||||
|
||||
def delete_examples(instance_ids):
|
||||
"""删除案例(批量删除)"""
|
||||
if not instance_ids:
|
||||
return R.failed("记录ID不存在")
|
||||
|
||||
try:
|
||||
id_list = [int(id_str.strip()) for id_str in instance_ids.split(',') if id_str.strip()]
|
||||
if not id_list:
|
||||
return R.failed("无效的记录ID")
|
||||
|
||||
updated_count = Example.objects.filter(
|
||||
id__in=id_list,
|
||||
is_delete=False
|
||||
).update(is_delete=True)
|
||||
|
||||
return R.ok(msg=f"本次共删除{updated_count}条数据")
|
||||
except ValueError:
|
||||
return R.failed("记录ID格式错误")
|
||||
except Exception as e:
|
||||
logging.error(f"删除案例异常,IDs: {instance_ids}, 错误: {str(e)}")
|
||||
return R.failed(msg="删除失败,请稍后重试")
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 状态更新服务
|
||||
# =============================================================
|
||||
|
||||
def update_example_status(request):
|
||||
"""更新案例状态"""
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
instance_id = data.get('id')
|
||||
if not instance_id:
|
||||
return R.failed("案例ID无效")
|
||||
|
||||
status = data.get('status')
|
||||
if status is None:
|
||||
return R.failed("状态值不能为空")
|
||||
|
||||
try:
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if not instance:
|
||||
return R.failed("案例不存在")
|
||||
|
||||
instance.status = int(status)
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
|
||||
return R.ok(msg="状态更新成功")
|
||||
except Exception as e:
|
||||
logging.error(f"更新案例状态异常,ID: {instance_id}, 错误: {str(e)}")
|
||||
return R.failed(msg="状态更新失败,请稍后重试")
|
||||
```
|
||||
|
||||
## 关键约定
|
||||
|
||||
### 请求体解析
|
||||
|
||||
所有 POST/PUT/DELETE 请求使用 `parse_request_body(request)` 解析 JSON 请求体:
|
||||
|
||||
```python
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
```
|
||||
|
||||
### 表单验证
|
||||
|
||||
在 ORM 操作之前进行表单验证:
|
||||
|
||||
```python
|
||||
form = forms.ExampleForm(data)
|
||||
if not form.is_valid():
|
||||
return R.failed(msg=regular.get_err(form))
|
||||
```
|
||||
|
||||
### 操作人记录
|
||||
|
||||
创建和更新操作需记录操作人:
|
||||
|
||||
```python
|
||||
from utils.security import get_username
|
||||
|
||||
instance.create_user = get_username(request)
|
||||
instance.update_user = get_username(request)
|
||||
```
|
||||
|
||||
### 异常处理
|
||||
|
||||
所有 Service 函数使用 try/except 包裹,记录日志并返回友好错误信息:
|
||||
|
||||
```python
|
||||
try:
|
||||
# 业务逻辑
|
||||
except Exception as e:
|
||||
logging.error(f"操作异常: {str(e)}")
|
||||
return R.failed(msg="操作失败,请稍后重试")
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Service 层采用函数式风格,每个函数对应一个业务操作。关键约定:使用 `parse_request_body` 解析请求体、表单验证在 ORM 操作之前、记录操作人、try/except 包裹异常处理。所有查询过滤 `is_delete=False`,返回标准 R 响应。
|
||||
@@ -0,0 +1,350 @@
|
||||
# 测试最佳实践
|
||||
|
||||
:::tip 说明
|
||||
本文在 [测试规范](/zh/volume7/testing.html) 基础上,介绍更高级的测试技巧,包括 Mock、Fixture、API 测试、覆盖率实战等。
|
||||
:::
|
||||
|
||||
## Mock 与 Patch
|
||||
|
||||
### 什么是 Mock
|
||||
|
||||
Mock 用于模拟外部依赖,隔离被测代码:
|
||||
|
||||
```python
|
||||
# 不使用 Mock:测试依赖 Redis 连接
|
||||
def test_get_captcha():
|
||||
# 如果 Redis 未启动,测试会失败
|
||||
captcha = generate_captcha() # 依赖 Redis 存储
|
||||
|
||||
# 使用 Mock:隔离 Redis 依赖
|
||||
@patch('utils.redis.get_redis_connection')
|
||||
def test_get_captcha(mock_redis):
|
||||
mock_redis.return_value = MagicMock()
|
||||
captcha = generate_captcha() # 不依赖真实 Redis
|
||||
```
|
||||
|
||||
### unittest.mock.patch
|
||||
|
||||
```python
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
class UserServiceTest(TestCase):
|
||||
"""用户服务测试"""
|
||||
|
||||
@patch('application.user.services.get_redis_connection')
|
||||
def test_login_with_mock_redis(self, mock_redis):
|
||||
"""测试登录(Mock Redis)"""
|
||||
# 配置 Mock
|
||||
mock_redis.return_value.get.return_value = None
|
||||
|
||||
# 执行测试
|
||||
result = login_service(request)
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(result['code'], 0)
|
||||
|
||||
@patch('utils.common.get_client_ip')
|
||||
def test_record_log_with_mock_ip(self, mock_ip):
|
||||
"""测试记录日志(Mock IP 获取)"""
|
||||
mock_ip.return_value = '127.0.0.1'
|
||||
|
||||
record_login_log(request, 'admin', 1, 0)
|
||||
# 验证日志记录
|
||||
```
|
||||
|
||||
### Mock 返回值配置
|
||||
|
||||
```python
|
||||
# Mock 返回固定值
|
||||
mock_func.return_value = 'mocked_value'
|
||||
|
||||
# Mock 返回不同值(按调用顺序)
|
||||
mock_func.side_effect = ['first', 'second', 'third']
|
||||
|
||||
# Mock 抛出异常
|
||||
mock_func.side_effect = Exception('connection failed')
|
||||
|
||||
# Mock 根据参数返回不同值
|
||||
def side_effect_func(arg):
|
||||
if arg == 'valid':
|
||||
return True
|
||||
return False
|
||||
mock_func.side_effect = side_effect_func
|
||||
```
|
||||
|
||||
## Fixture 管理
|
||||
|
||||
### Django TestCase setUp
|
||||
|
||||
```python
|
||||
class ExampleServiceTest(TestCase):
|
||||
"""案例服务测试"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备:创建测试数据"""
|
||||
self.user = User.objects.create(
|
||||
username='testuser',
|
||||
realname='测试用户',
|
||||
password=make_password('123456'),
|
||||
status=1
|
||||
)
|
||||
|
||||
self.example = Example.objects.create(
|
||||
name='测试案例',
|
||||
type=1,
|
||||
status=1,
|
||||
sort=1,
|
||||
create_user='testuser'
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""测试后清理(可选,Django TestCase 自动回滚)"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 批量创建测试数据
|
||||
|
||||
```python
|
||||
def create_test_examples(count=10):
|
||||
"""批量创建测试数据"""
|
||||
examples = []
|
||||
for i in range(count):
|
||||
examples.append(Example(
|
||||
name=f'案例{i}',
|
||||
type=i % 3 + 1,
|
||||
status=1,
|
||||
sort=i,
|
||||
create_user='admin'
|
||||
))
|
||||
Example.objects.bulk_create(examples)
|
||||
return examples
|
||||
```
|
||||
|
||||
### Factory Boy(推荐大型项目)
|
||||
|
||||
```python
|
||||
# pip install factory-boy
|
||||
|
||||
import factory
|
||||
from application.example.models import Example
|
||||
|
||||
class ExampleFactory(factory.django.DjangoModelFactory):
|
||||
"""案例工厂"""
|
||||
class Meta:
|
||||
model = Example
|
||||
|
||||
name = factory.Sequence(lambda n: f'案例{n}')
|
||||
type = 1
|
||||
status = 1
|
||||
sort = factory.Sequence(lambda n: n)
|
||||
create_user = 'admin'
|
||||
|
||||
# 使用
|
||||
example = ExampleFactory() # 创建单个
|
||||
examples = ExampleFactory.create_batch(10) # 批量创建
|
||||
example = ExampleFactory(name='自定义名称') # 自定义字段
|
||||
```
|
||||
|
||||
## API 测试
|
||||
|
||||
### Django Client
|
||||
|
||||
```python
|
||||
from django.test import TestCase, Client
|
||||
|
||||
class ExampleViewTest(TestCase):
|
||||
"""案例视图测试"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
# 创建测试数据
|
||||
Example.objects.create(name='案例1', type=1, status=1, sort=1)
|
||||
|
||||
def test_page_without_login(self):
|
||||
"""测试未登录访问"""
|
||||
response = self.client.get('/example/page')
|
||||
self.assertIn(response.status_code, [401, 302])
|
||||
|
||||
def test_page_with_login(self):
|
||||
"""测试登录后访问"""
|
||||
# 模拟登录
|
||||
self.client.force_login(User.objects.first())
|
||||
|
||||
response = self.client.get('/example/page')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
self.assertEqual(data['code'], 0)
|
||||
self.assertIn('records', data['data'])
|
||||
```
|
||||
|
||||
### RequestFactory
|
||||
|
||||
```python
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
class ExampleServiceTest(TestCase):
|
||||
"""案例服务测试"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_get_page(self):
|
||||
"""测试分页查询"""
|
||||
request = self.factory.get('/example/page', {'pageNo': 1, 'pageSize': 10})
|
||||
result = get_example_page(request)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_add(self):
|
||||
"""测试添加"""
|
||||
request = self.factory.post(
|
||||
'/example/add',
|
||||
data=json.dumps({'name': '新案例', 'type': 1}),
|
||||
content_type='application/json'
|
||||
)
|
||||
result = add_example(request)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
```
|
||||
|
||||
## 常见测试场景
|
||||
|
||||
### 权限测试
|
||||
|
||||
```python
|
||||
class PermissionTest(TestCase):
|
||||
"""权限测试"""
|
||||
|
||||
def test_admin_bypass_permission(self):
|
||||
"""测试管理员跳过权限检查"""
|
||||
admin = User.objects.get(id=1)
|
||||
self.client.force_login(admin)
|
||||
|
||||
response = self.client.get('/example/page')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_user_permission_check(self):
|
||||
"""测试普通用户权限检查"""
|
||||
user = User.objects.create(username='normal_user')
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.get('/example/page')
|
||||
# 无权限时应返回 401
|
||||
self.assertIn(response.status_code, [401, 403])
|
||||
```
|
||||
|
||||
### 文件上传测试
|
||||
|
||||
```python
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
class UploadTest(TestCase):
|
||||
"""文件上传测试"""
|
||||
|
||||
def test_upload_image(self):
|
||||
"""测试上传图片"""
|
||||
image = SimpleUploadedFile(
|
||||
name='test.png',
|
||||
content=b'\x89PNG\r\n...',
|
||||
content_type='image/png'
|
||||
)
|
||||
|
||||
response = self.client.post('/upload/uploadFile', {'file': image})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
```
|
||||
|
||||
### 软删除测试
|
||||
|
||||
```python
|
||||
class SoftDeleteTest(TestCase):
|
||||
"""软删除测试"""
|
||||
|
||||
def test_soft_delete(self):
|
||||
"""测试软删除"""
|
||||
example = Example.objects.create(name='待删除', type=1, status=1)
|
||||
|
||||
# 软删除
|
||||
example.is_delete = True
|
||||
example.save()
|
||||
|
||||
# 验证查询不到
|
||||
self.assertFalse(
|
||||
Example.objects.filter(is_delete=False, id=example.id).exists()
|
||||
)
|
||||
|
||||
# 验证包含已删除可以查到
|
||||
self.assertTrue(
|
||||
Example.objects.filter(id=example.id).exists()
|
||||
)
|
||||
```
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
### 配置 coverage
|
||||
|
||||
```bash
|
||||
# 安装
|
||||
pip install coverage
|
||||
|
||||
# 运行测试并生成覆盖率
|
||||
coverage run --source='application' manage.py test
|
||||
|
||||
# 查看报告
|
||||
coverage report
|
||||
|
||||
# 生成 HTML 报告
|
||||
coverage html
|
||||
|
||||
# 查看未覆盖的行
|
||||
coverage report --show-missing
|
||||
```
|
||||
|
||||
### 覆盖率目标
|
||||
|
||||
| 层级 | 目标 | 说明 |
|
||||
|------|------|------|
|
||||
| Model 层 | 90%+ | 模型方法、字段约束 |
|
||||
| Service 层 | 80%+ | 业务逻辑、异常处理 |
|
||||
| View 层 | 70%+ | 请求处理、权限校验 |
|
||||
| 工具函数 | 90%+ | 通用工具函数 |
|
||||
|
||||
### CI 集成
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
- name: Run tests
|
||||
run: |
|
||||
coverage run --source='application' manage.py test
|
||||
coverage report --fail-under=80
|
||||
```
|
||||
|
||||
## 测试组织
|
||||
|
||||
### 按模块组织
|
||||
|
||||
```
|
||||
application/
|
||||
├── user/
|
||||
│ ├── models.py
|
||||
│ ├── services.py
|
||||
│ ├── views.py
|
||||
│ └── tests.py # 该模块的测试
|
||||
├── role/
|
||||
│ └── tests.py
|
||||
└── example/
|
||||
└── tests.py
|
||||
```
|
||||
|
||||
### 按类型组织
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_models.py # 模型测试
|
||||
├── test_services.py # Service 层测试
|
||||
├── test_views.py # View 层测试
|
||||
├── test_forms.py # Form 层测试
|
||||
└── test_utils.py # 工具函数测试
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
测试最佳实践涵盖 Mock/patch(隔离外部依赖)、Fixture(测试数据管理)、API 测试(Client/RequestFactory)、常见场景(权限/上传/软删除)、覆盖率(coverage + CI 集成)等方面。核心原则:隔离依赖、覆盖关键路径、自动化运行。
|
||||
@@ -0,0 +1,262 @@
|
||||
# 测试规范
|
||||
|
||||
## 概述
|
||||
|
||||
项目采用 Django 内置测试框架(基于 `unittest`),支持单元测试和集成测试。测试使用 `python manage.py test` 命令运行。
|
||||
|
||||
:::tip 测试体系
|
||||
1. 单元测试(Unit):验证单个函数/方法的逻辑正确性
|
||||
2. 集成测试(Integration):验证模块间协作、真实数据库操作
|
||||
:::
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_models.py # 模型测试
|
||||
├── test_services.py # Service 层测试
|
||||
├── test_views.py # View 层测试
|
||||
├── test_forms.py # Form 层测试
|
||||
└── test_utils.py # 工具函数测试
|
||||
```
|
||||
|
||||
或按应用组织:
|
||||
|
||||
```
|
||||
application/example/
|
||||
├── models.py
|
||||
├── services.py
|
||||
├── views.py
|
||||
├── forms.py
|
||||
└── tests.py # 该应用的测试
|
||||
```
|
||||
|
||||
## Django TestCase 示例
|
||||
|
||||
### 模型测试
|
||||
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from application.example.models import Example
|
||||
|
||||
|
||||
class ExampleModelTest(TestCase):
|
||||
"""案例模型测试"""
|
||||
|
||||
def test_create_example(self):
|
||||
"""测试创建案例"""
|
||||
example = Example.objects.create(
|
||||
name='测试案例',
|
||||
type=1,
|
||||
status=1,
|
||||
sort=1,
|
||||
create_user='admin'
|
||||
)
|
||||
self.assertIsNotNone(example.id)
|
||||
self.assertEqual(example.name, '测试案例')
|
||||
self.assertFalse(example.is_delete)
|
||||
|
||||
def test_soft_delete(self):
|
||||
"""测试软删除"""
|
||||
example = Example.objects.create(
|
||||
name='待删除案例',
|
||||
type=1,
|
||||
status=1,
|
||||
sort=1,
|
||||
)
|
||||
example.is_delete = True
|
||||
example.save()
|
||||
|
||||
# 验证软删除后查询不到
|
||||
self.assertFalse(
|
||||
Example.objects.filter(is_delete=False, id=example.id).exists()
|
||||
)
|
||||
|
||||
# 验证包含已删除记录可以查到
|
||||
self.assertTrue(
|
||||
Example.objects.filter(id=example.id).exists()
|
||||
)
|
||||
```
|
||||
|
||||
### Service 层测试
|
||||
|
||||
```python
|
||||
from django.test import TestCase, RequestFactory
|
||||
from application.example.services import get_example_page, add_example
|
||||
|
||||
|
||||
class ExampleServiceTest(TestCase):
|
||||
"""案例 Service 层测试"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备"""
|
||||
self.factory = RequestFactory()
|
||||
# 创建测试数据
|
||||
Example.objects.create(name='案例1', type=1, status=1, sort=1)
|
||||
Example.objects.create(name='案例2', type=1, status=1, sort=2)
|
||||
|
||||
def test_get_example_page(self):
|
||||
"""测试分页查询"""
|
||||
request = self.factory.get('/example/page', {'pageNo': 1, 'pageSize': 10})
|
||||
result = get_example_page(request)
|
||||
# 验证返回格式
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_get_example_page_with_filter(self):
|
||||
"""测试带筛选条件的分页查询"""
|
||||
request = self.factory.get('/example/page', {'name': '案例1'})
|
||||
result = get_example_page(request)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
```
|
||||
|
||||
### View 层测试
|
||||
|
||||
```python
|
||||
from django.test import TestCase, Client
|
||||
from application.example.models import Example
|
||||
|
||||
|
||||
class ExampleViewTest(TestCase):
|
||||
"""案例 View 层测试"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备"""
|
||||
self.client = Client()
|
||||
# 创建测试数据
|
||||
Example.objects.create(name='案例1', type=1, status=1, sort=1)
|
||||
|
||||
def test_page_without_login(self):
|
||||
"""测试未登录访问分页接口"""
|
||||
response = self.client.get('/example/page')
|
||||
# 未登录应返回 401 或重定向
|
||||
self.assertIn(response.status_code, [401, 302])
|
||||
|
||||
def test_page_with_login(self):
|
||||
"""测试登录后访问分页接口"""
|
||||
# 模拟登录(需根据实际认证方式调整)
|
||||
self.client.force_login(user=User.objects.first())
|
||||
response = self.client.get('/example/page')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
```
|
||||
|
||||
### Form 层测试
|
||||
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from application.example.forms import ExampleForm
|
||||
|
||||
|
||||
class ExampleFormTest(TestCase):
|
||||
"""案例 Form 层测试"""
|
||||
|
||||
def test_valid_form(self):
|
||||
"""测试有效表单"""
|
||||
data = {
|
||||
'name': '测试案例',
|
||||
'type': 1,
|
||||
'status': 1,
|
||||
'sort': 1,
|
||||
}
|
||||
form = ExampleForm(data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_invalid_form_missing_name(self):
|
||||
"""测试缺少必填字段"""
|
||||
data = {
|
||||
'type': 1,
|
||||
'status': 1,
|
||||
'sort': 1,
|
||||
}
|
||||
form = ExampleForm(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('name', form.errors)
|
||||
|
||||
def test_invalid_form_type_out_of_range(self):
|
||||
"""测试类型值超出范围"""
|
||||
data = {
|
||||
'name': '测试案例',
|
||||
'type': 99, # 超出范围
|
||||
'status': 1,
|
||||
'sort': 1,
|
||||
}
|
||||
form = ExampleForm(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('type', form.errors)
|
||||
```
|
||||
|
||||
## 测试命名规范
|
||||
|
||||
| 类型 | 文件命名 | 函数命名 |
|
||||
|------|---------|---------|
|
||||
| 模型测试 | `test_*.py` | `test_*` |
|
||||
| Service 测试 | `test_*.py` | `test_*` |
|
||||
| View 测试 | `test_*.py` | `test_*` |
|
||||
| Form 测试 | `test_*.py` | `test_*` |
|
||||
|
||||
:::tip 函数命名规则
|
||||
测试函数名应清晰描述被测行为:
|
||||
- `test_create_example`:验证创建案例
|
||||
- `test_soft_delete`:验证软删除
|
||||
- `test_get_page_with_filter`:验证带筛选的分页查询
|
||||
:::
|
||||
|
||||
## 运行命令
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python manage.py test
|
||||
|
||||
# 运行指定应用的测试
|
||||
python manage.py test application.example
|
||||
|
||||
# 运行指定测试文件
|
||||
python manage.py test application.example.tests
|
||||
|
||||
# 运行指定测试类
|
||||
python manage.py test application.example.tests.ExampleModelTest
|
||||
|
||||
# 运行指定测试方法
|
||||
python manage.py test application.example.tests.ExampleModelTest.test_create_example
|
||||
|
||||
# 显示详细输出
|
||||
python manage.py test -v 2
|
||||
|
||||
# 运行测试并显示覆盖率
|
||||
python manage.py test --parallel
|
||||
```
|
||||
|
||||
## 测试数据库
|
||||
|
||||
Django 测试框架会自动创建测试数据库(命名为 `test_{DATABASE_NAME}`),测试完成后自动销毁。
|
||||
|
||||
:::tip 测试数据库配置
|
||||
- 测试数据库与生产数据库完全隔离
|
||||
- 每个测试方法运行在独立事务中,测试结束后自动回滚
|
||||
- 使用 `TestCase` 而非 `TransactionTestCase` 可获得更好的性能
|
||||
:::
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
```bash
|
||||
# 安装覆盖率插件
|
||||
pip install coverage
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
coverage run --source='application' manage.py test
|
||||
|
||||
# 查看报告
|
||||
coverage report
|
||||
|
||||
# 生成 HTML 报告
|
||||
coverage html
|
||||
```
|
||||
|
||||
:::tip 覆盖率目标
|
||||
- Model 层:90%+
|
||||
- Service 层:80%+
|
||||
- View 层:70%+
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
测试规范使用 Django 内置测试框架,支持模型、Service、View、Form 各层测试。测试文件命名 `test_*.py`,函数命名 `test_*`。使用 `python manage.py test` 运行测试,测试数据库自动创建和销毁。建议使用 `coverage` 工具监控测试覆盖率。
|
||||
@@ -0,0 +1,207 @@
|
||||
# View层规范
|
||||
|
||||
## 概述
|
||||
|
||||
View 层负责处理 HTTP 请求和返回响应,使用 Django 类视图(Class-Based Views)。每个视图类对应一个 API 端点,通过装饰器实现登录验证、权限检查和操作日志记录。
|
||||
|
||||
:::tip 设计原则
|
||||
- 使用类视图(继承 `View`)
|
||||
- 通过 `@method_decorator(check_login)` 验证登录
|
||||
- 通过 `PermissionRequired` mixin 检查权限
|
||||
- 通过 `@operation_log` 记录操作日志
|
||||
- 写操作检查 `DJANGO_DEMO` 演示模式
|
||||
:::
|
||||
|
||||
## 完整示例
|
||||
|
||||
以案例模块为例,`application/example/views.py`:
|
||||
|
||||
```python
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from application.example import services
|
||||
from config.env import DJANGO_DEMO
|
||||
from middleware.login_middleware import check_login
|
||||
from middleware.permission_middleware import PermissionRequired
|
||||
from utils import R
|
||||
from application.operation_log.constants import LogType
|
||||
from application.operation_log.decorators import operation_log
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 分页查询视图
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
"""案例分页数据查询视图"""
|
||||
permission_required = ("sys:example:page",)
|
||||
|
||||
@operation_log(title="案例管理-查询分页列表", log_type=LogType.QUERY)
|
||||
def get(self, request):
|
||||
result = services.get_example_page(request)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 详情查询视图
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="get")
|
||||
class ExampleDetailView(PermissionRequired, View):
|
||||
"""案例详情查询视图"""
|
||||
permission_required = ("sys:example:detail",)
|
||||
|
||||
@operation_log(title="案例管理-查询记录详情", log_type=LogType.QUERY)
|
||||
def get(self, request, id):
|
||||
data = services.get_example_detail(id)
|
||||
return R.ok(data=data)
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 添加视图
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="post")
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
"""添加案例视图"""
|
||||
permission_required = ("sys:example:add",)
|
||||
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
def post(self, request):
|
||||
# 演示环境禁止操作
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
|
||||
result = services.add_example(request)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 更新视图
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="put")
|
||||
class ExampleUpdateView(PermissionRequired, View):
|
||||
"""更新案例视图"""
|
||||
permission_required = ('sys:example:update',)
|
||||
|
||||
@operation_log(title="案例管理-更新记录", log_type=LogType.UPDATE)
|
||||
def put(self, request):
|
||||
# 演示环境禁止操作
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
|
||||
result = services.update_example(request)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 删除视图(支持批量)
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="delete")
|
||||
class ExampleDeleteView(PermissionRequired, View):
|
||||
"""删除案例视图"""
|
||||
permission_required = ('sys:example:delete',)
|
||||
|
||||
@operation_log(title="案例管理-删除记录", log_type=LogType.DELETE)
|
||||
def delete(self, request, id):
|
||||
# 演示环境禁止操作
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
|
||||
result = services.delete_examples(id)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 状态更新视图
|
||||
# =============================================================
|
||||
|
||||
@method_decorator(check_login, name="put")
|
||||
class ExampleStatusView(PermissionRequired, View):
|
||||
"""设置案例状态视图"""
|
||||
permission_required = ('sys:example:status',)
|
||||
|
||||
def put(self, request):
|
||||
# 演示环境禁止操作
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
|
||||
result = services.update_example_status(request)
|
||||
return result
|
||||
```
|
||||
|
||||
## 装饰器说明
|
||||
|
||||
### @method_decorator(check_login)
|
||||
|
||||
验证 JWT 登录状态,从 `Authorization: Bearer <token>` 头解析用户信息。未登录返回 401。
|
||||
|
||||
```python
|
||||
@method_decorator(check_login, name="get") # 验证 GET 请求
|
||||
@method_decorator(check_login, name="post") # 验证 POST 请求
|
||||
@method_decorator(check_login, name="put") # 验证 PUT 请求
|
||||
@method_decorator(check_login, name="delete") # 验证 DELETE 请求
|
||||
```
|
||||
|
||||
### PermissionRequired
|
||||
|
||||
权限检查 mixin,用户 ID 为 1 的超级管理员自动跳过权限检查。
|
||||
|
||||
```python
|
||||
class ExamplePageView(PermissionRequired, View):
|
||||
permission_required = ("sys:example:page",) # 权限标识
|
||||
```
|
||||
|
||||
### @operation_log
|
||||
|
||||
操作日志记录装饰器,自动记录请求参数、响应结果、执行时间等。
|
||||
|
||||
```python
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
```
|
||||
|
||||
日志类型:
|
||||
|
||||
| 类型 | 说明 |
|
||||
|:----|:----|
|
||||
| `LogType.QUERY` | 查询操作 |
|
||||
| `LogType.ADD` | 添加操作 |
|
||||
| `LogType.UPDATE` | 更新操作 |
|
||||
| `LogType.DELETE` | 删除操作 |
|
||||
|
||||
## 演示模式检查
|
||||
|
||||
写操作(add/update/delete/status)需检查 `DJANGO_DEMO` 配置:
|
||||
|
||||
```python
|
||||
if DJANGO_DEMO:
|
||||
return R.failed("演示环境,暂无操作权限")
|
||||
```
|
||||
|
||||
## 权限标识规范
|
||||
|
||||
| 操作 | 权限标识 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 分页查询 | `sys:{module}:page` | 查询分页数据 |
|
||||
| 列表查询 | `sys:{module}:list` | 查询列表数据 |
|
||||
| 详情查询 | `sys:{module}:detail` | 查询单条详情 |
|
||||
| 添加 | `sys:{module}:add` | 添加记录 |
|
||||
| 更新 | `sys:{module}:update` | 更新记录 |
|
||||
| 删除 | `sys:{module}:delete` | 删除记录 |
|
||||
| 状态 | `sys:{module}:status` | 更新状态 |
|
||||
|
||||
## View 层职责
|
||||
|
||||
View 层只负责:
|
||||
1. 接收 HTTP 请求
|
||||
2. 调用 Service 层函数
|
||||
3. 返回 R 响应
|
||||
|
||||
禁止在 View 层直接操作数据库或编写业务逻辑。
|
||||
|
||||
## 总结
|
||||
|
||||
View 层使用 Django 类视图,通过装饰器实现登录验证(`check_login`)、权限检查(`PermissionRequired`)和操作日志记录(`operation_log`)。写操作检查 `DJANGO_DEMO` 演示模式。View 层只负责请求分发和响应返回,业务逻辑集中在 Service 层。
|
||||
Reference in New Issue
Block a user