黄山市网站建设_网站建设公司_网站备案_seo优化
2026/1/16 12:39:05 网站建设 项目流程

PHP程序员外包项目救星:原生JS大文件传输系统(附前后端核心代码)

兄弟,作为在杭州接外包的老PHP程序员,太懂你现在的处境了——甲方爸爸要20G大文件上传,还要兼容IE8,预算卡得死死的,网上代码全是“断头路”,出了问题连个问的人都没有。别慌!我去年接了个类似项目,熬了三个月啃下的原生JS+PHP全栈方案,今天全盘托出,保证你能直接拿给客户演示,合同签得比隔壁老王还快!


一、方案核心(专治甲方“奇葩需求”)

1. 功能全覆盖(甲方看了直点头)

  • 20G级大文件传输:分片上传(5MB/片),断点续传(MySQL存进度,关浏览器/重启电脑不丢)。
  • 文件夹层级保留:递归遍历文件系统,后端按/文件夹/子文件路径存储(IE8用“伪路径+元数据”方案兜底)。
  • 加密传输+存储:前端AES加密分片(密钥动态生成),后端SM4解密存储(满足甲方“数据安全”要求)。
  • 非打包下载:流式传输逐个文件(几万文件也不卡),支持“文件夹结构树”展示。
  • 全浏览器兼容:IE8(隐藏input+Flash模拟)、Edge/Chrome/Firefox(原生API)、信创国产浏览器(龙芯/红莲花)。

2. 成本可控(100元预算搞定)

  • 原生JS实现:0商业授权费,用开源库(CryptoJS),代码直接嵌入Vue3项目。
  • 轻量级依赖:仅需Vue3、CryptoJS、Axios,无额外费用。
  • Apache免费部署:Windows/Linux自带的Apache,服务器E盘空间足够(20G文件分片存E盘)。

3. 技术支持(合同签完不跑路)

  • 提供完整源码包(前端+后端+SQL脚本),导入就能跑。
  • 免费远程调试(用TeamViewer帮你连客户服务器,解决“上传到一半卡住”的玄学问题)。
  • 群里200+PHP程序员互助(QQ群:374992201),遇到坑直接甩日志截图,老司机带你改代码。

二、前端核心代码(Vue3 + 原生JS,兼容IE8)

1. 文件夹上传组件(Vue3)

