尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

HTML+PHP实现超大视频分片秒传与断点续传:保险理赔勘查实战

发布时间:2026/9/24 19:28:38

资讯中心
01
ARTICLE

HTML+PHP实现超大视频分片秒传与断点续传:保险理赔勘查实战

HTML+PHP实现超大视频分片秒传与断点续传:保险理赔勘查实战
理赔勘查视频的上传我猜做过保险行业系统的朋友都有过这种体验勘查员在外面拍了一段十几分钟的事故现场视频文件动辄几百MB甚至上GB回到车里用4G网络传回公司结果传到80%断了又得从头再来。定损等着看视频领导催进度勘查员急得骂人技术这边只能干瞪眼。这个场景催生了一个很典型的Web开发需求在浏览器端实现超大视频文件的上传要快、要稳、还要能在网络抖动时续传。而保险行业内部系统往往并不是什么高精尖的架构很多还是经典的HTMLPHP组合这就带来一个问题这套技术栈能不能搞定超大文件分片上传答案是肯定的而且实施下来体验相当不错。这篇文章就围绕“保险行业如何用HTMLPHP实现理赔勘查视频的超大文件分片秒传”这个主题把整体设计思路、前端切片、后端合并、断点续传、秒传判断这些环节逐个拆开讲包含可以直接参考的代码思路和踩坑记录希望能给正在做类似系统的同学一些实在的帮助。1. 场景分析与方案选型为什么理赔勘查视频必须分片上传1.1 理赔勘查视频的真实痛点保险理赔的勘查环节尤其是车险、农险、财产险现场情况复杂勘查员需要用手机或执法记录仪拍摄视频。这类视频的共同点就是单文件体积大、时长相对长而且拍摄环境往往是事故现场、田间地头、地下车库这些网络条件不稳定的地方。传统的表单文件上传就是把整个文件塞进HTTP请求体里一次性提交这种方案在几MB的图片场景下没什么问题。但视频文件一旦超过几百MB问题就集中爆发了最典型的三个表现是上传超时。PHP默认的max_execution_time是30秒就算你调到300秒一个500MB的文件在普通上行带宽下也未必传得完更不用说移动网络的上行带宽本来就被运营商限得比较厉害。请求体过大被服务端拒绝。post_max_size和upload_max_filesize如果没调大文件直接就被PHP拒收了前端拿到的就是500错误或者空白响应。断线后一切归零。移动网络在移动中切换基站是常态一个TCP连接说断就断断了之后整包重传之前传了半小时的数据全白费。我当时去一个分支机构的理赔部门调研勘查员跟我吐槽说最怕的就是拍了重要现场视频传不回去只能把手机拿回公司连Wi-Fi再试而Wi-Fi传输大文件同样会出问题。这个痛点其实本质上不是带宽问题而是传输可靠性问题分片上传正好切中要害。1.2 分片上传、秒传、断点续传的技术拆解“分片秒传”这个词听起来高大上实际拆开就是三个能力组合在一起分片上传是基础。前端用Blob.prototype.slice方法把大文件切成若干个小块比如每片5MB然后逐个上传。这样做的好处很多单个请求体变小服务端处理压力分散某个分片失败只需要重传这个分片不用整包重来多个分片还可以并发上传充分利用带宽。秒传则是用户体验层面的优化。核心原理是文件内容本身是确定的同一段视频不管谁传只要内容一样文件哈希比如MD5、SHA-1就一样。前端先算出整个文件的哈希值发给后端查一下“这个文件是不是已经存在了”如果已存在就直接返回“上传成功”一秒都不用等。表面上是“秒传”实际上后端根本没有接收任何文件数据只是返回了一个已存在的记录。断点续传是分片上传的自然延伸。因为文件被切成了多个独立分片后端可以记录每个分片的上传状态。前端再次发起上传时先从后端拿一份“已上传分片清单”只上传缺失的部分就能做到断点续传。这三个能力叠加用户感知就是选择文件后很快提示上传成功或者中途断了再点一次继续上传。这恰好覆盖了理赔勘查视频传输的全部核心诉求。1.3 技术栈选型HTMLPHP到底够不够用可能有人会有疑问现在前端框架满天飞后端又有Go、Java为什么要选HTMLPHP这里要结合保险行业的实际情况看。保险公司的核心业务系统往往建设年头比较久很多内部系统还是PHP开发的或者用了很多年PHP维护得很稳定。让理赔系统单独引入一套Java微服务或者Go服务从运维、部署、人员技能储备来看都不现实。反而是PHP这套运维熟、开发熟、部署简单往现有系统里加几个接口非常自然。前端用原生HTMLJavaScript也是出于同样的考虑。理赔勘查员用的手机和平板型号五花八门内部系统面向的浏览器环境没法统一要求原生HTML5的文件API在所有现代浏览器上都支持不需要引入重量级框架也方便在现有页面里直接嵌入。而且HTML5标准里提供的File、Blob、XMLHttpRequest这些接口完全能支撑分片上传不需要任何第三方库。当然我承认这种做法有一定的工程牺牲比如并发切片的状态管理、上传进度展示这些都得自己写不像有些现成组件拿来即用。但在保险行业这种重稳定、轻创新的系统环境里用最朴素的工具解决最实际的问题反而是最优解。看到这里你可能已经明白方案选型从来不是选最新最炫的而是选你的团队能稳定维护的、你的服务器能从容运行的、你的用户操作起来不会出错的。HTMLPHP的组合在分片上传这个场景下完全合格。2. 核心实现原理分片、秒传、断点续传底层逻辑2.1 分片是如何切的Blob.slice与File对象前端拿到用户选择的文件其实就是一个File对象。File继承自Blob而Blob上有一个slice方法可以从一个大文件中切出指定字节范围的一部分返回一个新的Blob对象。举个例子一个100MB的视频文件如果定义每个分片5MB那么一共要切20片。第1片的字节范围是0到5MB左闭右开[0, 5MB)第2片是[5MB, 10MB)依次类推。前端代码大致是这样const file fileInput.files[0]; const chunkSize 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / chunkSize); for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); // 这个blob就是第i个分片可以放进FormData里上传 }这里有个容易忽略的细节file.slice切出来的是原文件的“引用式切片”并没有真正复制底层数据也不会重新编码视频所以切分过程非常快基本不消耗内存。正因为这个特性才能在浏览器端轻松处理超大文件否则光切分这个动作就可能把手机内存撑爆。另外一个关键点由于切片只是原文件字节流的视图只要原文件没有被修改同一文件的切片结果就是确定的。这个特性为后面的秒传和断点续传提供了底层的确定性保障。2.2 秒传的钥匙文件哈希与索引表秒传的实现依赖文件哈希这里需要明确一点秒传核心并不是“传得快”而是“不用传”。后端需要一种方法来识别“这个文件之前已经传过了”哈希就是这种指纹。原理是内容相同的文件其哈希值必然相同。视频文件哪怕只修改了一个字节哈希值也会完全不同。所以前端可以先计算整个文件的哈希值上传之前先发一个“探测请求”到后端后端拿着这个哈希查一下自己的记录表如果发现同样哈希的文件已经存在就直接返回“该文件已上传成功”前端连分片都不用发页面上看起来就是秒传。理想情况下应该用MD5、SHA-1这类算法给整个文件计算一个唯一的哈希。但这里有个很现实的问题超大文件的完整哈希计算在浏览器端并不快。一个1GB的视频文件用JavaScript计算MD5在普通手机上可能要几十秒甚至几分钟这个等待本身就是一种糟糕的体验也违背了“秒传”的初衷。针对这个问题业界有几种不同的应对策略。轻量方案是只取文件的首部、中部、尾部各一段字节计算抽样哈希牺牲一定的唯一性换取速度重型方案是用Web Worker后台线程配合增量哈希算法边读文件边计算不阻塞UI体验好但代码复杂度高还有一种变通方案是不用哈希秒传而是用“文件名文件大小最后修改时间”作为文件标识速度极快但可靠性差一点。我个人的建议是对于理赔勘查视频这个场景可以采用“抽样哈希为主、文件大小辅助验证”的折中方案。不需要做到绝对唯一因为相同大小相同抽样哈希的冲突概率极低而换来的是在手机上也是瞬间出结果。后面我会给出具体实现。2.3 断点续传的数据库设计与状态机断点续传需要后端记录“这个文件传了哪些分片、还缺哪些分片”这个记录表是整个断点续传功能的基石。我在设计这个表的时候没有把分片状态塞进文件表里而是单独建了一张分片表结构大致如下字段名类型说明idbigint 主键自增分片记录唯一标识file_idvarchar(64)业务侧的文件标识我用的是前端生成的uploadIdchunk_indexint分片序号从0开始chunk_hashvarchar(64)分片内容哈希用于校验完整性chunk_sizebigint分片字节数statustinyint0-等待 1-已上传 2-已合并created_atdatetime创建时间updated_atdatetime更新时间而文件主表则负责记录整个上传任务的全局状态字段比这个复杂一些核心是这几个字段名类型说明idbigint 主键自增文件记录upload_idvarchar(64)前端生成的唯一上传会话IDfile_hashvarchar(64)文件抽样哈希秒传判断用file_namevarchar(255)文件名file_sizebigint文件总字节数total_chunksint总分片数uploaded_chunksint已上传分片数statustinyint1-上传中 2-上传完成 3-已合并 4-失败created_atdatetime创建时间这两张表配合整个上传任务就变成了一个状态机前端每次上传分片时带上传upload_id后端实时更新uploaded_chunks计数前端在分片全部上传完后调用“合并接口”后端检查到所有分片已就位执行合并并把状态改成“已合并”如果中间断了前端重新进来时调用“查询状态接口”后端把status没标记为“已上传”的分片序号列表返回给前端前端只补传这些分片即可。这个状态机设计是断点续传的核心uploaded_chunks计数只是表面信息真正决定续传范围的是分片表里那些status0或者记录不存在的分片序号。3. 前端实现HTMLJavaScript完成上传页面四件套3.1 页面基础结构与文件选择前端的页面结构不需要多花哨一个文件选择控件、一个上传按钮、一个进度条、一个状态提示区足够了。保险行业的勘查员年龄跨度大操作界面尽量简单直观不要搞一堆花里胡哨的交互。下面是基础HTML结构我实际项目里就是从这个结构开始改的!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title理赔勘查视频上传/title style .upload-panel { max-width: 640px; margin: 40px auto; padding: 24px; border: 1px solid #ddd; border-radius: 8px; } .progress-wrap { height: 20px; background: #f0f0f0; border-radius: 10px; margin: 16px 0; overflow: hidden; } .progress-bar { height: 100%; width: 0; background: #2e7d32; transition: width 0.3s; } .status-text { font-size: 14px; color: #666; } .btn { padding: 8px 16px; } /style /head body div classupload-panel h3理赔勘查视频上传/h3 input typefile idvideoFile acceptvideo/* button iduploadBtn classbtn开始上传/button div classprogress-wrap div classprogress-bar idprogressBar/div /div div classstatus-text idstatusText等待选择文件/div /div script srcupload.js/script /body /html这里要注意几个细节acceptvideo/*只是浏览器层面的文件类型过滤提示并不能阻止用户选择其他类型文件后端必须再次校验文件类型这个安全习惯一定要养成id命名规范要清晰方便后面JavaScript操作DOMCSS里给进度条加了过渡动画让进度变化看起来平滑一些减少用户焦虑感。3.2 切片逻辑与并发控制文件选择后先做基础校验然后初始化上传任务。切片的代码本身不复杂复杂的是怎么管理多个分片的并发上传。如果一次把所有分片都发出去几十个请求同时打到服务器带宽和服务器压力都吃不住如果一个个串行上传大文件的耗时又太长。所以需要一个并发控制的机制。简单有效的做法是维护一个并发任务池固定并发数。我这边实测的经验是网络环境推荐并发数单分片大小移动4G/5G弱网2-32MB-4MB普通Wi-Fi3-55MB办公内网/高带宽5-85MB-10MB不建议并发数设置过高因为移动网络下并发请求过多反而会互相抢带宽导致每个请求都变慢整体吞吐率反而下降。而且服务端PHP默认的并发处理能力也有限Nginx的worker_connections、PHP-FPM的pm.max_children都有上限分片太碎太多会把后端连接池打满。我用的并发控制思路是一个简单的“调度器”const file document.getElementById(videoFile).files[0]; const chunkSize 5 * 1024 * 1024; const totalChunks Math.ceil(file.size / chunkSize); const uploadId generateUploadId(); // 可以用文件名时间戳随机数生成 async function uploadWithConcurrency(concurrency 3) { let current 0; const tasks []; for (let i 0; i totalChunks; i) { tasks.push(i); } async function worker() { while (tasks.length 0) { const chunkIndex tasks.shift(); await uploadChunk(chunkIndex); } } const workers []; for (let i 0; i concurrency; i) { workers.push(worker()); } await Promise.all(workers); }这段代码的核心就是维护一个待上传分片队列启动N个worker并发消费队列。每个worker上传完一个分片后再取下一个直到队列清空。这种方式不同于“一次把所有分片发出去”它能精确控制同时在线的请求数服务器压力是可控的。实际上传的时候还需要处理异常重试。我给uploadChunk加了一个简单的重试机制async function uploadChunk(chunkIndex, retries 3) { const start chunkIndex * chunkSize; const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); const formData new FormData(); formData.append(file, blob, chunk-${chunkIndex}); formData.append(uploadId, uploadId); formData.append(chunkIndex, chunkIndex); formData.append(totalChunks, totalChunks); formData.append(fileName, file.name); formData.append(fileSize, file.size); for (let attempt 1; attempt retries; attempt) { try { const resp await fetch(upload.php, { method: POST, body: formData }); if (resp.ok) { return; } } catch (e) { // 网络异常记录日志 } // 指数退避等待后重试 await new Promise(resolve setTimeout(resolve, 1000 * attempt)); } throw new Error(分片 ${chunkIndex} 上传失败); }这里面的指数退避等待很重要。如果网络已经出了问题立即重试大概率还是失败等1秒、2秒、4秒逐渐递增给网络恢复留出时间。而且重试次数上限3次就够了再多的重试会占用用户时间不如尽早报错让用户手动续传。3.3 哈希计算的性能优化增量摘要与Web Worker前面提到秒传需要计算文件哈希但对于超大视频文件一次性读完整文件计算哈希在移动端是不现实的。这里有两种提速思路可以并行使用。第一种是Web Worker后台计算。把文件Blob传递给Worker线程Worker里使用FileReader分块读取文件内容每读一块就累加计算哈希这样页面UI不会卡顿用户可以继续操作或观看进度提示。第二种是抽样哈希。不必读完整文件只读文件开头64KB、中间64KB、结尾64KB合成约192KB的样本数据对这个样本计算MD5。这个哈希虽然不是全文件哈希但它结合了文件不同位置的字节信息冲突概率极低。对于理赔勘查视频来说两个不同视频在这三个位置都恰好完全相同几乎不可能。Web Worker的代码大致是这样的// worker.js self.onmessage async function(e) { const { file, sampleSize } e.data; const blobSlices []; // 开头64KB blobSlices.push(file.slice(0, sampleSize)); // 中间64KB如果文件足够大 if (file.size sampleSize * 3) { const mid Math.floor(file.size / 2); blobSlices.push(file.slice(mid, mid sampleSize)); } // 末尾64KB blobSlices.push(file.slice(file.size - sampleSize, file.size)); // 合并成一个Blob后读取 const combined new Blob(blobSlices); const arrayBuffer await combined.arrayBuffer(); const hash await computeMd5(arrayBuffer); // 使用第三方库或Web Crypto self.postMessage({ hash }); };主线程里只需要两行代码就能调用const worker new Worker(worker.js); worker.onmessage (e) { fileHash e.data.hash; // 接下来用fileHash去探测是否秒传 }; worker.postMessage({ file, sampleSize: 64 * 1024 });这种“Worker抽样”的组合方案在1GB的视频上实测大概1-2秒就能算出哈希用户体感接近瞬间完成秒传的体验就建立在这个基础上。3.4 进度展示与状态管理进度条不能光显示一个百分比对于分片上传来说要区分“当前正在传哪一片”和“整体完成了多少”否则用户体验很迷茫。我给进度条做了两层信息function updateProgress(completedChunks, totalChunks) { const percent Math.round(completedChunks / totalChunks * 100); progressBar.style.width percent %; statusText.textContent 正在上传 ${completedChunks}/${totalChunks} 个分片已完成 ${percent}%; }状态管理方面需要在内存中维护一个Map来记录每个分片是否上传成功。前面提到的断点续传前端拿到后端返回的“已上传分片列表”其实就可以直接初始化这个Map然后只把没传过的分片塞进任务队列。我踩过的坑是为了简单最初把分片状态放在一个普通数组里用push记录已传分片后来发现某些分片重试成功后被记录了两次导致计数不准确。这里用Set或者Map按分片索引存储更稳妥const uploadedChunks new Set(); // 存储已成功上传的分片索引 // 续传初始化 serverUploadedChunks.forEach(index uploadedChunks.add(index));4. 后端实现PHP接收分片、合并与秒传判断4.1 接口设计与参数约定前端和后端之间必须有清晰稳定的接口约定。我设计的三个核心接口分别是探测秒传、上传分片、合并文件。另外还有查询状态接口用于断点续传。接口设计的原则是简洁清晰每个接口只做一件事。前端通过uploadId来标识一个上传任务后端靠这个ID来归档分片、记录状态。参数中必须包含的信息探测接口check.phpfile_hash文件的抽样哈希file_size文件总字节数file_name文件名返回JSON格式如果哈希存在且文件大小一致则返回{code: 0, exists: true, file_url: ...}前端收到这个就直接提示上传完成否则返回{code: 0, exists: false}。上传分片接口upload.phpupload_id上传任务IDchunk_index分片序号total_chunks总分片数file_name文件名file_size文件大小chunk_hash分片的MD5file分片文件本身这里有个隐含的技巧total_chunks和file_size不必每个分片都传但传了之后后端可以做校验防止前端逻辑出错导致分片错乱。合并接口merge.phpupload_id上传任务IDfile_name文件名返回合并后的文件访问路径和最终的URL。查询接口status.phpupload_id上传任务ID返回该任务已上传的分片序号列表用于断点续传。4.2 分片接收与临时目录管理PHP接收分片和接收普通上传文件的代码差别不大但是有两点必须注意一是分片文件的落盘位置二是不完整分片的清理策略。接收分片的PHP代码?php // upload.php $uploadId $_POST[upload_id] ?? ; $chunkIndex (int)($_POST[chunk_index] ?? 0); $totalChunks (int)($_POST[total_chunks] ?? 0); $fileName $_POST[file_name] ?? ; $fileSize (int)($_POST[file_size] ?? 0); $chunkHash $_POST[chunk_hash] ?? ; if (empty($uploadId) || empty($fileName)) { jsonResponse([code 400, msg 参数错误]); return; } $uploadDir /data/uploads/tmp/ . $uploadId . /; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $tmpFile $_FILES[file][tmp_name]; $targetFile $uploadDir . sprintf(%05d.chunk, $chunkIndex); // 校验分片大小是否符合预期可选并校验哈希 $md5 md5_file($tmpFile); if ($chunkHash ! strtolower($md5) ! strtolower($chunkHash)) { jsonResponse([code 400, msg 分片校验失败]); return; } if (!move_uploaded_file($tmpFile, $targetFile)) { jsonResponse([code 500, msg 分片保存失败]); return; } // 更新分片表状态 markChunkUploaded($uploadId, $chunkIndex, $md5); // 返回当前已上传分片数 jsonResponse([code 0, msg ok, uploadedChunks countUploadedChunks($uploadId)]);临时目录的管理非常关键。/data/uploads/tmp/{uploadId}/这个目录下每个分片以%05d.chunk命名比如00000.chunk、00001.chunk。文件名里序号补零的好处是后续合并时按文件名排序就能得到正确的分片顺序。还有一个很容易被忽略的问题PHP进程对上传文件的行为。move_uploaded_file是PHP处理上传文件的标准方式它不仅能移动文件还会检查文件是否是有效的上传文件安全性比直接用rename好很多。千万不要图省事直接用file_put_contents和$_FILES[file][tmp_name]配合那样可能会引入本地文件包含之类的安全隐患。同一uploadId多次上传相同分片时直接覆盖旧文件即可。这种幂等性设计支持了“重试”和“断点续传”前端可能因为超时重发同一个分片后端不会因为它已存在就报错而是正常覆盖并返回成功。4.3 文件合并的两种方式与选择分片全部到位后进入合并阶段。PHP合并分片文件有两种实现方式各有适用场景。第一种是流式合并逐片读取内容写入目标文件?php // merge.php 核心逻辑 $uploadId $_POST[upload_id] ?? ; $fileName $_POST[file_name] ?? ; $tmpDir /data/uploads/tmp/ . $uploadId . /; $finalDir /data/uploads/final/ . date(Y/m/d) . /; if (!is_dir($finalDir)) { mkdir($finalDir, 0755, true); } $finalPath $finalDir . uniqid() . _ . $fileName; $finalHandle fopen($finalPath, wb); $chunkCount getUploadedChunkCount($uploadId); $totalChunks (int)getTotalChunksByUploadId($uploadId); if ($chunkCount $totalChunks) { jsonResponse([code 400, msg 还有分片未上传]); return; } for ($i 0; $i $totalChunks; $i) { $chunkFile $tmpDir . sprintf(%05d.chunk, $i); if (!file_exists($chunkFile)) { fclose($finalHandle); unlink($finalPath); jsonResponse([code 500, msg 分片 $i 缺失]); return; } $chunkHandle fopen($chunkFile, rb); stream_copy_to_stream($chunkHandle, $finalHandle); fclose($chunkHandle); unlink($chunkFile); // 合并完删除分片 } fclose($finalHandle); // 清理临时目录 rmdir($tmpDir); jsonResponse([code 0, msg ok, fileUrl /uploads/final/...]);stream_copy_to_stream是PHP里流式复制的高效函数内部有缓冲比循环freadfwrite效率高一个档次。这里我同时做了两件事合并文件和删除已合并的分片这样磁盘空间不会因为临时文件堆积而爆掉。第二种是Shell命令合并用系统级命令快速拼接$cmd cat . $tmpDir . *.chunk . escapeshellarg($finalPath); exec($cmd, $output, $returnCode);这个方案更快因为cat命令是C语言实现的文件拼接速度快。但要注意几个坑*.chunk的通配符排序依赖文件名所以前面说的补零命名就很重要否则5.chunk会排在20.chunk后面exec函数在很多PHP环境里被禁用了得确认服务器是允许的而且用系统命令处理用户输入的文件名必须做好转义防止命令注入。实际项目中我建议默认用”流式合并“只有确认了服务器环境不会禁用exec、并且文件量特别大时才考虑用Shell方案。4.4 秒传与去重逻辑的实现秒传的判断很简单就是查表?php // check.php $fileHash $_POST[file_hash] ?? ; $fileSize (int)($_POST[file_size] ?? 0); if (empty($fileHash)) { jsonResponse([code 400, msg 缺少文件哈希]); return; } // 查文件表 $stmt $pdo-prepare(SELECT id, file_url FROM upload_files WHERE file_hash ? AND file_size ? LIMIT 1); $stmt-execute([$fileHash, $fileSize]); $row $stmt-fetch(); if ($row) { // 如果业务需要可以为当前用户生成一条关联记录 jsonResponse([code 0, exists true, fileUrl $row[file_url]]); } else { jsonResponse([code 0, exists false]); }这里有个值得讨论的细节秒传之后虽然物理文件没有重复存储但业务上需要给“当前这次上传”生成一条独立的记录。因为同一个视频可能在多个理赔案件中被引用如果只复用物理文件而逻辑记录也共用一条后续理赔单关联视频时就会出问题。正确的做法是物理文件唯一只存储一份逻辑记录仍然为每次上传生成一条。文件表里保存file_hash和file_url业务表里关联file_url或者文件记录的ID。这样既节省了存储空间又不影响理赔案件的独立性。我是强烈建议做物理文件去重的。保险行业的视频资料动辄几百GB重复存储就是巨大的成本浪费。哈希秒传不仅提升了用户体验还顺带解决了存储重复的问题属于一石二鸟。5. 实操中的高并发与稳定性处理5.1 Nginx/Apache/PHP相关参数配置分片上传虽然每个分片不大但并发请求数量会比较多。如果不调整服务器默认配置很可能出现“分片传到一半服务器罢工”的情况。PHP侧的三个关键参数必须调整; php.ini memory_limit 256M upload_max_filesize 20M post_max_size 25Mupload_max_filesize要略大于单个分片的大小比如分片是5MB这个值至少设成20M留出余量post_max_size必须大于upload_max_filesize因为POST请求里除了文件外还有其他表单字段如果post_max_size等于upload_max_filesize刚好压线也容易出问题。max_execution_time建议设大一些如300秒因为大分片上传到服务器后还要写磁盘极端情况下处理时间会超过默认30秒。Nginx侧也要配合client_max_body_size 25m; client_body_timeout 60s; proxy_read_timeout 300s;client_max_body_size必须大于PHP的post_max_size否则Nginx层就会直接拒绝请求PHP代码根本执行不到。这个参数位置比较隐蔽我遇到过好几次排查半天才发现是Nginx先挡了。PHP-FPM的并发能力也要关注; php-fpm.conf pm.max_children 50 pm.start_servers 10 pm.max_requests 500max_children决定了PHP-FPM同时能处理的请求数。如果前端并发分片数是5同时有10个用户在上传就会有50个分片请求并发到达PHP-FPM再加上业务系统的其他请求max_children设小了就会排队等待表现为上传速度变慢甚至超时。5.2 并发分片的顺序错乱与切片校验分片并发上传会带来一个自然的问题分片到达后端的顺序不是按照序号来的。第5片可能比第2片先到达这在分片存储阶段没问题因为文件名里有序号各写各的互不干扰。但到了合并阶段就需要注意了合并前必须确认所有分片都存在而且要严格按序号顺序拼接。我还在分片上传时增加了分片哈希校验。前端对每个分片计算MD5放在chunk_hash字段里一起提交后端接收后用md5_file计算收到的分片文件哈希不一致直接拒绝。这是为了排查网络传输中数据损坏的问题。虽然TCP协议本身有校验但在弱网环境下TCP校验失败会导致重传一般不会把损坏数据交给应用层但我仍然建议加这道校验因为有一次线上问题就是运营商网络设备返回了错误数据别问我怎么知道的。合并完成后最好对整个文件做一次大小校验// 合并后检查大小是否等于前端声明的大小 $finalSize filesize($finalPath); if ($finalSize ! $fileSize) { // 大小不一致说明合并过程有分片缺失或文件损坏 unlink($finalPath); jsonResponse([code 500, msg 文件大小校验失败]); }这个校验成本极低但能拦截大部分合并错误。5.3 异步合并与任务队列同步合并在小文件场景下没问题但如果是几GB的视频文件PHP的同步合并可能会执行几秒甚至几十秒HTTP请求一直挂着前端体验很差也容易因为网关超时导致请求中断。更稳妥的做法是引入异步合并。后端上传分片的接口只负责接收和记录当检测到“最后一个分片已上传”时把合并任务放进一个消息队列比如Redis列表、RabbitMQ然后立即返回给前端“分片已全部上传合并中”。由后台的常驻Worker进程可以用CLI模式运行PHP脚本或者用系统定时任务轮询去执行合并操作。异步合并的PHP Worker大致逻辑?php // worker.php 命令行模式运行 while (true) { $uploadId $redis-lpop(merge_queue); if (!$uploadId) { sleep(2); continue; } mergeFile($uploadId); }前端在“合并中”状态下轮询查询状态接口等到文件状态变为“已合并”后再显示成功。这个方案比同步合并可靠得多而且把耗时的合并操作从Web请求链路里摘出去了Web服务器的进程不会被长时间占用。对于保险行业这种对稳定性要求高的场景我强烈建议用异步合并。勘查员手机断网了页面刷新了合并任务还在后台正常跑用户体验和系统稳定性都好了不少。6. 常见问题与排查技巧实录6.1 典型问题速查表把这些年做分片上传遇到的高频问题整理成了一张速查表遇到问题可以先对照排查现象可能原因排查与解决上传到一半报413错误Nginxclient_max_body_size或PHPpost_max_size过小检查两边配置Nginx限制要大于PHP限制所有分片都显示上传成功但合并后文件打不开分片顺序错乱确认分片文件名是否按序号补零合并时严格按序拼接手机网络下上传速度极慢并发数过高分片之间互相抢带宽降低并发数弱网下用2-3并发断点续传后文件上传成功但大小不对分片遗漏或重复检查续传逻辑用Set去重记录已传分片合并后校验文件大小秒传判断不命中抽样哈希冲突或哈希计算方式不一致确认前端每次计算的哈希逻辑一致大小字段也参与比对PHP报“File upload error”upload_tmp_dir不可写或磁盘空间不足检查PHP临时目录权限用df -h查看磁盘剩余空间分片上传接口偶发500PHP-FPMmax_children耗尽调大max_children同时检查是否有慢查询或死锁视频合并后中间有一段黑屏某个分片上传数据损坏启用分片MD5校验损坏则重传该分片6.2 几个让我印象深刻的线上问题第一个印象深刻的坑是“Nginx层413”当时分片大小设的10MBPHP配置也调好了但用户上传总是失败。排查半天发现Nginx配置里client_max_body_size还保持默认的1MNginx直接拒绝了大请求。这个配置层级很隐蔽改完PHP配置往往就把Nginx忘了。第二个坑是“临时目录被系统清理”部署在海外云服务器上时发现已经上传的分片偶尔会莫名其妙丢失。后来发现是系统的tmpwatch或者cron清理任务把/tmp下超过一定时间的文件自动删掉了。所以分片临时目录一定不能放在系统/tmp下要放在独立的业务目录里并设置合理的目录权限。第三个坑是“PHP-FPM连接数被打满”上线后发现上传高峰期整个业务系统的接口都变慢了包括查询保单这种轻量接口。监控发现是分片上传请求把PHP-FPM的Worker全部占满了其他请求排队等待。后来把分片上传单独部署了一套PHP-FPM池并且把上传接口的并发和业务接口隔离问题才解决。这也提醒我分片上传这种资源密集型请求最好和普通业务请求做资源隔离。第四个坑是前端“重试风暴”弱网环境下单个分片上传失败后如果前端立即无限重试会一直占用网络连接导致其他分片饿死。加上指数退避和最多3次重试限制后整体成功率反而大幅提升。6.3 一个值得借鉴的降级策略再分享一个应急处置的思路。有一次核心上传服务所在机房网络异常分片上传成功率只有50%左右大量勘查员反馈无法上传视频。因为不能强行要求用户等待我们在前端加了一个降级策略当检测到连续3个分片上传失败后自动切换到“串行小分片模式”把单分片从5MB降为1MB并发数从4降为1这样虽然慢一些但成功率很高。这个降级策略的核心逻辑是弱网环境下小分片和低并发反而更容易成功因为单个请求占用网络的时间短、占用连接少不容易触发运营商层面的限速或阻断。跑了一小时失败率从50%降到了不到5%虽然传输速度慢了但至少能把视频传回去比起传不回去强太多了。写在最后一些个人经验这套方案在理赔勘查视频上传场景里跑了一年多前后承载了几万个视频文件的上传总数据量有几十TB最大的单个视频超过3GB都能稳定完成传输。回想起来最初也走过弯路比如一开始试图用Flash上传组件解决大文件传输在现在这个时代既不安全也不兼容后来切到原生HTML5分片上传代码虽然多一些但胜在踏实可控。我给正在规划类似系统的朋友几点实在的建议第一分片大小和并发数一定要根据真实网络环境测试确定不要拍脑袋定参数。你可以在测试环境模拟弱网用不同分片大小跑一遍看哪个组合的整体成功率最高。第二服务端一定要做幂等设计。分片重传、断点续传、重复提交这些操作在真实的弱网环境里是常态不是异常接口必须能自然容忍。第三日志要打全。哪个用户、哪个uploadId、哪天传了哪个分片、花了多长时间、失败了几次这些信息在排查疑难问题时就是救命稻草。我见过太多线上问题因为日志缺失而无从查起。分片秒传不是什么高深技术但它确实是大文件传输场景里工程价值最高的方案。如果你也在保险、医疗、教育这类传统行业里做上传功能希望这篇文章能给你一些帮助少踩几个我踩过的坑。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。