Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/api-spec/upload.md
T
2026-10-08 17:50:29 +08:00

308 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 8.4 文件上传接口
:::tip 概述
文件上传接口将文件保存到 `temp/` 临时目录,返回文件相对路径与 URL。前端拿到 `filePath` 后提交表单,Service 的 `fileFields` 经 `save_file()` 把临时文件迁移到正式目录并入库,读取时自动补全域名。通过 `type` 参数区分后缀白名单(文件/图片)。
:::
## 接口信息
| 项目 | 说明 |
|------|------|
| URL | `POST /api/upload/uploadFile` |
| Content-Type | `multipart/form-data` |
| 权限 | **需认证**(`auth.jwt` 组内)+ `sys:upload:file` 权限 |
| 日志 | `#[OperationLog]` TYPE_UPLOAD(上传记录进操作日志) |
:::warning 不是公开接口
上传接口位于认证中间件组内且标注 `#[Permission('sys:upload:file', '上传文件')]`——请求头必须携带 `Authorization: Bearer <token>`,且角色需分配该权限节点。
:::
## 请求参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `file` | File | 是 | - | 文件二进制数据(FormData 字段名必须为 `file`) |
| `type` | string | 否 | `file` | 上传类型:`file`(所有合法文件)/ `image`(仅图片) |
### 成功响应
```json
{
"code": 0,
"ok": true,
"msg": "上传成功",
"data": {
"fileUrl": "http://localhost:8000/uploads/temp/20261002/123456_a1b2c3d4e5f6.jpg",
"filePath": "temp/20261002/123456_a1b2c3d4e5f6.jpg",
"originalName": "photo.jpg",
"fileSize": 102400,
"fileExtension": "jpg",
"fileType": "image/jpeg",
"fileName": "123456_a1b2c3d4e5f6.jpg"
}
}
```
### 响应字段
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `fileUrl` | string | 可访问 URL(`FILE_DOMAIN_URL` 为空时等于相对路径) | `http://localhost:8000/uploads/temp/...` |
| `filePath` | string | 相对存储路径(**提交表单建议用此字段**) | `temp/20261002/123456_a1b2....jpg` |
| `originalName` | string | 原始文件名 | `photo.jpg` |
| `fileSize` | number | 文件大小(字节) | `102400` |
| `fileExtension` | string | 扩展名 | `jpg` |
| `fileType` | string | MIME 类型(`getMimeType()`) | `image/jpeg` |
| `fileName` | string | 服务器文件名(时间+随机串) | `123456_a1b2c3d4e5f6.jpg` |
### type 参数说明
| type 值 | 说明 | 允许的后缀 | 存储目录 |
|---------|------|-----------|---------|
| `file`(默认) | 所有合法文件 | 由 `config/file.php` 的 `file_ext` 控制 | `uploads/temp/Ymd/` |
| `image` | 仅图片 | 由 `config/file.php` 的 `image_ext` 控制 | `uploads/temp/Ymd/` |
两种 `type` 的存储目录相同,均为 `temp/` 临时目录;正式目录在表单提交时由 `save_file()` 按 Service 的 `fileSaveDir` 生成。
**file_ext 默认值:**
```text
gif, jpg, jpeg, png, bmp, webp,
doc, docx, xls, xlsx, ppt, pptx,
pdf, zip, rar, txt, csv
```
**image_ext 默认值(无 svg——内嵌 JS 有 XSS 风险):**
```text
gif, jpg, jpeg, png, bmp, webp
```
## 失败响应
```json
{
"code": 1,
"ok": false,
"msg": "不支持的文件格式,允许:gif, jpg, jpeg, png, bmp, webp",
"data": null
}
```
## 常见错误(真实文案)
| 错误信息 | 原因 | 解决方案 |
|---------|------|----------|
| `未提供认证令牌`(code 401) | 请求头未携带 Token | 携带 `Authorization: Bearer <token>` |
| `无访问权限:sys:upload:file`(HTTP 403) | 角色无上传权限 | 分配权限节点 |
| `请选择文件` | 表单字段名不是 `file` 或上传无效 | 检查 FormData 的 key 是否为 `file` |
| `不支持的上传类型,允许:image,file` | type 参数非法 | 传 `image` 或 `file` |
| `不支持的文件格式,允许:...` | 后缀不在白名单 | 转换格式或调整 `FILE_IMAGE_EXT`/`FILE_EXT` |
| `文件大小不能超过 NMB` | 超过 `FILE_MAX_SIZE` | 压缩文件或调大限制 |
## 前端使用
### 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'"
:data="{ 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.filePath; // 建议存相对路径
}
};
const handleImageSuccess = (response) => {
if (response.code === 0) {
form.value.avatar = response.data.filePath;
}
};
</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.filePath = "temp/20261002/123456_a1b2....jpg"
// res.fileUrl = "http://localhost:8000/uploads/temp/20261002/..."
// 上传图片(指定 type=image)
const res2 = await http.request({
url: '/upload/uploadFile?type=image',
method: 'POST',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
});
```
### 提交表单时传路径
```typescript
// 推荐提交 filePath(相对路径),Service 层归一化后入库
await exampleAdd({
name: '测试',
avatar: 'temp/20261002/123456_a1b2c3d4e5f6.jpg',
});
// 后端 ExampleService(use HasFileProcessing)自动:
// 1. 识别 avatar 为 fileFields 字段
// 2. save_file() 归一化(去域名 / 迁移 temp 到 fileSaveDir / 下载外部 URL)
// 3. 正式目录相对路径入库(如 example/20261002/...)
// 读取时 processFileFieldsOnRead 补全 FILE_DOMAIN_URL 域名
```
### 头像上传示例
```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) => {
if (response.code === 0) {
form.value.avatar = response.data.filePath;
}
};
</script>
```
## 上传后处理流程
```text
前端上传 → public/uploads/temp/Ymd/xxx.jpg(临时目录)
│
▼ 提交表单(携带 filePath)
ExampleService::add()
│
├─ processFileFieldsOnSave()
│ └─ save_file() → temp/ 文件迁移到 example/,并归一化(去域名 / 外部 URL 下载)
│
└─ 数据库存储正式目录相对路径(example/Ymd/xxx.jpg)
│
▼ 读取时
processFileFieldsOnRead()
└─ get_file_url() → 拼接 FILE_DOMAIN_URL 返回完整 URL
```
详见 [7.1 文件上传全流程](/zh/tools/upload.html)。
## 配置项
```ini
# .env(config/file.php 读取)
FILE_UPLOAD_DIR=uploads # 上传根目录(相对 public/)
FILE_DOMAIN_URL=https://cdn.example.com # 文件访问域名
FILE_MAX_SIZE=10 # 单文件最大 10MB
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv
```
## 安全设计
| 安全措施 | 说明 |
|---------|------|
| 认证 + 权限 | `auth.jwt` 组内 + `#[Permission('sys:upload:file')]` |
| 上传有效性校验 | `UploadedFile::isValid()` 捕获 PHP 上传错误 |
| 文件类型白名单 | 按 `type` 从 `config/file.php` 读取后缀白名单 |
| 文件大小限制 | `FILE_MAX_SIZE`(默认 10MB),且受 `php.ini` 双层限制 |
| 随机文件名 | `His + bin2hex(random_bytes(8))` 防冲突与路径遍历 |
| 移除高危后缀 | `image_ext` 不含 svg(内嵌 JS 的 XSS 风险) |
| Nginx 防护 | 仅入口 `index.php` 可执行,上传的 `.php` 一律 404 |
| 操作审计 | `#[OperationLog(TYPE_UPLOAD)]` 记录上传行为 |
## 常见问题
### 问题 1:上传返回 401
**原因:** 上传接口需要认证,但请求头未携带 Token。
**解决:** 确保请求头包含 `Authorization: Bearer <token>`。
### 问题 2:上传返回"不允许的文件类型"
**原因:** 文件扩展名不在白名单中。
**解决:** 检查 `.env` 中 `FILE_EXT` / `FILE_IMAGE_EXT` 配置(改后 `php artisan config:clear`)。
### 问题 3:上传后图片不显示
**原因:** `FILE_DOMAIN_URL` 配置错误或与前端不同源。
**解决:** 检查 `.env` 中 `FILE_DOMAIN_URL` 是否为前端可访问地址;留空时用相对路径(同源/代理)。
### 问题 4:大文件上传失败
**原因:** PHP 或 Nginx 限制了上传大小。
**解决:**
```ini
; php.ini(修改后重启)
upload_max_filesize = 50M
post_max_size = 50M
```
```nginx
# nginx.conf(生产按需调整)
client_max_body_size 50M;
```
同时调大 `.env` 的 `FILE_MAX_SIZE`(业务层限制)。