ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

教育行业大文件上传:WebUploader+PHP分片与秒传实践

教育行业大文件上传:WebUploader+PHP分片与秒传实践 做教育行业文件管理系统最大的痛之一就是传大文件。课件视频动不动几百MB试卷扫描件几十MB学籍材料里还可能混进几个G的压缩包。传统表单上传碰到这些文件经常传一半超时、连接断开学生家长那边看到的永远是转圈圈这边老师急得抓耳挠腮。我这些年接过好几个教育机构的项目最终都落到同一个组合上前端用 WebUploader 做分片后端用 PHP 接收和合并再配合文件指纹实现秒传。这套方案能解决大文件上传的稳定性问题也能在重复文件上省下大量带宽和存储今天把我的完整做法和踩坑经历写出来给你直接抄作业。这套方案适合谁凡是做在线教育平台、校内文件共享、作业提交系统、教师备课资源库以及任何需要处理大文件上传的PHP项目都可以参考。就算你完全没用过 WebUploader只要懂基本的 HTML、JavaScript、PHP按下面的思路走一遍也能跑通而且我会把关键细节讲到“为什么这么做”的层面而不是只丢一段代码。1. 整体设计思路为什么选 WebUploader PHP1.1 教育行业文件管理的典型场景教育场景的文件上传和普通电商传图片完全不是一回事。最常见的几类文件课程视频一节课录制下来720p 起步就是 500MB1080p 经常 1GB 以上。课件与压缩包PPT、PDF 加上配套素材打包几十MB到几百MB很常见。试卷扫描件和答题卡扫描仪出来的图片序列单页 3MB一套试卷几十页。学生档案身份证照片、学籍表、体检报告批量导入时会出现几百个小文件集中上传。这些文件的共同点是体积大、数量多、对完整性要求高。老师传了一节课的录像如果因为网络波动失败他多半不会再传第二次而是直接打电话骂技术部门。我用 PHP 做传统$_FILES上传时遇到过几个绕不开的坎upload_max_filesize和post_max_size限制、执行时间超时、nginx 的client_max_body_size拦截。改这些配置只能治标一个 2GB 文件照样会把 PHP-FPM 进程长时间占住内存和CPU都会报警。后来我意识到不能把“上传”当成一次性动作得把它拆成小片一段一段传传完再拼起来。这就是分片上传的出发点。WebUploader 本身就是一个成熟的分片上传组件虽然官方维护停了好几年但稳定性和浏览器兼容性依然能打尤其对 IE 的支持比很多现代库都强——教育行业内部系统里总有一两台老电脑装着老浏览器这一点很关键。1.2 分片与秒传的核心逻辑分片上传的原理不复杂前端把大文件按固定大小切块例如每片 2MB然后一片一片用 HTTP 请求发到后端后端先临时保存每个分片等所有分片都到了再按顺序合并成完整文件。这里面有三个核心点分片大小要合理。大小了请求数量多网络开销大太大了达不到分片的意义单次请求失败还要重传大块数据。通常 2MB 到 8MB 比较合适。分片要记录序号。后端合并时必须知道哪片在前、哪片在后前端负责编号后端负责校验。任何一片失败都可以单独重传。用户不需要整个重来前端检测到失败片自动重试该片进度条基于已上传片数计算。秒传的逻辑建立在“内容寻址”上。两个不同用户上传同一个文件比如学校公共课件目录里那份《安全教育.pptx》每个班老师都要传实际上文件内容完全相同只是文件名和属主不同。如果系统能够识别出这份文件已经存在就不需要真的接收数据只需在数据库里创建一条关联记录让新用户直接引用原文件整个过程 0 字节上传体感就是“秒传”。判断文件是否相同不能只看文件名和大小因为同名同大小但内容不同的文件太多了。正确做法是对文件内容生成一个高强度的哈希值最常用的是 MD5 或 SHA-1。前端在文件选好后先计算整个文件的 MD5传给后端查询如果库里已经有这个 MD5就直接返回“秒传成功”否则走正常分片上传。1.3 技术选型与方案对比我在不同项目里试过三种方案原生 XMLHttpRequest 手写分片、WebUploader、以及较新的 resumable.js 或 tus-js-client。它们各有优劣。方案分片能力断点续传秒传配合老浏览器兼容开发成本手写 XMLHttpRequest自己实现自己实现自己写一般高WebUploader内置内置一部分可通过回调实现好支持IE8低resumable.js / tus强强需配合服务端尚可中单独用 PHP 做服务端的话WebUploader 的配套文档和示例最多网上能搜到大量现成代码虽然官方示例写得比较粗糙但核心思路清晰。对于教育系统这种“稳定压倒一切”的场景我宁愿选成熟但生态丰富的 WebUploader而不是自己造轮子。另外 WebUploader 的 UI 可以定制进度条、拖拽上传、队列管理都内置了省掉不少前端工作量。PHP 作为后端语言虽然在长连接和异步处理上不如 Java 或 Go但胜在部署简单大多数教育机构服务器就是一台普通的 Linux 加 NginxPHP-FPM 配好就够用。分片上传本质上是把大请求拆小PHP 的短生命周期反而成了优势每一个分片请求都是独立的处理完就释放不占用常驻内存非常适合这种模式。2. 前端接入 WebUploader 的关键配置2.1 引入依赖与页面基础结构WebUploader 依赖两个核心文件webuploader.css和webuploader.js。如果你不想从官网下载可以用 npm 安装webuploader包或者在页面里直接引用 CDN。教育系统内网可能无法访问公网 CDN所以建议下载到本地静态资源目录。基础 HTML 结构大概长这样div iduploader div classqueueList div iddndArea classplaceholder div idfilePicker/div p将文件拖到这里或点击选择文件/p /div /div div idprogressList/div /div然后初始化 WebUploadervar uploader WebUploader.create({ swf: /static/js/Uploader.swf, server: /api/upload.php, pick: #filePicker, dnd: #dndArea, accept: { title: Files, extensions: ppt,pptx,pdf,doc,docx,mp4,zip,rar, mimeTypes: *.* }, chunked: true, chunkSize: 2 * 1024 * 1024, threads: 3, fileNumLimit: 10, fileSizeLimit: 10 * 1024 * 1024 * 1024, fileSingleSizeLimit: 5 * 1024 * 1024 * 1024, duplicate: true, autoStart: false });这里有几个容易忽略的细节。swf是给老 IE 用的 Flash 兼容方案现代浏览器用 HTML5 模式但很多教育机构的电脑还装有 IE所以即使你的目标用户大部分是 Chrome也建议保留 swf 文件否则某些低版本 IE 会直接白屏。chunked: true表示启用分片chunkSize是单片大小我通常设为 2MB 或 4MB。threads是并发上传的线程数建议 3 到 5太大会把服务器带宽占满影响其他用户。2.2 分片参数怎么定最优分片大小不是拍脑袋定的要结合服务器的带宽、PHP 上传限制和内网/外网环境来考虑。我给一个经验公式服务器如果是 10Mbps 上行带宽理论每秒 1.25MB单片 2MB 需要 1.6 秒传完比较稳。如果用户走的是教育网或内网带宽较大单片可以设 5MB 或 8MB减少请求数。如果用户部分在公网部分在内网取中间值 4MB。同时要注意 PHP 侧的upload_max_filesize。这个值不是指整个文件大小而是指单次请求能接收的最大大小。分片模式下单片大小必须在upload_max_filesize之内。假如你设了 2MB 分片但 PHP 的upload_max_filesize只有 1M那么每个分片请求都会失败。我一般把服务器的upload_max_filesize设为 20Mpost_max_size设为 25M给分片留足余量又不至于让 PHP 接收超大请求。实际项目中我还会在accept里限制文件类型。教育系统里常见的是 Office 文档、PDF、视频、压缩包但要注意扩展名过滤只是前端体验后端必须再次校验因为请求可以伪造。2.3 秒传检测与进度展示秒传的实现依赖 WebUploader 的事件机制。核心思路是文件被加入队列后不立刻开始上传而是先计算整个文件的 MD5把 MD5 发给后端检查。WebUploader 没有内置文件整体哈希计算需要借助md5插件或 SparkMD5。推荐使用spark-md5它支持分片读取文件不会因为文件太大而把浏览器卡死。关键代码uploader.on(fileQueued, function(file) { // 标记文件状态为“验证中” file.uploading false; var fileReader new FileReader(); var blobSlice File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; var chunkSize 2 * 1024 * 1024; var chunks Math.ceil(file.size / chunkSize); var currentChunk 0; var spark new SparkMD5.ArrayBuffer(); var fileReader new FileReader(); fileReader.onload function(e) { spark.append(e.target.result); currentChunk; if (currentChunk chunks) { loadNext(); } else { file.md5 spark.end(); // 发请求检查秒传 $.post(/api/check.php, { md5: file.md5, filename: file.name, size: file.size }, function(res) { if (res.status success) { uploader.skipFile(file); // 跳过实际上传 // 在界面上标记为秒传成功 showFileStatus(file, 秒传成功); } else { uploader.upload(file); } }, json); } }; function loadNext() { var start currentChunk * chunkSize; var end ((start chunkSize) file.size) ? file.size : start chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); });这里要注意uploader.skipFile(file)会把文件标记为“跳过”但它不会触发“上传成功”相关事件所以需要在回调里自己更新 UI。另外计算 MD5 本身也消耗时间大的视频文件可能要几十秒建议在文件加入队列后立刻显示“计算指纹中”的提示避免用户以为没反应。对于超大文件比如 5GB 视频前端计算整体 MD5 还是会卡很久。我在线上系统里做了一个取舍超过 500MB 的文件不计算完整 MD5改用“大小 前几片内容哈希 最后一片内容哈希”组合出一个弱指纹秒传命中率会降低但能保证大文件快速进入上传流程。这个看你的业务需求如果更重视用户体验可以这么做。2.4 前端常见坑第一个坑是并发线程太多导致请求超时。WebUploader 的threads如果设置成 6 或 8在弱网环境下很容易出现同一时间多个请求阻塞。我试过教育机构里那种 20 人同时上传的场景服务器带宽被占满所有请求都变慢。后来把threads改为 3整体吞吐反而更高因为每个请求能更快完成释放连接。经验是并发数应该是一个接近带宽/单片大小的值但不要超过 5。第二个坑是进度条异常。WebUploader 默认的进度是基于已上传分片数量计算的如果你用了分片但服务端返回的响应格式不是它预期的进度条可能不前进。确保后台每个分片接口返回 JSON 中包含success字段或者在uploadAccept回调里正确判断。第三个坑是文件名重复。不同老师上传一个同名文件很正常WebUploader 默认用文件名生成上传参数但你在服务端保存分片时要以文件唯一标识比如file.id chunkIndex命名不能用文件名拼接否则两个同名文件的分片会互相覆盖。这个我在第 3 章会详细讲。3. PHP 后端处理分片上传与合并3.1 接口设计初始化、分片上传、合并后端接口我分成三个check.php秒传检测、upload.php接收分片、merge.php合并分片。有些方案把秒传检测直接放在 upload 接口里但分开逻辑更清晰也方便前端按流程调用。接口约定check.php接收md5、filename、size返回是否已存在。存在则直接返回秒传链接。upload.php接收分片文件字段file和参数identifier文件唯一标识、chunkIndex、totalChunks、originalName、md5。保存分片到临时目录。merge.php接收identifier、originalName、md5、totalChunks把所有分片合并成最终文件并校验 MD5。为了让前端能正常调用我建议用统一格式的返回{ status: success, message: ..., data: {} }前端 WebUploader 在收到任意响应时如果 HTTP 状态码是 200就会认为该分片上传成功。所以后端遇到错误时不仅要返回非 200 状态码还要在前端uploadAccept里做提示。我的做法是返回 200 但 status 字段为 fail然后在uploadError里处理业务错误这样不会触发 WebUploader 认为 HTTP 层失败。3.2 临时目录与分片校验分片文件不能直接丢到正式目录需要先放到一个临时目录比如/data/edu_upload_tmp/{identifier}/。identifier用前端生成的 GUID 或 md5 加时间戳保证唯一。每个分片文件名用0.part、1.part这种序号命名方便合并时按顺序读取。PHP 接收分片的代码如下?php public function uploadChunk() { $identifier $_POST[identifier]; $chunkIndex intval($_POST[chunkIndex]); $totalChunks intval($_POST[totalChunks]); $md5 $_POST[md5] ?? ; $originalName $_POST[originalName] ?? ; if (empty($identifier) || !isset($_FILES[file])) { return json_encode([status fail, message 参数错误]); } $tmpDir /data/edu_upload_tmp/ . $identifier . /; if (!is_dir($tmpDir)) { mkdir($tmpDir, 0755, true); } $targetFile $tmpDir . $chunkIndex . .part; if (!move_uploaded_file($_FILES[file][tmp_name], $targetFile)) { return json_encode([status fail, message 分片保存失败]); } // 校验分片大小是否合法 $chunkSize filesize($targetFile); $expectSize $_POST[chunkSize] ?? 2097152; if ($chunkSize 0 || $chunkSize 20971520) { unlink($targetFile); return json_encode([status fail, message 分片大小异常]); } return json_encode([status success, message OK]); }这里有几个安全点要注意。第一move_uploaded_file只接受 PHP 上传临时文件防止你直接 POST 一个伪路径到服务器。第二必须校验分片大小因为攻击者可能构造一个超大请求把你的磁盘占满而这个请求不会经过 WebUploader。第三临时目录要设置权限禁止 PHP 执行权限防止有人上传恶意脚本。虽然分片内容只是数据但合并后的文件如果保存在可执行目录就麻烦了。3.3 文件合并与并发控制合并的逻辑是前端在最后一个分片上传成功后调用merge.php后端按分片序号依次读取.part文件并写入最终文件。如果文件很大不能用file_get_contents一次性读入要使用流式读取逐块写入防止内存被打爆。核心合并代码?php public function mergeChunks() { $identifier $_POST[identifier]; $originalName $_POST[originalName] ?? file_ . time(); $totalChunks intval($_POST[totalChunks]); $md5 $_POST[md5] ?? ; $tmpDir /data/edu_upload_tmp/ . $identifier . /; $finalDir /data/edu_files/ . date(Y/m/d) . /; if (!is_dir($finalDir)) { mkdir($finalDir, 0755, true); } // 生成最终文件名避免使用原始文件名防止路径穿越 $ext pathinfo($originalName, PATHINFO_EXTENSION); $finalName $identifier . . . ($ext ? $ext : bin); $finalPath $finalDir . $finalName; $fp fopen($finalPath, wb); if (!$fp) { return json_encode([status fail, message 无法创建最终文件]); } for ($i 0; $i $totalChunks; $i) { $partFile $tmpDir . $i . .part; if (!file_exists($partFile)) { fclose($fp); unlink($finalPath); return json_encode([status fail, message 缺少分片 . $i]); } $partFp fopen($partFile, rb); while (!feof($partFp)) { $data fread($partFp, 8192); fwrite($fp, $data); } fclose($partFp); } fclose($fp); // 校验最终文件大小 if ($_POST[fileSize] 0) { $finalSize filesize($finalPath); if ($finalSize ! $_POST[fileSize]) { unlink($finalPath); return json_encode([status fail, message 文件大小不符]); } } // 校验MD5可选但视频文件很大时建议抽样校验 if ($md5 filesize($finalPath) 200 * 1024 * 1024) { if (md5_file($finalPath) ! $md5) { unlink($finalPath); return json_encode([status fail, message MD5校验失败]); } } // 合并成功后清理临时目录 array_map(unlink, glob($tmpDir . *.part)); rmdir($tmpDir); return json_encode([status success, message 合并完成, data [url $finalName]]); }并发控制是很多人忽略的坑。同一个identifier的合并请求可能被前端重复发起比如用户网络超时后点了一次合并WebUploader 在某个事件里又触发了一次。如果没有并发控制两个进程同时读写同一个最终文件会导致文件内容错乱。我通常用文件锁解决$lockFile $tmpDir . merge.lock; $lockHandle fopen($lockFile, w); if (!flock($lockHandle, LOCK_EX)) { return json_encode([status fail, message 合并进行中]); }拿到锁之后再执行合并合并完释放锁并删除锁文件。另外前端也要在合并接口调用前加一个状态位防止同一文件多次触发。3.4 迁移到对象存储的扩展教育系统如果用云服务器迟早会把文件迁到对象存储。对象存储一般自带分片上传接口比如阿里云 OSS 的 multipart upload、腾讯云 COS 的 multipart。你的分片逻辑在迁移时可以这样调整PHP 后端不再保存分片而是作为代理前端把分片 POST 给后端后端立即转发给对象存储的分片上传接口再把对象存储返回的 ETag 记下来最后合并时按顺序提交所有分片。这个方案的好处是前端代码基本不用变WebUploader 依然以你的域名作为 server只是 PHP 侧从“保存分片”变成了“中转分片”。缺点是中转会增加一次网络传输带宽成本翻倍。更极致的方案是让浏览器直接向对象存储上传分片但需要处理签名和跨域实现复杂度高。如果只是一般教育场景先用本地磁盘保存足够等文件量大了再迁移不迟。4. 秒传背后的去重机制设计4.1 文件指纹生成MD5还是别的文件指纹是秒传的核心。MD5 虽然理论上存在碰撞风险但在实际业务中我还没遇到过故意构造 MD5 冲突的情况所以仍可使用。不过要明白MD5 只用于索引和快速查询真正决定“这个文件是否完全一样”的还可以靠文件大小加上 MD5 双重判断进一步降低碰撞可能。教育场景里同一个文件经常在不同老师之间转发文件名可能被改成“最终版”“最新版”“再改一次”但内容没有变化。所以指纹只基于二进制内容和文件名无关。后端在保存文件后计算一次 MD5 存入文件表这个字段加唯一索引。这样当另一个老师上传同一个文件时check.php就能命中。指纹计算的效率问题也要考虑。一个 1GB 视频文件计算 MD5 大概需要几秒到十几秒取决于服务器 CPU。如果每次上传都计算服务器压力大。我在方案里让前端在“秒传检测”阶段计算一次随后上传过程中后端不再计算只在合并后抽样校验。对于大的视频文件还可以只计算文件首尾各 8MB 的拼接哈希大幅减少计算时间同时保留较好的区分度。4.2 秒传处理流程一个完整的秒传交互流程如下用户选中文件前端立刻开始计算文件指纹。指纹计算完成后前端向后端check.php发送请求包含md5、fileSize、fileName。后端检查文件表是否已存在相同md5且相同大小。如果存在后端创建一条新的文件归属记录指向已存在的物理文件返回秒传成功。如果不存在前端进入正常分片上传流程。上传完成后后端合并文件并更新文件表。这里有一个重要的业务逻辑秒传不是说“不保存物理文件”而是“不再接收重复的数据”。第一次上传的文件保存在存储中后续所有相同内容的文件都在数据库里关联到那个物理文件。删除时也要做引用计数不能因为一个老师删除了自己的记录就把共享的物理文件删掉。教育系统里文件复用频率高引用计数必须做好。4.3 缓存与数据库表设计文件表设计至少包含这几个字段CREATE TABLE file_info ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, md5 varchar(32) NOT NULL DEFAULT , file_size bigint(20) NOT NULL DEFAULT 0, physical_path varchar(255) NOT NULL DEFAULT , original_name varchar(255) NOT NULL DEFAULT , extension varchar(20) NOT NULL DEFAULT , uploader_id bigint(20) NOT NULL DEFAULT 0, ref_count int(11) NOT NULL DEFAULT 1, status tinyint(4) NOT NULL DEFAULT 1, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_md5_size (md5, file_size), KEY idx_uploader (uploader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里md5和file_size建唯一索引check.php直接走这个唯一索引查询速度很快。physical_path指向服务器上的实际文件路径。ref_count是引用数每次秒传命中就在事务里ref_count1删除时ref_count-1减到 0 才允许删除物理文件。如果文件量很大建议在 Redis 里加一层缓存key file:md5:{md5}:size:{size}value 为物理文件 ID。check.php先查 Redis缓存不存在再查数据库把高概率命中的热点文件快速挡掉。需要注意check.php的秒传查询并不总能命中。如果前端计算出的 MD5 有误或者有人伪造就会出现秒传记录与实际上传文件不匹配的情况。所以在合并完成后后端可以再校验一次文件大小对于小于 200MB 的文件校验完整 MD5确保一致性。这样即便秒传判定错误也只是多传一次文件不会造成数据错乱。5. 常见问题与排查实录5.1 上传中断、分片丢失教育机构的办公网络经常不稳定尤其是老师办公室的 Wi-Fi传一半断网很常见。WebUploader 支持失败重传默认在请求失败后会重试几次但默认次数和间隔不一定合适。我用的时候会这样配置uploader.on(uploadError, function(file, reason) { if (file.isError file.state error) { // 给用户一个手动重试的按钮 } });分片丢失更多是服务端没有正确保存chunkIndex对应的内容。有一次我排查发现某个分片文件大小是 0原因是前端发起了并发请求服务端的move_uploaded_file在并发写入同一个分片文件时冲突了。WebUploader 默认不会并发上传同一个分片但如果用户手动重试或前端事件重复触发就可能出现重复写入。解决方案是写入分片前先检查该分片文件是否已存在且大小符合预期如果存在就直接返回成功不要覆盖。5.2 合并后文件损坏合并后文件打不开是高频问题。最常见的原因是分片顺序错乱。检查分片命名是否从0.part开始连续递增合并循环是否按$i0; $i$totalChunks; $i读取。另一个原因是某些分片缺失但被忽略我上面的代码里查到缺分片会直接返回失败但有同事写过一个版本是“跳过缺失分片继续合并”结果文件损坏且难以排查。所以在合并代码里任何异常都必须终止并清理已产生的文件。还有一种情况是网络问题导致分片字节数不对。比如某个分片只传了一半但服务端因为move_uploaded_file成功而返回成功。这时可以用一个简单的规则记录每个分片的实际大小合并前比对分片大小是否都相等最后一片可以小于分片大小但必须有值。如果某个分片大小为 0 或远超预期说明传输异常应该触发重传。5.3 并发上传导致死锁或错乱同一用户同时上传多个大文件WebUploader 默认是支持队列并发的但 PHP-FPM 本身可以并行处理多个请求。问题出在合并阶段两个合并请求同时处理同一个identifier就会锁冲突。另外如果多个不同文件同时写同一个临时目录目录名用identifier可以隔离但如果identifier生成规则太简单比如用了文件名加时间戳两个同名文件可能生成相同的 identifier导致分片互相覆盖。我的办法是前端在文件加入队列后用new Date().getTime() _ file.id生成 identifier后端再拼接一个随机字符串确保全局唯一。合并时的文件锁也必不可少。如果系统并发很高还可以引入消息队列把合并请求丢到队列里串行处理但这对于普通教育机构部署来说可能过度设计了。5.4 大文件内存溢出PHP 处理大文件最常见的错误是内存溢出。合并时如果用了file_get_contents($filePath)把整个分片读进内存2GB 文件直接崩溃。我在代码里始终用fopenfread流式读取每次读取 8192 字节这样内存占用始终是常数级别。还有一个容易忽略的点md5_file()函数也会读取整个文件如果文件太大它会占用较多内存并耗时很长。超过 200MB 的文件我在线上直接跳过完整 MD5 校验只比对大小同时在前端计算弱指纹时给出更多随机性保证绝大多数场景下内容一致性没问题。另外PHP 的memory_limit至少调到 256Mmax_execution_time在合并时也要适当提高建议 600 秒。但这些只是兜底真正的上限取决于磁盘 IO 和流式写入效率。6. 实际项目中的优化与经验6.1 断点续传的完整落地WebUploader 本身提供chunkRetry失败重试能力但如果页面刷新已经传好的分片会全部丢失。要做到真正的断点续传还需要在前端把已上传分片的状态记录到 localStorage刷新后重新初始化时先向后端查询该文件已经存在哪些分片跳过这些分片继续上传。后端可以提供一个status.php接口接收identifier返回已存在的分片序号列表。前端拿到这个列表后在 WebUploader 的uploadChunk回调里如果发现当前分片序号已在列表中就认为该分片已上传修改不存在的分片才真正上传。实现时可以用uploader.upload()传入需要上传的分片号数组但 WebUploader 的原生机制不太支持按需跳过中间分片常用技巧是将所有分片都加入队列但在before-send-file或before-send事件里根据后端返回的已上传列表把已上传的分片直接返回成功。这个方案比后端去重更可靠因为后端不知道“目标文件最终由哪些片组成”只有前端知道。对于教育系统里经常被老师关闭页面又重新打开的场景断点续传能显著降低抱怨。6.2 临时文件清理临时文件的清理是线上最容易忽视的问题。用户传一半取消或分片传完但合并失败都会让临时目录里残留几十个.part文件。如果不清理磁盘很快被占满。我写了一个计划任务每小时执行一次find /data/edu_upload_tmp -mindepth 1 -maxdepth 1 -type d -mmin 60 -exec rm -rf {} \;这个命令删除所有最后修改时间超过 60 分钟的临时目录。要小心不要删掉正在上传的目录所以时间窗口设得宽裕一些。另外合并成功后立即删除临时目录代码里已经有这一步但不是所有异常路径都会执行所以定时清理是必需品不能省。6.3 权限与安全教育行业文件可能涉及学生信息安全必须做实。文件上传目录不能放在 Web 根目录下应该放到/data/edu_files这样非公开路径然后通过 PHP 脚本加权限控制后再提供下载。如果必须放在 Web 根目录至少要加访问验证防止任何人直接输入 URL 下载学生档案。上传接口本身也要做身份验证。我用的是 session 加 token 双重验证check.php、upload.php、merge.php都需要登录态。尤其是合并接口缺少验证可能导致任意文件被合并到服务器。另外originalName是用户输入的合并后要重命名不能直接使用原始文件名否则可能有人构造../../evil.php这样的文件名实现路径穿越。我生成的最终文件名一律是identifier.扩展名扩展名白名单校验。文件类型校验也不能只看扩展名。视频、图片文件可以读取文件头判断真实类型。教育系统内至少要在文档上传时禁止可执行文件并在下载时设置Content-Disposition: attachment避免浏览器直接渲染 PHP 或 HTML 文件。6.4 监控与统计文件上传系统上线后我建议加一个简单的监控记录每次上传的文件大小、耗时、分片失败率、秒传命中率。这些数据能帮你发现网络问题和用户体验瓶颈。我在数据库加了一张上传日志表插入记录不阻塞主流程。上线两周后发现秒传命中率只有 12%说明大部分文件都是独立新文件于是我调整策略把秒传检测的重点转向相同课件重复上传的场景并在页面给老师提示“该文件已有其他老师上传过”鼓励他引用公共资源间接提高了命中率。监控还可以实现告警当分片失败率超过 30%或合并失败次数增多立刻告警因为这说明服务器带宽或磁盘 IO 可能出问题了。教育系统在开学和考试周会有明显的上传高峰提前做好准备能少挨很多骂。我在几个教育项目里跑这套方案整体稳定后基本不需要大改。WebUploader 虽然老但分片、队列、进度、重试这些能力都是现成的PHP 后端只要把接口分清楚把校验做扎实表现并不比 Java 或 Go 的方案差。最后再分享一个小经验前端计算 MD5 期间WebUploader 的队列里文件可能会显示“等待上传”但一动不动很多老师会以为卡住了一定在界面上给出明确提示比如“正在校验文件请稍候”。别小看这个细节它能省掉你们技术组一大半咨询量。教育系统的用户不是专业技术人员系统越稳定、提示越清晰他们就越信任你。这比单纯追求技术栈的新鲜感重要得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进