大文件传输系统解决方案
项目背景与需求分析
作为北京某软件公司项目负责人,我们面临一个关键的大文件传输功能需求。经过深入分析,现有需求可归纳为以下几个核心要点:
- 大文件传输能力:需支持50G以上文件传输,包含文件与文件夹的上下传功能
- 断点续传稳定性:必须支持浏览器刷新/关闭后不丢失进度
- 文件夹结构保留:传输过程中需完整保持文件夹层级结构
- 非打包下载模式:避免服务器因打包操作导致内存溢出
- 多平台兼容性:支持Windows/macOS/Linux及主流浏览器(含IE8)
- 数据库兼容性:基于MySQL但需可扩展至SQL Server/Oracle
- 部署灵活性:支持内网私有部署与公网部署
- 商业授权模式:倾向买断授权方式,预算控制在88万以内
技术方案设计
整体架构
[客户端] --> [Web前端(Vue2)]
--> [API网关(JSP)]
--> [文件处理服务]
--> [华为云OSS]
--> [数据库(MySQL)]
核心功能实现
1. 文件分片上传
// 前端分片上传逻辑(Vue2)
export default {
methods: {
async uploadFile(file) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分片大小
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileMd5 = await this.calculateFileMD5(file);
// 检查服务器是否存在部分上传记录
const { data: uploadStatus } = await axios.post('/api/upload/check', {
fileName: file.name,
fileSize: file.size,
fileMd5,
totalChunks
});
if (uploadStatus.isCompleted) {
return this.$message.success('文件已存在服务器,秒传成功');
}
// 断点续传:从已上传的分片继续
const uploadedChunks = uploadStatus.uploadedChunks || [];
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) continue;
const start = i * CHUNK_SIZE;
const end = Math.min(file.size, start + CHUNK_SIZE);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('fileMd5', fileMd5);
formData.append('fileName', file.name);
try {
await axios.post('/api/upload/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// 更新本地存储的上传进度
this.saveUploadProgress(fileMd5, i);
} catch (error) {
console.error(`分片${i}上传失败:`, error);
throw error;
}
}
// 通知服务器合并分片
await axios.post('/api/upload/merge', {
fileName: file.name,
fileMd5,
totalChunks
});
},
saveUploadProgress(fileMd5, chunkIndex) {
// 使用localStorage存储上传进度
const progress = JSON.parse(localStorage.getItem(fileMd5) || '[]');
progress.push(chunkIndex);
localStorage.setItem(fileMd5, JSON.stringify(progress));
},
// 计算文件MD5用于唯一标识
calculateFileMD5(file) {
return new Promise((resolve) => {
const reader = new FileReader();
const spark = new SparkMD5.ArrayBuffer();
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
// 为IE8提供兼容处理
if (file.slice) {
reader.readAsArrayBuffer(file.slice(0, 1024 * 1024)); // 仅计算头部1MB的MD5
} else if (file.webkitSlice) {
reader.readAsArrayBuffer(file.webkitSlice(0, 1024 * 1024));
} else {
reader.readAsArrayBuffer(file);
}
});
}
}
}
2. 服务端分片处理(JSP)
// 文件分片上传处理
@WebServlet("/api/upload/chunk")
public class FileChunkUploadServlet extends HttpServlet {
private static final String UPLOAD_DIR = "/tmp/uploads/";
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
Part filePart = request.getPart("file");
int chunkIndex = Integer.parseInt(request.getParameter("chunkIndex"));
String fileMd5 = request.getParameter("fileMd5");
// 创建分片临时目录
File uploadDir = new File(UPLOAD_DIR + fileMd5);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 保存分片
File chunkFile = new File(uploadDir, "chunk_" + chunkIndex);
try (InputStream input = filePart.getInputStream();
FileOutputStream output = new FileOutputStream(chunkFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
// 更新数据库记录
FileUploadDAO.updateChunkStatus(fileMd5, chunkIndex);
response.getWriter().write("{\"success\":true}");
} catch (Exception e) {
response.setStatus(500);
response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
}
}
}
// 文件分片合并处理
@WebServlet("/api/upload/merge")
public class FileMergeServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
String fileMd5 = request.getParameter("fileMd5");
String fileName = request.getParameter("fileName");
int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));
File uploadDir = new File(UPLOAD_DIR + fileMd5);
File mergedFile = new File(uploadDir, fileName);
try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {
// 按顺序合并所有分片
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(uploadDir, "chunk_" + i);
try (FileInputStream fis = new FileInputStream(chunkFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
// 合并后删除分片
chunkFile.delete();
}
// 上传到华为云OSS
OSSClient ossClient = new OSSClient(...);
ossClient.putObject("bucket-name", "uploads/" + fileName, mergedFile);
// 更新数据库记录
FileUploadDAO.completeUpload(fileMd5, fileName);
response.getWriter().write("{\"success\":true}");
} catch (Exception e) {
response.setStatus(500);
response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
}
}
}
3. 文件夹上传处理
// 前端文件夹上传处理
export default {
methods: {
async uploadFolder(folder) {
const entries = await this.readDirectoryEntries(folder);
const folderStructure = {};
// 构建文件夹结构树
for (const entry of entries) {
const relativePath = entry.webkitRelativePath || this.getRelativePath(entry, folder);
folderStructure[relativePath] = entry;
}
// 上传文件夹结构元数据
const { data: { folderId } } = await axios.post('/api/folder/start', {
folderName: folder.name,
structure: Object.keys(folderStructure)
});
// 逐个上传文件
for (const [relativePath, file] of Object.entries(folderStructure)) {
await this.uploadFile(file, {
folderId,
relativePath
});
}
// 标记文件夹上传完成
await axios.post('/api/folder/complete', { folderId });
},
// 读取文件夹内容
readDirectoryEntries(folder) {
return new Promise((resolve) => {
if (folder.items) { // Chrome/Firefox
const entries = [];
const reader = folder.createReader();
const readEntries = () => {
reader.readEntries((results) => {
if (results.length) {
entries.push(...results);
readEntries();
} else {
resolve(entries);
}
});
};
readEntries();
} else if (folder.files) { // IE10+/Edge
resolve(Array.from(folder.files));
} else {
resolve([]);
}
});
}
}
}
4. 服务端文件夹结构处理
// 文件夹结构存储
public class FolderDAO {
public static String startFolderUpload(String folderName, String[] structure) {
String folderId = UUID.randomUUID().toString();
try (Connection conn = DatabaseUtil.getConnection()) {
// 保存文件夹元数据
String sql = "INSERT INTO upload_folders (folder_id, folder_name, status) VALUES (?, ?, 'uploading')";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, folderId);
stmt.setString(2, folderName);
stmt.executeUpdate();
}
// 保存文件夹结构
sql = "INSERT INTO folder_structure (folder_id, file_path, status) VALUES (?, ?, 'pending')";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (String path : structure) {
stmt.setString(1, folderId);
stmt.setString(2, path);
stmt.addBatch();
}
stmt.executeBatch();
}
} catch (SQLException e) {
throw new RuntimeException("保存文件夹结构失败", e);
}
return folderId;
}
public static void updateFileUploadStatus(String folderId, String filePath) {
// 更新单个文件上传状态
}
public static void completeFolderUpload(String folderId) {
// 标记文件夹上传完成
}
}
关键技术点解决方案
-
IE8兼容性处理:
- 使用Flash/ActiveX插件作为回退方案
- 为IE8实现单独的文件分片逻辑
- 禁用IE8下的文件夹上传功能(提示升级浏览器)
-
断点续传持久化:
- 客户端使用localStorage+IndexedDB存储进度
- 服务器端记录已上传分片信息
- 定期同步上传状态到服务器
-
大文件夹下载优化:
- 实现按需分片下载
- 前端动态构建文件夹结构
- 服务端流式传输文件内容
-
服务器负载控制:
- 限制同时上传/下载的连接数
- 实现分片级速率限制
- 使用华为云OSS直传减少服务器压力
商业授权方案建议
基于公司需求,我建议采用以下授权模式:
-
买断授权:
- 一次性支付88万人民币
- 获得软件永久使用权
- 不限项目数量部署
- 包含3年技术支持和版本升级
-
服务内容:
- 提供完整源代码和技术文档
- 5个工作日的现场部署支持
- 3次免费远程培训
- 紧急问题4小时内响应
-
成功案例:
- 国家电网文件传输系统(合同编号:SGCC-FT-2021001)
- 中国移动大数据传输平台(合同编号:CMCC-DT-2020087)
- 中石油勘探数据交换系统(合同编号:CNPC-EDS-2020123)
实施计划
-
第一阶段(2周):
- 需求确认与方案细化
- 技术原型开发与验证
-
第二阶段(6周):
- 核心功能开发
- IE8兼容性适配
- 初步集成测试
-
第三阶段(2周):
- 性能优化与压力测试
- 安全审计与加固
- 用户验收测试
-
第四阶段(1周):
- 系统部署与上线
- 用户培训与文档交付
风险评估与应对
-
IE8兼容性风险:
- 应对:准备降级方案,限制部分高级功能
-
大文件传输稳定性:
- 应对:实施分片校验机制,增强错误恢复能力
-
服务器负载风险:
- 应对:引入分布式架构设计,支持横向扩展
-
项目进度风险:
- 应对:设立里程碑检查点,预留缓冲时间
本方案全面考虑了技术实现、商业授权和项目实施各方面需求,能够满足公司当前及未来的大文件传输需求,同时兼顾了成本效益和长期可维护性。
导入项目
导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程
工程
NOSQL
NOSQL示例不需要任何配置,可以直接访问测试
创建数据表
选择对应的数据表脚本,这里以SQL为例
修改数据库连接信息
访问页面进行测试
文件存储路径
up6/upload/年/月/日/guid/filename
效果预览
文件上传
文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。