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

13 KiB
Raw Blame History

7.1 文件上传全流程

:::tip 概述 文件处理分为三个阶段:上传(uploadFile 接口校验后存入 temp/ 临时目录)→ 入库(Service 的 fileFields 经 save_file() 归一化:去域名、迁移临时文件到正式目录)→ 读取(get_file_url() 补全域名)。上传与业务逻辑解耦:前端先上传拿到 URL/路径,提交表单时框架自动完成字段处理。 :::

完整流程图

┌─────────────────────────────────────────────────────────────────────┐
│                         第一阶段:上传                                │
└─────────────────────────────────────────────────────────────────────┘

前端                          后端
 │                             │
 │── POST /api/upload/ ───────►│   (auth.jwt 组内,需 sys:upload:file 权限)
 │   uploadFile                │
 │   Content-Type:             │
 │   multipart/form-data       │
 │   file: <二进制>             │
 │   type: image | file        │
 │                             │
 │                      ┌──────┴───────────┐
 │                      │ UploadController │
 │                      │ ::uploadFile()   │
 │                      └──────┬───────────┘
 │                             │
 │                      1. 校验 type 合法性(image / file)
 │                      2. 文件存在且上传有效(isValid)
 │                      3. 按类型读取扩展名白名单(config/file.php)
 │                      4. 校验文件大小(file.max_size,默认 10MB)
 │                      5. 生成路径:temp/Ymd/His_随机串.扩展名
 │                      6. 采集元数据:原始名 / MIME(finfo)/ 大小
 │                      7. mkdir + move 保存
 │                             │
 │◄── { fileUrl, filePath, ────│
 │     originalName, fileSize, │
 │     fileExtension, fileType, │
 │     fileName }               │
 │                             │
 │ 前端暂存 URL/路径            │

┌─────────────────────────────────────────────────────────────────────┐
│                         第二阶段:入库                                │
└─────────────────────────────────────────────────────────────────────┘

