Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/tools/upload-config.md
T
2026-10-08 17:50:29 +08:00

314 lines
10 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.2 上传配置详解
:::tip 概述
文件上传配置定义在 `config/file.php` 和 `.env` 中,控制上传目录、访问域名、大小限制、允许的文件类型等。所有值通过 `env()` 函数从 `.env` 读取,支持多环境切换;`config/file.php` 中的注释明确标注了每个配置的**读取方**。
:::
## 配置文件
```php
// config/file.php — 文件上传配置(来自项目实际代码,节选)
return [
// 文件上传根目录(相对 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,
];
```
## 配置项详解
| 配置项 | .env 变量 | 默认值 | 说明 |
|--------|----------|--------|------|
| `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=uploads
```
**要求:**
- 本项目为**相对 `public/` 的目录**,Web 可直接访问其中的静态文件
- 目录必须存在且有写入权限(Docker 镜像已创建并授权 `public/uploads`)
- 上传文件随项目目录持久化(Docker 下即宿主机目录,不依赖命名卷)
**各环境示例:**
| 环境 | 值 | 实际位置 |
|------|------|---------|
| 本地开发 | `uploads` | `项目/public/uploads/` |
| Linux 生产 | `uploads`(或绝对路径) | Nginx root 下可直接访问 |
| Docker | `uploads` | 挂载的项目目录内 |
### domain_url — 文件访问域名
```ini
# .env
FILE_DOMAIN_URL=https://cdn.example.com
```
**要求:**
- 不要以 `/` 结尾(读取方会 `rtrim` 处理,但配置时保持规范)
- 必须是前端可访问的域名
- 本地开发可留空(接口返回相对路径,由同源/Vite 代理直接访问)
**各环境示例:**
| 环境 | 域名示例 |
|------|---------|
| 本地开发 | (留空,使用相对路径) |
| 虚拟域名开发 | `http://file.laravel13.elevue` |
| 测试环境 | `https://test-cdn.example.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(单位 MB,config 内 ×1024×1024 转字节)
FILE_MAX_SIZE=10
```
**建议值:**
| 场景 | 建议值 | 说明 |
|------|--------|------|
| 头像上传 | 2 MB | 图片通常不会太大 |
| 文章封面 | 5 MB | 压缩后的图片 |
| 通用文件上传 | 10 MB | 默认值 |
| 文档上传 | 20 MB | PDF/Word 等 |
:::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 # 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)校验
```
**说明:**
- `image_ext` 用于 `type=image`,只允许图片格式;**svg 已移除**(内嵌 JS 的 XSS 风险)
- `file_ext` 用于 `type=file`,允许图片、文档、压缩包等常见格式
- 白名单为空数组时跳过校验(防御性兜底),生产环境务必保留非空配置
## .env 配置示例
```ini
# 开发环境
FILE_UPLOAD_DIR=uploads
FILE_DOMAIN_URL=
FILE_MAX_SIZE=10
# 生产环境(示例)
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
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/` | 临时文件目录(上传接口落盘) | `config/file.php → temp_dir` |
| `{module}/` | 按模块的正式目录 | Service 的 `fileSaveDir`(未配置按模型名推导) |
| `{Ymd}/` | 日期子目录 | 自动创建 |
| `{His}_{random}.{ext}` | 时间+随机文件名 | 防冲突与可预测性 |
## 文件生命周期
```text
1. 前端上传 → POST /api/upload/uploadFile
保存到: public/uploads/temp/20261002/123456_a1b2c3d4e5f6.jpg
返回: filePath + fileUrl
2. 前端提交表单 → 携带 filePath
POST /api/example/add { name: "测试", avatar: "temp/20261002/..." }
3. ExampleService::add() → processFileFieldsOnSave → save_file()
temp/ → 迁移到 fileSaveDir(example/)后,正式目录相对路径入库
(其它分支:去掉域名直接用;外部 URL → 下载保存)
4. ExampleService::detail() → processFileFieldsOnRead → get_file_url()
相对路径 → 拼接 FILE_DOMAIN_URL
example/20261002/...
→ https://cdn.example.com/example/20261002/...
```
## 与 Service 层的配合
```php
class ArticleService extends BaseTenantService
{
use HasFileProcessing, /* 其他 Trait */;
protected array $fileFields = ['cover'];
protected string $fileSaveDir = 'article';
}
```
**实际路径映射:**
```text
配置:
FILE_UPLOAD_DIR = uploads
FILE_DOMAIN_URL = https://cdn.example.com
fileSaveDir = article
结果:
上传文件: public/uploads/temp/20261002/xxx.jpg
Service 归一化后入库: article/20261002/xxx.jpg
前端显示: https://cdn.example.com/article/20261002/xxx.jpg
```
## Nginx 配置
### 静态文件直接由 Nginx 返回
```nginx
# 项目方案:public/uploads 位于站点根内,location / 由 try_files 直接返回静态文件
location / {
try_files $uri $uri/ /index.php?$query_string;
}
```
### 禁止上传目录执行 PHP
```nginx
# docker/nginx.conf 已内置:仅入口 index.php 可执行,其余 .php 一律 404
location ~ \.php$ {
return 404;
}
```
### 禁止访问隐藏文件
```nginx
location ~ /\. {
deny all;
}
```
## 常见问题
### 问题 1:上传提示"目录不可写"
**原因:** `public/uploads` 没有写入权限。
**解决:**
```bash
# Linux
chmod -R 755 public/uploads
chown -R www-data:www-data public/uploads
# Docker:镜像内已 chown(storage / bootstrap/cache / public/uploads)
```
### 问题 2:上传后图片不显示
**原因:** `FILE_DOMAIN_URL` 配置错误或与前端不同源。
**解决:**
1. 检查 `.env` 中 `FILE_DOMAIN_URL` 是否为前端可访问的地址(改后 `php artisan config:clear`)
2. 留空时用相对路径——确认前端与后端同源或经代理
3. 浏览器直接打开图片 URL 验证
### 问题 3:文件大小限制不生效
**原因:** PHP 层限制小于业务层。
**解决:**
```ini
; php.ini(修改后重启服务)
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300
```
### 问题 4:临时文件未清理
**原因:** 上传后未提交表单(或表单未引用)的文件停留在 `temp/`,不会被迁移。
**解决:** 定期清理 `temp/` 中超过 1 天的文件:
```bash
# 定时任务:每天凌晨清理 1 天前的临时文件
find public/uploads/temp/ -type f -mtime +1 -delete
```
## 安全建议
| 建议 | 说明 |
|------|------|
| 扩展名白名单 | 只允许已知安全的文件类型(`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/` 内未被引用的文件按周期清理,避免磁盘空间被占满 |