Files
easygoadmin-wiki/EasyGoAdmin_Gin_AntdVue/zh/tools/upload.md
T
2026-10-20 21:19:44 +08:00

14 KiB
Raw Blame History

7.1 文件上传全流程

:::tip 概述 文件处理分为两个阶段:上传到临时目录(upload_file)→ 入库时迁移到正式目录(save_file)。这种"两阶段"设计将文件上传与业务逻辑解耦,前端先上传获取临时 URL,提交表单时 Logic 层自动将临时文件迁移到正式目录。 :::

完整流程图

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

前端                          后端
 │                             │
 │── POST /api/upload ────────►│
 │   Content-Type:             │
 │   multipart/form-data       │
 │   file: <二进制>             │
 │                             │
 │                      ┌──────┴──────┐
 │                      │upload_file()│
 │                      │             │
 │                      │ 1. 检查上传状态
 │                      │ 2. finfo 检测真实 MIME
 │                      │ 3. 按类型校验扩展名
 │                      │ 4. 校验文件大小
 │                      │ 5. 图片额外 getimagesize 校验
 │                      │ 6. 保存到 temp/日期/随机文件名
 │                      └──────┬──────┘
 │                             │
 │◄── { fileUrl, filePath, ────│
 │     originalName, fileSize, │
 │     fileExtension, fileType }│
 │                             │
 │ 前端显示预览               │
 │ 或暂存 URL                 │

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

