Files
2026-10-11 19:06:33 +08:00

10 KiB
Raw Permalink 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://admin.laravel13.naivevue/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.naivevue/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 压缩文件或调大限制

前端使用

NaiveUI 上传组件(n-upload)

<template>
  <!-- 上传文件(默认 type=file) -->
  <n-upload
    :action="'/api/upload/uploadFile'"
    :headers="uploadHeaders"
    @before-upload="beforeUpload"
    @finish="handleUploadFinish"
  >
    <n-button type="primary">上传文件</n-button>
  </n-upload>

  <!-- 上传图片(指定 type=image) -->
  <n-upload
    :action="'/api/upload/uploadFile?type=image'"
    :headers="uploadHeaders"
    accept="image/*"
    @finish="handleImageFinish"
  >
    <n-button type="primary">上传图片</n-button>
  </n-upload>
</template>

<script setup>
import { useMessage } from '@/hooks/web/useMessage';

const $message = useMessage();
const token = localStorage.getItem('access_token');
const uploadHeaders = { Authorization: `Bearer ${token}` };

// n-upload 的 before-upload 参数为 { file },原生文件对象在 file.file
const beforeUpload = ({ file }) => {
  const isLt10M = file.file.size / 1024 / 1024 < 10;
  if (!isLt10M) {
    $message.error('文件大小不能超过 10MB');
    return false;
  }
  return true;
};

// 上传结束,解析原始响应(n-upload 的 finish 事件拿到 event)
const parseResponse = (event) => {
  const data = JSON.parse(event.target.response);
  if (data.code === 0) {
    return data.data.filePath;   // 建议存相对路径
  }
  $message.error(data.msg || '上传失败');
  return null;
};

const handleUploadFinish = ({ event }) => {
  form.value.file = parseResponse(event);
};

const handleImageFinish = ({ event }) => {
  form.value.avatar = parseResponse(event);
};
</script>

项目内也可直接使用封装组件 BasicUpload(ui/src/components/Upload/,内嵌 n-upload,自带大小/类型校验与 useMessage 错误提示),图片字段用 UploadImg(ui/src/components/Upload/Image.vue)。

手动上传

// 上传文件
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.naivevue/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">
    <n-avatar :src="form.avatar" :size="80" />
    <UploadImg
      v-model:image-url="form.avatar"
      :fileType="['image/jpeg', 'image/png', 'image/jpg', 'image/gif', 'image/webp']"
      :fileSize="10"
    >
      <template #tip>支持扩展名: jpg png jpeg gif webp; 文件大小不超过10M</template>
    </UploadImg>
  </div>
</template>

<script setup>
import UploadImg from '@/components/Upload/Image.vue';

const form = ref({ avatar: '' });
</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(业务层限制)。