签入版本
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 7.1 文件上传全流程
|
||||
|
||||
:::tip 概述
|
||||
文件处理分为两个阶段:上传到临时目录(upload_file)→ 入库时迁移到正式目录(save_file)。这种"两阶段"设计将文件上传与业务逻辑解耦,前端先上传获取临时 URL,提交表单时 Logic 层自动将临时文件迁移到正式目录。
|
||||
文件处理分为三个阶段:**上传**(`uploadFile` 接口校验后存入 `temp/` 临时目录)→ **入库**(Service 的 `fileFields` 经 `save_file()` 归一化:去域名、迁移临时文件到正式目录)→ **读取**(`get_file_url()` 补全域名)。上传与业务逻辑解耦:前端先上传拿到 URL/路径,提交表单时框架自动完成字段处理。
|
||||
:::
|
||||
|
||||
## 完整流程图
|
||||
@@ -13,28 +13,32 @@
|
||||
|
||||
前端 后端
|
||||
│ │
|
||||
│── POST /api/upload ────────►│
|
||||
│── POST /api/upload/ ───────►│ (auth.jwt 组内,需 sys:upload:file 权限)
|
||||
│ uploadFile │
|
||||
│ Content-Type: │
|
||||
│ multipart/form-data │
|
||||
│ file: <二进制> │
|
||||
│ type: image | file │
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │upload_file()│
|
||||
│ │ │
|
||||
│ │ 1. 检查上传状态
|
||||
│ │ 2. finfo 检测真实 MIME
|
||||
│ │ 3. 按类型校验扩展名
|
||||
│ │ 4. 校验文件大小
|
||||
│ │ 5. 图片额外 getimagesize 校验
|
||||
│ │ 6. 保存到 temp/日期/随机文件名
|
||||
│ └──────┬──────┘
|
||||
│ ┌──────┴───────────┐
|
||||
│ │ 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 }│
|
||||
│ fileExtension, fileType, │
|
||||
│ fileName } │
|
||||
│ │
|
||||
│ 前端显示预览 │
|
||||
│ 或暂存 URL │
|
||||
│ 前端暂存 URL/路径 │
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 第二阶段:入库 │
|
||||
@@ -44,39 +48,24 @@
|
||||
│ │
|
||||
│── POST /api/example/add ──►│
|
||||
│ { name: "测试", │
|
||||
│ avatar: "https:// │
|
||||
│ cdn.example.com/ │
|
||||
│ temp/20260920/ │
|
||||
│ abc123.jpg" } │
|
||||
│ avatar: "temp/ │
|
||||
│ 20261002/123456_ab.jpg" }│
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ BaseLogic │
|
||||
│ │ ::add() │
|
||||
│ └──────┬──────┘
|
||||
│ ┌──────┴──────────────┐
|
||||
│ │ ExampleService::add │
|
||||
│ │ (use HasFileProcessing)│
|
||||
│ └──────┬──────────────┘
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ processFile │
|
||||
│ │ FieldsOn │
|
||||
│ │ Save() │
|
||||
│ └──────┬──────┘
|
||||
│ ┌──────┴──────────────┐
|
||||
│ │ processFileFieldsOnSave │
|
||||
│ │ → save_file() │
|
||||
│ │ 1. 去掉域名得相对路径 │
|
||||
│ │ 2. temp/ 开头 → 迁移 │
|
||||
│ │ 3. 外部URL → 下载保存 │
|
||||
│ │ 4. 正式路径 → 直接返回│
|
||||
│ └──────┬──────────────┘
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ save_file() │
|
||||
│ │ │
|
||||
│ │ 1. 去掉域名得到相对路径
|
||||
│ │ 2. 包含 temp/ → 移动到正式目录
|
||||
│ │ 3. 外部URL → 下载保存
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ 数据库存储 │
|
||||
│ │ /uploads/ │
|
||||
│ │ example/ │
|
||||
│ │ 20260920/ │
|
||||
│ │ abc123.jpg │
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│◄── { code:0, data:{id:1} }─│
|
||||
│◄── { code:0, data:{id:1} }──│
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 第三阶段:读取 │
|
||||
@@ -86,180 +75,130 @@
|
||||
│ │
|
||||
│── GET /api/example/detail/1►│
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │processFile │
|
||||
│ │FieldsOnRead │
|
||||
│ │() │
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │get_file_url │
|
||||
│ │() │
|
||||
│ │拼接域名 │
|
||||
│ └──────┬──────┘
|
||||
│ ┌──────┴───────────────┐
|
||||
│ │ processFileFieldsOnRead │
|
||||
│ │ → get_file_url() │
|
||||
│ │ 拼接 FILE_DOMAIN_URL │
|
||||
│ └──────┬───────────────┘
|
||||
│ │
|
||||
│◄── { avatar: "https://──────│
|
||||
│ cdn.example.com/uploads/ │
|
||||
│ example/20260920/ │
|
||||
│ abc123.jpg" } │
|
||||
│ example/20261002/..." } │
|
||||
```
|
||||
|
||||
## upload_file 函数
|
||||
:::tip 临时目录中转
|
||||
上传接口把文件写入 `temp/` 临时目录,**不直接落正式目录**;提交表单时 `save_file()` 依据 `temp/` 前缀把文件迁移到 Service 的 `fileSaveDir` 子目录(`user/`、`article/`、`example/` …),正式目录与业务模块一一对应。上传后未被表单引用的文件停留在 `temp/`,可按周期清理。
|
||||
:::
|
||||
|
||||
```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
|
||||
### 上传文件
|
||||
|
||||
```bash
|
||||
POST /api/upload/uploadFile
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
|
||||
file: <二进制文件>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `file` | File | 是 | 上传的文件 |
|
||||
| `type` | string | 否 | `image`(校验图片后缀白名单)/ `file`(校验全量文件后缀白名单,默认) |
|
||||
|
||||
**响应(来自项目实际代码):**
|
||||
|
||||
```json
|
||||
{
|
||||
// ... 完整实现见 app/common.php
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
**响应字段说明:**
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `$formName` | string | `'file'` | 表单字段名 |
|
||||
| `$type` | string | `'file'` | 文件类型:`image`(仅图片)/ `file`(所有文件) |
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `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/...` |
|
||||
|
||||
### 返回值
|
||||
|
||||
**成功:**
|
||||
|
||||
```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"
|
||||
{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
|
||||
```
|
||||
|
||||
## get_file_url 函数
|
||||
- `type` 只影响后缀白名单,存储目录始终是 `temp/`
|
||||
- 随机文件名避免重名与**可预测性攻击**(时间戳 + `random_bytes(8)`)
|
||||
- 目录不存在时自动 `mkdir(0755, true)` 创建
|
||||
|
||||
```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"
|
||||
上传文件安全检查(UploadController::uploadFile):
|
||||
│
|
||||
▼ get_file_url()
|
||||
▼ 1. 权限校验(auth.jwt + #[Permission('sys:upload:file')])
|
||||
│
|
||||
▼ 拼接 domain_url
|
||||
输出: "https://cdn.example.com/uploads/example/20260920/abc123.jpg"
|
||||
▼ 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
|
||||
```
|
||||
|
||||
## 在 Logic 中配置
|
||||
:::warning svg 已移除
|
||||
`FILE_IMAGE_EXT` 默认白名单**不含 svg**——svg 可内嵌 JavaScript,上传后直接作为静态资源访问存在 XSS 风险。如业务必须使用,请做内容清洗或以 attachment 方式返回。
|
||||
:::
|
||||
|
||||
## 入库处理(HasFileProcessing)
|
||||
|
||||
Service 配置文件字段后,保存/读取自动处理:
|
||||
|
||||
```php
|
||||
class ExampleLogic extends BaseLogic
|
||||
// app/Services/ExampleService.php(形态示例)
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['avatar', 'cover'];
|
||||
use HasFileProcessing, /* 其他 Trait */;
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
// 需要自动处理的文件字段
|
||||
protected array $fileFields = ['avatar'];
|
||||
|
||||
// 多文件字段(逗号分隔的多个 URL)
|
||||
protected array $multiFileFields = ['images'];
|
||||
|
||||
// 文件保存子目录(未指定时按模型类名小写推导)
|
||||
protected string $fileSaveDir = 'example';
|
||||
}
|
||||
```
|
||||
@@ -268,99 +207,44 @@ class ExampleLogic extends BaseLogic
|
||||
|
||||
| 操作 | 自动行为 |
|
||||
|------|---------|
|
||||
| `add()` | `processFileFieldsOnSave()` → 调用 `save_file()` 迁移临时文件 |
|
||||
| `update()` | `processFileFieldsOnSave()` → 调用 `save_file()` 迁移临时文件 |
|
||||
| `detail()` | `processFileFieldsOnRead()` → 调用 `get_file_url()` 补全域名 |
|
||||
| `pageList()` | `processFileFieldsOnReadList()` → 调用 `get_file_url()` 补全域名 |
|
||||
| `allList()` | `processFileFieldsOnReadList()` → 调用 `get_file_url()` 补全域名 |
|
||||
| `add()` / `update()` | `processFileFieldsOnSave()` → `save_file()` 归一化(去域名、迁移 temp、下载外部 URL) |
|
||||
| `detail()` | `processFileFieldsOnRead()` → `get_file_url()` 补全域名 |
|
||||
| `pageList()` / `allList()` | `processFileFieldsOnReadList()` 逐条补全域名 |
|
||||
|
||||
### 多文件字段
|
||||
**处理细节(来自 `HasFileProcessing` 源码):**
|
||||
|
||||
- 空值跳过;`save_file()` 返回 `false` 时**保留原值**(避免误清空)
|
||||
- 多文件按逗号拆分逐个处理,单个失败保留原 URL
|
||||
- 未 `use HasFileProcessing` 时占位方法直接透传(见 [4.7 横切机制](/zh/architecture/cross-cutting.html))
|
||||
|
||||
## 核心辅助函数(app/Helpers/helpers.php)
|
||||
|
||||
```php
|
||||
class UserLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['avatar'];
|
||||
/**
|
||||
* 保存文件(归一化入库路径)
|
||||
*
|
||||
* 处理逻辑:
|
||||
* 1. 带域名 URL → 去掉域名得到相对路径
|
||||
* 2. 包含 temp/ → 移动到正式目录(fileSaveDir)
|
||||
* 3. 外部 URL → save_remote_file() 下载保存
|
||||
* 4. 已在正式目录 → 直接返回
|
||||
*/
|
||||
function save_file(string $fileUrl, string $saveDir = ''): string|false
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'user/avatar';
|
||||
}
|
||||
/**
|
||||
* 获取文件完整 URL
|
||||
* - 空值 → '';已是 http(s) 开头 → 原样返回
|
||||
* - 相对路径 → 拼接 config/file.php 的 domain_url
|
||||
*/
|
||||
function get_file_url(string $path): string
|
||||
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['cover', 'attachment'];
|
||||
/** 生成存储路径:{saveDir}/Ymd/random.扩展名 */
|
||||
function create_file_path(string $saveDir = '', string $ext = ''): string
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'article';
|
||||
}
|
||||
/** 下载外部 URL 并保存(配合 is_internal_ip 做内网地址拦截,防 SSRF) */
|
||||
function save_remote_file(string $url, string $saveDir = ''): string|false
|
||||
```
|
||||
|
||||
## 前端上传接口
|
||||
|
||||
### 上传文件
|
||||
|
||||
```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 上传组件
|
||||
@@ -369,11 +253,12 @@ file: <二进制图片>
|
||||
<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-button type="primary">上传图片</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
@@ -389,9 +274,8 @@ const beforeUpload = (file) => {
|
||||
};
|
||||
|
||||
const handleUploadSuccess = (response) => {
|
||||
// response.data.fileUrl 是完整 URL
|
||||
// 提交表单时使用此 URL
|
||||
form.value.avatar = response.data.fileUrl;
|
||||
// 提交表单时保存相对路径(filePath)最稳妥
|
||||
form.value.avatar = response.data.filePath;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
@@ -399,66 +283,36 @@ const handleUploadSuccess = (response) => {
|
||||
### 提交表单
|
||||
|
||||
```typescript
|
||||
// 前端提交时,avatar 字段的值是上传接口返回的 fileUrl
|
||||
const formData = {
|
||||
name: '测试',
|
||||
avatar: 'https://cdn.example.com/temp/20260920/abc123.jpg',
|
||||
avatar: 'temp/20261002/123456_a1b2c3d4e5f6.jpg',
|
||||
};
|
||||
|
||||
// 后端 Logic 层自动:
|
||||
// 1. 从 avatar 字段检测到临时文件 URL
|
||||
// 2. 调用 save_file() 迁移到正式目录
|
||||
// 3. 去掉域名,只存相对路径入库
|
||||
// 后端 Service 层自动:
|
||||
// 1. 识别 avatar 为 fileFields 字段
|
||||
// 2. save_file() 归一化(去域名 / 迁移 temp 到 fileSaveDir / 下载外部 URL)
|
||||
// 3. 正式目录相对路径入库(如 example/20261002/123456_a1b2c3d4e5f6.jpg)
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
```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
|
||||
# .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
|
||||
```
|
||||
|
||||
```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'),
|
||||
];
|
||||
```
|
||||
详见 [7.2 上传配置详解](/zh/tools/upload-config.html)。
|
||||
|
||||
## 安全检查链
|
||||
:::warning 同时调整 PHP 限制
|
||||
业务层 `FILE_MAX_SIZE=10` 生效的前提是 PHP 层放行,确认 `php.ini`:
|
||||
|
||||
```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;
|
||||
}
|
||||
```ini
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 50M
|
||||
max_execution_time = 300
|
||||
```
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user