前端                          后端
 │                             │
 │── POST /api/example/add ──►│
 │   { name: "测试",            │
 │     avatar: "temp/           │
 │       20261002/123456_ab.jpg" }│
 │                             │
 │                      ┌──────┴──────────────┐
 │                      │ ExampleService::add │
 │                      │ (use HasFileProcessing)│
 │                      └──────┬──────────────┘
 │                             │
 │                      ┌──────┴──────────────┐
 │                      │ processFileFieldsOnSave │
 │                      │ → save_file()       │
 │                      │ 1. 去掉域名得相对路径 │
 │                      │ 2. temp/ 开头 → 迁移  │
 │                      │ 3. 外部URL → 下载保存 │
 │                      │ 4. 正式路径 → 直接返回│
 │                      └──────┬──────────────┘
 │                             │
 │◄── { code:0, data:{id:1} }──│

┌─────────────────────────────────────────────────────────────────────┐
│                         第三阶段:读取                                │
└─────────────────────────────────────────────────────────────────────┘

前端                          后端
 │                             │
 │── GET /api/example/detail/1►│
 │                             │
 │                      ┌──────┴───────────────┐
 │                      │ processFileFieldsOnRead │
 │                      │ → get_file_url()    │
 │                      │ 拼接 FILE_DOMAIN_URL │
 │                      └──────┬───────────────┘
 │                             │
 │◄── { avatar: "https://──────│
 │   cdn.example.com/uploads/  │
 │   example/20261002/..." }    │

:::tip 临时目录中转 上传接口把文件写入 temp/ 临时目录,不直接落正式目录;提交表单时 save_file() 依据 temp/ 前缀把文件迁移到 Service 的 fileSaveDir 子目录(user/、article/、example/ …),正式目录与业务模块一一对应。上传后未被表单引用的文件停留在 temp/,可按周期清理。 :::

上传接口

上传文件

POST /api/upload/uploadFile
Content-Type: multipart/form-data
Authorization: Bearer <token>

file: <二进制文件>

参数:

参数 类型 必填 说明
file File 是 上传的文件
type string 否 image(校验图片后缀白名单)/ file(校验全量文件后缀白名单,默认)

响应(来自项目实际代码):

{
    "code": 0,
    "ok": true,
    "msg": "上传成功",
    "data": {
        "originalName": "photo.jpg",
        "fileExtension": "jpg",
        "fileType": "image/jpeg",
        "fileSize": 102400,
        "fileName": "123456_a1b2c3d4e5f6.jpg",
        "filePath": "temp/20261002/123456_a1b2c3d4e5f6.jpg",
        "fileUrl": "http://cdn.example.com/temp/20261002/123456_a1b2c3d4e5f6.jpg"
    }
}

响应字段说明:

字段 说明 示例
originalName 原始文件名 photo.jpg
fileExtension 扩展名 jpg
fileType MIME 类型(getMimeType()) image/jpeg
fileSize 文件大小(字节) 102400
fileName 服务器文件名(时间+随机串) 123456_a1b2c3d4e5f6.jpg
filePath 相对存储路径(提交表单建议用此字段) temp/20261002/...
fileUrl 可访问 URL(FILE_DOMAIN_URL 为空时等于相对路径) http://cdn.example.com/...

存储规则

{upload_dir}/                      ← config/file.php → upload_dir(默认 uploads)
└── temp/                          ← 所有类型统一存入(config/file.php → temp_dir)
    └── 20261002/                  ← Ymd 分目录
        └── 123456_a1b2c3d4e5f6.jpg ← His_ + 随机8字节hex + 扩展名

{upload_dir}/                      ← 正式目录,落库时由 save_file() 迁移生成
├── user/  article/  example/ …    ← Service 的 fileSaveDir
└── 20261002/
    └── 143052_a1b2c3d4e5f6.jpg
  • type 只影响后缀白名单,存储目录始终是 temp/
  • 随机文件名避免重名与可预测性攻击(时间戳 + random_bytes(8))
  • 目录不存在时自动 mkdir(0755, true) 创建

安全检查链

上传文件安全检查(UploadController::uploadFile):
    │
    ▼ 1. 权限校验(auth.jwt + #[Permission('sys:upload:file')])
    │
    ▼ 2. type 参数合法性(仅 image / file)
    │
    ▼ 3. 文件存在且上传有效(UploadedFile::isValid,捕获 PHP 上传错误)
    │
    ▼ 4. 扩展名白名单(image → file.image_ext;file → file.file_ext)
    │
    ▼ 5. 文件大小限制(config/file.php → max_size,默认 10MB)
    │
    ▼ 6. 服务端生成随机路径(防路径遍历)
    │
    ▼ 7. Nginx 仅允许入口 index.php 执行 —— 上传的 .php 一律 404

:::warning svg 已移除 FILE_IMAGE_EXT 默认白名单不含 svg——svg 可内嵌 JavaScript,上传后直接作为静态资源访问存在 XSS 风险。如业务必须使用,请做内容清洗或以 attachment 方式返回。 :::

入库处理(HasFileProcessing)

Service 配置文件字段后,保存/读取自动处理:

// app/Services/ExampleService.php(形态示例)
class ExampleService extends BaseService
{
    use HasFileProcessing, /* 其他 Trait */;

    // 需要自动处理的文件字段
    protected array $fileFields = ['avatar'];

    // 多文件字段(逗号分隔的多个 URL)
    protected array $multiFileFields = ['images'];

    // 文件保存子目录(未指定时按模型类名小写推导)
    protected string $fileSaveDir = 'example';
}

自动行为

操作 自动行为
add() / update() processFileFieldsOnSave() → save_file() 归一化(去域名、迁移 temp、下载外部 URL)
detail() processFileFieldsOnRead() → get_file_url() 补全域名
pageList() / allList() processFileFieldsOnReadList() 逐条补全域名

处理细节(来自 HasFileProcessing 源码):

  • 空值跳过;save_file() 返回 false 时保留原值(避免误清空)
  • 多文件按逗号拆分逐个处理,单个失败保留原 URL
  • 未 use HasFileProcessing 时占位方法直接透传(见 4.7 横切机制)

核心辅助函数(app/Helpers/helpers.php)

/**
 * 保存文件(归一化入库路径)
 *
 * 处理逻辑:
 * 1. 带域名 URL → 去掉域名得到相对路径
 * 2. 包含 temp/ → 移动到正式目录(fileSaveDir)
 * 3. 外部 URL → save_remote_file() 下载保存
 * 4. 已在正式目录 → 直接返回
 */
function save_file(string $fileUrl, string $saveDir = ''): string|false

/**
 * 获取文件完整 URL
 * - 空值 → '';已是 http(s) 开头 → 原样返回
 * - 相对路径 → 拼接 config/file.php 的 domain_url
 */
function get_file_url(string $path): string

/** 生成存储路径:{saveDir}/Ymd/random.扩展名 */
function create_file_path(string $saveDir = '', string $ext = ''): string

/** 下载外部 URL 并保存(配合 is_internal_ip 做内网地址拦截,防 SSRF) */
function save_remote_file(string $url, string $saveDir = ''): string|false

前端集成

Element Plus 上传组件

<template>
  <el-upload
    :action="'/api/upload/uploadFile'"
    :data="{ type: 'image' }"
    :headers="{ Authorization: `Bearer ${token}` }"
    :on-success="handleUploadSuccess"
    :before-upload="beforeUpload"
  >
    <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) => {
  // 提交表单时保存相对路径(filePath)最稳妥
  form.value.avatar = response.data.filePath;
};
</script>

提交表单

const formData = {
  name: '测试',
  avatar: 'temp/20261002/123456_a1b2c3d4e5f6.jpg',
};

// 后端 Service 层自动:
// 1. 识别 avatar 为 fileFields 字段
// 2. save_file() 归一化(去域名 / 迁移 temp 到 fileSaveDir / 下载外部 URL)
// 3. 正式目录相对路径入库(如 example/20261002/123456_a1b2c3d4e5f6.jpg)

配置项

# .env(config/file.php 读取)
FILE_UPLOAD_DIR=uploads         # 上传根目录(相对 public/)
FILE_DOMAIN_URL=                # 文件访问域名(空则返回相对路径)
FILE_MAX_SIZE=10                # 单文件大小上限(MB)
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp   # 图片后缀白名单(svg 已移除)
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv

详见 7.2 上传配置详解。

:::warning 同时调整 PHP 限制 业务层 FILE_MAX_SIZE=10 生效的前提是 PHP 层放行,确认 php.ini:

upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300

:::