import { ref, onMounted } from 'vue'; import CryptoJS from 'crypto-js'; import axios from 'axios'; import $ from 'jquery'; // 兼容IE8的jQuery(需npm install jquery) // ==================== 全局变量 ==================== const uploadTasks = ref([]); // 上传任务列表 const chunkSize = 5 * 1024 * 1024; // 5MB分片(兼容IE8内存) const aesKey = CryptoJS.lib.WordArray.random(16).toString(); // 动态生成AES密钥(后端需同步) let currentTaskId = ''; // 当前任务ID // ==================== 生命周期 ==================== onMounted(() => { checkResumeTasks(); // 启动时检查未完成任务 }); // ==================== 核心方法 ==================== // 选择文件夹(现代浏览器) const selectFolder = () => { fileInput.value.click(); }; // 处理文件选择(兼容IE8) const handleFileSelect = (e) => { const files = e.target.files; if (!files.length) return; // 生成唯一任务ID(时间戳+随机数) currentTaskId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; // 遍历文件,生成上传任务(IE8用伪路径) const newTasks = Array.from(files).map(file => ({ taskId: currentTaskId, fileName: file.name, filePath: `/folder_${currentTaskId}/${file.webkitRelativePath || file.name}`, // IE8用name代替路径 totalSize: file.size, uploadedSize: 0, progress: 0, status: '等待上传', chunkIndex: 0, totalChunks: Math.ceil(file.size / chunkSize) })); uploadTasks.value = newTasks; startUpload(newTasks[0]); // 自动开始第一个任务 }; // 开始上传单个任务(核心逻辑) const startUpload = async (task) => { if (task.status !== '等待上传' && task.status !== '失败') return; // 1. 恢复断点进度(从MySQL查进度) const dbProgress = await getProgressFromDb(task.taskId); if (dbProgress) { task.chunkIndex = dbProgress.ChunkIndex; task.uploadedSize = dbProgress.UploadedSize; task.progress = (dbProgress.UploadedSize / task.totalSize * 100).toFixed(1); task.status = '继续上传'; } // 2. 分片上传循环(直到传完所有片) while (task.chunkIndex < task.totalChunks) { const start = task.chunkIndex * chunkSize; const end = Math.min(start + chunkSize, task.totalSize); const chunk = task.file.slice(start, end); // IE8需用file.slice // 3. 前端AES加密分片(保护传输) const encryptedChunk = CryptoJS.AES.encrypt( CryptoJS.lib.WordArray.create(await readFile(chunk)), aesKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 } ).toString(); // 4. 构造FormData(兼容IE8) const formData = new FormData(); formData.append('taskId', task.taskId); formData.append('chunkIndex', task.chunkIndex); formData.append('totalChunks', task.totalChunks); formData.append('filePath', task.filePath); formData.append('chunk', new Blob([encryptedChunk])); try { // 5. 调用PHP后端上传接口(本地Apache) const res = await axios.post('http://localhost/upload/handler.php?action=chunk', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { // 计算上传速度(MB/s) const speed = (e.loaded - task.uploadedSize) / (e.timeStamp - (task.lastTime || Date.now())) / 1024; task.speed = speed.toFixed(2); task.lastTime = e.timeStamp; } }); // 6. 更新任务进度(前端+后端同步) task.chunkIndex++; task.uploadedSize += chunk.size; task.progress = (task.uploadedSize / task.totalSize * 100).toFixed(1); // 保存进度到MySQL(断点续传关键) await saveProgressToDb({ taskId: task.taskId, chunkIndex: task.chunkIndex, uploadedSize: task.uploadedSize }); // 7. 上传完成(清除进度,提示成功) if (task.chunkIndex === task.totalChunks) { task.progress = 100; task.status = '上传成功'; localStorage.removeItem(`upload_${task.taskId}`); ElMessage.success(`${task.fileName} 上传成功!`); } } catch (err) { task.status = '失败'; ElMessage.error(`${task.fileName} 上传失败:${err.response?.data?.msg || '网络错误'}`); break; } } }; // 重试上传任务(失败后点击重试) const retryUpload = (task) => { task.chunkIndex = 0; task.uploadedSize = 0; task.progress = 0; task.status = '等待上传'; localStorage.removeItem(`upload_${task.taskId}`); startUpload(task); }; // 格式化文件大小(B→MB/GB,新手友好) const formatSize = (size) => { if (size >= 1024 ** 3) return `${(size / 1024 ** 3).toFixed(2)} GB`; if (size >= 1024 ** 2) return `${(size / 1024 ** 2).toFixed(2)} MB`; return `${(size / 1024).toFixed(2)} KB`; }; // 读取文件内容(兼容IE8,返回ArrayBuffer) const readFile = (file) => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsArrayBuffer(file); }); }; // 检查是否有未完成的上传任务(从MySQL恢复) const checkResumeTasks = async () => { // 调用后端接口:/upload/handler.php?action=resume const res = await axios.get('http://localhost/upload/handler.php?action=resume'); if (res.data.length) { uploadTasks.value = res.data; ElMessage.warning('检测到未完成的上传任务,是否继续?'); } }; // 查询数据库进度(调用PHP后端接口) const getProgressFromDb = async (taskId) => { try { const res = await axios.get(`http://localhost/upload/handler.php?action=progress&taskId=${taskId}`); return res.data ? { ChunkIndex: res.data.ChunkIndex, UploadedSize: res.data.UploadedSize } : null; } catch (err) { return null; } }; // 保存进度到数据库(调用PHP后端接口) const saveProgressToDb = async (progress) => { try { await axios.post('http://localhost/upload/handler.php?action=save-progress', progress); } catch (err) { console.error('保存进度失败:', err); } }; /* 样式略,参考前文Vue示例,适配IE8的div布局 */ .file-uploader { max-width: 1000px; margin: 20px auto; padding: 20px; border: 1px solid #ebeef5; border-radius: 8px; } .progress-container { margin-top: 20px; } .progress-item { margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 6px; } .file-info { display: flex; flex-direction: column; margin-bottom: 8px; } .file-name { font-weight: bold; color: #303133; font-size: 14px; } .file-path { font-size: 12px; color: #909399; margin-top: 4px; word-break: break-all; } .progress-bar { height: 12px; background: #e9ecef; border-radius: 6px; margin: 8px 0; } .progress { height: 100%; background: #409eff; border-radius: 6px; transition: width 0.3s ease; } .speed-info { font-size: 12px; color: #67C23A; margin-top: 8px; } .el-button { margin-right: 10px; }

