Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp6_NaiveVue/zh/tools/upload.md
T
2026-09-29 10:14:04 +08:00

465 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 7.1 文件上传全流程
:::tip 概述
文件处理分为两个阶段:上传到临时目录(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
}
```
### 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `$formName` | string | `'file'` | 表单字段名 |
| `$type` | string | `'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` |
| `fileType` | MIME 类型 | `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;
}
```