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

9.5 KiB
Raw Blame History

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://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 默认值:

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 压缩文件或调大限制

前端使用

Element Plus Upload 组件

<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>

手动上传

// 上传文件
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' },
});

提交表单时传路径

// 推荐提交 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">
    <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>

上传后处理流程

前端上传 → 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(业务层限制)。