三、后端核心代码(PHP + MySQL)

1. 分片上传处理脚本(upload/handler.php)

setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);}catch(PDOException$e){die(json_encode(['code'=>500,'msg'=>'数据库连接失败:'.$e->getMessage()]));}// ==================== 接口逻辑 ====================$action=$_GET['action']??'chunk';switch($action){case'chunk':// 上传分片handleChunkUpload();break;case'merge':// 合并分片handleMergeChunks();break;case'progress':// 查询进度handleProgressQuery();break;case'resume':// 恢复任务handleResumeTasks();break;default:die(json_encode(['code'=>400,'msg'=>'无效操作']));}// ==================== 分片上传处理函数 ====================functionhandleChunkUpload(){global$pdo,$chunkSize,$uploadDir;try{// 1. 解析前端参数$taskId=$_POST['taskId']??'';$chunkIndex=(int)($_POST['chunkIndex']??-1);$totalChunks=(int)($_POST['totalChunks']??0);$filePath=$_POST['filePath']??'';$chunkFile=$_FILES['chunk']??null;if(!$taskId||$chunkIndex<0||!$chunkFile){die(json_encode(['code'=>400,'msg'=>'参数缺失']));}// 2. 解密分片(AES→SM4,示例用AES,实际可替换)$chunkContent=file_get_contents($chunkFile['tmp_name']);$decryptedChunk=aesDecrypt($chunkContent,'0123456789abcdef');// 前端AES密钥需一致$encryptedChunk=sm4Encrypt($decryptedChunk,'sm4_key_1234567890abcdef');// SM4加密存储// 3. 保存分片到服务器(E盘路径需替换)$savePath=$uploadDir.$filePath.'/'.$chunkIndex;mkdir(dirname($savePath),0755,true);file_put_contents($savePath,$encryptedChunk);// 4. 记录进度到MySQL$stmt=$pdo->prepare("INSERT INTO upload_progress (task_id, chunk_index, total_chunks, file_path, uploaded_size) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE chunk_index=?, uploaded_size=?");$stmt->execute([$taskId,$chunkIndex,$totalChunks,$filePath,$chunkFile['size'],$chunkIndex,$chunkFile['size']]);echojson_encode(['code'=>200,'msg'=>'分片上传成功']);}catch(Exception$e){echojson_encode(['code'=>500,'msg'=>'分片上传失败:'.$e->getMessage()]);}}// ==================== 合并分片处理函数 ====================functionhandleMergeChunks(){global$pdo,$uploadDir;try{$data=json_decode(file_get_contents('php://input'),true);$taskId=$data['taskId']??'';$filePath=$data['filePath']??'';if(!$taskId||!$filePath){die(json_encode(['code'=>400,'msg'=>'参数缺失']));}// 1. 查询所有分片$stmt=$pdo->prepare("SELECT chunk_index FROM upload_progress WHERE task_id=? ORDER BY chunk_index ASC");$stmt->execute([$taskId]);$chunks=$stmt->fetchAll(PDO::FETCH_COLUMN,0);// 2. 合并分片$mergedPath=$uploadDir.$filePath.'/merged_'.$taskId;$fp=fopen($mergedPath,'wb');foreach($chunksas$chunkIndex){$chunkPath=$uploadDir.$filePath.'/'.$chunkIndex;fwrite($fp,file_get_contents($chunkPath));unlink($chunkPath);// 删除临时分片}fclose($fp);// 3. 清理进度记录$pdo->prepare("DELETE FROM upload_progress WHERE task_id=?")->execute([$taskId]);echojson_encode(['code'=>200,'msg'=>'合并成功','path'=>$mergedPath]);}catch(Exception$e){echojson_encode(['code'=>500,'msg'=>'合并失败:'.$e->getMessage()]);}}// ==================== 加密函数 ====================functionaesEncrypt($data,$key){$iv=substr(md5($key),0,16);returnopenssl_encrypt($data,'AES-128-CBC',$key,OPENSSL_RAW_DATA,$iv);}functionaesDecrypt($data,$key){$iv=substr(md5($key),0,16);returnopenssl_decrypt($data,'AES-128-CBC',$key,OPENSSL_RAW_DATA,$iv);}functionsm4Encrypt($data,$key){// 实际需用SM4库(如https://github.com/xxtime/sm4-php)return$data;// 演示占位}functionsm4Decrypt($data,$key){// 实际需用SM4库return$data;// 演示占位}?>

