签入版本

This commit is contained in:
2026-10-08 17:50:29 +08:00
parent 809750b47e
commit e213a9b31d
444 changed files with 39366 additions and 37533 deletions
@@ -1,21 +1,35 @@
# 7.2 上传配置详解
:::tip 概述
文件上传配置定义在 `config/file.php` 和 `.env` 中,控制上传目录、域名、大小限制、允许的文件类型等。所有值通过 `env()` 函数从 `.env` 文件读取,支持多环境切换。
文件上传配置定义在 `config/file.php` 和 `.env` 中,控制上传目录、访问域名、大小限制、允许的文件类型等。所有值通过 `env()` 函数从 `.env` 读取,支持多环境切换;`config/file.php` 中的注释明确标注了每个配置的**读取方**。
:::
## 配置文件
```php
// config/file.php — 文件上传配置
// 所有值通过 env() 函数从 .env 文件读取,支持多环境切换
// config/file.php — 文件上传配置(来自项目实际代码,节选)
return [
'upload_dir' => env('file.upload_dir', ''), // 上传根目录(绝对路径)
'domain_url' => env('file.domain_url', ''), // 文件访问域名(拼接完整 URL 用)
'temp_dir' => 'temp', // 临时目录名(相对 upload_dir)
'image_ext' => env('file.image_ext', 'gif,jpg,jpeg,png,bmp,webp,svg'), // 允许的图片后缀
'file_ext' => env('file.file_ext', 'gif,jpg,jpeg,png,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv,mp3,mp4,sql,js,css'), // 允许的文件后缀
'max_size' => env('file.max_size', 10) * 1024 * 1024, // 最大大小(MB → 字节)
// 文件上传根目录(相对 public),所有上传文件均存放于此
// 读取方:UploadController::uploadFile()、save_file()、save_remote_file()
'upload_dir' => env('FILE_UPLOAD_DIR', 'uploads'),
// 文件访问域名,用于拼接完整 URL;为空时返回相对路径
// 读取方:get_file_url()、save_content()、processContentFieldsOnRead()
'domain_url' => env('FILE_DOMAIN_URL', ''),
// 临时文件目录名(相对 upload_dir)
// 读取方:UploadController::uploadFile()(上传落盘目录)、save_file()(识别 temp/ 开头的临时文件后迁移到正式目录)
'temp_dir' => 'temp',
// 允许上传的图片后缀(逗号分隔)
// 安全说明:svg 已移除,因其可内嵌 JavaScript 导致 XSS 风险
'image_ext' => env('FILE_IMAGE_EXT', 'gif,jpg,jpeg,png,bmp,webp'),
// 允许上传的文件后缀(逗号分隔)
'file_ext' => env('FILE_EXT', 'gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv'),
// 单文件最大尺寸(MB),转换为字节存储
'max_size' => (int) env('FILE_MAX_SIZE', 10) * 1024 * 1024,
];
```
@@ -23,67 +37,76 @@ return [
| 配置项 | .env 变量 | 默认值 | 说明 |
|--------|----------|--------|------|
| `upload_dir` | `[FILE] UPLOAD_DIR` | - | 上传根目录(绝对路径),所有文件存储于此 |
| `domain_url` | `[FILE] DOMAIN_URL` | - | 文件访问域名,用于拼接完整 URL |
| `temp_dir` | - | `temp` | 临时目录名(相对 upload_dir) |
| `image_ext` | `[FILE] IMAGE_EXT` | `gif,jpg,jpeg,png,bmp,webp,svg` | 图片允许后缀 |
| `file_ext` | `[FILE] FILE_EXT` | 多种后缀 | 文件允许后缀 |
| `max_size` | `[FILE] MAX_SIZE` | `10` | 单文件最大大小(MB) |
| `upload_dir` | `FILE_UPLOAD_DIR` | `uploads` | 上传根目录(**相对 `public/`**),所有文件存储于此 |
| `domain_url` | `FILE_DOMAIN_URL` | (空) | 文件访问域名,用于拼接完整 URL |
| `temp_dir` | — | `temp` | 临时目录名(相对 upload_dir),上传接口的落盘目录,`save_file()` 据此识别待迁移文件 |
| `image_ext` | `FILE_IMAGE_EXT` | `gif,jpg,jpeg,png,bmp,webp` | 图片允许后缀(**无 svg**) |
| `file_ext` | `FILE_EXT` | 图片+文档+压缩包等 | 文件允许后缀 |
| `max_size` | `FILE_MAX_SIZE` | `10` | 单文件最大大小(MB,配置内自动转字节) |
### upload_dir — 上传根目录
```ini
; .env
[FILE]
UPLOAD_DIR = D:/uploads/rxthinkcmf
# .env
FILE_UPLOAD_DIR=uploads
```
**要求:**
- 必须是绝对路径
- 目录必须存在且有写入权限
- 生产环境建议放在项目目录外(防止被 Web 直接访问)
- 本项目为**相对 `public/` 的目录**,Web 可直接访问其中的静态文件
- 目录必须存在且有写入权限(Docker 镜像已创建并授权 `public/uploads`)
- 上传文件随项目目录持久化(Docker 下即宿主机目录,不依赖命名卷)
**各环境示例:**
| 环境 | 路径示例 |
|------|---------|
| Windows 开发 | `D:/uploads/rxthinkcmf` |
| Linux 开发 | `/home/www/uploads/rxthinkcmf` |
| Linux 生产 | `/data/uploads/rxthinkcmf` |
| Docker | `/uploads/rxthinkcmf`(挂载卷) |
| 环境 | 值 | 实际位置 |
|------|------|---------|
| 本地开发 | `uploads` | `项目/public/uploads/` |
| Linux 生产 | `uploads`(或绝对路径) | Nginx root 下可直接访问 |
| Docker | `uploads` | 挂载的项目目录内 |
### domain_url — 文件访问域名
```ini
; .env
[FILE]
DOMAIN_URL = https://cdn.example.com
# .env
FILE_DOMAIN_URL=https://cdn.example.com
```
**要求:**
- 不要以 `/` 结尾
- 不要以 `/` 结尾(读取方会 `rtrim` 处理,但配置时保持规范)
- 必须是前端可访问的域名
- 本地开发可留空(使用相对路径)
- 本地开发可留空(接口返回相对路径,由同源/Vite 代理直接访问)
**各环境示例:**
| 环境 | 域名示例 |
|------|---------|
| 本地开发 | (留空,使用相对路径) |
| 虚拟域名开发 | `http://file.laravel13.elevue` |
| 测试环境 | `https://test-cdn.example.com` |
| 生产环境 | `https://cdn.example.com` |
| 对象存储 | `https://bucket-name.oss-cn-hangzhou.aliyuncs.com` |
| 生产环境 | `https://cdn.example.com` 或对象存储域名 |
### temp_dir — 临时目录
```php
// config/file.php
'temp_dir' => 'temp',
```
**说明:**
- 上传接口 `uploadFile` 把文件写入 `{upload_dir}/temp/Ymd/`,不直接落正式目录
- `save_file()` / `save_content()` 依据 `temp/` 前缀把文件迁移到 Service 的 `fileSaveDir` 子目录
- 正式目录未被表单引用的临时文件可按周期清理(见下方「问题 4」)
### max_size — 文件大小限制
```ini
; .env
[FILE]
MAX_SIZE = 10
# .env(单位 MB,config 内 ×1024×1024 转字节)
FILE_MAX_SIZE=10
```
**单位:** MB(配置文件中会自动转换为字节:`10 * 1024 * 1024`)
**建议值:**
| 场景 | 建议值 | 说明 |
@@ -92,141 +115,95 @@ MAX_SIZE = 10
| 文章封面 | 5 MB | 压缩后的图片 |
| 通用文件上传 | 10 MB | 默认值 |
| 文档上传 | 20 MB | PDF/Word 等 |
| 视频上传 | 100 MB | 需配合分片上传 |
### image_ext — 图片允许后缀
:::warning 双层限制
业务层限制生效前,PHP 层必须放行:`php.ini` 的 `upload_max_filesize` / `post_max_size` 需 **大于等于** `FILE_MAX_SIZE`(建议 50M)。
:::
### image_ext / file_ext — 后缀白名单
```ini
; .env
[FILE]
IMAGE_EXT = gif,jpg,jpeg,png,bmp,webp,svg
# .env
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp # uploadFile?type=image 校验
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv # uploadFile(默认 type=file)校验
```
**默认值:** `gif,jpg,jpeg,png,bmp,webp,svg`
**说明:**
**说明:** 用于 `uploadFile?type=image` 接口,只允许图片格式上传。
### file_ext — 文件允许后缀
```ini
; .env
[FILE]
FILE_EXT = gif,jpg,jpeg,png,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv,mp3,mp4,sql,js,css
```
**默认值:** 包含图片、文档、压缩包、音视频等常见格式。
**说明:** 用于 `uploadFile` 接口,允许所有合法文件格式。
- `image_ext` 用于 `type=image`,只允许图片格式;**svg 已移除**(内嵌 JS 的 XSS 风险)
- `file_ext` 用于 `type=file`,允许图片、文档、压缩包等常见格式
- 白名单为空数组时跳过校验(防御性兜底),生产环境务必保留非空配置
## .env 配置示例
### 开发环境
```ini
[FILE]
UPLOAD_DIR = D:/uploads/rxthinkcmf
DOMAIN_URL =
MAX_SIZE = 10
```
# 开发环境
FILE_UPLOAD_DIR=uploads
FILE_DOMAIN_URL=
FILE_MAX_SIZE=10
### 测试环境
```ini
[FILE]
UPLOAD_DIR = /home/www/uploads/rxthinkcmf
DOMAIN_URL = https://test-cdn.example.com
MAX_SIZE = 10
```
### 生产环境
```ini
[FILE]
UPLOAD_DIR = /data/uploads/rxthinkcmf
DOMAIN_URL = https://cdn.example.com
MAX_SIZE = 10
IMAGE_EXT = jpg,jpeg,png,gif,bmp,webp
FILE_EXT = jpg,jpeg,png,gif,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar
```
### 对象存储(OSS)
```ini
; 如果使用阿里云 OSS 等对象存储,domain_url 设为 OSS 域名
[FILE]
UPLOAD_DIR = /data/uploads/rxthinkcmf
DOMAIN_URL = https://your-bucket.oss-cn-hangzhou.aliyuncs.com
MAX_SIZE = 50
# 生产环境(示例)
FILE_UPLOAD_DIR=uploads
FILE_DOMAIN_URL=https://cdn.example.com
FILE_MAX_SIZE=10
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar
```
## 目录结构
```text
{upload_dir}/ # upload_dir(上传根目录)
├── temp/ # 临时目录(temp_dir)
│ └── 20260920/ # 按日期分目录
│ ├── a1b2c3d4e5f6.jpg # 随机文件名(防止冲突)
│ └── f6e5d4c3b2a1.png
├── article/ # 正式目录(由 fileSaveDir 配置)
│ └── 20260920/
│ └── 143052_xK7pMn3q.jpg
├── example/
│ └── 20260920/
├── user/
│ └── avatar/
│ └── 20260920/
└── file_template/
└── 20260920/
public/
└── {upload_dir}/ # upload_dir(默认 uploads)
├── temp/ # 临时目录(UploadController 上传落盘)
│ └── 20261002/
│ └── 123456_a1b2c3d4e5f6.jpg
├── article/ # 正式目录(由 Service 的 fileSaveDir 配置)
│ └── 20261002/
│ └── 153052_9f8e7d6c5b4a.pdf
├── example/
├── user/
│ └── avatar/
└── file_template/
```
### 目录命名规则
| 目录 | 说明 | 示例 |
| 目录 | 说明 | 来源 |
|------|------|------|
| `temp/` | 临时文件目录 | 上传后暂存于此 |
| `{module}/` | 正式目录(按模块) | 由 Logic 的 `fileSaveDir` 配置 |
| `{date}/` | 日期子目录 | `20260920`(自动创建) |
| `{random}.{ext}` | 随机文件名 | `a1b2c3d4e5f6.jpg`(防止冲突) |
| `temp/` | 临时文件目录(上传接口落盘) | `config/file.php → temp_dir` |
| `{module}/` | 按模块的正式目录 | Service 的 `fileSaveDir`(未配置按模型名推导) |
| `{Ymd}/` | 日期子目录 | 自动创建 |
| `{His}_{random}.{ext}` | 时间+随机文件名 | 防冲突与可预测性 |
## 文件生命周期
```text
1. 前端上传 → upload_file()
保存到: {upload_dir}/temp/20260920/a1b2c3d4e5f6.jpg
返回: https://cdn.example.com/temp/20260920/a1b2c3d4e5f6.jpg
1. 前端上传 → POST /api/upload/uploadFile
保存到: public/uploads/temp/20261002/123456_a1b2c3d4e5f6.jpg
返回: filePath + fileUrl
2. 前端提交表单 → 携带 fileUrl
POST /api/example/add { name: "测试", avatar: "https://..." }
2. 前端提交表单 → 携带 filePath
POST /api/example/add { name: "测试", avatar: "temp/20261002/..." }
3. BaseLogic::add() → save_file()
检测到 temp/ → 移动到正式目录
{upload_dir}/temp/20260920/a1b2c3d4e5f6.jpg
→ {upload_dir}/example/20260920/a1b2c3d4e5f6.jpg
返回相对路径入库: /uploads/example/20260920/a1b2c3d4e5f6.jpg
3. ExampleService::add() → processFileFieldsOnSave → save_file()
temp/ → 迁移到 fileSaveDir(example/)后,正式目录相对路径入库
(其它分支:去掉域名直接用;外部 URL → 下载保存)
4. BaseLogic::detail() → get_file_url()
相对路径 → 拼接域名
/uploads/example/20260920/a1b2c3d4e5f6.jpg
→ https://cdn.example.com/uploads/example/20260920/a1b2c3d4e5f6.jpg
4. ExampleService::detail() → processFileFieldsOnRead → get_file_url()
相对路径 → 拼接 FILE_DOMAIN_URL
example/20261002/...
→ https://cdn.example.com/example/20261002/...
```
## 与 Logic 层的配合
## 与 Service 层的配合
```php
class ArticleLogic extends BaseTenantLogic
class ArticleService extends BaseTenantService
{
/**
* 需要自动处理的文件上传字段
*
* @var array
*/
protected array $fileFields = ['cover'];
use HasFileProcessing, /* 其他 Trait */;
/**
* 文件保存的子目录
*
* @var string
*/
protected array $fileFields = ['cover'];
protected string $fileSaveDir = 'article';
}
```
@@ -235,14 +212,14 @@ class ArticleLogic extends BaseTenantLogic
```text
配置:
upload_dir = /data/uploads/rxthinkcmf
fileSaveDir = article
FILE_UPLOAD_DIR = uploads
FILE_DOMAIN_URL = https://cdn.example.com
fileSaveDir = article
结果:
临时文件: /data/uploads/rxthinkcmf/temp/20260920/abc.jpg
正式文件: /data/uploads/rxthinkcmf/article/20260920/abc.jpg
数据库存: /uploads/article/20260920/abc.jpg
前端显示: https://cdn.example.com/uploads/article/20260920/abc.jpg
上传文件: public/uploads/temp/20261002/xxx.jpg
Service 归一化后入库: article/20261002/xxx.jpg
前端显示: https://cdn.example.com/article/20261002/xxx.jpg
```
## Nginx 配置
@@ -250,20 +227,18 @@ class ArticleLogic extends BaseTenantLogic
### 静态文件直接由 Nginx 返回
```nginx
# 配置 Nginx 直接处理文件请求,不经过 PHP
location /uploads/ {
alias /data/uploads/rxthinkcmf/;
expires 30d;
add_header Cache-Control "public, immutable";
# 项目方案:public/uploads 位于站点根内,location / 由 try_files 直接返回静态文件
location / {
try_files $uri $uri/ /index.php?$query_string;
}
```
### 禁止上传目录执行 PHP
```nginx
# 安全:禁止上传目录执行 PHP 文件
location ~* /uploads/.*\.php$ {
deny all;
# docker/nginx.conf 已内置:仅入口 index.php 可执行,其余 .php 一律 404
location ~ \.php$ {
return 404;
}
```
@@ -279,57 +254,60 @@ location ~ /\. {
### 问题 1:上传提示"目录不可写"
**原因:** `upload_dir` 目录没有写入权限。
**原因:** `public/uploads` 没有写入权限。
**解决:**
```bash
# Linux
chmod -R 755 /data/uploads/rxthinkcmf
chown -R www:www /data/uploads/rxthinkcmf
chmod -R 755 public/uploads
chown -R www-data:www-data public/uploads
# Docker:镜像内已 chown(storage / bootstrap/cache / public/uploads)
```
### 问题 2:上传后图片不显示
**原因:** `domain_url` 配置错误或 Nginx 未配置静态文件处理。
**原因:** `FILE_DOMAIN_URL` 配置错误或与前端不同源。
**解决:**
1. 检查 `.env` 中 `DOMAIN_URL` 是否正确
2. 检查 Nginx 是否配置了 `/uploads/` 的 alias
3. 浏览器打开图片 URL 看是否能直接访问
1. 检查 `.env` 中 `FILE_DOMAIN_URL` 是否为前端可访问的地址(改后 `php artisan config:clear`)
2. 留空时用相对路径——确认前端与后端同源或经代理
3. 浏览器直接打开图片 URL 验证
### 问题 3:文件大小限制不生效
**原因:** PHP 配置限制了上传大小。
**原因:** PHP 层限制小于业务层。
**解决:**
```ini
; php.ini
upload_max_filesize = 20M
post_max_size = 25M
; php.ini(修改后重启服务)
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300
```
### 问题 4:临时文件未清理
**原因:** `temp/` 目录中的文件在表单提交后未被 `save_file()` 迁移。
**原因:** 上传后未提交表单(或表单未引用)的文件停留在 `temp/`,不会被迁移。
**解决:** 定期清理 `temp/` 目录中超过 1 天的文件:
**解决:** 定期清理 `temp/` 中超过 1 天的文件:
```bash
# 定时任务:每天凌晨清理 1 天前的临时文件
find /data/uploads/rxthinkcmf/temp/ -type f -mtime +1 -delete
find public/uploads/temp/ -type f -mtime +1 -delete
```
## 安全建议
| 建议 | 说明 |
|------|------|
| 上传目录放在项目外 | 防止被 Web 直接访问源码 |
| Nginx 禁止执行 PHP | 防止上传恶意 PHP 文件并执行 |
| 扩展名白名单 | 只允许已知安全的文件类型 |
| MIME 类型检查 | `finfo` 检测真实 MIME,防止伪造扩展名 |
| 随机文件名 | 防止路径遍历和文件名注入 |
| 文件大小限制 | 防止大文件 DoS 攻击 |
| 定期清理临时文件 | 避免磁盘空间被占满 |
| 扩展名白名单 | 只允许已知安全的文件类型(`FILE_IMAGE_EXT`/`FILE_EXT`) |
| 移除高危后缀 | svg 已移除;生产白名单去掉 `sql/js/css` 等可执行类 |
| MIME 类型检查 | 导入等场景配合 `finfo` 检测真实 MIME(见 [7.4 数据导入导出](/zh/tools/excel.html)) |
| 随机文件名 | 服务端生成(时间+随机串),防止路径遍历和文件名注入 |
| 文件大小限制 | `FILE_MAX_SIZE` 防止大文件 DoS |
| Nginx 禁止执行 PHP | 仅入口 `index.php` 可执行(Docker 配置已内置) |
| 定期清理临时文件 | `temp/` 内未被引用的文件按周期清理,避免磁盘空间被占满 |