rootPath是物理存储根目录(默认public/uploads),savePath才是决定子目录层级的关键,支持date()等动态值;ThinkPHP 6.x推荐用move()第二个参数传闭包生成路径,5.1需手动拼接;务必确保多级目录可自动创建或手动兜底。

ThinkPHP 上传路径配置中 rootPath 和 savePath 的区别
直接改 rootPath 不起作用,它只是底层存储根目录(默认是 public/uploads),真正决定文件存到哪一级子目录的是 savePath。这个值支持动态拼接,比如用 date('Ym/d') 生成按年月日分层的路径。
常见错误是把日期逻辑写死在配置文件里,结果每次请求都用同一个路径;正确做法是在调用 move() 前动态计算 savePath,或通过闭包传入。
-
rootPath是物理根目录,建议保持默认,避免和 Web 访问路径脱节 -
savePath是相对于rootPath的子路径,支持变量、函数返回值 - 如果用了
validate(['size'=>1024*1024]),验证必须在move()前完成,否则savePath动态逻辑可能不生效
ThinkPHP 6.x 中使用闭包动态生成上传子目录
官方推荐方式:在 move() 方法第二个参数传入闭包,内部返回带日期的路径字符串。闭包会自动接收 File 对象,可从中提取原始文件名、后缀等信息。
$file = $this->request->file('image');
$savePath = $file->move( 'uploads', function($file) {
return date('Ym') . '/' . date('d') . '/';
});
注意:move() 第一个参数是 rootPath 下的相对路径(如 uploads),不是完整路径;闭包返回的才是最终子目录,结尾不加斜杠也没关系,框架会自动补全。
立即学习“PHP免费学习笔记(深入)”;
- 闭包内调用
date()是安全的,每次上传都会重新执行 - 不要在闭包里拼接
rootPath,框架已自动处理层级关系 - 若需按用户 ID 再分一层,可写成
date('Ym') . '/u_' . $uid . '/' . date('d')
ThinkPHP 5.1 中通过配置数组设置动态 savePath
5.1 不支持闭包传参,得靠配置数组中的 sub_name 或手动拼接 savePath。最稳的方式是先获取当前日期,再组装路径:
$dateDir = date('Ym') . DS . date('d');
$info = $file->move(ROOT_PATH . 'public' . DS . 'uploads', $dateDir);
这里 DS 是系统目录分隔符,ROOT_PATH 指向项目根目录,所以实际路径是 public/uploads/202406/15/xxx.jpg。务必确认 public/uploads 目录有写权限,否则报错 move_uploaded_file(): Unable to move。
- 不能直接写
'uploads/'.date('Ym/d'),因为move()第二个参数是子路径,不是完整路径 -
sub_name配置项只影响文件名前缀,不管目录结构 - 如果启用了
app_debug = false,目录创建失败不会抛异常,只会返回null,要主动检查$info
Web 访问路径与物理路径不一致时的坑
按日期生成子目录后,前端访问图片地址得同步更新。比如文件存在 public/uploads/202406/15/test.png,那 URL 就是 /uploads/202406/15/test.png。但很多人配置了 Nginx 别名或 Apache 重写,导致路径映射错乱。
最容易忽略的是:ThinkPHP 默认不自动创建多级子目录,move() 时如果 202406/15 不存在,会报错 failed to open stream: No such file or directory。框架虽有 mkdir 逻辑,但在某些 PHP 版本或 SELinux 环境下会静默失败。
- 上线前务必测试首次上传是否能自动建好
202406/15这类嵌套目录 - 用
is_dir()+mkdir($path, 0755, true)手动兜底更可靠 - CDN 回源配置要匹配新路径规则,否则缓存 404



















