签入版本

This commit is contained in:
2026-09-12 23:12:15 +08:00
commit 91998056ca
1841 changed files with 341325 additions and 0 deletions
@@ -0,0 +1,295 @@
# 文件上传完整流程
本章从配置到使用,完整说明文件上传功能的实现方式,包括后端接口、前端组件、安全机制和最佳实践。
## 上传架构
```
前端 Vue3 后端 FastAPI 文件系统
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Upload │ multipart │ 安全校验 │ 写入 │ uploads/ │
│ 组件 │ ─────────> │ ─ 扩展名白名单│ ───────> │ 2026/ │
│ │ │ ─ 魔术字节 │ │ 09/ │
│ │ │ ─ 大小限制 │ │ 05/ │
│ │ │ ─ 路径遍历 │ │ xxx.jpg│
└──────────┘ └──────────────┘ └──────────┘
│
▼
┌──────────────┐
│ 返回 fileUrl │
│ 前端存入字段 │
└──────────────┘
```
## 环境变量配置
| 变量 | 默认值 | 说明 |
|:----|:----|:----|
| `UPLOAD_ALLOWED_EXTS` | `.jpg,.jpeg,.png,.gif,...,.zip` | 允许上传的文件扩展名(逗号分隔,带点号) |
| `UPLOAD_MAX_SIZE_MB` | `10` | 上传文件大小上限(MB) |
| `FASTAPI_FILE_URL` | `http://file.fastapi.elevue` | 文件访问域名 |
| `FASTAPI_UPLOAD_DIR` | `uploads` | 上传文件存储目录 |
## 上传接口
系统提供统一的文件上传入口,支持图片、文档、压缩包等各类文件:
### 上传文件
```
POST /api/v1/upload/uploadFile
Content-Type: multipart/form-data
Authorization: Bearer <token>
file: <文件>
```
支持格式由 `UPLOAD_ALLOWED_EXTS` 配置控制,默认包含:
```
图片:.jpg, .jpeg, .png, .gif, .bmp, .ico
文档:.pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt
音视频:.mp3, .mp4
压缩:.zip, .rar, .7z
```
成功响应:
```json
{
"code": 0,
"msg": "上传成功",
"data": {
"originalName": "photo.jpg",
"fileExtension": "jpg",
"fileType": "image/jpeg",
"fileSize": 306995,
"fileName": "2025030614302512345.jpg",
"filePath": "/20250306/2025030614302512345.jpg",
"fileUrl": "http://127.0.0.1:8031/api/file/20250306/2025030614302512345.jpg"
},
"ok": true
}
```
:::tip 富文本编辑器集成
富文本编辑器(如 TinyMCE、CKEditor、WangEditor 等)的图片上传同样使用此接口。编辑器配置上传地址为 `/api/v1/upload/uploadFile`,上传成功后从 `response.data.fileUrl` 获取图片 URL 即可。
:::
## 安全机制
### 扩展名白名单
只允许 `UPLOAD_ALLOWED_EXTS` 中配置的扩展名。配置的扩展名会与内置安全集合取交集,确保不会绕过安全校验。留空时回落到内置智能集合。
禁止上传的扩展名(硬编码黑名单,无论配置如何一律拒绝):
```
.exe, .bat, .cmd, .sh, .ps1, .php, .asp, .aspx, .jsp, .py, .rb, .js, .html, .htm
.svg(无魔数签名,且存在存储型 XSS 风险)
.webp(魔数仅 RIFF 前缀,无可靠二进制签名)
```
### 魔术字节校验
不仅检查扩展名,还读取文件头部魔术字节验证真实类型:
| 类型 | 魔术字节 |
|:----|:----|
| JPEG | `FF D8 FF` |
| PNG | `89 50 4E 47` |
| GIF | `47 49 46 38` |
| PDF | `25 50 44 46` |
| ZIP | `50 4B 03 04` |
防止攻击者通过修改扩展名上传恶意文件。
### 路径遍历防护
文件名经过安全校验,拒绝包含 `..` 或路径分隔符的文件名:
```python
def _is_safe_filename(filename: str) -> bool:
normalized = os.path.normpath(filename)
if '..' in normalized.split(os.sep):
return False
basename = os.path.basename(normalized)
return basename == normalized
```
### 大小限制
上传文件通过流式读取校验大小,超过 `UPLOAD_MAX_SIZE_MB` 限制立即中断,避免整文件读入内存:
```python
async def _read_upload_limited(file, max_size_bytes):
chunks = []
total = 0
while True:
chunk = await file.read(65536)
if not chunk:
break
total += len(chunk)
if total > max_size_bytes:
return None, R.failed(msg="文件大小超过限制")
chunks.append(chunk)
return b''.join(chunks), None
```
## 文件存储策略
### 目录结构
```
uploads/
├── 2026/
│ ├── 09/
│ │ ├── 05/
│ │ │ ├── 2026090514302512345.jpg
│ │ │ ├── 2026090515011278901.pdf
│ │ │ └── ...
│ │ └── 06/
│ └── 10/
└── 2025/
```
- 按日期分目录存储(`{UPLOAD_DIR}/{YYYYMMDD}/`),避免单目录文件过多
- 文件名使用 `年月日时分秒 + 5位随机数` 重命名,避免中文文件名和冲突
- 保留原始扩展名
### 文件访问 URL
上传成功后返回的 `fileUrl` 是拼接了 `FASTAPI_FILE_URL` 前缀的完整 URL:
```
http://127.0.0.1:8031/api/file/20250306/2025030614302512345.jpg
```
Service 层的 `BaseService._enrich_file_urls()` 会自动为文件字段补此前缀。
## 前端集成
### 文件上传组件
```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/v1/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>
```
### 图片预览
```vue
<template>
<el-image
v-if="form.cover"
:src="getFileUrl(form.cover)"
:preview-src-list="[getFileUrl(form.cover)]"
fit="cover"
style="width: 100px; height: 100px"
/>
</template>
<script setup>
function getFileUrl(path) {
if (!path) return '';
if (path.startsWith('http')) return path;
return import.meta.env.VITE_FILE_URL + path;
}
</script>
```
## 业务模块中的文件字段
在 Service 中声明 `file_fields`,基类自动处理文件迁移和 URL 补全:
```python
class ArticleService(BaseService[Article]):
# 文件字段:add/update 时自动迁移临时文件,列表/详情自动补全 URL
file_fields = ('cover', 'attachment')
```
### 自动处理逻辑
1. **新增/更新时**:检测文件字段值是否为临时路径,如果是则迁移到正式目录
2. **列表/详情时**:自动为文件字段补全 `FASTAPI_FILE_URL` 前缀
## 富文本中的图片
在 Service 中声明 `rich_text_fields`,基类自动处理:
```python
class ArticleService(BaseService[Article]):
# 富文本字段:自动迁移嵌入图片 + XSS 清洗
rich_text_fields = ('content',)
```
### 自动处理逻辑
1. **新增/更新时**:解析富文本中的 `<img src="...">` 标签
2. 将临时路径的图片迁移到正式目录
3. 替换富文本中的图片路径
4. 进行 XSS 清洗(使用 bleach 库)
## 常见问题
### 上传报 413 Request Entity Too Large
**原因**:文件大小超过 `UPLOAD_MAX_SIZE_MB` 限制。
**解决**:修改 `.env` 中的 `UPLOAD_MAX_SIZE_MB` 值。
### 上传报 403 Forbidden
**原因**:文件扩展名不在白名单中。
**解决**:检查 `UPLOAD_ALLOWED_EXTS` 配置,添加所需扩展名。
### 图片上传成功但不显示
**原因**:`FASTAPI_FILE_URL` 配置错误或前端未拼接域名。
**解决**:检查 `.env` 中的 `FASTAPI_FILE_URL` 配置,确保前端正确拼接。
### 上传报演示环境无权限
**原因**:演示模式下写操作被拦截。
**解决**:将 `.env` 中的 `FASTAPI_DEMO` 设为 `False`。
## 总结
文件上传功能通过扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 大小限制四层安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 `uploadFile` 接口支持图片和各类文件上传。Service 层通过 `file_fields` 和 `rich_text_fields` 声明即可自动处理文件迁移和 URL 补全。