前端                          后端
 │                             │
 │── POST /api/example/add ──►│
 │   { name: "测试",            │
 │     avatar: "https://       │
 │     cdn.example.com/        │
 │     temp/20260920/          │
 │     abc123.jpg" }           │
 │                             │
 │                      ┌──────┴──────┐
 │                      │ BaseLogic   │
 │                      │ ::add()     │
 │                      └──────┬──────┘
 │                             │
 │                      ┌──────┴──────┐
 │                      │ processFile │
 │                      │ FieldsOn    │
 │                      │ Save()      │
 │                      └──────┬──────┘
 │                             │
 │                      ┌──────┴──────┐
 │                      │ save_file() │
 │                      │             │
 │                      │ 1. 去掉域名得到相对路径
 │                      │ 2. 包含 temp/ → 移动到正式目录
 │                      │ 3. 外部URL → 下载保存
 │                      └──────┬──────┘
 │                             │
 │                      ┌──────┴──────┐
 │                      │ 数据库存储   │
 │                      │ /uploads/   │
 │                      │ example/    │
 │                      │ 20260920/   │
 │                      │ abc123.jpg  │
 │                      └──────┬──────┘
 │                             │
 │◄── { code:0, data:{id:1} }─│

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

前端                          后端
 │                             │
 │── GET /api/example/detail/1►│
 │                             │
 │                      ┌──────┴──────┐
 │                      │processFile  │
 │                      │FieldsOnRead │
 │                      │()           │
 │                      └──────┬──────┘
 │                             │
 │                      ┌──────┴──────┐
 │                      │get_file_url │
 │                      │()           │
 │                      │拼接域名      │
 │                      └──────┬──────┘
 │                             │
 │◄── { avatar: "https://──────│
 │   cdn.example.com/uploads/  │
 │   example/20260920/         │
 │   abc123.jpg" }             │

upload_file 函数

// app/common.php

/**
 * 上传文件到临时目录
 *
 * 处理流程:
 * 1. 检查上传状态,细分错误码
 * 2. 用 finfo 检测真实 MIME(防止伪造扩展名)
 * 3. 按类型校验扩展名白名单
 * 4. 校验文件大小(config/file.php → max_size)
 * 5. 图片额外用 getimagesize 校验(防止非图片伪装)
 * 6. 在 temp/日期/ 下保存,随机文件名
 *
 * @param string $formName 表单字段名
 * @param string $type 文件类型:image|file
 * @return array 成功返回 [url, path, name, size, ext, mime];失败返回 [error]
 */
function upload_file(string $formName = 'file', string $type = 'file'): array
{
    // ... 完整实现见 app/common.php
}

参数说明

参数 类型 默认值 说明
$formName string 'file' 表单字段名
$type string 'file' 文件类型:image(仅图片)/ file(所有文件)

返回值

成功:

[
    'url'    => 'https://cdn.example.com/temp/20260920/abc123.jpg',  // 完整 URL
    'path'   => 'temp/20260920/abc123.jpg',                         // 相对路径
    'name'   => 'photo.jpg',                                        // 原始文件名
    'size'   => 102400,                                             // 文件大小(字节)
    'ext'    => 'jpg',                                              // 扩展名
    'mime'   => 'image/jpeg',                                       // MIME 类型
]

失败:

['error' => '错误信息']

错误类型

错误信息 说明
未选择文件 表单字段为空
文件上传失败 PHP 上传错误(代码 1-7)
不允许的文件类型 扩展名不在白名单中
文件大小超出限制 超过 config/file.php 中的 max_size
图片文件不合法 图片类型校验失败(getimagesize 验证)

save_file 函数

/**
 * 保存文件(临时→正式)
 *
 * 处理逻辑:
 * 1. 带域名 URL → 去掉域名得到相对路径
 * 2. 包含 temp/ → 移动到正式目录
 * 3. 外部 URL → 下载保存
 * 4. 已在正式目录 → 直接返回
 *
 * @param string $fileUrl 文件 URL(带域名)或相对路径
 * @param string $saveDir 正式保存的子目录
 * @return string|false 入库路径(不带域名),失败返回 false
 */
function save_file(string $fileUrl, string $saveDir = '')
{
    // 1. 带域名URL → 去掉域名得到相对路径
    // 2. 包含 temp/ → 移动到正式目录
    // 3. 外部URL → 下载保存
    // 4. 已在正式目录 → 直接返回
}

处理逻辑

输入 URL: "https://cdn.example.com/temp/20260920/abc123.jpg"
    │
    ▼ 去掉域名
相对路径: "temp/20260920/abc123.jpg"
    │
    ▼ 包含 temp/ → 需要迁移
    │
    ▼ 移动到正式目录
目标路径: "/uploads/example/20260920/abc123.jpg"
    │
    ▼ 返回入库路径
输出: "/uploads/example/20260920/abc123.jpg"

get_file_url 函数

/**
 * 获取文件完整 URL
 *
 * 将数据库中的相对路径拼接为完整 URL:
 * - 已是完整 URL → 原样返回
 * - 相对路径 → 拼接配置的域名
 * - 空值 → 返回空字符串
 *
 * @param string $path 数据库中的相对路径
 * @return string 完整 URL
 */
function get_file_url(string $path): string
{
    // 1. 空值 → 返回 ''
    // 2. 已是 http/https 开头 → 原样返回
    // 3. 拼接 config/file.php 中的 domain_url
}

转换示例

数据库值: "/uploads/example/20260920/abc123.jpg"
    │
    ▼ get_file_url()
    │
    ▼ 拼接 domain_url
输出: "https://cdn.example.com/uploads/example/20260920/abc123.jpg"

在 Logic 中配置

class ExampleLogic extends BaseLogic
{
    /**
     * 需要自动处理的文件上传字段
     *
     * @var array
     */
    protected array $fileFields = ['avatar', 'cover'];

    /**
     * 文件保存的子目录
     *
     * @var string
     */
    protected string $fileSaveDir = 'example';
}

自动行为

操作 自动行为
add() processFileFieldsOnSave() → 调用 save_file() 迁移临时文件
update() processFileFieldsOnSave() → 调用 save_file() 迁移临时文件
detail() processFileFieldsOnRead() → 调用 get_file_url() 补全域名
pageList() processFileFieldsOnReadList() → 调用 get_file_url() 补全域名
allList() processFileFieldsOnReadList() → 调用 get_file_url() 补全域名

多文件字段

class UserLogic extends BaseTenantLogic
{
    /**
     * 需要自动处理的文件上传字段
     *
     * @var array
     */
    protected array $fileFields = ['avatar'];

    /**
     * 文件保存的子目录
     *
     * @var string
     */
    protected string $fileSaveDir = 'user/avatar';
}

class ArticleLogic extends BaseTenantLogic
{
    /**
     * 需要自动处理的文件上传字段
     *
     * @var array
     */
    protected array $fileFields = ['cover', 'attachment'];

    /**
     * 文件保存的子目录
     *
     * @var string
     */
    protected string $fileSaveDir = 'article';
}

前端上传接口

上传文件

POST /api/upload/uploadFile
Content-Type: multipart/form-data

file: <二进制文件>

响应:

{
    "code": 0,
    "data": {
        "fileUrl": "https://cdn.example.com/temp/20260920/abc123.jpg",
        "filePath": "temp/20260920/abc123.jpg",
        "originalName": "photo.jpg",
        "fileSize": 102400,
        "fileExtension": "jpg",
        "fileType": "image/jpeg",
        "fileName": "abc123.jpg"
    }
}

上传图片(指定 type=image)

POST /api/upload/uploadFile?type=image
Content-Type: multipart/form-data

file: <二进制图片>

响应字段说明

字段 说明 示例
fileUrl 完整 URL(含域名) https://cdn.example.com/temp/...
filePath 相对路径 temp/20260920/abc123.jpg
originalName 原始文件名 photo.jpg
fileSize 文件大小(字节) 102400
fileExtension 扩展名 jpg
fileType MIME 类型 image/jpeg
fileName 服务器文件名 abc123.jpg

前端集成

ElementPlus 上传组件

<template>
  <el-upload
    :action="'/api/upload/uploadFile'"
    :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) => {
  // response.data.fileUrl 是完整 URL
  // 提交表单时使用此 URL
  form.value.avatar = response.data.fileUrl;
};
</script>

提交表单

// 前端提交时,avatar 字段的值是上传接口返回的 fileUrl
const formData = {
  name: '测试',
  avatar: 'https://cdn.example.com/temp/20260920/abc123.jpg',
};

// 后端 Logic 层自动:
// 1. 从 avatar 字段检测到临时文件 URL
// 2. 调用 save_file() 迁移到正式目录
// 3. 去掉域名,只存相对路径入库

配置项

; .env
[FILE]
; 文件上传域名
DOMAIN_URL = https://cdn.example.com
; 最大文件大小(字节)
MAX_SIZE = 10485760
; 允许的图片扩展名
ALLOW_IMAGE_EXT = jpg,jpeg,png,gif,bmp,webp
; 允许的文件扩展名
ALLOW_FILE_EXT = jpg,jpeg,png,gif,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar
// config/file.php
return [
    'domain_url'      => env('file.domain_url', ''),
    'max_size'        => env('file.max_size', 10485760),  // 10MB
    'allow_image_ext' => env('file.allow_image_ext', 'jpg,jpeg,png,gif,bmp,webp'),
    'allow_file_ext'  => env('file.allow_file_ext', 'jpg,jpeg,png,gif,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar'),
];

安全检查链

上传文件安全检查:
    │
    ▼ 1. PHP 上传状态检查(UPLOAD_ERR_OK)
    │
    ▼ 2. finfo 检测真实 MIME 类型(防止伪造扩展名)
    │
    ▼ 3. 扩展名白名单校验
    │
    ▼ 4. 文件大小限制(config/file.php → max_size)
    │
    ▼ 5. 图片额外 getimagesize 校验(防止非图片伪装)
    │
    ▼ 6. 生成随机文件名(防止路径遍历攻击)
    │
    ▼ 7. 存储到 Web 根目录之外(或 Nginx 禁止执行 PHP)
# Nginx 配置:上传目录禁止执行 PHP
location ~* /uploads/.*\.php$ {
    deny all;
}