开源代码示例:SpringMVC如何基于HTTP实现Word文档的目录结构分片上传?
·
毕业设计大作战:文件管理系统开发指南
前言
各位学弟学妹们好,我是你们即将毕业的学长,现在正在为毕业设计和找工作焦头烂额。最近我接到了一个"看似简单"的任务:开发一个支持10G大文件上传的文件管理系统。本以为就是做个普通的上传功能,没想到这简直就是开发界的"珠穆朗玛峰"啊!
项目需求分析
老板(其实是导师)给了我一堆"简单"的要求:
- 必须用原生JS实现(Vue3都给我靠边站)
- 支持10G文件上传(我的4G内存笔记本瑟瑟发抖)
- 加密传输+存储(感觉自己在搞军工项目)
- 文件夹上传还要保留结构(树形结构警告)
- 断点续传+离线保存(浏览器关了进度不能丢)
- 兼容IE8到最新Edge(Windows7+IE9的遗老遗少们)
- 后端SpringBoot+MySQL(标准配置)
- 对象存储用阿里云OSS(私有云公有云混合云)
最关键的是:网上找的代码都是残次品,出了问题找不到人!这不就是我现在的写照吗?
技术选型
经过三天三夜的挣扎(实际是刷知乎刷到凌晨三点),我决定:
- 前端:WebUploader(百度开源)+ 原生JS(说好的Vue3呢?)
- 后端:SpringBoot(标准企业级选择)
- 存储:阿里云OSS(毕竟学校给报销云服务费用)
- 兼容性:IE8+(向Windows7+IE9用户致敬)
前端实现(原生JS版)
HTML结构
毕业设计-文件管理系统
body { font-family: Arial, sans-serif; margin: 20px; }
.uploader-container { margin: 20px 0; }
#filePicker { margin: 10px 0; }
.progress { width: 100%; background: #eee; margin: 5px 0; }
.progress-bar { height: 20px; background: #4CAF50; width: 0%; }
.file-list { margin-top: 20px; }
.file-item { padding: 5px; border-bottom: 1px solid #ddd; }
.ie-warning { color: red; font-weight: bold; }
毕业设计-大文件上传系统
注意:您正在使用旧版浏览器,部分功能可能受限,建议使用Chrome或Firefox。
选择文件/文件夹
开始上传
暂停
上传速度: 0 | 已上传: 0
JavaScript核心代码 (js/uploader.js)
// 检测IE版本
function detectIE() {
var ua = window.navigator.userAgent;
var msie = ua.indexOf('MSIE ');
if (msie > 0) {
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
}
var trident = ua.indexOf('Trident/');
if (trident > 0) {
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
return false;
}
// 显示IE警告
var ieVersion = detectIE();
if (ieVersion && ieVersion <= 9) {
document.getElementById('ieWarning').style.display = 'block';
}
// 上传器配置
var uploader = WebUploader.create({
swf: 'https://cdn.bootcdn.net/ajax/libs/webuploader/0.1.1/Uploader.swf',
server: '/api/upload',
pick: {
id: '#filePicker',
multiple: true
},
compress: false,
chunked: true,
chunkSize: 5 * 1024 * 1024, // 5MB一片
threads: 3,
formData: {
// 可以在这里添加额外参数
},
// 兼容性设置
accept: {
title: 'All',
extensions: '*',
mimeTypes: '*'
},
// 文件夹上传支持(非标准,需要额外处理)
duplicate: true
});
// 文件加密函数
function encryptFile(file, chunk, callback) {
// 使用CryptoJS进行简单加密(实际项目中应该用更安全的方案)
var reader = new FileReader();
reader.onload = function(e) {
var wordArray = CryptoJS.enc.Latin1.parse(e.target.result);
var encrypted = CryptoJS.AES.encrypt(wordArray, 'secret-key-123').toString();
callback(encrypted);
};
// 读取分片内容
var blob = chunk ? file.slice(chunk.start, chunk.end) : file;
reader.readAsBinaryString(blob);
}
// 解密函数(服务器端需要对应解密)
function decryptData(encrypted) {
var decrypted = CryptoJS.AES.decrypt(encrypted, 'secret-key-123');
return decrypted.toString(CryptoJS.enc.Latin1);
}
// 断点续传支持
function saveUploadProgress(file, progress) {
var key = 'upload_progress_' + file.name + '_' + file.size;
localStorage.setItem(key, JSON.stringify(progress));
}
function getUploadProgress(file) {
var key = 'upload_progress_' + file.name + '_' + file.size;
var progress = localStorage.getItem(key);
return progress ? JSON.parse(progress) : null;
}
// 文件添加事件
uploader.on('fileQueued', function(file) {
// 检查是否有未完成的上传
var progress = getUploadProgress(file);
if (progress) {
file.uploadedBytes = progress.uploadedBytes || 0;
file.chunks = progress.chunks || [];
}
// 显示文件列表
var fileItem = document.createElement('div');
fileItem.id = file.id;
fileItem.className = 'file-item';
fileItem.innerHTML =
'' + file.name + ' (' + (file.size / 1024 / 1024).toFixed(2) + 'MB)' +
'' +
'等待上传...';
document.getElementById('fileList').appendChild(fileItem);
});
// 上传进度事件
uploader.on('uploadProgress', function(file, percentage) {
var fileItem = document.getElementById(file.id);
var progressBar = fileItem.querySelector('.progress-bar');
progressBar.style.width = (percentage * 100) + '%';
// 保存进度
saveUploadProgress(file, {
uploadedBytes: file.size * percentage,
chunks: file.chunks || []
});
});
// 上传成功事件
uploader.on('uploadSuccess', function(file, response) {
var fileItem = document.getElementById(file.id);
fileItem.querySelector('.status').innerHTML = '上传成功!';
fileItem.querySelector('.progress-bar').style.backgroundColor = '#4CAF50';
});
// 上传错误事件
uploader.on('uploadError', function(file, reason) {
var fileItem = document.getElementById(file.id);
fileItem.querySelector('.status').innerHTML = '上传失败: ' + reason;
fileItem.querySelector('.progress-bar').style.backgroundColor = '#f44336';
});
// 开始上传按钮
document.getElementById('uploadBtn').addEventListener('click', function() {
uploader.upload();
document.getElementById('pauseBtn').style.display = 'inline-block';
});
// 暂停按钮
document.getElementById('pauseBtn').addEventListener('click', function() {
uploader.stop(true);
this.style.display = 'none';
});
// 文件夹上传处理(非标准方法)
document.getElementById('filePicker').addEventListener('change', function(e) {
var files = e.target.files;
if (files.length > 0) {
// 模拟文件夹结构(实际浏览器限制,无法直接获取文件夹结构)
// 这里需要用户手动选择文件夹中的所有文件
for (var i = 0; i < files.length; i++) {
// 可以在文件名中编码路径信息,如 "folder/subfolder/file.txt"
// 服务器端需要解析这种结构
uploader.addFiles(files[i]);
}
}
});
// 分片上传前处理
uploader.on('uploadBeforeSend', function(block, data) {
// 加密分片数据
var deferred = WebUploader.Deferred();
// 读取文件分片
var file = block.file;
var chunk = file.source.slice(block.start, block.end);
encryptFile(file, {start: block.start, end: block.end}, function(encrypted) {
// 添加额外参数
data.chunk = block.chunk;
data.chunks = block.chunks;
data.md5 = block.file.md5 || ''; // 需要在文件添加时计算MD5
data.relativePath = block.file.relativePath || ''; // 文件夹路径
data.encryptedData = encrypted;
deferred.resolve();
});
return deferred.promise();
});
// 初始化时恢复未完成的上传
function resumeIncompleteUploads() {
// 实际项目中需要更复杂的逻辑来恢复上传
// 这里只是简单示例
var files = uploader.getFiles();
files.forEach(function(file) {
var progress = getUploadProgress(file);
if (progress && progress.uploadedBytes > 0 && progress.uploadedBytes < file.size) {
file.uploadedBytes = progress.uploadedBytes;
file.chunks = progress.chunks;
}
});
}
// 页面加载时恢复上传
window.onload = function() {
resumeIncompleteUploads();
};
后端实现(SpringBoot版)
pom.xml 依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.0
com.example
file-management
0.0.1-SNAPSHOT
file-management
毕业设计-文件管理系统
1.8
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.2
com.aliyun.oss
aliyun-sdk-oss
3.15.1
org.projectlombok
lombok
true
commons-io
commons-io
2.11.0
org.bouncycastle
bcprov-jdk15on
1.70
org.springframework.boot
spring-boot-maven-plugin
application.properties 配置
# Server
server.port=8080
# MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/file_management?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.filemanagement.entity
# Aliyun OSS
aliyun.oss.endpoint=your-oss-endpoint
aliyun.oss.accessKeyId=your-access-key-id
aliyun.oss.accessKeySecret=your-access-key-secret
aliyun.oss.bucketName=your-bucket-name
# File Storage
file.storage.path=/data/file_storage/
核心控制器 (UploadController.java)
package com.example.filemanagement.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectResult;
import com.example.filemanagement.entity.FileChunk;
import com.example.filemanagement.entity.FileInfo;
import com.example.filemanagement.service.FileService;
import com.example.filemanagement.util.CryptoUtil;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api")
public class UploadController {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${file.storage.path}")
private String localStoragePath;
@Autowired
private FileService fileService;
/**
* 检查文件分片状态(用于断点续传)
*/
@GetMapping("/chunk/check")
public Map checkChunk(
@RequestParam String fileMd5,
@RequestParam Integer chunk,
@RequestParam Integer chunks) {
Map result = new HashMap<>();
// 检查分片是否已上传
boolean exists = fileService.checkChunk(fileMd5, chunk);
result.put("uploaded", exists);
if (exists) {
result.put("message", "分片已上传,跳过");
} else {
result.put("message", "需要上传分片 " + chunk);
}
return result;
}
/**
* 上传文件分片
*/
@PostMapping("/chunk/upload")
public Map uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam String fileMd5,
@RequestParam Integer chunk,
@RequestParam Integer chunks,
@RequestParam(required = false) String relativePath,
@RequestParam String encryptedData) throws IOException {
Map result = new HashMap<>();
try {
// 解密数据(与前端加密对应)
byte[] decryptedData = CryptoUtil.decrypt(encryptedData);
// 保存分片到临时目录
String chunkPath = localStoragePath + "temp/" + fileMd5;
String chunkFileName = chunkPath + "/" + chunk;
// 确保目录存在
java.io.File chunkDir = new java.io.File(chunkPath);
if (!chunkDir.exists()) {
chunkDir.mkdirs();
}
// 保存分片
IOUtils.write(decryptedData, new java.io.FileOutputStream(chunkFileName));
// 记录分片信息到数据库
FileChunk fileChunk = new FileChunk();
fileChunk.setFileMd5(fileMd5);
fileChunk.setChunkNumber(chunk);
fileChunk.setTotalChunks(chunks);
fileChunk.setPath(chunkFileName);
fileService.saveChunk(fileChunk);
// 如果是最后一个分片,合并文件
if (chunk == chunks - 1) {
String mergedFilePath = mergeChunks(fileMd5, chunks, relativePath);
// 上传到OSS
String ossPath = uploadToOSS(mergedFilePath, relativePath);
// 保存文件信息到数据库
FileInfo fileInfo = new FileInfo();
fileInfo.setName(file.getOriginalFilename());
fileInfo.setPath(ossPath);
fileInfo.setSize(file.getSize());
fileInfo.setType(file.getContentType());
fileInfo.setMd5(fileMd5);
fileInfo.setRelativePath(relativePath);
fileService.saveFileInfo(fileInfo);
// 删除临时分片
deleteChunkDirectory(chunkPath);
result.put("merged", true);
result.put("path", ossPath);
}
result.put("success", true);
result.put("message", "分片上传成功");
} catch (Exception e) {
result.put("success", false);
result.put("message", "分片上传失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 合并分片
*/
private String mergeChunks(String fileMd5, int chunks, String relativePath) throws IOException {
String tempDir = localStoragePath + "temp/" + fileMd5;
String mergedFilePath;
if (relativePath != null && !relativePath.isEmpty()) {
// 处理文件夹结构
String dirPath = localStoragePath + "files/" + relativePath;
new java.io.File(dirPath).mkdirs();
mergedFilePath = dirPath + "/" + UUID.randomUUID() + ".tmp";
} else {
mergedFilePath = localStoragePath + "files/" + UUID.randomUUID() + ".tmp";
}
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(mergedFilePath, true)) {
for (int i = 0; i < chunks; i++) {
String chunkPath = tempDir + "/" + i;
java.io.File chunkFile = new java.io.File(chunkPath);
if (chunkFile.exists()) {
byte[] bytes = org.apache.commons.io.FileUtils.readFileToByteArray(chunkFile);
fos.write(bytes);
}
}
}
return mergedFilePath;
}
/**
* 上传到OSS
*/
private String uploadToOSS(String filePath, String relativePath) throws IOException {
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 生成OSS路径
String objectName;
if (relativePath != null && !relativePath.isEmpty()) {
objectName = "uploads/" + relativePath + "/" + UUID.randomUUID() + "_" + new java.io.File(filePath).getName();
} else {
objectName = "uploads/" + UUID.randomUUID() + "_" + new java.io.File(filePath).getName();
}
// 上传文件
java.io.File file = new java.io.File(filePath);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.length());
PutObjectResult putResult = ossClient.putObject(bucketName, objectName, new java.io.FileInputStream(file), metadata);
return objectName;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 删除分片目录
*/
private void deleteChunkDirectory(String chunkPath) {
try {
org.apache.commons.io.FileUtils.deleteDirectory(new java.io.File(chunkPath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
加密工具类 (CryptoUtil.java)
package com.example.filemanagement.util;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
public class CryptoUtil {
static {
// 添加BouncyCastle提供者
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String KEY = "secret-key-123456"; // 实际项目中应该从配置读取
private static final String IV = "1234567890123456"; // 初始化向量
/**
* 解密数据(与前端加密对应)
*/
public static byte[] decrypt(String encryptedData) throws Exception {
// 这里简化处理,实际应该使用更安全的解密方式
// 前端使用CryptoJS.AES.encrypt,这里使用对应的解密方式
// 实际项目中应该使用正确的解密方式,这里只是示例
// 通常应该使用Base64解码,然后解密
// 由于前端使用了toString(),这里需要特殊处理
// 实际项目中应该避免这种不规范的加密传输方式
// 这里只是一个示例,实际项目中应该使用标准的加密传输方式
return encryptedData.getBytes();
}
/**
* 更好的解密方法(如果前端使用标准AES加密)
*/
public static byte[] decryptProperly(String encryptedBase64) throws Exception {
// 初始化密钥和IV
SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
// 初始化Cipher
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
// Base64解码
byte[] encryptedBytes = org.bouncycastle.util.encoders.Base64.decode(encryptedBase64);
// 解密
return cipher.doFinal(encryptedBytes);
}
}
数据库设计
文件信息表 (file_info)
CREATE TABLE `file_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '文件名',
`path` varchar(512) NOT NULL COMMENT '存储路径',
`size` bigint NOT NULL COMMENT '文件大小(字节)',
`type` varchar(100) DEFAULT NULL COMMENT '文件类型',
`md5` varchar(64) DEFAULT NULL COMMENT '文件MD5',
`relative_path` varchar(512) DEFAULT NULL COMMENT '相对路径(用于文件夹结构)',
`upload_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
`uploader` varchar(100) DEFAULT NULL COMMENT '上传者',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-删除,1-正常)',
PRIMARY KEY (`id`),
KEY `idx_md5` (`md5`),
KEY `idx_path` (`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件信息表';
文件分片表 (file_chunk)
CREATE TABLE `file_chunk` (
`id` bigint NOT NULL AUTO_INCREMENT,
`file_md5` varchar(64) NOT NULL COMMENT '文件MD5',
`chunk_number` int NOT NULL COMMENT '分片编号(从0开始)',
`total_chunks` int NOT NULL COMMENT '总分片数',
`path` varchar(512) NOT NULL COMMENT '分片存储路径',
`upload_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_file_chunk` (`file_md5`,`chunk_number`),
KEY `idx_file_md5` (`file_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件分片表';
兼容性处理
IE8/9 特殊处理
由于IE8/9对HTML5支持有限,我们需要:
- 使用Flash版本的WebUploader(已配置在HTML中)
- 避免使用现代JavaScript特性
- 提供友好的降级提示
// 在uploader.js中添加IE特殊处理
if (ieVersion && ieVersion <= 9) {
// 修改分片大小为2MB(IE内存限制)
uploader.option('chunkSize', 2 * 1024 * 1024);
// 禁用多线程(IE不稳定)
uploader.option('threads', 1);
// 添加IE进度条轮询(IE不支持Progress事件)
var ieProgressInterval = setInterval(function() {
var stats = uploader.getStats();
var progress = stats.successNum / (stats.successNum + stats.uploadFailNum + stats.queueNum);
document.getElementById('progressBar').style.width = (progress * 100) + '%';
}, 1000);
// 清除轮询
uploader.on('uploadFinished', function() {
clearInterval(ieProgressInterval);
});
}
部署说明
-
前端部署:
- 将HTML和JS文件放在SpringBoot的
src/main/resources/static目录 - 或者使用Vue CLI构建后部署到Nginx
- 将HTML和JS文件放在SpringBoot的
-
后端部署:
- 打包为JAR:
mvn clean package - 运行:
java -jar file-management-0.0.1-SNAPSHOT.jar
- 打包为JAR:
-
数据库初始化:
- 执行SQL脚本创建表
- 配置
application.properties中的数据库连接
-
OSS配置:
- 在阿里云控制台创建Bucket
- 获取AccessKey并配置到
application.properties
测试建议
-
小文件测试(<100MB):
- 验证基本上传功能
- 检查文件是否正确存储在OSS
-
大文件测试(>1GB):
- 测试断点续传
- 测试网络中断后恢复
-
文件夹测试:
- 上传包含子文件夹的结构
- 验证服务器端是否保留结构
-
兼容性测试:
- 在Chrome/Firefox/IE9中测试
- 在Windows7+IE9的老机器上测试
常见问题解决
-
上传失败:
- 检查后端日志
- 验证OSS配置是否正确
- 检查本地存储路径权限
-
断点续传不工作:
- 确保localStorage可用
- 检查文件MD5计算是否一致
-
IE兼容性问题:
- 确保引入了Flash fallback
- 避免使用ES6+语法
总结
这个项目简直是我毕业设计的"噩梦",但也是最好的学习机会。通过这个项目,我学会了:
- 如何处理大文件上传
- 前端加密的基本方法
- 断点续传的实现原理
- 兼容各种浏览器的痛苦
- 实际项目与学校项目的巨大差异
最重要的是,我明白了为什么网上那些"开源项目"都不敢留联系方式——因为这代码质量,出了问题真的会被骂死啊!
欢迎各位学弟学妹加入我们的QQ群:374992201,一起来完善这个项目。新人有红包,推荐有提成,说不定还能找到工作呢!
最后呼吁:如果有师兄师姐在IT公司工作,求内推啊!小弟这里感激不尽,毕业后请你吃饭!
SQL示例
创建数据库

配置数据库连接

自动下载maven依赖

启动项目

启动成功

访问及测试
默认页面接口定义

在浏览器中访问

数据表中的数据

示例下载
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)