关注

医疗系统中,PHP如何安全实现大文件上传及断点续传?

开发者日记:2023年X月X日 星期X 武汉 阴

项目背景
今日正式启动客户的大文件传输系统项目,需求明确:支持20G文件/文件夹上传下载、跨平台(Windows/macOS/Linux)、全浏览器兼容(含IE8)、断点续传、保留文件夹层级结构。后端使用PHP + MySQL,存储用阿里云OSS,前端为Vue3 CLI + WebUploader/H5。客户强调高频文件夹上传场景,需极致优化用户体验。免费开源代码和7*24支持的压力依旧,但技术栈熟悉度较高,信心渐增。


技术选型与调整

  1. 前端框架

    • Vue3 CLI:组件化开发,配合@babel/polyfill兼容IE8。
    • WebUploader:核心上传组件,深度定制文件夹解析逻辑。
    • H5 File API:现代浏览器备用方案,IE8回退到Flash上传。
  2. 后端架构

    • PHP 8.1:处理分片上传、MySQL元数据存储、OSS直传。
    • MySQL 8.0:存储文件路径、分片状态、用户ID(InnoDB引擎保证事务)。
  3. 核心难点

    • 高频文件夹上传:优化递归解析性能,避免前端卡顿。
    • 断点续传稳定性:MySQL事务保证分片记录一致性。
    • IE8兼容性:Flash上传需处理跨域问题(crossdomain.xml)。

前端代码实现(Vue3 + WebUploader)

1. 文件夹上传组件
// src/components/FolderUploader.vue



import { ref, onMounted } from 'vue';
import WebUploader from 'webuploader';
import 'webuploader/dist/webuploader.css';

export default {
  setup() {
    const uploader = ref(null);
    const folderTree = ref([]);

    onMounted(() => {
      // 动态加载Flash(IE8兼容)
      if (!WebUploader.Uploader.support('HTML5')) {
        WebUploader.Uploader.register({
          name: 'flash',
          fn: () => '/assets/Uploader.swf'
        });
      }

      uploader.value = new WebUploader.Uploader({
        swf: '/assets/Uploader.swf',
        server: '/api/upload.php',
        chunked: true,
        chunkSize: 4 * 1024 * 1024, // 4MB分片
        formData: {
          fileId: '',
          relativePath: '' // 关键:记录文件相对路径
        }
      });
    });

    const triggerFolderInput = () => {
      document.getElementById('folderInput').click();
    };

    // 递归解析文件夹(兼容现代浏览器)
    const handleFolderSelect = (e) => {
      const items = e.target.files;
      if (items) {
        const tree = [];
        for (let i = 0; i < items.length; i++) {
          const file = items[i];
          const path = file.webkitRelativePath || file.name; // 相对路径
          const segments = path.split('/');
          
          // 构建树形结构
          let currentLevel = tree;
          segments.slice(0, -1).forEach(segment => {
            let dir = currentLevel.find(item => item.name === segment);
            if (!dir) {
              dir = { name: segment, type: 'directory', children: [] };
              currentLevel.push(dir);
            }
            currentLevel = dir.children;
          });
          
          // 添加文件节点
          currentLevel.push({
            name: segments.pop(),
            type: 'file',
            file: file,
            relativePath: path
          });
        }
        folderTree.value = tree;
        uploadFolder(tree);
      }
    };

    // 上传文件夹内容
    const uploadFolder = (tree) => {
      tree.forEach(node => {
        if (node.type === 'directory') {
          uploadFolder(node.children); // 递归上传子目录
        } else {
          const formData = uploader.value.option('formData');
          formData.relativePath = node.relativePath;
          uploader.value.addFile(node.file, node.relativePath);
        }
      });
      uploader.value.upload(); // 触发上传
    };

    return { triggerFolderInput, folderTree };
  }
};

2. IE8兼容性处理



后端代码实现(PHP + MySQL)

