9.7 KiB
9.7 KiB
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(仅图片) |
成功响应
{
"code": 0,
"ok": true,
"msg": "上传成功",
"data": {
"fileUrl": "http://admin.laravel13.antdvue/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://admin.laravel13.antdvue/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 默认值:
gif, jpg, jpeg, png, bmp, webp,
doc, docx, xls, xlsx, ppt, pptx,
pdf, zip, rar, txt, csv
image_ext 默认值(无 svg——内嵌 JS 有 XSS 风险):
gif, jpg, jpeg, png, bmp, webp
失败响应
{
"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 |
压缩文件或调大限制 |
前端使用
AntDesign Upload 组件
<template>
<!-- 上传文件(默认 type=file) -->
<a-upload
:action="'/api/upload/uploadFile'"
:headers="{ Authorization: `Bearer ${token}` }"
:before-upload="beforeUpload"
:show-upload-list="false"
@change="handleUploadChange"
>
<a-button type="primary">上传文件</a-button>
</a-upload>
<!-- 上传图片(指定 type=image) -->
<a-upload
:action="'/api/upload/uploadFile?type=image'"
:data="{ type: 'image' }"
:headers="{ Authorization: `Bearer ${token}` }"
:show-upload-list="false"
accept="image/*"
@change="handleImageChange"
>
<a-button type="primary">上传图片</a-button>
</a-upload>
</template>
<script setup>
import { message } from 'ant-design-vue';
const token = localStorage.getItem('access_token');
const beforeUpload = (file) => {
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('文件大小不能超过 10MB');
}
return isLt10M;
};
const handleUploadChange = ({ file }) => {
if (file.status === 'done' && file.response.code === 0) {
form.value.file = file.response.data.filePath; // 建议存相对路径
}
};
const handleImageChange = ({ file }) => {
if (file.status === 'done' && file.response.code === 0) {
form.value.avatar = file.response.data.filePath;
}
};
</script>
手动上传
// 上传文件
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://admin.laravel13.antdvue/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' },
});
提交表单时传路径
// 推荐提交 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 域名
头像上传示例
<template>
<div class="avatar-upload">
<a-avatar :src="form.avatar" :size="80" />
<a-upload
:action="'/api/upload/uploadFile?type=image'"
:headers="{ Authorization: `Bearer ${token}` }"
:show-upload-list="false"
accept="image/*"
@change="handleAvatarChange"
>
<a-button size="small">更换头像</a-button>
</a-upload>
</div>
</template>
<script setup>
const form = ref({ avatar: '' });
const handleAvatarChange = ({ file }) => {
if (file.status === 'done' && file.response.code === 0) {
form.value.avatar = file.response.data.filePath;
}
};
</script>
上传后处理流程
前端上传 → 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 文件上传全流程。
配置项
# .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 限制了上传大小。
解决:
; php.ini(修改后重启)
upload_max_filesize = 50M
post_max_size = 50M
# nginx.conf(生产按需调整)
client_max_body_size 50M;
同时调大 .env 的 FILE_MAX_SIZE(业务层限制)。