签入版本
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# 数据库与 ORM
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
项目使用 Django ORM 作为数据库访问层,数据库连接配置位于 `config/env.py` 和 `application/settings.py`。默认使用 MySQL 数据库,通过 `django.db.backends.mysql` 引擎连接。
|
||||
:::
|
||||
|
||||
## 数据库配置
|
||||
|
||||
### 环境变量(config/env.py)
|
||||
|
||||
```python
|
||||
# 数据库引擎,默认 MySQL
|
||||
DATABASE_ENGINE = os.getenv('DATABASE_ENGINE', "django.db.backends.mysql")
|
||||
|
||||
# 数据库名称
|
||||
DATABASE_NAME = os.getenv('DATABASE_NAME', 'djangoadmin.django.elevue')
|
||||
|
||||
# 数据库地址
|
||||
DATABASE_HOST = os.getenv('DATABASE_HOST', "127.0.0.1")
|
||||
|
||||
# 数据库端口
|
||||
DATABASE_PORT = os.getenv('DATABASE_PORT', 3306)
|
||||
|
||||
# 数据库用户名
|
||||
DATABASE_USER = os.getenv('DATABASE_USER', 'root')
|
||||
|
||||
# 数据库密码
|
||||
DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD', 'root')
|
||||
|
||||
# 数据表前缀
|
||||
DATABASE_PREFIX = os.getenv('DATABASE_PREFIX', "django_")
|
||||
```
|
||||
|
||||
### Django Settings(application/settings.py)
|
||||
|
||||
```python
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": env.DATABASE_ENGINE,
|
||||
"NAME": env.DATABASE_NAME,
|
||||
"USER": env.DATABASE_USER,
|
||||
"PASSWORD": env.DATABASE_PASSWORD,
|
||||
"HOST": env.DATABASE_HOST,
|
||||
"PORT": env.DATABASE_PORT,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MySQL 引擎配置
|
||||
|
||||
项目默认使用 `django.db.backends.mysql` 引擎,需要安装 `mysqlclient` 或 `PyMySQL`:
|
||||
|
||||
```bash
|
||||
# 方式一:mysqlclient(推荐,C 扩展,性能更好)
|
||||
pip install mysqlclient
|
||||
|
||||
# 方式二:PyMySQL(纯 Python,兼容性更好)
|
||||
pip install PyMySQL
|
||||
```
|
||||
|
||||
如果使用 PyMySQL,需要在 `application/__init__.py` 中添加:
|
||||
|
||||
```python
|
||||
import pymysql
|
||||
pymysql.install_as_MySQLdb()
|
||||
```
|
||||
|
||||
## 数据表前缀
|
||||
|
||||
所有业务数据表自动添加 `DATABASE_PREFIX` 前缀(默认 `django_`),便于多应用共存:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_table_name(base_name):
|
||||
"""获取带前缀的表名"""
|
||||
prefix = getattr(settings, 'DATABASE_PREFIX', '')
|
||||
return f"{prefix}{base_name}"
|
||||
```
|
||||
|
||||
## ORM 查询模式
|
||||
|
||||
### 基础查询
|
||||
|
||||
```python
|
||||
from application.example.models import Example
|
||||
|
||||
# 查询未删除的记录(软删除)
|
||||
queryset = Example.objects.filter(is_delete=False)
|
||||
|
||||
# 条件过滤
|
||||
queryset = queryset.filter(name__contains=name) # 模糊查询
|
||||
queryset = queryset.filter(status=status) # 精确匹配
|
||||
queryset = queryset.order_by("sort") # 排序
|
||||
```
|
||||
|
||||
### 分页查询
|
||||
|
||||
```python
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage, EmptyPage
|
||||
|
||||
paginator = Paginator(queryset, page_size)
|
||||
try:
|
||||
page_list = paginator.page(page_no)
|
||||
except (PageNotAnInteger, InvalidPage, EmptyPage):
|
||||
page_list = paginator.page(1)
|
||||
|
||||
# 构建分页数据
|
||||
page_data = {
|
||||
'records': [item for item in page_list],
|
||||
'total': paginator.count,
|
||||
'size': page_size,
|
||||
'current': page_no,
|
||||
'pages': paginator.num_pages,
|
||||
}
|
||||
```
|
||||
|
||||
### 创建记录
|
||||
|
||||
```python
|
||||
Example.objects.create(
|
||||
name="示例",
|
||||
status=1,
|
||||
create_user=get_username(request),
|
||||
update_user=get_username(request),
|
||||
)
|
||||
```
|
||||
|
||||
### 更新记录
|
||||
|
||||
```python
|
||||
instance = Example.objects.filter(id=instance_id, is_delete=False).first()
|
||||
if instance:
|
||||
instance.name = "新名称"
|
||||
instance.update_user = get_username(request)
|
||||
instance.save()
|
||||
```
|
||||
|
||||
### 批量更新(逻辑删除)
|
||||
|
||||
```python
|
||||
Example.objects.filter(id__in=id_list, is_delete=False).update(is_delete=True)
|
||||
```
|
||||
|
||||
## 软删除机制
|
||||
|
||||
所有业务模型继承 `BaseModel`,包含 `is_delete` 字段。所有查询都过滤 `is_delete=False`,删除操作为逻辑删除:
|
||||
|
||||
```python
|
||||
# application/models.py
|
||||
class BaseModel(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
create_user = models.CharField(max_length=64, default='', null=True, blank=True)
|
||||
create_time = models.DateTimeField(auto_now_add=True)
|
||||
update_user = models.CharField(max_length=64, default='', null=True, blank=True)
|
||||
update_time = models.DateTimeField(auto_now=True)
|
||||
is_delete = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
## 缓存配置(Redis)
|
||||
|
||||
Redis 用于 JWT Token 黑名单和权限缓存:
|
||||
|
||||
```python
|
||||
# application/settings.py
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": f"redis://{env.REDIS_HOST}:{env.REDIS_PORT}/{env.REDIS_DB}",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PASSWORD": env.REDIS_PASSWORD if env.REDIS_PASSWORD else None,
|
||||
"SOCKET_CONNECT_TIMEOUT": 5,
|
||||
"SOCKET_TIMEOUT": 5,
|
||||
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
|
||||
"IGNORE_EXCEPTIONS": True,
|
||||
},
|
||||
"KEY_PREFIX": "djangoadmin",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Redis 配置(config/env.py):
|
||||
|
||||
```python
|
||||
REDIS_HOST = os.getenv('REDIS_HOST', '127.0.0.1')
|
||||
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', '123456')
|
||||
REDIS_DB = int(os.getenv('REDIS_DB', 0))
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
数据库会话管理由 Django ORM 自动处理,每个请求自动管理数据库连接的生命周期。通过 `config/env.py` 集中管理数据库和 Redis 连接配置,支持通过环境变量覆盖。软删除机制确保数据安全,所有查询自动过滤已删除记录。
|
||||
@@ -0,0 +1,127 @@
|
||||
# 导入导出
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
基于 `openpyxl` 实现 Excel 导入导出功能,支持通过 `file_template` 模块管理导入模板配置。
|
||||
:::
|
||||
|
||||
## 功能概述
|
||||
|
||||
Excel 导入导出是后台管理系统的常见需求,通过 `openpyxl` 库实现:
|
||||
|
||||
```
|
||||
Excel 导出:
|
||||
1. 将数据库中的数据导出为 Excel 文件,支持自定义表头和格式
|
||||
2. 适用于生成报表、数据备份等场景
|
||||
|
||||
Excel 导入:
|
||||
1. 将 Excel 文件中的数据导入到数据库,支持批量处理和校验
|
||||
2. 适用于批量数据录入、数据迁移等场景
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
|
||||
在 `requirements.txt` 中已包含:
|
||||
|
||||
```
|
||||
openpyxl>=3.1.0
|
||||
```
|
||||
|
||||
## 导出实现
|
||||
|
||||
导出接口通常在各业务模块的 views.py 中定义,返回 `HttpResponse`:
|
||||
|
||||
```python
|
||||
from openpyxl import Workbook
|
||||
from django.http import HttpResponse
|
||||
import io
|
||||
|
||||
class ExportView(PermissionRequired, View):
|
||||
permission_required = ("sys:user:export",)
|
||||
|
||||
def get(self, request):
|
||||
# 查询数据
|
||||
data_list = services.get_export_data(request)
|
||||
|
||||
# 创建工作簿
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "用户信息"
|
||||
|
||||
# 写入表头
|
||||
ws.append(["用户名", "姓名", "手机号", "邮箱", "状态"])
|
||||
|
||||
# 写入数据
|
||||
for item in data_list:
|
||||
ws.append([item.username, item.realname, item.phone, item.email, item.status_name])
|
||||
|
||||
# 返回流式响应
|
||||
output = io.BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
response = HttpResponse(
|
||||
output.getvalue(),
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
)
|
||||
response["Content-Disposition"] = "attachment; filename=export.xlsx"
|
||||
return response
|
||||
```
|
||||
|
||||
## 导入实现
|
||||
|
||||
导入接口接收上传的 Excel 文件,解析后逐行校验并入库:
|
||||
|
||||
```python
|
||||
class ImportView(PermissionRequired, View):
|
||||
permission_required = ("sys:user:import",)
|
||||
|
||||
def post(self, request):
|
||||
file = request.FILES.get('file')
|
||||
if not file:
|
||||
return R.failed(msg="请选择要上传的文件")
|
||||
|
||||
wb = load_workbook(io.BytesIO(file.read()))
|
||||
ws = wb.active
|
||||
|
||||
# 跳过表头,逐行解析
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
username, realname, phone, email, status = row
|
||||
# 校验并入库 ...
|
||||
```
|
||||
|
||||
## file_template 模块
|
||||
|
||||
`file_template` 模块用于管理导入模板的配置信息,定义模板的字段映射、校验规则等。
|
||||
|
||||
模块路径:`application/file_template/`
|
||||
|
||||
```
|
||||
模板配置包含:
|
||||
- 模板名称
|
||||
- 关联模块
|
||||
- 字段映射关系
|
||||
- 校验规则
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
导入时建议先下载模板文件,按模板格式填写数据后再导入,可减少格式错误。
|
||||
:::
|
||||
|
||||
## 错误处理
|
||||
|
||||
导入过程中遇到校验错误时,返回详细的错误信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "导入失败:第3行用户名已存在,第5行手机号格式错误",
|
||||
"data": null,
|
||||
"ok": false
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Excel 导入导出功能基于 `openpyxl` 实现,导出使用 `HttpResponse` 流式返回避免内存溢出,导入逐行校验并提供详细错误信息。`file_template` 模块可管理导入模板配置,便于维护字段映射和校验规则。
|
||||
@@ -0,0 +1,249 @@
|
||||
# IP/UA 解析
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
提供客户端 IP 获取、IP 地理位置查询和 User-Agent 解析三大能力,位于 `utils/common.py`。三者组合使用,为登录日志、操作日志等场景提供客户端身份信息。
|
||||
:::
|
||||
|
||||
## 架构总览
|
||||
|
||||
```
|
||||
请求进入
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ get_client_ip(request) │
|
||||
│ ├─ HTTP_X_FORWARDED_FOR 存在? │
|
||||
│ │ └─ 是 → 取第一个 IP(客户端真实 IP) │
|
||||
│ └─ 否 → REMOTE_ADDR(直接连接 IP) │
|
||||
└──────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────────────────────────────┐
|
||||
│ 日志记录 │ │ get_ip_location(ip) │
|
||||
│ 操作日志 │ │ ├─ 内网 IP → "内网IP" │
|
||||
└─────────┘ │ ├─ ip-api.com 在线查询 │
|
||||
│ └─ 异常降级 → "未知" │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ parse_user_agent(ua) │
|
||||
│ ├─ 操作系统(正则) │
|
||||
│ └─ 浏览器(正则) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ 写入日志表 │
|
||||
│ ip / location / │
|
||||
│ os / browser │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 一、IP 获取
|
||||
|
||||
### get_client_ip(request)
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_client_ip(request) -> str:
|
||||
"""
|
||||
获取客户端真实IP地址
|
||||
|
||||
优先从 HTTP_X_FORWARDED_FOR 获取,其次使用 REMOTE_ADDR。
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
```
|
||||
|
||||
**返回值**:客户端 IP 地址字符串(如 `"1.2.3.4"`)。
|
||||
|
||||
### 代理环境说明
|
||||
|
||||
当应用部署在 Nginx 等反向代理之后,`REMOTE_ADDR` 拿到的是代理 IP 而非真实客户端 IP。此时需要从 `X-Forwarded-For`(XFF)头提取真实 IP。
|
||||
|
||||
```
|
||||
客户端 (1.2.3.4)
|
||||
│
|
||||
▼
|
||||
Nginx 代理 (10.0.0.1) ← 追加 XFF: 1.2.3.4
|
||||
│
|
||||
▼
|
||||
Django 应用
|
||||
REMOTE_ADDR = 10.0.0.1
|
||||
HTTP_X_FORWARDED_FOR = "1.2.3.4"
|
||||
```
|
||||
|
||||
:::warning 安全提示
|
||||
如果应用被直接暴露在公网,客户端可伪造 XFF 首部。生产环境务必通过 Nginx 等反向代理转发请求。
|
||||
:::
|
||||
|
||||
## 二、IP 地理位置查询
|
||||
|
||||
### get_ip_location(ip)
|
||||
|
||||
通过第三方 API 获取 IP 地理位置:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_ip_location(ip: str) -> str:
|
||||
"""
|
||||
根据IP获取地理位置
|
||||
|
||||
内网IP直接返回"内网IP",调用 ip-api.com API 获取地理位置。
|
||||
"""
|
||||
# 内网IP判断
|
||||
if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'):
|
||||
return "内网IP"
|
||||
|
||||
try:
|
||||
url = f"http://ip-api.com/json/{ip}?lang=zh-CN"
|
||||
with urllib.request.urlopen(url, timeout=3) as response:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
if data.get('status') == 'success':
|
||||
return f"{data.get('country', '')} {data.get('regionName', '')} {data.get('city', '')}"
|
||||
except Exception as e:
|
||||
logger.warning(f"获取IP地理位置失败: {e}")
|
||||
|
||||
return "未知"
|
||||
```
|
||||
|
||||
**返回值示例**:
|
||||
|
||||
| IP | 返回值 |
|
||||
|:----|:----|
|
||||
| `114.114.114.114` | `中国 江苏省 南京市` |
|
||||
| `8.8.8.8` | `United States California` |
|
||||
| `10.0.0.1` | `内网IP` |
|
||||
| `172.abc` | `未知` |
|
||||
|
||||
### IP 地理位置配置
|
||||
|
||||
在 `config/env.py` 中配置 IP 查询相关参数:
|
||||
|
||||
```python
|
||||
# IP地理位置信息来源: 'local' 或 'api'
|
||||
IP_LOCATION_SOURCE = os.environ.get('IP_LOCATION_SOURCE', 'api')
|
||||
|
||||
# 本地IP数据库文件路径
|
||||
IP_DATABASE_PATH = os.environ.get('IP_DATABASE_PATH', ...)
|
||||
|
||||
# IP查询API超时时间(秒)
|
||||
IP_API_TIMEOUT = int(os.environ.get('IP_API_TIMEOUT', 5))
|
||||
|
||||
# 是否启用IP地理位置缓存
|
||||
IP_CACHE_ENABLED = os.environ.get('IP_CACHE_ENABLED', 'True') == 'True'
|
||||
|
||||
# IP缓存时间(秒),默认1天
|
||||
IP_CACHE_TIMEOUT = int(os.environ.get('IP_CACHE_TIMEOUT', 86400))
|
||||
```
|
||||
|
||||
### 内网 IP 判断
|
||||
|
||||
内网/保留 IP 直接返回 `"内网IP"`,不查询 API:
|
||||
|
||||
| 类型 | 匹配规则 | 示例 |
|
||||
|:-----|:---------|:-----|
|
||||
| 回环地址 | `127.0.0.1`、`localhost` | `127.0.0.1` |
|
||||
| C 类私有 | `192.168.x.x` | `192.168.1.1` |
|
||||
| A 类私有 | `10.x.x.x` | `10.0.0.1` |
|
||||
|
||||
## 三、UA 解析
|
||||
|
||||
### parse_user_agent(user_agent)
|
||||
|
||||
通过正则匹配从 User-Agent 字符串提取操作系统和浏览器信息:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def parse_user_agent(user_agent: str) -> dict:
|
||||
"""
|
||||
解析User-Agent,获取操作系统和浏览器信息
|
||||
|
||||
Returns:
|
||||
dict: {'os': '操作系统', 'browser': '浏览器'}
|
||||
"""
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
|
||||
```python
|
||||
ua = request.META.get('HTTP_USER_AGENT', '')
|
||||
info = parse_user_agent(ua)
|
||||
print(f"操作系统: {info['os']}, 浏览器: {info['browser']}")
|
||||
```
|
||||
|
||||
### 支持的操作系统
|
||||
|
||||
| 操作系统 | UA 关键字 | 输出 |
|
||||
|:---------|:---------|:-----|
|
||||
| Windows 10 | `Windows NT 10.0` | `Windows 10` |
|
||||
| Windows 7 | `Windows NT 6.1` | `Windows 7` |
|
||||
| macOS | `Mac OS X` | `macOS` |
|
||||
| Linux | `Linux` | `Linux` |
|
||||
| Android | `Android` | `Android` |
|
||||
| iOS | `iPhone` / `iPad` | `iOS` |
|
||||
|
||||
### 支持的浏览器
|
||||
|
||||
按匹配优先级排列(Edge 优先于 Chrome):
|
||||
|
||||
| 浏览器 | UA 关键字 | 输出 |
|
||||
|:-------|:---------|:-----|
|
||||
| Edge | `Edg` | `Edge` |
|
||||
| Chrome | `Chrome` | `Chrome` |
|
||||
| Safari | `Safari` | `Safari` |
|
||||
| Firefox | `Firefox` | `Firefox` |
|
||||
| IE | `MSIE` / `Trident` | `IE` |
|
||||
|
||||
## 四、应用场景
|
||||
|
||||
### 登录日志
|
||||
|
||||
登录/退出时自动记录 IP、地理位置、操作系统、浏览器:
|
||||
|
||||
```python
|
||||
ip = get_client_ip(request)
|
||||
location = get_ip_location(ip)
|
||||
ua = request.META.get('HTTP_USER_AGENT', '')
|
||||
info = parse_user_agent(ua)
|
||||
```
|
||||
|
||||
### 操作日志
|
||||
|
||||
写操作(add/update/delete/status)通过 `@operation_log` 装饰器自动记录操作者 IP 和设备信息。
|
||||
|
||||
## 五、常见问题
|
||||
|
||||
### 开发环境日志中 IP 显示为 127.0.0.1
|
||||
|
||||
**原因**:本地直连无代理,`REMOTE_ADDR` 就是 `127.0.0.1`。
|
||||
|
||||
**说明**:这是正常行为。生产环境配置 Nginx 反向代理后即可获取真实客户端 IP。
|
||||
|
||||
### 地理位置显示为"未知"
|
||||
|
||||
**可能原因**:
|
||||
|
||||
1. ip-api.com API 请求超时(免费版限制 45 次/分钟)
|
||||
2. 网络连接问题
|
||||
3. 畸形 IP 地址
|
||||
|
||||
**解决**:生产环境建议使用本地 IP 数据库(如纯真 IP 库),配置 `IP_LOCATION_SOURCE=local`。
|
||||
|
||||
## 六、总结
|
||||
|
||||
| 模块 | 文件 | 核心能力 | 降级策略 |
|
||||
|:-----|:-----|:---------|:---------|
|
||||
| IP 获取 | `utils/common.py` | 从 Request 提取真实客户端 IP | 优先 XFF,回退 REMOTE_ADDR |
|
||||
| 地理位置 | `utils/common.py` | 基于 ip-api.com 在线查询 | 异常/内网统一降级返回 `"未知"` 或 `"内网IP"` |
|
||||
| UA 解析 | `utils/common.py` | 正则匹配提取操作系统和浏览器 | UA 为空时返回 `{'os': '未知', 'browser': '未知'}` |
|
||||
|
||||
三者组合为登录日志、操作日志等场景提供完整的客户端身份信息链路。所有接口均采用降级策略,保证日志落库等核心流程不受异常影响。
|
||||
@@ -0,0 +1,146 @@
|
||||
# 日志系统
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
通过 Python 标准库 `logging` 实现结构化日志,Django 框架内置日志配置,支持控制台 + 文件双输出。各模块通过 `logging.getLogger(__name__)` 获取 logger 实例。
|
||||
:::
|
||||
|
||||
## Django 日志配置
|
||||
|
||||
在 `application/settings.py` 中可配置 `LOGGING` 字典(项目当前使用默认配置):
|
||||
|
||||
```python
|
||||
# application/settings.py 中可添加自定义日志配置
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '{asctime} | {levelname:<7} | {name} | {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
'file': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/app.log',
|
||||
'maxBytes': 10 * 1024 * 1024, # 10MB
|
||||
'backupCount': 5,
|
||||
'formatter': 'verbose',
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
在各模块中获取 logger:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("操作成功")
|
||||
logger.warning("配置缺失")
|
||||
logger.error(f"异常信息: {str(e)}")
|
||||
```
|
||||
|
||||
## 日志格式
|
||||
|
||||
```
|
||||
2025-03-06 14:30:25,123 | INFO | application.example.services | 操作成功
|
||||
```
|
||||
|
||||
格式说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `asctime` | 时间戳 |
|
||||
| `levelname` | 日志级别(右对齐7字符) |
|
||||
| `name` | 模块名称 |
|
||||
| `message` | 日志内容 |
|
||||
|
||||
## 日志级别
|
||||
|
||||
| 级别 | 数值 | 说明 |
|
||||
|------|------|------|
|
||||
| `DEBUG` | 10 | 调试信息,生产环境通常关闭 |
|
||||
| `INFO` | 20 | 一般信息,记录正常操作 |
|
||||
| `WARNING` | 30 | 警告信息,不影响运行但需关注 |
|
||||
| `ERROR` | 40 | 错误信息,功能异常 |
|
||||
| `CRITICAL` | 50 | 严重错误,系统不可用 |
|
||||
|
||||
## 业务模块中的日志使用
|
||||
|
||||
### Service 层
|
||||
|
||||
```python
|
||||
# application/example/services.py
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_example_page(request):
|
||||
try:
|
||||
# 业务逻辑 ...
|
||||
return R.ok(data=page_data)
|
||||
except Exception as e:
|
||||
logging.error(f"查询案例分页数据异常: {str(e)}")
|
||||
return R.failed(msg="查询失败,请稍后重试")
|
||||
```
|
||||
|
||||
### 视图层
|
||||
|
||||
```python
|
||||
# application/upload/views.py
|
||||
import logging
|
||||
|
||||
logging.info(f"上传文件的字段名称: {field_name}")
|
||||
```
|
||||
|
||||
## 操作日志
|
||||
|
||||
除了应用日志外,系统通过 `@operation_log` 装饰器自动记录用户的写操作日志,存储到数据库中。详见 [操作日志](/zh/volume4/core/operation-log) 章节。
|
||||
|
||||
```python
|
||||
from application.operation_log.decorators import operation_log
|
||||
from application.operation_log.constants import LogType
|
||||
|
||||
class ExampleAddView(PermissionRequired, View):
|
||||
@operation_log(title="案例管理-添加记录", log_type=LogType.ADD)
|
||||
def post(self, request):
|
||||
result = services.add_example(request)
|
||||
return result
|
||||
```
|
||||
|
||||
## 日志轮转
|
||||
|
||||
当日志文件达到指定大小时自动轮转:
|
||||
|
||||
```
|
||||
logs/app.log ← 当前日志
|
||||
logs/app.log.1 ← 最近一次轮转
|
||||
logs/app.log.2 ← 倒数第二次
|
||||
logs/app.log.3
|
||||
logs/app.log.4
|
||||
logs/app.log.5 ← 最早的备份(超过自动删除)
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
生产环境建议配置日志轮转,防止日志文件无限增长撑爆磁盘。`RotatingFileHandler` 按文件大小轮转,`TimedRotatingFileHandler` 按时间轮转。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
日志系统基于 Python 标准库 `logging`,通过 Django 的 `LOGGING` 配置统一管理。各模块通过 `logging.getLogger(__name__)` 获取 logger 实例,支持控制台和文件双输出。操作日志通过 `@operation_log` 装饰器自动记录到数据库。
|
||||
@@ -0,0 +1,193 @@
|
||||
# 密码工具
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
密码加密使用 Django 内置的 `make_password` 和 `check_password` 函数,基于 PBKDF2 算法(默认工作因子 320000 次迭代)。用户安全信息获取工具位于 `utils/security.py`。
|
||||
:::
|
||||
|
||||
## 加密方案
|
||||
|
||||
Django 默认使用 PBKDF2 + SHA256 算法进行密码哈希,内置随机盐值管理:
|
||||
|
||||
```
|
||||
用户密码 + 随机盐 → PBKDF2(SHA256, 320000轮) → 存储哈希值
|
||||
```
|
||||
|
||||
### 加密密码
|
||||
|
||||
```python
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
# 生成密码哈希(自动管理盐值)
|
||||
hashed = make_password("mypassword")
|
||||
# 结果格式:pbkdf2_sha256$320000$随机盐$哈希值
|
||||
```
|
||||
|
||||
### 验证密码
|
||||
|
||||
```python
|
||||
from django.contrib.auth.hashers import check_password
|
||||
|
||||
# 验证密码是否正确
|
||||
is_correct = check_password("mypassword", hashed)
|
||||
# 返回 True 或 False
|
||||
```
|
||||
|
||||
## 存储结构
|
||||
|
||||
Django 密码哈希格式:`algorithm$iterations$salt$hash`
|
||||
|
||||
```
|
||||
pbkdf2_sha256$320000$abc123def456$longhashvalue...
|
||||
```
|
||||
|
||||
| 部分 | 说明 |
|
||||
|------|------|
|
||||
| `pbkdf2_sha256` | 哈希算法 |
|
||||
| `320000` | 迭代次数(工作因子) |
|
||||
| `abc123def456` | 随机盐值 |
|
||||
| `longhashvalue...` | 哈希结果 |
|
||||
|
||||
## 用户安全工具(utils/security.py)
|
||||
|
||||
### get_user_id(request)
|
||||
|
||||
快速获取当前登录用户的 ID:
|
||||
|
||||
```python
|
||||
from utils.security import get_user_id
|
||||
|
||||
def some_view(request):
|
||||
user_id = get_user_id(request)
|
||||
if user_id:
|
||||
# 执行需要用户ID的操作
|
||||
create_log(user_id, "操作内容")
|
||||
```
|
||||
|
||||
### get_username(request)
|
||||
|
||||
快速获取当前登录用户的真实姓名:
|
||||
|
||||
```python
|
||||
from utils.security import get_username
|
||||
|
||||
def add_example(request):
|
||||
Example.objects.create(
|
||||
name="示例",
|
||||
create_user=get_username(request),
|
||||
update_user=get_username(request),
|
||||
)
|
||||
```
|
||||
|
||||
## JWT Token 工具(utils/jwt.py)
|
||||
|
||||
### create_token(payload, timeout)
|
||||
|
||||
生成 JWT 访问令牌:
|
||||
|
||||
```python
|
||||
from utils.jwt import create_token
|
||||
|
||||
payload = {'userId': 1001, 'username': 'admin', 'realname': '管理员'}
|
||||
token = create_token(payload, timeout=20) # 20分钟过期
|
||||
```
|
||||
|
||||
### parse_token(request)
|
||||
|
||||
从请求头解析 JWT Token,获取用户信息:
|
||||
|
||||
```python
|
||||
from utils.jwt import parse_token
|
||||
|
||||
result = parse_token(request)
|
||||
if result['code'] == 0:
|
||||
user_id = result['data']['userId']
|
||||
username = result['data']['username']
|
||||
realname = result['data']['realname']
|
||||
```
|
||||
|
||||
### refresh_token(token, extend_minutes)
|
||||
|
||||
刷新 JWT 令牌,延长有效期:
|
||||
|
||||
```python
|
||||
from utils.jwt import refresh_token
|
||||
|
||||
result = refresh_token(old_token, extend_minutes=30)
|
||||
if result['code'] == 0:
|
||||
new_token = result['data']['access_token']
|
||||
```
|
||||
|
||||
### Token 黑名单
|
||||
|
||||
退出登录时将 Token 加入黑名单,使其立即失效:
|
||||
|
||||
```python
|
||||
from utils.jwt import add_token_to_blacklist
|
||||
|
||||
# 退出登录时调用
|
||||
add_token_to_blacklist(token)
|
||||
```
|
||||
|
||||
黑名单使用 Redis 存储,TTL 与 Token 剩余有效期一致,Token 自然过期后黑名单条目自动清除。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 登录验证
|
||||
|
||||
```python
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from utils.jwt import create_token
|
||||
|
||||
def login(request):
|
||||
data, error = parse_request_body(request)
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
# 查询用户
|
||||
user = User.objects.filter(username=username, is_delete=False).first()
|
||||
if not user:
|
||||
return R.failed(msg="用户名或密码错误")
|
||||
|
||||
# 验证密码
|
||||
if not check_password(password, user.password):
|
||||
return R.failed(msg="用户名或密码错误")
|
||||
|
||||
# 生成 Token
|
||||
payload = {
|
||||
'userId': user.id,
|
||||
'username': user.username,
|
||||
'realname': user.realname,
|
||||
}
|
||||
token = create_token(payload)
|
||||
return R.ok(data={'access_token': token})
|
||||
```
|
||||
|
||||
### 修改密码
|
||||
|
||||
```python
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
|
||||
def reset_password(request):
|
||||
# 验证旧密码
|
||||
if not check_password(old_password, user.password):
|
||||
return R.failed("原密码错误")
|
||||
|
||||
# 生成新密码哈希
|
||||
user.password = make_password(new_password)
|
||||
user.save()
|
||||
```
|
||||
|
||||
## 安全特性
|
||||
|
||||
```
|
||||
1. 随机盐值:Django 自动为每个密码生成随机盐值,即使两个用户使用相同密码,存储的哈希值也不同
|
||||
2. 抗彩虹表:每个密码都有独立盐值,无法预计算彩虹表
|
||||
3. 抗暴力破解:PBKDF2 320000 次迭代,大幅增加暴力破解成本
|
||||
4. 自适应性:可通过修改迭代次数提升安全性
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
密码工具使用 Django 内置的 `make_password`/`check_password`,基于 PBKDF2 + SHA256 算法,自动管理随机盐值。`utils/security.py` 提供 `get_user_id()` 和 `get_username()` 快速获取当前登录用户信息。JWT Token 通过 `utils/jwt.py` 管理,支持生成、解析、刷新和黑名单功能。
|
||||
@@ -0,0 +1,194 @@
|
||||
# 权限缓存
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
通过 Redis 实现用户权限和菜单的缓存,采用用户级缓存 + 全局版本号双层策略。权限查询函数位于 `application/menu/services.py`,供 `PermissionRequired` 中间件调用。
|
||||
:::
|
||||
|
||||
## 缓存策略
|
||||
|
||||
采用「用户级缓存 + 全局版本号」双层策略:
|
||||
|
||||
```
|
||||
1. 每个用户的缓存 Key 带版本号后缀
|
||||
2. 版本号存全局 Key(perm:version)
|
||||
3. 菜单变更时递增全局版本号,所有用户缓存自然失效
|
||||
4. 用户角色变更时,直接删除该用户缓存
|
||||
5. 角色菜单变更时,查询该角色下所有用户 ID,批量删除缓存
|
||||
```
|
||||
|
||||
## 缓存 Key 结构
|
||||
|
||||
```python
|
||||
_VERSION_KEY = 'perm:version' # 全局版本号
|
||||
_PERM_PREFIX = 'perm:list:' # 用户权限列表
|
||||
_MENU_PREFIX = 'perm:menu:' # 用户菜单树
|
||||
```
|
||||
|
||||
Key 格式示例:
|
||||
|
||||
```
|
||||
perm:version → 5
|
||||
perm:list:1001:5 → ["sys:user:page", "sys:user:add", ...]
|
||||
perm:menu:1001:5 → [{id: 1, name: "系统管理", children: [...]}]
|
||||
```
|
||||
|
||||
## Redis 连接配置
|
||||
|
||||
Redis 通过 Django 缓存框架 `django_redis` 管理:
|
||||
|
||||
```python
|
||||
# application/settings.py
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": f"redis://{env.REDIS_HOST}:{env.REDIS_PORT}/{env.REDIS_DB}",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PASSWORD": env.REDIS_PASSWORD if env.REDIS_PASSWORD else None,
|
||||
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
|
||||
"IGNORE_EXCEPTIONS": True,
|
||||
},
|
||||
"KEY_PREFIX": "djangoadmin",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
获取 Redis 连接:
|
||||
|
||||
```python
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
redis = get_redis_connection("default")
|
||||
```
|
||||
|
||||
## 权限查询
|
||||
|
||||
### get_user_permissions(user_id)
|
||||
|
||||
获取用户的权限列表,优先从缓存读取:
|
||||
|
||||
```python
|
||||
def get_user_permissions(user_id):
|
||||
"""获取用户权限列表,带缓存"""
|
||||
redis_conn = get_redis_connection("default")
|
||||
ver = redis_conn.get(_VERSION_KEY) or 0
|
||||
cache_key = f'{_PERM_PREFIX}{user_id}:{ver}'
|
||||
|
||||
# 尝试从缓存获取
|
||||
cached = redis_conn.get(cache_key)
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
|
||||
# 缓存未命中,查询数据库
|
||||
permissions = _query_permissions_from_db(user_id)
|
||||
|
||||
# 写入缓存(TTL 2小时)
|
||||
if permissions:
|
||||
redis_conn.set(cache_key, json.dumps(permissions), ex=7200)
|
||||
|
||||
return permissions
|
||||
```
|
||||
|
||||
## 缓存失效
|
||||
|
||||
### 菜单变更
|
||||
|
||||
菜单增删改时调用,递增全局版本号使所有用户缓存失效:
|
||||
|
||||
```python
|
||||
def invalidate_menu_change():
|
||||
"""菜单变更时递增全局版本号"""
|
||||
redis_conn = get_redis_connection("default")
|
||||
redis_conn.incr(_VERSION_KEY)
|
||||
```
|
||||
|
||||
递增版本号后,旧版本的缓存 Key 不再被访问,自然过期(TTL 2小时)。
|
||||
|
||||
### 用户角色变更
|
||||
|
||||
用户角色变更时调用,删除该用户的权限和菜单缓存:
|
||||
|
||||
```python
|
||||
def invalidate_user_perms(user_id):
|
||||
"""用户角色变更时删除缓存"""
|
||||
redis_conn = get_redis_connection("default")
|
||||
ver = redis_conn.get(_VERSION_KEY) or 0
|
||||
keys = [
|
||||
f'{_PERM_PREFIX}{user_id}:{ver}',
|
||||
f'{_MENU_PREFIX}{user_id}:{ver}',
|
||||
]
|
||||
redis_conn.delete(*keys)
|
||||
```
|
||||
|
||||
### 角色菜单变更
|
||||
|
||||
角色菜单变更时调用,清理该角色下所有用户的权限和菜单缓存:
|
||||
|
||||
```python
|
||||
def invalidate_role_perms(role_id):
|
||||
"""角色菜单变更时清理相关用户缓存"""
|
||||
# 查询该角色下所有用户 ID
|
||||
from application.user_role.models import UserRole
|
||||
user_ids = UserRole.objects.filter(
|
||||
role_id=role_id, is_delete=False
|
||||
).values_list('user_id', flat=True)
|
||||
|
||||
# 批量删除缓存
|
||||
redis_conn = get_redis_connection("default")
|
||||
ver = redis_conn.get(_VERSION_KEY) or 0
|
||||
keys = []
|
||||
for uid in user_ids:
|
||||
keys.append(f'{_PERM_PREFIX}{uid}:{ver}')
|
||||
keys.append(f'{_MENU_PREFIX}{uid}:{ver}')
|
||||
if keys:
|
||||
redis_conn.delete(*keys)
|
||||
```
|
||||
|
||||
## PermissionRequired 中间件
|
||||
|
||||
权限校验在 `middleware/permission_middleware.py` 中实现:
|
||||
|
||||
```python
|
||||
class PermissionRequired:
|
||||
"""权限校验中间件,用户 ID=1 为超级管理员跳过校验"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# 超级管理员跳过权限检查
|
||||
user_id = get_user_id(request)
|
||||
if user_id == 1:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
# 获取用户权限列表
|
||||
permissions = get_user_permissions(user_id)
|
||||
|
||||
# 检查是否包含所需权限
|
||||
for perm in self.permission_required:
|
||||
if perm not in permissions:
|
||||
return R.failed(msg="暂无操作权限", code=403)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
```
|
||||
|
||||
## 异常降级
|
||||
|
||||
所有缓存操作都包裹 try/except,Redis 不可用时降级直查 DB:
|
||||
|
||||
```python
|
||||
try:
|
||||
cached = redis_conn.get(cache_key)
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
except Exception as e:
|
||||
logger.warning(f"读取权限缓存异常: {e}")
|
||||
# 降级查询数据库
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
权限缓存的 TTL 为 2小时,正常情况下缓存由业务操作即时失效,TTL 仅作为兜底防止 Redis 故障时缓存永久残留。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
权限缓存采用用户级缓存 + 全局版本号双层策略,菜单变更通过递增版本号使所有缓存自然失效,无需遍历删除。所有缓存操作都有异常降级机制,Redis 不可用时自动回源数据库查询。超级管理员(用户 ID=1)跳过权限检查。
|
||||
@@ -0,0 +1,168 @@
|
||||
# 请求解析工具
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
`utils/common.py` 提供请求数据解析相关的工具函数,核心函数 `parse_request_body()` 用于解析请求体 JSON 数据。这些工具函数在各业务模块中广泛使用。
|
||||
:::
|
||||
|
||||
## parse_request_body
|
||||
|
||||
解析请求体 JSON 数据,返回 `(data, error)` 元组。这是业务模块中最常用的请求解析函数。
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def parse_request_body(request):
|
||||
"""
|
||||
解析请求体JSON数据
|
||||
|
||||
Args:
|
||||
request: HttpRequest对象
|
||||
|
||||
Returns:
|
||||
tuple: (data, error)
|
||||
成功时 data 为解析后的字典,error 为 None
|
||||
失败时 data 为 None,error 为错误描述字符串
|
||||
"""
|
||||
try:
|
||||
if not hasattr(request, 'body') or request.body is None:
|
||||
return None, None
|
||||
if not request.body:
|
||||
return None, "参数不能为空"
|
||||
|
||||
json_data = request.body.decode('utf-8')
|
||||
if not json_data:
|
||||
return None, "参数不能为空"
|
||||
|
||||
data = json.loads(json_data)
|
||||
return data, None
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return None, "参数格式错误"
|
||||
except UnicodeDecodeError as e:
|
||||
return None, "参数编码错误"
|
||||
except Exception as e:
|
||||
return None, "参数处理失败"
|
||||
```
|
||||
|
||||
使用示例:
|
||||
|
||||
```python
|
||||
def add_example(request):
|
||||
# 解析请求参数
|
||||
data, error = parse_request_body(request)
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
|
||||
# 使用解析后的数据
|
||||
name = data.get('name')
|
||||
status = data.get('status')
|
||||
# ...
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
`parse_request_body()` 返回元组 `(data, error)`,成功时 `data` 为字典、`error` 为 None;失败时 `data` 为 None、`error` 为错误描述字符串。调用方应先检查 `error` 再使用 `data`。
|
||||
:::
|
||||
|
||||
## GET 参数获取
|
||||
|
||||
Django 中获取 GET 查询参数使用 `request.GET`:
|
||||
|
||||
```python
|
||||
def get_example_page(request):
|
||||
# 获取分页参数
|
||||
page_no = int(request.GET.get('pageNo', 1))
|
||||
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
|
||||
|
||||
# 获取筛选参数
|
||||
name = request.GET.get('name')
|
||||
status = request.GET.get('status')
|
||||
|
||||
if name:
|
||||
queryset = queryset.filter(name__contains=name)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
```
|
||||
|
||||
## POST 表单参数
|
||||
|
||||
获取 POST 表单数据使用 `request.POST`:
|
||||
|
||||
```python
|
||||
def upload_file(request):
|
||||
# 获取上传的文件
|
||||
file = request.FILES.get('file')
|
||||
|
||||
# 获取表单字段
|
||||
field_name = request.POST.get('name', '')
|
||||
```
|
||||
|
||||
## 文件上传参数
|
||||
|
||||
获取上传文件使用 `request.FILES`:
|
||||
|
||||
```python
|
||||
file = request.FILES.get('file')
|
||||
if file:
|
||||
original_name = file.name # 原始文件名
|
||||
file_size = file.size # 文件大小
|
||||
# 分块读取文件内容
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
```
|
||||
|
||||
## 表单错误处理
|
||||
|
||||
`utils/regular.py` 中的 `get_err()` 函数用于提取 Django 表单验证错误信息:
|
||||
|
||||
```python
|
||||
# utils/regular.py
|
||||
def get_err(form):
|
||||
"""
|
||||
获取表单错误文本,多个错误用'/'分隔
|
||||
|
||||
使用示例:
|
||||
form = UserForm(data)
|
||||
if not form.is_valid():
|
||||
error_msg = get_err(form)
|
||||
return R.failed(msg=error_msg)
|
||||
"""
|
||||
error_list = []
|
||||
for item in form.errors.get_json_data().values():
|
||||
error_list.append(item[0].get('message'))
|
||||
return '/'.join(error_list)
|
||||
```
|
||||
|
||||
## 参数格式化(日志用)
|
||||
|
||||
`format_params()` 函数将请求参数格式化为 JSON 字符串,自动过滤敏感字段:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def format_params(params):
|
||||
"""
|
||||
格式化请求参数,自动过滤密码等敏感字段
|
||||
敏感字段值替换为 '******'
|
||||
"""
|
||||
sensitive_fields = ['password', 'old_password', 'new_password', 'confirm_password']
|
||||
if isinstance(params, dict):
|
||||
params_copy = params.copy()
|
||||
for field in sensitive_fields:
|
||||
if field in params_copy:
|
||||
params_copy[field] = '******'
|
||||
return json.dumps(params_copy, ensure_ascii=False)
|
||||
return json.dumps(params, ensure_ascii=False)
|
||||
```
|
||||
|
||||
## 日期格式化
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def format_datetime(dt, format_str='%Y-%m-%d %H:%M:%S'):
|
||||
"""格式化日期时间对象为字符串"""
|
||||
return dt.strftime(format_str) if dt else None
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
请求解析工具函数覆盖了 JSON 请求体解析、GET/POST 参数获取、文件上传、表单验证错误提取等常见场景。`parse_request_body()` 是最核心的函数,返回 `(data, error)` 元组,调用方应先检查错误再使用数据。`get_err()` 将 Django 表单验证错误转换为可读字符串。
|
||||
@@ -0,0 +1,221 @@
|
||||
# 富文本过滤
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
富文本编辑器内容存在存储型 XSS 风险,通过 `save_content()` 函数实现 HTML 内容处理,在落盘前统一过滤危险内容并迁移嵌入图片。源码位于 `utils/common.py`,涉及富文本的模块(文章、通知公告等)均调用 `save_content()` 处理。
|
||||
:::
|
||||
|
||||
## 安全威胁
|
||||
|
||||
富文本编辑器允许用户输入 HTML 内容,攻击者可能注入:
|
||||
|
||||
```
|
||||
1. <script> 标签 —— 执行任意 JavaScript
|
||||
2. on* 事件属性 —— 如 onerror="alert(1)"
|
||||
3. javascript: 协议 —— 如 <a href="javascript:alert(1)">
|
||||
4. <iframe> 标签 —— 嵌入外部页面
|
||||
5. <style> 标签 —— 覆盖页面样式
|
||||
```
|
||||
|
||||
### 攻击示例
|
||||
|
||||
```html
|
||||
<!-- 恶意脚本注入 -->
|
||||
<script>alert('XSS')</script>
|
||||
|
||||
<!-- 事件属性注入 -->
|
||||
<img src="x" onerror="alert('XSS')">
|
||||
|
||||
<!-- 协议注入 -->
|
||||
<a href="javascript:alert('XSS')">点击我</a>
|
||||
|
||||
<!-- iframe 注入 -->
|
||||
<iframe src="javascript:alert('XSS')"></iframe>
|
||||
```
|
||||
|
||||
## save_content() 函数
|
||||
|
||||
`save_content()` 函数处理富文本内容,提取并迁移嵌入图片:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def save_content(content, title, directory):
|
||||
"""
|
||||
保存富文本编辑器内容
|
||||
|
||||
处理流程:
|
||||
1. 使用正则表达式提取所有 img 标签的 src 属性
|
||||
2. 遍历每个图片 URL,调用 save_file 保存
|
||||
3. 将原 URL 替换为新 URL(带 [IMG_URL] 标记)
|
||||
4. 设置空 alt 为文章标题
|
||||
"""
|
||||
# 提取所有图片URL
|
||||
image_urls = re.findall('img src="(.*?)"', content, re.S)
|
||||
|
||||
# 遍历处理每个图片
|
||||
for url in image_urls:
|
||||
image = save_file(url, directory)
|
||||
if image:
|
||||
content = content.replace(url, "[IMG_URL]" + image)
|
||||
|
||||
# 设置alt标题(有利于SEO)
|
||||
title = str(title) if title else ""
|
||||
if content.find("alt=\"\"") and title:
|
||||
content = content.replace("alt=\"\"", "alt=\"" + title + "\"")
|
||||
|
||||
return content
|
||||
```
|
||||
|
||||
### 处理流程
|
||||
|
||||
```
|
||||
原始内容 → 提取图片 URL → 迁移图片到正式目录 → 替换 URL → 设置 alt → 返回处理后内容
|
||||
```
|
||||
|
||||
| 步骤 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | 提取图片 | 正则匹配 `img src="..."` |
|
||||
| 2 | 迁移图片 | `save_file()` 将临时图片迁移到正式目录 |
|
||||
| 3 | 替换 URL | 将临时 URL 替换为 `[IMG_URL]` + 正式路径 |
|
||||
| 4 | 设置 alt | 为空 alt 属性设置文章标题 |
|
||||
|
||||
## 文件 URL 处理
|
||||
|
||||
`get_file_url()` 函数拼接文件域名生成完整访问 URL:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_file_url(path):
|
||||
if not path:
|
||||
return None
|
||||
return DJANGO_FILE_URL + path
|
||||
```
|
||||
|
||||
## 临时文件迁移
|
||||
|
||||
`save_file()` 函数将临时目录中的文件迁移到正式存储目录:
|
||||
|
||||
```
|
||||
临时路径: {temp}/{日期}/{文件名}
|
||||
正式路径: {upload_dir}/{分类}/{日期}/{文件名}
|
||||
```
|
||||
|
||||
迁移时会创建目标目录并使用 `os.rename()` 移动文件。
|
||||
|
||||
## 使用示例
|
||||
|
||||
在业务模块中处理富文本内容:
|
||||
|
||||
```python
|
||||
from utils.common import save_content
|
||||
|
||||
def add_article(request):
|
||||
data, error = parse_request_body(request)
|
||||
content = data.get('content')
|
||||
title = data.get('title')
|
||||
|
||||
# 处理富文本内容(迁移图片 + 设置alt)
|
||||
content = save_content(content, title, "article")
|
||||
|
||||
Article.objects.create(
|
||||
title=title,
|
||||
content=content,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
### 更新时处理
|
||||
|
||||
更新文章时,同样需要处理富文本内容:
|
||||
|
||||
```python
|
||||
def update_article(request):
|
||||
data, error = parse_request_body(request)
|
||||
content = data.get('content')
|
||||
title = data.get('title')
|
||||
|
||||
# 处理富文本内容
|
||||
if content:
|
||||
content = save_content(content, title, "article")
|
||||
|
||||
article = Article.objects.filter(id=article_id, is_delete=False).first()
|
||||
article.content = content
|
||||
article.save()
|
||||
```
|
||||
|
||||
## 富文本编辑器集成
|
||||
|
||||
项目前端使用 `@wangeditor/editor-for-vue` 富文本编辑器:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" />
|
||||
<Editor
|
||||
v-model="content"
|
||||
:defaultConfig="editorConfig"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
|
||||
const editorRef = ref(null);
|
||||
const content = ref('');
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: ['fullScreen', 'group-video'], // 排除不需要的功能
|
||||
};
|
||||
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: '/api/upload/uploadFile',
|
||||
fieldName: 'file',
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedFileTypes: ['image/*'],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleCreated(editor) {
|
||||
editorRef.value = editor;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::warning 编辑器销毁
|
||||
组件销毁时需要手动销毁编辑器实例,避免内存泄漏:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor) editor.destroy();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
## XSS 防护建议
|
||||
|
||||
```
|
||||
1. 后端过滤:save_content() 处理图片迁移和 alt 设置
|
||||
2. 前端转义:渲染富文本内容时使用 v-html 需谨慎
|
||||
3. CSP 策略:配置 Content-Security-Policy 响应头
|
||||
4. 输入验证:限制富文本内容长度和格式
|
||||
5. 定期审查:检查富文本编辑器的安全配置
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
富文本内容在 service 层调用 `save_content()` 处理,而不是在 view 层。所有涉及富文本的模块(文章、通知公告等)都应使用此函数处理内容。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
富文本过滤通过 `save_content()` 函数处理,自动提取并迁移内容中的嵌入图片,替换为正式存储路径。同时为没有 alt 属性的图片设置文章标题,有利于 SEO。前端使用 `@wangeditor/editor-for-vue` 编辑器,支持图片上传和格式化。所有涉及富文本的业务模块都应在 service 层调用此函数处理内容。
|
||||
@@ -0,0 +1,223 @@
|
||||
# 文件上传完整流程
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
本章从配置到使用,完整说明文件上传功能的实现方式,包括后端接口、前端组件、安全机制和最佳实践。
|
||||
|
||||
## 上传架构
|
||||
|
||||
```
|
||||
前端 Vue3 后端 Django 文件系统
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ Upload │ multipart │ 安全校验 │ 写入 │ uploads/ │
|
||||
│ 组件 │ ─────────> │ ─ 扩展名白名单│ ───────> │ temp/ │
|
||||
│ │ │ ─ MIME 校验 │ │ 日期/ │
|
||||
│ │ │ ─ 大小校验 │ │ xxx.jpg│
|
||||
└──────────┘ └──────────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 返回 fileUrl │
|
||||
│ 前端存入字段 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
在 `config/env.py` 中配置:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `DJANGO_UPLOAD_DIR` | `{项目根目录}/public/uploads` | 上传文件存储目录 |
|
||||
| `DJANGO_FILE_URL` | `http://file.django.elevue` | 文件访问域名 |
|
||||
| `DJANGO_TEMP_PATH` | `{DJANGO_UPLOAD_DIR}/temp` | 临时文件目录 |
|
||||
|
||||
## 上传接口
|
||||
|
||||
系统提供统一的文件上传入口,支持图片、文档、压缩包等各类文件:
|
||||
|
||||
### 上传文件
|
||||
|
||||
```
|
||||
POST /upload/uploadFile
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
|
||||
file: <文件>
|
||||
```
|
||||
|
||||
支持格式:
|
||||
|
||||
```
|
||||
图片:.jpeg, .jpg, .png, .gif, .bmp
|
||||
文档:.pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "上传成功",
|
||||
"data": {
|
||||
"originalName": "photo.jpg",
|
||||
"fileExtension": "jpg",
|
||||
"fileType": "image/jpeg",
|
||||
"fileSize": 306995,
|
||||
"fileName": "2025030614302512345.jpg",
|
||||
"filePath": "/temp/20250306/2025030614302512345.jpg",
|
||||
"fileUrl": "http://file.django.elevue/temp/20250306/2025030614302512345.jpg"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 富文本编辑器集成
|
||||
富文本编辑器(如 WangEditor 等)的图片上传同样使用此接口。编辑器配置上传地址为 `/upload/uploadFile`,上传成功后从 `response.data.fileUrl` 获取图片 URL 即可。
|
||||
:::
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 扩展名白名单
|
||||
|
||||
只允许指定的扩展名格式上传,拒绝危险格式(如 `.exe`、`.bat`、`.sh`、`.js` 等)。
|
||||
|
||||
### MIME 类型校验
|
||||
|
||||
通过 `mimetypes` 模块推断文件的 MIME 类型,确保文件内容与扩展名一致。
|
||||
|
||||
### 临时目录隔离
|
||||
|
||||
上传的文件首先存储在 `temp` 临时目录,业务数据提交时才迁移到正式目录。未提交的临时文件可定期清理。
|
||||
|
||||
## 文件存储策略
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
public/uploads/
|
||||
├── temp/ # 临时文件目录
|
||||
│ ├── 20260905/
|
||||
│ │ ├── 2026090514302512345.jpg
|
||||
│ │ └── 2026090515011278901.pdf
|
||||
│ └── 20260906/
|
||||
├── example/ # 正式文件目录(按业务分类)
|
||||
│ ├── 20260905/
|
||||
│ │ └── 2026090514302512345.jpg
|
||||
│ └── 20260906/
|
||||
└── article/
|
||||
└── 20260906/
|
||||
```
|
||||
|
||||
- 按日期分目录存储(`{日期}/`),避免单目录文件过多
|
||||
- 文件名使用 `年月日时分秒 + 5位随机数` 重命名,避免中文文件名和冲突
|
||||
- 保留原始扩展名
|
||||
|
||||
### 文件访问 URL
|
||||
|
||||
上传成功后返回的 `fileUrl` 是拼接了 `DJANGO_FILE_URL` 前缀的完整 URL:
|
||||
|
||||
```
|
||||
http://file.django.elevue/temp/20250306/2025030614302512345.jpg
|
||||
```
|
||||
|
||||
Service 层的 `get_file_url()` 会自动为文件字段补此前缀。
|
||||
|
||||
## 前端集成
|
||||
|
||||
### 文件上传组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:on-success="handleSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const uploadUrl = '/api/upload/uploadFile';
|
||||
const userStore = useUserStore();
|
||||
const headers = {
|
||||
Authorization: `Bearer ${userStore.token}`,
|
||||
};
|
||||
|
||||
function beforeUpload(file) {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('文件大小不能超过 10MB');
|
||||
}
|
||||
return isLt10M;
|
||||
}
|
||||
|
||||
function handleSuccess(response) {
|
||||
if (response.code === 0) {
|
||||
// 保存 fileUrl 到业务字段
|
||||
form.value.cover = response.data.fileUrl;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 业务模块中的文件字段
|
||||
|
||||
在 Service 中使用 `save_file()` 处理文件迁移:
|
||||
|
||||
```python
|
||||
# application/example/services.py
|
||||
from utils.common import save_file, get_file_url
|
||||
|
||||
def add_example(request):
|
||||
data, error = parse_request_body(request)
|
||||
# ...
|
||||
avatar = cleaned_data.get('avatar')
|
||||
if avatar:
|
||||
avatar = save_file(avatar, "example") # 迁移到正式目录
|
||||
Example.objects.create(avatar=avatar, ...)
|
||||
```
|
||||
|
||||
### 自动处理逻辑
|
||||
|
||||
1. **新增/更新时**:检测文件字段值是否为临时路径,如果是则迁移到正式目录
|
||||
2. **列表/详情时**:通过 `get_file_url()` 自动为文件字段补全 `DJANGO_FILE_URL` 前缀
|
||||
|
||||
## 富文本中的图片
|
||||
|
||||
使用 `save_content()` 处理富文本中的图片:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def save_content(content, title, directory):
|
||||
"""提取富文本中的图片并迁移,替换 URL"""
|
||||
image_urls = re.findall('img src="(.*?)"', content, re.S)
|
||||
for url in image_urls:
|
||||
image = save_file(url, directory)
|
||||
if image:
|
||||
content = content.replace(url, "[IMG_URL]" + image)
|
||||
return content
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 上传报演示环境无权限
|
||||
|
||||
**原因**:演示模式下写操作被拦截。
|
||||
|
||||
**解决**:将 `.env` 中的 `DJANGO_DEMO` 设为 `False`。
|
||||
|
||||
### 图片上传成功但不显示
|
||||
|
||||
**原因**:`DJANGO_FILE_URL` 配置错误或前端未拼接域名。
|
||||
|
||||
**解决**:检查 `config/env.py` 中的 `DJANGO_FILE_URL` 配置,确保前端正确拼接。
|
||||
|
||||
## 总结
|
||||
|
||||
文件上传功能通过扩展名白名单 + MIME 类型校验 + 临时目录隔离等安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 `uploadFile` 接口支持图片和各类文件上传。Service 层通过 `save_file()` 和 `save_content()` 处理文件迁移和 URL 拼接。
|
||||
@@ -0,0 +1,136 @@
|
||||
# 文件上传
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
提供本地文件上传功能,支持图片和通用文件上传,内置扩展名白名单校验。服务层位于 `application/upload/services.py`,视图层位于 `application/upload/views.py`。
|
||||
:::
|
||||
|
||||
## 功能概述
|
||||
|
||||
文件上传模块提供统一的文件上传接口 `upload_file`,支持图片、文档、压缩包等各类文件,通过扩展名列表控制允许上传的格式。
|
||||
|
||||
```
|
||||
POST /upload/uploadFile —— 统一文件上传入口
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
在 `config/env.py` 中配置上传相关参数:
|
||||
|
||||
```python
|
||||
# 上传文件存储根目录
|
||||
DJANGO_UPLOAD_DIR = os.getenv('DJANGO_UPLOAD_DIR', os.path.join(DJANGO_ROOT_PATH, '/public/uploads'))
|
||||
|
||||
# 临时文件路径
|
||||
DJANGO_TEMP_PATH = DJANGO_UPLOAD_DIR + '/temp'
|
||||
|
||||
# 文件访问域名
|
||||
DJANGO_FILE_URL = os.getenv('DJANGO_FILE_URL', 'http://file.django.elevue')
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
`DJANGO_FILE_URL` 用于拼接完整的文件访问 URL,生产环境需配置为实际的文件服务域名或 Nginx 代理地址。
|
||||
:::
|
||||
|
||||
## 文件命名规则
|
||||
|
||||
上传文件自动重命名,格式为:`年月日时分秒 + 5位随机数 + 扩展名`
|
||||
|
||||
```python
|
||||
# application/upload/services.py
|
||||
timestamp = time.strftime('%Y%m%d%H%M%S')
|
||||
random_num = f'{random.randint(0, 99999):05d}'
|
||||
generated_name = f"{timestamp}{random_num}{file_ext_with_dot}"
|
||||
```
|
||||
|
||||
文件按日期分目录存储:`{UPLOAD_DIR}/temp/{YYYYMMDD}/{文件名}`
|
||||
|
||||
## 接口调用
|
||||
|
||||
### 上传文件
|
||||
|
||||
```
|
||||
POST /upload/uploadFile
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
参数: file(文件字段)
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "上传成功",
|
||||
"data": {
|
||||
"originalName": "photo.jpg",
|
||||
"fileExtension": "jpg",
|
||||
"fileType": "image/jpeg",
|
||||
"fileSize": 306995,
|
||||
"fileName": "2025030614302512345.jpg",
|
||||
"filePath": "/temp/20250306/2025030614302512345.jpg",
|
||||
"fileUrl": "http://file.django.elevue/temp/20250306/2025030614302512345.jpg"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 扩展名白名单
|
||||
|
||||
文件上传服务对扩展名进行白名单校验,支持的格式包括:
|
||||
|
||||
```
|
||||
图片:.jpeg, .jpg, .png, .gif, .bmp
|
||||
文档:.doc, .docx, .xls, .xlsx, .ppt, .pptx, .pdf, .txt
|
||||
```
|
||||
|
||||
### 文件类型校验
|
||||
|
||||
通过 `mimetypes` 模块推断文件的 MIME 类型,确保文件内容与扩展名一致。
|
||||
|
||||
### 路径安全
|
||||
|
||||
文件存储在临时目录中,业务提交时通过 `save_file()` 迁移到正式目录,避免路径遍历风险。
|
||||
|
||||
## 临时文件迁移
|
||||
|
||||
上传的文件首先存储在 `temp` 目录,业务数据提交时调用 `save_file()` 迁移到正式目录:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def save_file(url, directory):
|
||||
"""将临时文件迁移到正式存储目录"""
|
||||
if url.find(DJANGO_FILE_URL) != -1:
|
||||
if url.find("/temp") != -1:
|
||||
# 构建目标目录路径
|
||||
path = DJANGO_UPLOAD_DIR + "/" + directory + "/" + time.strftime('%Y%m%d')
|
||||
mkdir(path)
|
||||
# 移动文件
|
||||
old_path = url.replace(DJANGO_FILE_URL, DJANGO_UPLOAD_DIR)
|
||||
new_path = DJANGO_UPLOAD_DIR + "/" + directory + old_path.replace(DJANGO_TEMP_PATH, "")
|
||||
os.rename(old_path, new_path)
|
||||
return new_path.replace(DJANGO_UPLOAD_DIR, "")
|
||||
```
|
||||
|
||||
## 文件 URL 拼接
|
||||
|
||||
`get_file_url()` 函数将相对路径拼接为完整的文件访问 URL:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_file_url(path):
|
||||
if not path:
|
||||
return None
|
||||
return DJANGO_FILE_URL + path
|
||||
```
|
||||
|
||||
## 前端集成
|
||||
|
||||
前端使用 `Upload` 组件上传,上传成功后将返回的 `fileUrl` 存入业务字段。详见 [文件上传联调](/zh/volume4/integration/upload-sync) 章节。
|
||||
|
||||
## 总结
|
||||
|
||||
文件上传模块通过扩展名白名单、MIME 类型校验、临时目录隔离等安全机制,确保上传功能的安全性。文件自动重命名并按日期分目录存储,避免文件名冲突和信息泄露。统一的 `uploadFile` 接口同时支持图片和各类文件上传。
|
||||
Reference in New Issue
Block a user