1. 分片上传接口
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $stmt = $pdo->prepare("
        INSERT INTO upload_progress (file_id, chunk_index, total_chunks, uploaded_at)
        VALUES (?, ?, ?, NOW())
        ON DUPLICATE KEY UPDATE updated_at = NOW()
    ");
    $stmt->execute([$fileId, $chunkIndex, $totalChunks]);

    // 如果是最后一块,合并并上传OSS
    if ($chunkIndex === $totalChunks - 1) {
        $finalPath = "{$tempDir}/final_file";
        $fp = fopen($finalPath, 'wb');
        for ($i = 0; $i < $totalChunks; $i++) {
            $chunk = file_get_contents("{$tempDir}/chunk_{$i}");
            fwrite($fp, $chunk);
        }
        fclose($fp);

        // 上传OSS(需引入阿里云OSS SDK)
        require_once 'oss-sdk/autoload.php';
        $ossClient = new OSS\OssClient($OSS_KEY, $OSS_SECRET, $OSS_ENDPOINT);
        $ossClient->putObject($OSS_BUCKET, "uploads/{$fileId}", $finalPath);

        // 清理临时文件
        array_map('unlink', glob("{$tempDir}/*"));
        rmdir($tempDir);

        // 保存文件元数据到MySQL
        $metaStmt = $pdo->prepare("
            INSERT INTO file_metadata (file_id, relative_path, size, uploaded_at)
            VALUES (?, ?, ?, NOW())
        ");
        $metaStmt->execute([$fileId, $relativePath, filesize($finalPath)]);
    }

    echo json_encode(['success' => true, 'fileId' => $fileId]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['error' => $e->getMessage()]);
}
?>
2. MySQL表结构
-- 文件分片进度表
CREATE TABLE `upload_progress` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `file_id` varchar(64) NOT NULL COMMENT '文件唯一ID',
  `chunk_index` int(11) NOT NULL COMMENT '分片索引',
  `total_chunks` int(11) NOT NULL COMMENT '总分片数',
  `uploaded_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_file_chunk` (`file_id`, `chunk_index`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 文件元数据表
CREATE TABLE `file_metadata` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `file_id` varchar(64) NOT NULL COMMENT '文件唯一ID',
  `relative_path` varchar(512) NOT NULL COMMENT '文件相对路径',
  `size` bigint(20) NOT NULL COMMENT '文件大小(字节)',
  `uploaded_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_file_path` (`file_id`, `relative_path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

断点续传实现

1. 前端恢复逻辑
// 检查未完成上传
const checkResume = async (fileId) => {
  const res = await fetch(`/api/progress.php?fileId=${fileId}`);
  const data = await res.json();
  
  if (data.completedChunks < data.totalChunks) {
    uploader.value.option('formData', { 
      fileId, 
      chunk: data.completedChunks 
    });
    uploader.value.upload();
  }
};

// 本地存储fileId(即使浏览器关闭)
window.addEventListener('beforeunload', () => {
  if (uploader.value.getFiles().length > 0) {
    localStorage.setItem('lastUploadId', uploader.value.option('formData').fileId);
  }
});
2. 后端进度查询
 'fileId is required']);
    exit;
}

try {
    $pdo = new PDO("mysql:host={$DB_HOST};dbname={$DB_NAME}", $DB_USER, $DB_PASS);
    $stmt = $pdo->prepare("
        SELECT COUNT(*) AS completed, MAX(total_chunks) AS total 
        FROM upload_progress 
        WHERE file_id = ?
    ");
    $stmt->execute([$fileId]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);

    echo json_encode([
        'completedChunks' => (int)$result['completed'],
        'totalChunks' => (int)$result['total']
    ]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['error' => $e->getMessage()]);
}
?>

今日总结

  • 进展:完成文件夹层级解析和PHP分片上传逻辑,IE8兼容性方案验证通过。
  • 问题
    1. WebUploader在IE8下对大文件夹性能较差,需优化DOM操作。
    2. MySQL事务需加强,避免分片记录残留。
  • 明日计划
    1. 实现OSS分片上传(避免本地合并临时文件)。
    2. 编写完整的开发文档和API接口说明。

求助:若有熟悉PHP文件处理或MySQL优化的高手,欢迎加入QQ群374992201指导!代码将完全开源回馈社区。


(注:实际项目需补充安全校验、OSS直传和性能监控代码。)

安装环境

PHP:7.2.14
Alt

调整块大小

Alt

NOSQL

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

SQL

创建数据库

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

配置数据库连接

Alt

安装依赖

Alt

访问页面进行测试

Alt

数据表中的数据

Alt

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

批量下载

支持文件批量下载
批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
下载续传

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
文件夹下载

免费下载示例

点击下载完整示例

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_52050572/article/details/151252137

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--