Xiuno BBS 头像上传后变成裂图?一段文件路径拼接代码的正误写法对照
给用户头像上传功能加个自定义存储路径,结果上传成功但页面上全是裂图。排查了半天,问题出在一行路径拼接代码上。这个坑不算深,但很容易被忽略,尤其是从其他框架转过来写 Xiuno 插件的时候。
先看错误写法。假设我们要把上传的头像从临时目录挪到 ./upload/avatar/ 下,并按用户 ID 分目录存放,很多人会顺手这么写:
// 错误写法
$uid = intval($user['uid']);
$dir = './upload/avatar/' . $uid;
if (!is_dir($dir)) {
mkdir($dir, 0777);
}
$dest = $dir . '/' . $filename;
move_uploaded_file($tmp_file, $dest);
这段代码在本地 Windows 环境跑起来没问题,上传后文件也确实出现在 upload/avatar/1/ 下面。但一部署到 Linux 服务器,头像就全部裂开。原因有三个,任何一个都足以让图片加载失败。
第一个问题:相对路径的基准点不是你以为的那个目录。Xiuno 的入口文件在根目录,但插件里的代码可能被 include 到不同层级。./upload/avatar/ 到底是相对于网站根目录,还是相对于当前执行脚本所在目录,取决于 PHP 的当前工作目录。本地调试时工作目录恰好是根目录,所以能跑通;线上环境经过 rewrite 或 CLI 调用后工作目录变了,文件就挪到了别的地方,数据库里存的路径和实际文件位置对不上。
第二个问题:mkdir 没有递归,也没有判断返回值。如果 upload/avatar/ 这个父目录不存在,mkdir($dir) 会直接失败并返回 false,但代码没做任何检查,继续往下走,move_uploaded_file 自然也跟着失败。更隐蔽的是,如果目录已存在但权限不对,is_dir 返回 true,跳过创建,移动文件时依然失败。
第三个问题:权限位写死 0777。在部分启用了 umask 或安全策略的服务器上,0777 会被拒绝,实际创建的目录权限可能变成 0755 甚至更小,导致后续写入失败。而且 0777 本身也不安全。
下面是正确写法,逐条对应修复:
// 正确写法
$uid = intval($user['uid']);
$base = APP_PATH . 'upload/avatar/'; // 用框架常量定位,不依赖工作目录
$dir = $base . $uid . '/';
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) { // 递归创建,检查返回值
message(-1, '头像目录创建失败,请检查 upload 目录权限');
}
}
$dest = $dir . $filename;
if (!move_uploaded_file($tmp_file, $dest)) {
message(-1, '头像文件移动失败');
}
// 数据库里存相对路径,方便换域名和迁移
$avatar_url = 'upload/avatar/' . $uid . '/' . $filename;
几个关键改动说明一下。APP_PATH 是 Xiuno 定义的根目录常量,用它拼绝对路径,不管当前工作目录是什么都不会错。mkdir 第三个参数传 true 开启递归,父目录不存在也能一次建好。权限用 0755,既保证可写又不会过宽。每一步都判断返回值,失败时用 message() 给出明确提示,而不是让用户对着裂图猜。最后数据库只存相对路径,前端拼接域名展示,以后换域名或者加 CDN 都不用改库。
还有一个容易漏的点:Xiuno 本身有 file_put_contents 和目录操作的封装函数,如果只是简单存文件,优先用框架自带的方法,它们内部已经处理了路径和权限问题。自己写 move_uploaded_file 通常是因为要处理临时文件,那就按上面的方式把路径和错误处理补全。
排查这类问题时,最快的验证方法是在移动文件前后各打一条日志,把 getcwd()、$dir、is_dir($dir)、file_exists($dest) 都输出一遍。裂图九成以上是路径不对或权限不足,把这两个值打印出来,一眼就能定位。