194 lines
5.0 KiB
Markdown
194 lines
5.0 KiB
Markdown
# 文件上传联调
|
||
|
||
:::tip 说明
|
||
文件上传涉及前端 Upload 组件、后端上传接口和 URL 拼接三个环节,需要前后端协同配置。
|
||
:::
|
||
|
||
## 上传流程
|
||
|
||
```
|
||
1. 前端 Upload 组件选择文件
|
||
2. VAxios.uploadFile() 发送 multipart/form-data POST 请求到 /api/v1/upload/uploadFile
|
||
3. 后端校验文件(扩展名、大小、魔术字节)
|
||
4. 后端保存文件到临时目录,返回文件信息(fileUrl 等)
|
||
5. 前端将 fileUrl 存入业务字段
|
||
6. 业务数据提交时携带 fileUrl
|
||
7. 后端 save_file() 将临时文件迁移到正式目录
|
||
```
|
||
|
||
## 前端上传实现
|
||
|
||
### VAxios.uploadFile
|
||
|
||
`VAxios` 类内置 `uploadFile` 方法:
|
||
|
||
```typescript
|
||
// src/utils/http/axios/Axios.ts
|
||
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
|
||
const formData = new window.FormData();
|
||
const customFilename = params.name || 'file';
|
||
|
||
if (params.filename) {
|
||
formData.append(customFilename, params.file, params.filename);
|
||
} else {
|
||
formData.append(customFilename, params.file);
|
||
}
|
||
|
||
if (params.data) {
|
||
Object.keys(params.data).forEach((key) => {
|
||
const value = params.data![key];
|
||
if (Array.isArray(value)) {
|
||
value.forEach((item) => formData.append(`${key}[]`, item));
|
||
return;
|
||
}
|
||
formData.append(key, params.data![key]);
|
||
});
|
||
}
|
||
|
||
return this.axiosInstance.request<T>({
|
||
method: 'POST',
|
||
data: formData,
|
||
headers: {
|
||
'Content-type': ContentTypeEnum.FORM_DATA,
|
||
ignoreCancelToken: true,
|
||
},
|
||
...config,
|
||
});
|
||
}
|
||
```
|
||
|
||
### Upload 组件使用
|
||
|
||
```vue
|
||
<template>
|
||
<Upload v-model="formData.cover" :limit="1" accept="image/*" />
|
||
</template>
|
||
|
||
<script setup>
|
||
const formData = ref({
|
||
name: '',
|
||
cover: '', // 存储文件 URL
|
||
})
|
||
</script>
|
||
```
|
||
|
||
Upload 组件内部调用上传接口:
|
||
|
||
```typescript
|
||
const handleUpload = async (file) => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
const res = await uploadFile(formData);
|
||
// res.data.fileUrl = "http://127.0.0.1:8031/api/file/20250306/xxx.jpg"
|
||
emit('update:modelValue', res.data.fileUrl);
|
||
};
|
||
```
|
||
|
||
## 后端上传接口
|
||
|
||
```
|
||
POST /api/v1/upload/uploadFile
|
||
Content-Type: multipart/form-data
|
||
参数: file(文件字段)
|
||
```
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"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"
|
||
},
|
||
"msg": "上传成功",
|
||
"ok": true
|
||
}
|
||
```
|
||
|
||
## URL 拼接
|
||
|
||
### 后端拼接
|
||
|
||
后端 `save_file()` 函数处理文件迁移后返回相对路径,`get_file_url()` 拼接完整 URL:
|
||
|
||
```python
|
||
def get_file_url(path):
|
||
if not path:
|
||
return ""
|
||
if path.find(FASTAPI_FILE_URL) != -1:
|
||
return path
|
||
return FASTAPI_FILE_URL + path
|
||
```
|
||
|
||
### 前端显示
|
||
|
||
前端显示图片时,需要拼接完整的 URL:
|
||
|
||
```typescript
|
||
const getFileUrl = (path) => {
|
||
if (!path) return '';
|
||
if (path.startsWith('http')) return path;
|
||
return import.meta.env.VITE_GLOB_FILE_URL + path;
|
||
};
|
||
```
|
||
|
||
## 临时文件迁移
|
||
|
||
业务数据提交时,后端 service 调用 `save_file()` 将临时文件迁移到正式目录:
|
||
|
||
```python
|
||
# 临时路径: {temp}/{日期}/{文件名}
|
||
# 正式路径: {upload_dir}/{分类}/{日期}/{文件名}
|
||
|
||
def save_file(url, directory):
|
||
disk_path = _url_to_disk_path(url)
|
||
if disk_path.startswith(FASTAPI_TEMP_PATH):
|
||
target_dir = os.path.join(FASTAPI_UPLOAD_DIR, directory, time.strftime('%Y%m%d'))
|
||
new_path = os.path.join(target_dir, os.path.basename(disk_path))
|
||
shutil.move(disk_path, new_path)
|
||
return _disk_to_url_path(new_path)
|
||
return _disk_to_url_path(disk_path)
|
||
```
|
||
|
||
## 配置一致性
|
||
|
||
确保以下配置一致:
|
||
|
||
| 配置项 | 位置 | 说明 |
|
||
|--------|------|------|
|
||
| `FASTAPI_FILE_URL` | 后端 `.env` | 文件访问域名 |
|
||
| `VITE_GLOB_FILE_URL` | 前端 `.env` | 文件访问域名 |
|
||
| Nginx `location` | Nginx 配置 | 文件代理路径 |
|
||
|
||
:::tip 温馨提示
|
||
开发环境和生产环境的文件访问域名不同,需要分别配置。`VITE_GLOB_FILE_URL` 通过 `useGlobSetting()` 获取,生产环境通常使用独立的文件服务器或 CDN。
|
||
:::
|
||
|
||
## 常见问题
|
||
|
||
### 图片不显示
|
||
|
||
```
|
||
1. 检查 VITE_GLOB_FILE_URL 配置是否正确
|
||
2. 检查 Nginx 是否配置了 /api/file/ 代理
|
||
3. 检查文件是否已从临时目录迁移到正式目录
|
||
```
|
||
|
||
### 上传失败
|
||
|
||
```
|
||
1. 检查文件扩展名是否在白名单中
|
||
2. 检查文件大小是否超过 UPLOAD_MAX_SIZE_MB 限制
|
||
3. 检查后端服务是否正常运行
|
||
```
|
||
|
||
## 总结
|
||
|
||
文件上传通过 `VAxios.uploadFile()` 发送 `multipart/form-data` 请求,后端保存到临时目录并返回 `fileUrl`。业务提交时后端 `save_file()` 迁移到正式目录。前端通过 `VITE_GLOB_FILE_URL` 拼接完整 URL 显示文件。
|