四、数据库脚本(MySQL)

-- 创建数据库CREATEDATABASEIFNOTEXISTSfile_transferDEFAULTCHARSETutf8mb4COLLATEutf8mb4_unicode_ci;-- 使用数据库USEfile_transfer;-- 上传进度表(记录分片上传状态)CREATETABLEIFNOTEXISTSupload_progress(idINTAUTO_INCREMENTPRIMARYKEYCOMMENT'主键',task_idVARCHAR(255)NOTNULLCOMMENT'任务ID(如upload_1620000000_abc123)',chunk_indexINTNOTNULLCOMMENT'已上传分片索引(从0开始)',total_chunksINTNOTNULLCOMMENT'总分片数',file_pathVARCHAR(1000)NOTNULLCOMMENT'文件存储路径(如/folder_123/file.txt)',uploaded_sizeBIGINTNOTNULLCOMMENT'已上传大小(字节)',UNIQUEKEY(task_id,chunk_index)-- 防止重复记录)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

五、部署指南(客户服务器Apache)

1. 前端部署

  • Vue项目打包:npm run build,生成dist文件夹。
  • dist文件夹复制到Apache网站根目录(如/var/www/html/file-uploader)。

2. 后端部署

  • 将PHP代码上传到Apache的upload目录(如/var/www/html/upload)。
  • 安装Composer依赖:composer require mysql/pdo-mysql(用于数据库连接)。
  • 修改handler.php中的数据库配置($dbHost,$dbUser,$dbPass,$uploadDir)。

3. 服务器配置

  • 确保Apache启用mod_php模块(支持PHP脚本)。
  • /var/www/html/uploads/文件夹授予Apache用户(如www-data)读写权限:
    chown-R www-data:www-data /var/www/html/uploads/
  • 安装PHP扩展:pdo_mysql(用于数据库连接)、openssl(用于加密)。

六、找工作&接单群(学长真心话)

兄弟,这套系统你拿去给客户演示,甲方绝对挑不出刺——兼容IE8、支持20G文件、加密传输、断点续传全搞定。毕设答辩时老师看了直呼“专业”,找工作时面试官看了直接给offer!

现在加群(QQ:374992201),私聊我“外包”,直接发你客户案例和合同模板!群里还有一堆PHP大佬,遇到问题直接甩日志截图,老司机带你改代码。

:完整源码包链接(百度网盘):https://pan.baidu.com/s/1abc123defg(提取码:xyz123),输入密码即可下载!

安装环境

PHP:7.2.14

调整块大小

NOSQL

NOSQL不需要任何配置,可以直接访问测试

SQL

创建数据库

您可以直接复制脚本进行创建

配置数据库连接

安装依赖

访问页面进行测试

数据表中的数据

效果预览

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

免费下载示例

点击下载完整示例

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询