☰
国企OA系统大文件分块上传与断点续传实战
2026/10/15 8:58:03 网站建设 项目流程

1. 大文件分块上传的国企OA场景解析

在国企日常办公场景中,OA系统经常需要处理红头文件、工程图纸、会议纪要等大体积附件。传统单次上传方式存在三个致命缺陷:网络闪断导致重传、服务器内存溢出风险、用户无法感知传输进度。某央企实际案例显示,当上传500MB以上的CAD图纸时,传统方式失败率高达34%。

分块上传技术将大文件切割为2-8MB的标准块(国企内网通常采用4MB分块),通过以下机制确保可靠性:

  1. 前端计算文件MD5指纹作为唯一标识
  2. 服务端记录已上传分块序号及校验值
  3. 断点续传时通过指纹比对实现秒传
  4. 进度状态持久化到数据库供回溯查询

2. Java分块上传核心实现

2.1 前端预处理关键代码

// 国企OA系统常用Vue+ElementUI实现 async handleUpload(file) { const chunkSize = 4 * 1024 * 1024 // 国企标准分块4MB const chunks = Math.ceil(file.size / chunkSize) const fileMd5 = await calculateMD5(file) // SparkMD5库计算 // 秒传验证请求 const { data } = await checkExist(fileMd5) if (data.exist) { this.$message.success('秒传成功!') return } // 分块上传 for (let i = 0; i < chunks; i++) { const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize) await uploadChunk(chunk, i, fileMd5) } }

2.2 服务端Java校验逻辑

// 秒传验证接口 @PostMapping("/checkExist") public Result checkExist(@RequestParam String fileMd5) { // 查询文件记录表 FileRecord record = fileRecordMapper.selectByMd5(fileMd5); if (record != null && record.getStatus() == 2) { // 2-已完成 return Result.success().data("exist", true); } // 查询分块记录表 List<Integer> chunkList = chunkRecordMapper.selectChunks(fileMd5); return Result.success() .data("exist", false) .data("uploaded", chunkList); }

3. 断点续传与进度回溯

3.1 分块上传Java实现

