Skip to content

Commit af0f58a

Browse files
zk020106Charles7c
authored andcommitted
feat(system/file): 新增多文件分片上传功能,支持本地存储和S3存储
Co-authored-by: kiki1373639299<[email protected]> # message auto-generated for no-merge-commit merge: merge upload into dev feat(system/file): 新增多文件分片上传功能,支持本地存储和S3存储 Created-by: kiki1373639299 Commit-by: kiki1373639299 Merged-by: Charles_7c Description: <!-- 非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。 --> <!-- 在 [] 中输入 x 来勾选) --> ## PR 类型 <!-- 您的 PR 引入了哪种类型的变更? --> <!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 --> - [X] 新 feature - [ ] Bug 修复 - [ ] 功能增强 - [ ] 文档变更 - [ ] 代码样式变更 - [ ] 重构 - [ ] 性能改进 - [ ] 单元测试 - [ ] CI/CD - [ ] 其他 ## PR 目的 <!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 --> ## 解决方案 <!-- 详细描述您是如何解决的问题 --> ## PR 测试 <!-- 如果可以,请为您的 PR 添加或更新单元测试。 --> <!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 --> ## Changelog | 模块 | Changelog | Related issues | |-----|-----------| -------------- | | | | | <!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 --> <!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 --> ## 其他信息 <!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 --> ## 提交前确认 - [X] PR 代码经过了完整测试,并且通过了代码规范检查 - [] 已经完整填写 Changelog,并链接到了相关 issues - [X] PR 代码将要提交到 dev 分支 See merge request: continew/continew-admin!11
1 parent 21b753e commit af0f58a

19 files changed

+1896
-2
lines changed

continew-common/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@
3131
<groupId>org.dromara.x-file-storage</groupId>
3232
<artifactId>x-file-storage-spring</artifactId>
3333
</dependency>
34-
<!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) -->
34+
<!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储)后续会移除替换1.x的版本 -->
3535
<dependency>
3636
<groupId>com.amazonaws</groupId>
3737
<artifactId>aws-java-sdk-s3</artifactId>
3838
</dependency>
3939

