必须弃用$_FILES['name']直接存文件,应提取合法扩展名后生成唯一文件名(如uniqid().'_'.random_int()),结合MIME与文件头双重校验,并采用扁平存储+元数据驱动归档,确保安全与可维护性。

上传后文件名被覆盖或丢失原始信息
PHP 的 $_FILES 数组里 name 是客户端传来的原始文件名,但直接用它存服务器极易出问题:空格、中文、特殊符号(如 audio file (2024).mp3)会导致 move_uploaded_file() 失败或路径截断。更麻烦的是,同名文件反复上传会覆盖——你根本不知道谁是谁。
解决办法是彻底弃用原始 name 作为存储名,改用唯一标识 + 保留扩展名:
- 用
pathinfo($file['name'], PATHINFO_EXTENSION)提取扩展名,确保音频类型合法(比如只允许mp3、wav、ogg) - 生成唯一文件名:推荐
uniqid() . '_' . random_int(1000, 9999),比单纯time()更抗并发 - 不要拼接用户输入进路径,例如
./uploads/{$_POST['category']}/—— 这是目录遍历漏洞温床
归档目录结构怎么设计才不乱
音频不是图片,常需按“项目-日期-说话人”多维归档。硬编码三层目录(如 uploads/project_x/202405/zhao.mp3)看似清晰,但一旦业务变(比如要加“语种”维度),所有旧逻辑就得重写。
更可持续的做法是“扁平存储 + 元数据驱动”:
立即学习“PHP免费学习笔记(深入)”;
- 所有上传音频统一存到一个时间分片目录,如
/var/www/audio/archive/2024/05/21/(用date('Y/m/d')生成) - 文件名用唯一 ID,如
7b3a9f2e_4821.mp3 - 同步写入一条 JSON 元数据记录到同级
_meta/7b3a9f2e_4821.json,内容含:original_name、uploader_id、project_code、record_time等 - 查询时查 JSON,而不是靠目录路径“猜”归属
move_uploaded_file() 总失败?检查这三处
上传成功但归档失败,90% 出在权限、路径或临时文件生命周期上:
-
move_uploaded_file()只能移动$_FILES['audio']['tmp_name']指向的临时文件,且该文件在脚本结束时自动销毁——不能先sleep(1)再挪,也不能在try/catch外调用 - 目标目录必须存在且 PHP 进程有写权限(常见坑:
chown www-data:www-data /var/www/audio/archive漏了-R) - 路径拼接别用字符串相加,用
dirname(__FILE__) . '/archive/' . $date_dir容易因末尾斜杠缺失导致路径错乱;改用__DIR__ . DIRECTORY_SEPARATOR . 'archive' . DIRECTORY_SEPARATOR . $date_dir
音频格式校验不能只看扩展名
仅靠 pathinfo(..., PATHINFO_EXTENSION) 判断是 mp3,等于给攻击者开后门。用户可把恶意 PHP 文件改成 shell.mp3 上传,然后通过 Web 路径直接执行。
必须做 MIME 类型+二进制头双重校验:
- 用
finfo_open(FILEINFO_MIME_TYPE)读真实 MIME,拒绝text/plain或application/x-php - 对前 128 字节做硬匹配:mp3 开头是
ID3或\xff\xe2,wav 是RIFF...WAVE,可用bin2hex(file_get_contents($tmp_file, 0, 16))快速判断 - 如果业务允许,上传后立刻用
ffmpeg -v error -i "$tmp" -f null - 2>&1验证能否被解码,失败则unlink()
归档逻辑越早和业务解耦越好。别让上传接口同时承担“验证、转码、通知、写数据库、发消息队列”所有事——先把文件落盘、写好元数据,剩下的丢进异步任务。否则一个 ffmpeg 超时,整个 HTTP 请求就卡死。



















