签入版本

This commit is contained in:
2026-10-08 17:50:29 +08:00
parent 809750b47e
commit e213a9b31d
444 changed files with 39366 additions and 37533 deletions
@@ -1,7 +1,7 @@
# 8.4 文件上传接口
:::tip 概述
文件上传接口将文件保存到临时目录,返回文件 URL。前端拿到 URL 后提交表单时传给后端,BaseLogic 在 add/update 时自动迁移到正式目录。通过 `type` 参数可区分上传类型(文件/图片)。
文件上传接口将文件保存到 `temp/` 临时目录,返回文件相对路径与 URL。前端拿到 `filePath` 后提交表单,Service 的 `fileFields` 经 `save_file()` 把临时文件迁移到正式目录并入库,读取时自动补全域名。通过 `type` 参数区分后缀白名单(文件/图片)。
:::
## 接口信息
@@ -10,13 +10,18 @@
|------|------|
| 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 | 是 | - | 文件二进制数据 |
| `file` | File | 是 | - | 文件二进制数据(FormData 字段名必须为 `file`) |
| `type` | string | 否 | `file` | 上传类型:`file`(所有合法文件)/ `image`(仅图片) |
### 成功响应
@@ -27,13 +32,13 @@
"ok": true,
"msg": "上传成功",
"data": {
"fileUrl": "https://cdn.example.com/temp/20260920/a1b2c3d4e5f6.jpg",
"filePath": "temp/20260920/a1b2c3d4e5f6.jpg",
"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": "a1b2c3d4e5f6.jpg"
"fileName": "123456_a1b2c3d4e5f6.jpg"
}
}
```
@@ -42,34 +47,35 @@
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `fileUrl` | string | 完整 URL(含域名) | `https://cdn.example.com/temp/...` |
| `filePath` | string | 相对路径 | `temp/20260920/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 类型 | `image/jpeg` |
| `fileName` | string | 服务器文件名(随机) | `a1b2c3d4e5f6.jpg` |
| `fileType` | string | MIME 类型(`getMimeType()`) | `image/jpeg` |
| `fileName` | string | 服务器文件名(时间+随机串) | `123456_a1b2c3d4e5f6.jpg` |
### type 参数说明
| type 值 | 说明 | 允许的后缀 | 适用场景 |
| type 值 | 说明 | 允许的后缀 | 存储目录 |
|---------|------|-----------|---------|
| `file`(默认) | 所有合法文件 | 由 `config/file.php` 的 `file_ext` 控制 | 文档、压缩包等 |
| `image` | 仅图片 | 由 `config/file.php` 的 `image_ext` 控制 | 头像、封面等 |
| `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, svg,
gif, jpg, jpeg, png, bmp, webp,
doc, docx, xls, xlsx, ppt, pptx,
pdf, zip, rar, txt, csv,
mp3, mp4, sql, js, css
pdf, zip, rar, txt, csv
```
**image_ext 默认值:**
**image_ext 默认值(无 svg——内嵌 JS 有 XSS 风险):**
```text
gif, jpg, jpeg, png, bmp, webp, svg
gif, jpg, jpeg, png, bmp, webp
```
## 失败响应
@@ -78,20 +84,21 @@ gif, jpg, jpeg, png, bmp, webp, svg
{
"code": 1,
"ok": false,
"msg": "不允许的文件类型: .exe",
"msg": "不支持的文件格式,允许:gif, jpg, jpeg, png, bmp, webp",
"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` |
| `未提供认证令牌`(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` | 压缩文件或调大限制 |
## 前端使用
@@ -112,6 +119,7 @@ gif, jpg, jpeg, png, bmp, webp, svg
<!-- 上传图片(指定 type=image) -->
<el-upload
:action="'/api/upload/uploadFile?type=image'"
:data="{ type: 'image' }"
:headers="{ Authorization: `Bearer ${token}` }"
:on-success="handleImageSuccess"
accept="image/*"
@@ -133,13 +141,13 @@ const beforeUpload = (file) => {
const handleUploadSuccess = (response) => {
if (response.code === 0) {
form.value.file = response.data.fileUrl;
form.value.file = response.data.filePath; // 建议存相对路径
}
};
const handleImageSuccess = (response) => {
if (response.code === 0) {
form.value.avatar = response.data.fileUrl;
form.value.avatar = response.data.filePath;
}
};
</script>
@@ -158,7 +166,8 @@ const res = await http.request({
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
});
// res.fileUrl = "https://cdn.example.com/temp/20260920/abc.jpg"
// res.filePath = "temp/20261002/123456_a1b2....jpg"
// res.fileUrl = "http://localhost:8000/uploads/temp/20261002/..."
// 上传图片(指定 type=image)
const res2 = await http.request({
@@ -169,19 +178,20 @@ const res2 = await http.request({
});
```
### 提交表单时传 URL
### 提交表单时传路径
```typescript
// 前端上传后拿到 fileUrl,提交表单时传给后端
// 推荐提交 filePath(相对路径),Service 层归一化后入库
await exampleAdd({
name: '测试',
avatar: 'https://cdn.example.com/temp/20260920/abc.jpg',
avatar: 'temp/20261002/123456_a1b2c3d4e5f6.jpg',
});
// 后端 BaseLogic::add() 自动:
// 1. 检测到 avatar 是临时文件 URL
// 2. 调用 save_file() 迁移到正式目录
// 3. 去掉域名,只存相对路径入库
// 后端 ExampleService(use HasFileProcessing)自动:
// 1. 识别 avatar 为 fileFields 字段
// 2. save_file() 归一化(去域名 / 迁移 temp 到 fileSaveDir / 下载外部 URL)
// 3. 正式目录相对路径入库(如 example/20261002/...)
// 读取时 processFileFieldsOnRead 补全 FILE_DOMAIN_URL 域名
```
### 头像上传示例
@@ -206,7 +216,9 @@ await exampleAdd({
const form = ref({ avatar: '' });
const handleAvatarSuccess = (response) => {
form.value.avatar = response.data.fileUrl;
if (response.code === 0) {
form.value.avatar = response.data.filePath;
}
};
</script>
```
@@ -214,43 +226,46 @@ const handleAvatarSuccess = (response) => {
## 上传后处理流程
```text
前端上传 → temp/日期/xxx.jpg(临时目录)
前端上传 → public/uploads/temp/Ymd/xxx.jpg(临时目录)
│
▼ 提交表单(携带 URL)
BaseLogic::add()
▼ 提交表单(携带 filePath)
ExampleService::add()
│
├─ processFileFieldsOnSave()
│ └─ save_file() → 正式目录/日期/xxx.jpg
│ └─ save_file() → temp/ 文件迁移到 example/,并归一化(去域名 / 外部 URL 下载)
│
└─ 数据库存储相对路径
└─ 数据库存储正式目录相对路径(example/Ymd/xxx.jpg)
│
▼ 读取时
processFileFieldsOnRead()
└─ get_file_url() → 拼接域名返回完整 URL
└─ get_file_url() → 拼接 FILE_DOMAIN_URL 返回完整 URL
```
详见 [7.1 文件上传全流程](/zh/tools/upload.html)。
## 配置项
```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
# .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
```
## 安全设计
| 安全措施 | 说明 |
|---------|------|
| 文件类型校验 | 同时检查后缀名和 MIME 类型(finfo),防止伪造扩展名 |
| 文件大小限制 | 通过 `config/file.php` 的 `max_size` 配置 |
| 图片内容校验 | 图片类型额外用 `getimagesize` 验证,防止非图片伪装 |
| 随机文件名 | 使用 `bin2hex(random_bytes(16))` 防止文件名冲突和路径遍历 |
| 临时目录隔离 | 上传文件先存入 `temp/` 目录,提交表单后才迁移到正式目录 |
| Nginx 防护 | 上传目录禁止执行 PHP 文件 |
| 认证 + 权限 | `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)]` 记录上传行为 |
## 常见问题
@@ -264,13 +279,13 @@ FILE_EXT = jpg,jpeg,png,gif,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,
**原因:** 文件扩展名不在白名单中。
**解决:** 检查 `.env` 中 `FILE.FILE_EXT` 或 `FILE.IMAGE_EXT` 配置。
**解决:** 检查 `.env` 中 `FILE_EXT` / `FILE_IMAGE_EXT` 配置(改后 `php artisan config:clear`)。
### 问题 3:上传后图片不显示
**原因:** `DOMAIN_URL` 配置错误或 Nginx 未配置静态文件处理。
**原因:** `FILE_DOMAIN_URL` 配置错误或与前端不同源。
**解决:** 检查 `.env` 中 `FILE.DOMAIN_URL` 是否正确,Nginx 是否配置了 `/uploads/` 的 alias。
**解决:** 检查 `.env` 中 `FILE_DOMAIN_URL` 是否为前端可访问地址;留空时用相对路径(同源/代理)。
### 问题 4:大文件上传失败
@@ -279,15 +294,14 @@ FILE_EXT = jpg,jpeg,png,gif,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,
**解决:**
```ini
; php.ini
upload_max_filesize = 20M
post_max_size = 25M
; .env
FILE.MAX_SIZE = 20
; php.ini(修改后重启)
upload_max_filesize = 50M
post_max_size = 50M
```
```nginx
# nginx.conf
client_max_body_size 20M;
# nginx.conf(生产按需调整)
client_max_body_size 50M;
```
同时调大 `.env` 的 `FILE_MAX_SIZE`(业务层限制)。