在在线教育平台的实际开发中,教案文件上传一直是个让人头疼的问题。我最近刚完成一个K12在线教育平台的升级项目,老师们上传的Word教案平均大小在50-200MB之间,传统的文件上传方式经常出现以下问题:
经过技术调研,我们最终采用了浏览器端分片直传的方案。这种方案的核心思想是:在浏览器端将大文件切割成多个小片段,然后并行上传到对象存储服务,最后在服务端合并。实测下来,200MB的Word文件上传成功率从原来的60%提升到了99.5%。
我们的Java Web插件采用了前后端分离的架构:
code复制[浏览器端]
→ 文件分片 → 签名生成 → 并行上传
→ [阿里云OSS]
→ 回调通知 → [Java后端]
→ 分片合并 → 元数据存储
前端分片库:选择了Uppy.js,因为它:
存储服务:采用阿里云OSS因为:
后端框架:基于Spring Boot实现,因为:
javascript复制// 初始化Uppy实例
const uppy = new Uppy({
restrictions: {
maxFileSize: 1024 * 1024 * 500, // 500MB限制
allowedFileTypes: ['.doc', '.docx']
},
autoProceed: true
})
// 分片配置
uppy.use(AwsS3Multipart, {
limit: 5, // 并行上传数
companionUrl: '/upload',
companionHeaders: {
'X-CSRF-TOKEN': getCSRFToken()
}
})
关键参数说明:
java复制@PostMapping("/generate-presigned-url")
public ResponseEntity<Map<String, String>> generatePresignedUrl(
@RequestParam String fileName,
@RequestParam String fileType,
@RequestParam Long fileSize) {
// 验证文件类型
if(!isValidFileType(fileType)) {
throw new InvalidFileTypeException();
}
// 生成上传ID
String uploadId = ossClient.initiateMultipartUpload(bucketName, fileName).getUploadId();
// 生成分片签名URL
Map<String, String> urls = new HashMap<>();
int partCount = (int) Math.ceil((double) fileSize / PART_SIZE);
for (int i = 1; i <= partCount; i++) {
urls.put("part_" + i,
ossClient.generatePresignedUrl(
bucketName,
fileName,
expiration,
HttpMethod.PUT,
uploadId,
i
).toString());
}
return ResponseEntity.ok(Map.of(
"uploadId", uploadId,
"urls", urls
));
}
注意:签名有效期建议设置为30分钟,既保证安全又避免过早失效
java复制@PostMapping("/complete-upload")
public ResponseEntity<String> completeUpload(
@RequestBody CompleteUploadRequest request) {
// 验证上传权限
verifyUploadPermission(request.getUserId(), request.getUploadId());
// 获取已上传分片
List<PartETag> partETags = request.getPartETags().stream()
.map(tag -> new PartETag(tag.getPartNumber(), tag.getETag()))
.collect(Collectors.toList());
// 执行合并
CompleteMultipartUploadRequest completeRequest =
new CompleteMultipartUploadRequest(
bucketName,
request.getFileName(),
request.getUploadId(),
partETags);
ossClient.completeMultipartUpload(completeRequest);
// 保存文件元数据
fileMetadataService.save(
request.getFileName(),
request.getFileSize(),
request.getUserId());
return ResponseEntity.ok("Upload completed");
}
我们实现了一个基于网络状况的动态分片算法:
java复制public long calculateDynamicPartSize(double bandwidthMbps, long fileSize) {
// 基准分片大小5MB
long baseSize = 5 * 1024 * 1024;
// 网络状况良好(>10Mbps)时增大分片
if (bandwidthMbps > 10) {
return Math.min(baseSize * 2, fileSize / 20);
}
// 网络状况差(<2Mbps)时减小分片
else if (bandwidthMbps < 2) {
return Math.max(baseSize / 2, 1 * 1024 * 1024);
}
return baseSize;
}
前端持久化方案:
json复制{
"file123": {
"uploadId": "xyz789",
"completedParts": [1,2,5],
"totalParts": 10
}
}
服务端校验机制:
在合并分片前必须执行:
java复制// 1. 文件类型校验
if(!FilenameUtils.getExtension(filename).matches("doc|docx")) {
throw new SecurityException("Invalid file type");
}
// 2. 病毒扫描
AVClient.scanFile(tempFilePath);
// 3. 内容安全检查
ContentValidator.validateWordContent(tempFilePath);
| 操作 | 学生 | 老师 | 管理员 |
|---|---|---|---|
| 上传教案 | × | ✓ | ✓ |
| 下载教案 | ✓ | ✓ | ✓ |
| 删除教案 | × | ✓ | ✓ |
| 查看上传历史 | × | ✓ | ✓ |
浏览器兼容性问题:
性能监控指标:
java复制// 记录关键指标
metrics.record("upload.start", userId);
metrics.record("upload.progress", uploadId, progress);
metrics.record("upload.complete", uploadId, duration);
异常处理清单:
实测性能数据:
上传完成后自动生成PDF预览:
java复制public void generatePreview(String wordPath, String pdfPath) {
try (InputStream docIn = Files.newInputStream(Paths.get(wordPath));
OutputStream pdfOut = Files.newOutputStream(Paths.get(pdfPath))) {
WordConverter converter = new WordConverter();
converter.convertToPdf(docIn, pdfOut);
// 缩略图生成
Thumbnailator.createThumbnail(
pdfPath,
thumbnailPath,
200, 200);
}
}
java复制@Transactional
public FileVersion saveVersion(Long fileId, MultipartFile file) {
// 获取当前版本
int currentVersion = versionRepo.findMaxVersionByFileId(fileId);
// 存储新版本
FileVersion version = new FileVersion();
version.setFileId(fileId);
version.setVersionNumber(currentVersion + 1);
version.setStoragePath(ossClient.upload(file));
return versionRepo.save(version);
}
yaml复制metrics:
upload_requests_total:
type: counter
help: "Total upload requests"
upload_duration_seconds:
type: histogram
buckets: [0.1, 0.5, 1, 5, 10]
upload_bytes_total:
type: counter
help: "Total uploaded bytes"
使用ELK收集分析以下日志:
存储分层策略:
流量节省技巧:
在实际项目中,这套方案已经稳定运行了8个月,累计处理了超过15万份教案上传,平均上传速度提升3倍,服务器负载降低40%。最让我意外的是老师们自发在内部培训中推荐这个上传功能,这可能是对技术方案最好的肯定。