Skip to content

7.1 文件上传全流程 ​

概述

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

完整流程图 ​

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

前端                          后端
 │                             │
 │── 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 函数 ​

php
// 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
}

参数说明 ​

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

返回值 ​

成功:

php
[
    '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 类型
]

失败:

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

错误类型 ​

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

save_file 函数 ​

php
/**
 * 保存文件(临时→正式)
 *
 * 处理逻辑:
 * 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. 已在正式目录 → 直接返回
}

处理逻辑 ​

text
输入 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 函数 ​

php
/**
 * 获取文件完整 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
}

转换示例 ​

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

在 Logic 中配置 ​

php
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() 补全域名

多文件字段 ​

php
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';
}

前端上传接口 ​

上传文件 ​

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

file: <二进制文件>

响应:

json
{
    "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) ​

bash
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
fileTypeMIME 类型image/jpeg
fileName服务器文件名abc123.jpg

前端集成 ​

Element Plus 上传组件 ​

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

提交表单 ​

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

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

配置项 ​

ini
; .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
php
// 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'),
];

安全检查链 ​

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

小蚂蚁云团队 · 提供技术支持