签入版本
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
# 8.4 文件上传接口
|
||||
|
||||
:::tip 概述
|
||||
文件上传接口将文件保存到临时目录,返回文件 URL。前端拿到 URL 后提交表单时传给后端,BaseLogic 在 add/update 时自动迁移到正式目录。通过 `type` 参数可区分上传类型(文件/图片)。
|
||||
:::
|
||||
|
||||
## 接口信息
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| URL | `POST /api/upload/uploadFile` |
|
||||
| Content-Type | `multipart/form-data` |
|
||||
| 权限 | 无需认证(公开接口) |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| `file` | File | 是 | - | 文件二进制数据 |
|
||||
| `type` | string | 否 | `file` | 上传类型:`file`(所有合法文件)/ `image`(仅图片) |
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"ok": true,
|
||||
"msg": "上传成功",
|
||||
"data": {
|
||||
"fileUrl": "https://cdn.example.com/temp/20260920/a1b2c3d4e5f6.jpg",
|
||||
"filePath": "temp/20260920/a1b2c3d4e5f6.jpg",
|
||||
"originalName": "photo.jpg",
|
||||
"fileSize": 102400,
|
||||
"fileExtension": "jpg",
|
||||
"fileType": "image/jpeg",
|
||||
"fileName": "a1b2c3d4e5f6.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `fileUrl` | string | 完整 URL(含域名) | `https://cdn.example.com/temp/...` |
|
||||
| `filePath` | string | 相对路径 | `temp/20260920/a1b2c3d4e5f6.jpg` |
|
||||
| `originalName` | string | 原始文件名 | `photo.jpg` |
|
||||
| `fileSize` | number | 文件大小(字节) | `102400` |
|
||||
| `fileExtension` | string | 扩展名 | `jpg` |
|
||||
| `fileType` | string | MIME 类型 | `image/jpeg` |
|
||||
| `fileName` | string | 服务器文件名(随机) | `a1b2c3d4e5f6.jpg` |
|
||||
|
||||
### type 参数说明
|
||||
|
||||
| type 值 | 说明 | 允许的后缀 | 适用场景 |
|
||||
|---------|------|-----------|---------|
|
||||
| `file`(默认) | 所有合法文件 | 由 `config/file.php` 的 `file_ext` 控制 | 文档、压缩包等 |
|
||||
| `image` | 仅图片 | 由 `config/file.php` 的 `image_ext` 控制 | 头像、封面等 |
|
||||
|
||||
**file_ext 默认值:**
|
||||
|
||||
```text
|
||||
gif, jpg, jpeg, png, bmp, webp, svg,
|
||||
doc, docx, xls, xlsx, ppt, pptx,
|
||||
pdf, zip, rar, txt, csv,
|
||||
mp3, mp4, sql, js, css
|
||||
```
|
||||
|
||||
**image_ext 默认值:**
|
||||
|
||||
```text
|
||||
gif, jpg, jpeg, png, bmp, webp, svg
|
||||
```
|
||||
|
||||
## 失败响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"ok": false,
|
||||
"msg": "不允许的文件类型: .exe",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误信息 | 原因 | 解决方案 |
|
||||
|---------|------|----------|
|
||||
| `未接收到上传文件` | 表单字段名不是 `file` | 检查 FormData 的 key 是否为 `file` |
|
||||
| `不允许的文件类型: .xxx` | 后缀不在白名单 | 检查 `config/file.php` 的 `image_ext`/`file_ext` |
|
||||
| `文件大小超过限制` | 超过 `max_size` | 调整 `.env` 中 `FILE.MAX_SIZE` 配置 |
|
||||
| `图片文件内容无效` | 图片损坏或非图片伪装 | 重新选择文件 |
|
||||
| `文件上传失败` | PHP 上传错误 | 检查 `php.ini` 的 `upload_max_filesize` |
|
||||
|
||||
## 前端使用
|
||||
|
||||
### Element Plus Upload 组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 上传文件(默认 type=file) -->
|
||||
<el-upload
|
||||
:action="'/api/upload/uploadFile'"
|
||||
:headers="{ Authorization: `Bearer ${token}` }"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
|
||||
<!-- 上传图片(指定 type=image) -->
|
||||
<el-upload
|
||||
:action="'/api/upload/uploadFile?type=image'"
|
||||
:headers="{ Authorization: `Bearer ${token}` }"
|
||||
:on-success="handleImageSuccess"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button type="primary">上传图片</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('文件大小不能超过 10MB');
|
||||
}
|
||||
return isLt10M;
|
||||
};
|
||||
|
||||
const handleUploadSuccess = (response) => {
|
||||
if (response.code === 0) {
|
||||
form.value.file = response.data.fileUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 0) {
|
||||
form.value.avatar = response.data.fileUrl;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 手动上传
|
||||
|
||||
```typescript
|
||||
// 上传文件
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const res = await http.request({
|
||||
url: '/upload/uploadFile',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
// res.fileUrl = "https://cdn.example.com/temp/20260920/abc.jpg"
|
||||
|
||||
// 上传图片(指定 type=image)
|
||||
const res2 = await http.request({
|
||||
url: '/upload/uploadFile?type=image',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
```
|
||||
|
||||
### 提交表单时传 URL
|
||||
|
||||
```typescript
|
||||
// 前端上传后拿到 fileUrl,提交表单时传给后端
|
||||
await exampleAdd({
|
||||
name: '测试',
|
||||
avatar: 'https://cdn.example.com/temp/20260920/abc.jpg',
|
||||
});
|
||||
|
||||
// 后端 BaseLogic::add() 自动:
|
||||
// 1. 检测到 avatar 是临时文件 URL
|
||||
// 2. 调用 save_file() 迁移到正式目录
|
||||
// 3. 去掉域名,只存相对路径入库
|
||||
```
|
||||
|
||||
### 头像上传示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="avatar-upload">
|
||||
<el-avatar :src="form.avatar" :size="80" />
|
||||
<el-upload
|
||||
:action="'/api/upload/uploadFile?type=image'"
|
||||
:headers="{ Authorization: `Bearer ${token}` }"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button size="small">更换头像</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const form = ref({ avatar: '' });
|
||||
|
||||
const handleAvatarSuccess = (response) => {
|
||||
form.value.avatar = response.data.fileUrl;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## 上传后处理流程
|
||||
|
||||
```text
|
||||
前端上传 → temp/日期/xxx.jpg(临时目录)
|
||||
│
|
||||
▼ 提交表单(携带 URL)
|
||||
BaseLogic::add()
|
||||
│
|
||||
├─ processFileFieldsOnSave()
|
||||
│ └─ save_file() → 正式目录/日期/xxx.jpg
|
||||
│
|
||||
└─ 数据库存储相对路径
|
||||
│
|
||||
▼ 读取时
|
||||
processFileFieldsOnRead()
|
||||
└─ get_file_url() → 拼接域名返回完整 URL
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
```ini
|
||||
; .env
|
||||
[FILE]
|
||||
UPLOAD_DIR = D:/uploads/rxthinkcmf ; 上传根目录
|
||||
DOMAIN_URL = https://cdn.example.com ; 文件访问域名
|
||||
MAX_SIZE = 10 ; 单文件最大 10MB
|
||||
IMAGE_EXT = jpg,jpeg,png,gif,bmp,webp,svg
|
||||
FILE_EXT = jpg,jpeg,png,gif,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv,mp3,mp4,sql,js,css
|
||||
```
|
||||
|
||||
## 安全设计
|
||||
|
||||
| 安全措施 | 说明 |
|
||||
|---------|------|
|
||||
| 文件类型校验 | 同时检查后缀名和 MIME 类型(finfo),防止伪造扩展名 |
|
||||
| 文件大小限制 | 通过 `config/file.php` 的 `max_size` 配置 |
|
||||
| 图片内容校验 | 图片类型额外用 `getimagesize` 验证,防止非图片伪装 |
|
||||
| 随机文件名 | 使用 `bin2hex(random_bytes(16))` 防止文件名冲突和路径遍历 |
|
||||
| 临时目录隔离 | 上传文件先存入 `temp/` 目录,提交表单后才迁移到正式目录 |
|
||||
| Nginx 防护 | 上传目录禁止执行 PHP 文件 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题 1:上传返回 401
|
||||
|
||||
**原因:** 上传接口需要认证,但请求头未携带 Token。
|
||||
|
||||
**解决:** 确保请求头包含 `Authorization: Bearer <token>`。
|
||||
|
||||
### 问题 2:上传返回"不允许的文件类型"
|
||||
|
||||
**原因:** 文件扩展名不在白名单中。
|
||||
|
||||
**解决:** 检查 `.env` 中 `FILE.FILE_EXT` 或 `FILE.IMAGE_EXT` 配置。
|
||||
|
||||
### 问题 3:上传后图片不显示
|
||||
|
||||
**原因:** `DOMAIN_URL` 配置错误或 Nginx 未配置静态文件处理。
|
||||
|
||||
**解决:** 检查 `.env` 中 `FILE.DOMAIN_URL` 是否正确,Nginx 是否配置了 `/uploads/` 的 alias。
|
||||
|
||||
### 问题 4:大文件上传失败
|
||||
|
||||
**原因:** PHP 或 Nginx 限制了上传大小。
|
||||
|
||||
**解决:**
|
||||
|
||||
```ini
|
||||
; php.ini
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 25M
|
||||
|
||||
; .env
|
||||
FILE.MAX_SIZE = 20
|
||||
```
|
||||
|
||||
```nginx
|
||||
# nginx.conf
|
||||
client_max_body_size 20M;
|
||||
```
|
||||
Reference in New Issue
Block a user