@PostMapping("/uploadChunk") public Result uploadChunk( @RequestParam MultipartFile chunk, @RequestParam Integer chunkIndex, @RequestParam String fileMd5) throws IOException { // 校验分块MD5(国企安全要求) String chunkMd5 = DigestUtils.md5Hex(chunk.getInputStream()); if (!chunkMd5.equals(request.getHeader("Chunk-MD5"))) { throw new SecurityException("分块校验失败"); } // 存储分块到临时目录 String chunkPath = "/tmp/chunks/" + fileMd5 + "/" + chunkIndex; FileUtils.writeByteArrayToFile(new File(chunkPath), chunk.getBytes()); // 记录分块信息 ChunkRecord record = new ChunkRecord(); record.setFileMd5(fileMd5); record.setChunkIndex(chunkIndex); record.setChunkMd5(chunkMd5); chunkRecordMapper.insert(record); // 检查是否全部完成 int totalChunks = Integer.parseInt(request.getHeader("Total-Chunks")); if (chunkRecordMapper.countChunks(fileMd5) == totalChunks) { mergeChunks(fileMd5, totalChunks); } return Result.success(); }

3.2 进度回溯数据库设计

CREATE TABLE `t_file_record` ( `id` bigint NOT NULL COMMENT '主键', `file_md5` varchar(32) NOT NULL COMMENT '文件指纹', `file_name` varchar(255) NOT NULL COMMENT '原始文件名', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-上传中 1-合并中 2-已完成', `progress` int DEFAULT '0' COMMENT '上传进度百分比', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_md5` (`file_md5`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_chunk_record` ( `id` bigint NOT NULL COMMENT '主键', `file_md5` varchar(32) NOT NULL COMMENT '关联文件指纹', `chunk_index` int NOT NULL COMMENT '分块序号', `chunk_md5` varchar(32) NOT NULL COMMENT '分块校验值', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_md5_index` (`file_md5`,`chunk_index`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4. 分块合并与异常处理

4.1 Java分块合并实现

private void mergeChunks(String fileMd5, int totalChunks) throws IOException { // 标记文件为合并中状态 updateFileStatus(fileMd5, 1); // 创建最终文件 String destPath = "/data/files/" + fileMd5.substring(0,2) + "/" + fileMd5; try (RandomAccessFile destFile = new RandomAccessFile(destPath, "rw")) { byte[] buffer = new byte[4096]; // 按序号合并所有分块 for (int i = 0; i < totalChunks; i++) { String chunkPath = "/tmp/chunks/" + fileMd5 + "/" + i; try (FileInputStream fis = new FileInputStream(chunkPath)) { int len; while ((len = fis.read(buffer)) != -1) { destFile.write(buffer, 0, len); } } } } // 更新文件状态 updateFileStatus(fileMd5, 2); // 清理临时分块(建议异步执行) FileUtils.deleteDirectory(new File("/tmp/chunks/" + fileMd5)); }

4.2 国企场景特殊处理

  1. 审计日志记录:每个分块上传需记录操作人、时间、IP
@Aspect @Component public class UploadLogAspect { @AfterReturning("execution(* com..*.upload*(..)) && args(..,principal)") public void logUpload(Principal principal) { String username = principal.getName(); String operation = joinPoint.getSignature().getName(); auditLogService.log(username, operation); } }
  1. 传输加密要求:国企内网通常需要SSL+内容加密
// 在application.yml中配置 server: ssl: enabled: true key-store: classpath:keystore.jks key-store-password: 国企密码规范 // 文件内容加密 public String encryptChunk(byte[] data) { return Base64.getEncoder().encodeToString( RSAUtils.encrypt(data, publicKey) ); }

5. 性能优化实战技巧

5.1 内存控制方案

  1. Spring Boot配置调整
# 限制单分块内存占用 spring.servlet.multipart.max-file-size=4MB spring.servlet.multipart.max-request-size=4MB # Tomcat临时文件阈值 server.tomcat.max-swallow-size=4MB
  1. 流式处理避免OOM
@PostMapping("/streamUpload") public void streamUpload(@RequestParam String fileMd5, InputStream inputStream) { try (OutputStream out = new FileOutputStream("/tmp/" + fileMd5)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } }

5.2 国企级容灾方案

  1. 分块存储多副本策略
// 存储到多个节点(国企通常要求至少双副本) @Async public void saveWithReplica(byte[] data, String path) { // 主存储 FileUtils.writeByteArrayToFile(new File("/node1" + path), data); // 备份存储 FileUtils.writeByteArrayToFile(new File("/node2" + path), data); }
  1. 断点续传补偿机制
// 每日凌晨校验未完成文件 @Scheduled(cron = "0 0 3 * * ?") public void checkUnfinishedFiles() { List<FileRecord> records = fileRecordMapper.selectUnfinished(); records.forEach(record -> { int uploaded = chunkRecordMapper.countChunks(record.getFileMd5()); int progress = (int) (uploaded * 100.0 / record.getTotalChunks()); fileRecordMapper.updateProgress(record.getId(), progress); }); }

6. 安全合规要点

  1. 等保2.0要求实现
  • 文件完整性校验(SHA-256+MD5双校验)
  • 传输通道加密(TLS1.2+)
  • 操作日志留存6个月以上
  1. 敏感文件检测
public void checkSensitiveContent(File file) { // 检查文件头特征 byte[] header = FileUtils.readFileToByteArray(file, 0, 8); if (isOfficeFile(header)) { // 使用POI解析检查关键词 checkOfficeContent(file); } // 图片文件需检测隐写 if (isImageFile(header)) { checkImageSteganography(file); } }
  1. 访问控制策略
@PreAuthorize("hasPermission(#fileMd5, 'download')") @GetMapping("/download") public ResponseEntity<Resource> download( @RequestParam String fileMd5, @CurrentUser User user) { // 校验部门权限 if (!departmentService.checkAccess(user, fileMd5)) { throw new AccessDeniedException(); } // 返回文件流 File file = fileService.getByMd5(fileMd5); return ResponseEntity.ok() .header("Content-Disposition", "attachment") .body(new FileSystemResource(file)); }

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

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

立即咨询