40+
<!-- Amazon S3 2.x version (Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) -->
41+
<dependency>
42+
<groupId>software.amazon.awssdk</groupId>
43+
<artifactId>s3</artifactId>
44+
</dependency>
45+
4046
<!-- FreeMarker(模板引擎) -->
4147
<dependency>
4248
<groupId>org.freemarker</groupId>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.admin.system.constant;
18+
19+
/**
20+
* 分片上传常量
21+
*
22+
* @author KAI
23+
* @since 2025/7/30 17:40
24+
*/
25+
public class MultipartUploadConstants {
26+
//todo 后续改为从配置文件读取
27+
/**
28+
* MD5到uploadId的映射前缀
29+
* <p>
30+
* 用于存储文件MD5到uploadId的映射关系,实现基于MD5的双列Map结构。
31+
* 键格式:multipart:md5_to_upload:{md5}
32+
* 值格式:Hash结构,包含uploadId和fileInfo
33+
* </p>
34+
*/
35+
public static final String MD5_TO_UPLOAD_ID_PREFIX = "multipart:md5_to_upload:";
36+
37+
/**
38+
* 分片上传信息前缀
39+
* <p>
40+
* 用于存储分片上传的初始化信息,包含uploadId、bucket、path等基本信息。
41+
* 键格式:multipart:upload:{uploadId}
42+
* 值格式:JSON字符串,包含MultipartInitResp的序列化数据
43+
* </p>
44+
*/
45+
public static final String MULTIPART_UPLOAD_PREFIX = "multipart:upload:";
46+
47+
/**
48+
* 分片信息前缀
49+
* <p>
50+
* 用于存储所有分片的上传信息,使用Hash结构存储。
51+
* 键格式:multipart:parts:{uploadId}
52+
* 值格式:Hash结构,field为分片编号,value为FilePartInfo的JSON序列化数据
53+
* </p>
54+
*/
55+
public static final String MULTIPART_PARTS_PREFIX = "multipart:parts:";
56+
57+
/**
58+
* 元数据前缀
59+
* <p>
60+
* 用于存储分片上传的元数据信息,如文件名、大小、类型等。
61+
* 键格式:multipart:metadata:{uploadId}
62+
* 值格式:Hash结构,field为元数据键,value为元数据值
63+
* </p>
64+
*/
65+
public static final String MULTIPART_METADATA_PREFIX = "multipart:metadata:";
66+
67+
/**
68+
* 过期时间前缀
69+
* <p>
70+
* 用于存储分片上传的过期时间,用于定期清理过期数据。
71+
* 键格式:multipart:expire:{uploadId}
72+
* 值格式:ISO格式的时间字符串
73+
* </p>
74+
*/
75+
public static final String MULTIPART_EXPIRE_PREFIX = "multipart:expire:";
76+
77+
/**
78+
* 默认过期时间(小时)
79+
* <p>
80+
* 分片上传缓存数据的默认过期时间,超过此时间的数据会被自动清理。
81+
* 设置为24小时,平衡存储空间和用户体验。
82+
* </p>
83+
*/
84+
public static final long DEFAULT_EXPIRE_HOURS = 24;
85+
86+
/**
87+
* 临时文件夹
88+
* <p>
89+
* 分片上传的临时文件夹名称
90+
* </p>
91+
*/
92+
public static final String TEMP_DIR_NAME = "temp";
93+
94+
/**
95+
* 分片大小
96+
*/
97+
public static final long MULTIPART_UPLOAD_PART_SIZE = 5 * 1024 * 1024;
98+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package top.continew.admin.system.controller;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import jakarta.validation.Valid;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.web.bind.annotation.*;
7+
import org.springframework.web.multipart.MultipartFile;
8+
import top.continew.admin.system.model.entity.FileDO;
9+
import top.continew.admin.system.model.req.MultipartUploadInitReq;
10+
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
11+
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
12+
import top.continew.admin.system.service.MultipartUploadService;
13+
14+
/**
15+
* 分片上传控制器
16+
*
17+
* @author KAI
18+
* @since 2025/7/30 16:38
19+
*/
20+
@RestController
21+
@RequestMapping("/system/multipart-upload")
22+
@RequiredArgsConstructor
23+
public class MultipartUploadController {
24+
25+
private final MultipartUploadService multipartUploadService;
26+
27+
/**
28+
* 初始化分片上传
29+
*
30+
* @param multiPartUploadInitReq 分片上传信息
31+
* @return 初始化响应
32+
*/
33+
@Operation(summary = "初始化分片上传", description = "初始化分片上传,返回uploadId等信息")
34+
@PostMapping("/init")
35+
public MultipartUploadInitResp initMultipartUpload(@RequestBody @Valid MultipartUploadInitReq multiPartUploadInitReq) {
36+
return multipartUploadService.initMultipartUpload(multiPartUploadInitReq);
37+
}
38+
39+
/**
40+
* 上传分片
41+
*
42+
* @param file 分片文件
43+
* @param uploadId 上传ID
44+
* @param partNumber 分片编号
45+
* @param path 文件路径
46+
* @return 上传结果
47+
*/
48+
@Operation(summary = "上传分片", description = "上传单个分片")
49+
@PostMapping("/part")
50+
public MultipartUploadResp uploadPart(@RequestPart("file") MultipartFile file, @RequestParam("uploadId") String uploadId,
51+
@RequestParam("partNumber") Integer partNumber, @RequestParam("path") String path) {
52+
return multipartUploadService.uploadPart(file, uploadId, partNumber, path);
53+
}
54+
55+
/**
56+
* 合并分片
57+
*
58+
* @param uploadId 上传ID
59+
*/
60+
@Operation(summary = "完成分片上传", description = "合并所有分片,完成上传")
61+
@GetMapping("/complete/{uploadId}")
62+
public FileDO completeMultipartUpload(@PathVariable String uploadId) {
63+
return multipartUploadService.completeMultipartUpload(uploadId);
64+
}
65+
66+
/**
67+
* 取消分片上传
68+
*
69+
* @param uploadId 上传ID
70+
*/
71+
@Operation(summary = "取消分片上传", description = "删除缓存信息,分片数据")
72+
@GetMapping("/cancel/{uploadId}")
73+
public void cancelMultipartUpload(@PathVariable String uploadId) {
74+
multipartUploadService.cancelMultipartUpload(uploadId);
75+
}
76+
77+
78+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package top.continew.admin.system.dao;
2+
3+
import top.continew.admin.system.model.resp.file.FilePartInfo;
4+
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
/**
10+
* 分片上传持久化接口
11+
* <p>
12+
* 纯粹的缓存操作,不包含业务逻辑:
13+
* 1. MD5到uploadId的映射管理
14+
* 2. 分片信息缓存
15+
* 3. 上传状态缓存
16+
* </p>
17+
*
18+
* @author KAI
19+
* @since 2.14.0
20+
*/
21+
public interface MultipartUploadDao {
22+
23+
/**
24+
* 根据MD5获取uploadId
25+
*
26+
* @param md5 文件MD5值
27+
* @return uploadId,如果不存在则返回null
28+
*/
29+
String getUploadIdByMd5(String md5);
30+
31+
/**
32+
* 缓存MD5到uploadId的映射
33+
*
34+
* @param md5 文件MD5值
35+
* @param uploadId 上传ID
36+
*/
37+
void setMd5Mapping(String md5, String uploadId);
38+
39+
/**
40+
* 删除MD5映射
41+
*
42+
* @param md5 文件MD5值
43+
*/
44+
void deleteMd5Mapping(String md5);
45+
46+
/**
47+
* 设置缓存分片上传信息
48+
*
49+
* @param uploadId 上传ID
50+
* @param initResp 初始化响应
51+
* @param metadata 元数据
52+
*/
53+
void setMultipartUpload(String uploadId, MultipartUploadInitResp initResp, Map<String, String> metadata);
54+
55+
/**
56+
* 获取分片上传信息
57+
*
58+
* @param uploadId 上传ID
59+
* @return 分片上传信息,如果不存在则返回null
60+
*/
61+
MultipartUploadInitResp getMultipartUpload(String uploadId);
62+
63+
/**
64+
* 删除分片上传信息
65+
*
66+
* @param uploadId 上传ID
67+
*/
68+
void deleteMultipartUpload(String uploadId);
69+
70+
void deleteMultipartUploadAll(String uploadId);
71+
72+
/**
73+
* 设置缓存分片信息
74+
*
75+
* @param uploadId 上传ID
76+
* @param filePartInfo 分片信息
77+
*/
78+
void setFilePart(String uploadId, FilePartInfo filePartInfo);
79+
80+
/**
81+
* 获取所有分片信息
82+
*
83+
* @param uploadId 上传ID
84+
* @return 分片信息列表
85+
*/
86+
List<FilePartInfo> getFileParts(String uploadId);
87+
88+
/**
89+
* 删除所有分片信息
90+
*
91+
* @param uploadId 上传ID
92+
*/
93+
void deleteFileParts(String uploadId);
94+
95+
/**
96+
* 检查分片是否存在
97+
*
98+
* @param uploadId 上传ID
99+
* @param partNumber 分片编号
100+
* @return 是否存在
101+
*/
102+
boolean existsFilePart(String uploadId, int partNumber);
103+
104+
/**
105+
* 清理过期的缓存数据
106+
*/
107+
void cleanupExpiredData();
108+
}

0 commit comments

Comments
 (0)