以下是关于若依(RuoYi)OSS上传学习记录的总结博客内容,你可以根据需要进一步修改和完善。
若依(RuoYi)OSS上传学习记录总结
在学习若依(RuoYi)框架的OSS(对象存储服务)上传功能过程中,我深入研究了其整合AWS S3协议OSS功能的实现方式、相关配置以及数据库设计等内容,收获颇丰。以下是详细的学习记录总结。
一、相关学习资源
- Spring Boot 整合 AWS S3协议 OSS功能 支持 七牛、阿里、Minio等一切支持S3协议的云厂商:这篇博客详细介绍了如何在Spring Boot项目中整合AWS S3协议的OSS功能,支持多种云厂商,包括七牛、阿里、Minio等,为实现OSS上传功能提供了很好的参考。
- 什么是 Amazon S3? - Amazon Simple Storage Service:通过阅读Amazon S3的官方文档,我对其功能、工作原理、数据一致性模型等方面有了全面的了解,这有助于更好地理解若依框架整合S3协议OSS功能的实现原理和优势。
- 一文读懂 AWS S3:这篇文章以简洁明了的方式介绍了AWS S3的基本概念,如Bucket(存储桶)和Object(对象)等,让我对S3有了更直观的认识,也为学习若依OSS上传功能打下了基础。
- 【RuoYi-Vue-Plus】学习笔记 01 - OSS模块(一)OSS加载流程:这篇学习笔记详细分析了RuoYi-Vue-Plus框架中OSS模块的加载流程,包括配置类的加载、缓存配置信息等,帮助我从代码层面深入理解了OSS功能的实现机制。
二、数据库设计
若依框架为OSS功能设计了两张关键的数据库表,分别是sys_oss_config
表和sys_oss
表,以下是表结构的详细介绍:
(一)sys_oss_config
表
CREATE TABLE `sys_oss_config` (
`oss_config_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_key` varchar(20) NOT NULL COMMENT '配置标识',
`access_key` varchar(255) NOT NULL COMMENT '访问密钥 AccessKey',
`secret_key` varchar(255) NOT NULL COMMENT '访问秘钥 SecretKey',
`bucket_name` varchar(255) NOT NULL COMMENT '存储桶名称',
`region` varchar(50) DEFAULT NULL COMMENT '存储区域(可从 endpoint 推导)',
`endpoint` varchar(255) NOT NULL COMMENT '访问地址',
`domain` varchar(255) DEFAULT NULL COMMENT '自定义域名',
`prefix` varchar(255) DEFAULT '' COMMENT '存储路径前缀',
`is_https` char(1) NOT NULL DEFAULT 'N' COMMENT '是否启用 HTTPS(Y=是,N=否)',
`access_policy` char(1) NOT NULL DEFAULT '1' COMMENT '权限策略(0=private, 1=public, 2=custom)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`oss_config_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OSS 对象存储配置表';
oss_config_id
:作为主键,唯一标识每条OSS配置记录,采用自增方式生成。config_key
:配置标识,用于区分不同的OSS配置,例如可以为不同的存储桶或不同的云厂商配置设置不同的config_key
。access_key
和secret_key
:分别对应云厂商提供的访问密钥和访问秘钥,用于身份验证和授权,确保对OSS资源的安全访问。bucket_name
:存储桶名称,是OSS中存储对象的容器,每个对象都必须存储在某个存储桶中。region
:存储区域,虽然可以通过endpoint
推导,但在配置中明确指定可以更清晰地表明存储桶所在的地理位置,有助于优化性能和满足合规性要求。endpoint
:访问地址,用于访问OSS服务的入口地址,不同的云厂商和区域可能有不同的endpoint
。domain
:自定义域名,允许用户为存储桶设置自定义的访问域名,提升访问的灵活性和用户体验。prefix
:存储路径前缀,可以为存储在存储桶中的对象设置统一的路径前缀,方便对对象进行分类和管理。is_https
:标识是否启用HTTPS,Y
表示启用,N
表示不启用,启用HTTPS可以提高数据传输的安全性。access_policy
:权限策略,0
表示私有,只有拥有者可以访问;1
表示公开,任何人都可以访问;2
表示自定义,可以根据具体需求灵活设置访问权限。create_time
和update_time
:分别记录配置的创建时间和更新时间,便于对配置的变更进行追溯。remark
:备注字段,用于存储对配置的额外说明信息。
(二)sys_oss
表
CREATE TABLE `sys_oss` (
`oss_id` bigint NOT NULL AUTO_INCREMENT COMMENT '对象存储主键',
`config_key` varchar(20) NOT NULL COMMENT '存储配置标识(关联 sys_oss_config.config_key)',
`file_name` varchar(255) NOT NULL COMMENT '存储后的文件名(唯一标识)',
`original_name` varchar(255) NOT NULL COMMENT '上传时的原始文件名',
`file_suffix` varchar(20) NOT NULL COMMENT '文件后缀名(扩展名)',
`url` varchar(500) NOT NULL COMMENT '文件访问 URL',
`size` bigint NOT NULL COMMENT '文件大小(字节)',
`content_type` varchar(100) DEFAULT NULL COMMENT 'MIME 类型(如 image/png)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` bigint DEFAULT NULL COMMENT '上传人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`status` char(1) NOT NULL DEFAULT '1' COMMENT '状态(1=正常,0=已删除)',
PRIMARY KEY (`oss_id`) USING BTREE,
INDEX `idx_config_key` (`config_key`),
INDEX `idx_file_name` (`file_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OSS 对象存储文件表';
oss_id
:主键,唯一标识每条OSS文件记录,采用自增方式生成。config_key
:存储配置标识,与sys_oss_config
表中的config_key
关联,用于确定文件存储所使用的OSS配置。file_name
:存储后的文件名,是文件在OSS中的唯一标识,用于在存储桶中区分不同的文件。original_name
:上传时的原始文件名,记录用户上传文件时的原始文件名称,便于后续查看和管理。file_suffix
:文件后缀名,即文件的扩展名,有助于识别文件的类型和格式。url
:文件访问URL,通过该URL可以访问存储在OSS中的文件,其
三.分片上传,端点续传学习
分片上传的本质:
- 分片上传将一个大文件拆分为多个较小的分片,每个分片会单独上传。
- 每个分片上传时,上传请求中并不会直接包含整个文件,而是包含文件的某一部分(即当前分片)。上传请求中的 分片编号(
partNumber
) 就是标识当前分片的标识符。
上传分片的 URL:
- 对于每个分片的上传,S3 需要一个 预签名 URL,这是一个带有权限的临时 URL,允许客户端在特定时间内上传该分片。
- 后端生成的这个 预签名 URL 是针对 分片上传请求 的,而非整个文件。每次上传分片时,客户端通过该 URL 将数据发送到 S3,而上传的数据是一个分片。
上传分片的过程:
- 客户端会通过分片上传的 API 获取每个分片的 上传 URL,这个 URL 是预签名的,可以临时访问并上传当前分片。
- 生成的 URL 包含了分片的信息(如分片编号、上传 ID 等),所以客户端上传的 文件内容 是分片,而不是整个文件。每个分片会上传到 S3 并与其他分片一起组成最终的完整文件。
1.分片上传接口
@SaCheckPermission("system:oss:multipart")
@PostMapping(value = "/multipart")
public R<?> multipart(@RequestBody MultipartBo multipartBo) {
return switch (multipartBo.getOssStatus()) {
case "initiate" -> {
// 校验文件原名和MD5摘要不能为空
if (StringUtils.isNotEmpty(multipartBo.getOriginalName()) && StringUtils.isNotEmpty(multipartBo.getMd5Digest())) {
yield R.ok(ossService.initiateMultipart(multipartBo)); // 初始化上传任务
} else {
yield R.fail("Original name and MD5 digest cannot be empty"); // 返回失败响应
}
}
case "upload" -> {
// 校验上传分片的必要参数
ValidatorUtils.validate(multipartBo, AddGroup.class);
yield R.ok(ossService.uploadPart(multipartBo)); // 上传分片
}
case "query" -> {
// 校验查询分片列表的必要参数
ValidatorUtils.validate(multipartBo, QueryGroup.class);
yield R.ok(ossService.uploadPartList(multipartBo)); // 查询分片进度
}
case "complete" -> {
// 校验完成上传的必要参数
ValidatorUtils.validate(multipartBo, EditGroup.class);
yield R.ok(ossService.completeMultipartUpload(multipartBo)); // 完成上传
}
default -> R.fail("Invalid OSS status"); // 无效的上传状态
};
}
2.分片上传BO
/**
* 分片上传
*
* @author AprilWind
*/
@Data
public class MultipartBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分片类型(必传)
*/
private String ossStatus;
/**
* 文件原名(分片初始化的时候使用)
*/
@Size(min = 2, max = 255, message = "文件原名长度必须在2到255之间")
private String originalName;
/**
* 用于分片上传任务的 Upload ID
* 在初始化分片上传时获取,并在后续的分片上传和完成上传过程中使用
*/
@NotBlank(message = "上传任务ID不能为空", groups = {QueryGroup.class, AddGroup.class, EditGroup.class})
private String uploadId;
/**
* 分片编号(从1开始递增)
*/
@NotNull(message = "分片编号不能为空", groups = AddGroup.class)
@Range(min = 1, max = 10000, message = "分片编号必须介于1和10,000之间", groups = AddGroup.class)
private Integer partNumber;
/**
* 内容的 MD5 摘要
* initiate初始化需要第一片的md5值用来判断断点续传,upload状态时,非必需(如果有值会校验)
*/
@Size(max = 255, message = "内容的 MD5 摘要,如果有的话不能超过255", groups = AddGroup.class)
private String md5Digest;
/**
* 最大返回的分片数(默认为1000,最大值1000)
* 最多分片一万,一次性返回会造成前端性能问题,需要前端多次校验
*/
@NotNull(message = "最大返回的分片数不能为空", groups = QueryGroup.class)
@Range(max = 1000, message = "最大返回的分片数不能超过1000", groups = QueryGroup.class)
private Integer maxParts;
/**
* 分片编号的标记,用于分页查询(默认为0,表示从第一个分片开始查询)
*/
@NotNull(message = "分片编号的标记不能为空", groups = QueryGroup.class)
@Range(min = 0, max = 10000, message = "分片编号的标记长度必须在0到10000之间", groups = QueryGroup.class)
private Integer partNumberMarker;
/**
* 已上传列表(最大长度一万)
* 必须是唯一且按照递增顺序排列,严格检查是否漏传
*/
@Valid
@NotNull(message = "已上传列表不能为空")
@Size(min = 2, max = 10000, message = "已上传列表长度必须在2到10000之间", groups = EditGroup.class)
private List<PartUploadResult> partUploadList;
@Data
public static class PartUploadResult implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分片编号(从1开始递增)
*/
@NotNull(message = "分片编号不能为空", groups = EditGroup.class)
@Range(min = 1, max = 10000, message = "分片编号必须介于1和10,000之间", groups = EditGroup.class)
private Integer partNumber;
/**
* 从上传部分的内容生成的实体标签
*/
@NotBlank(message = "实体标签不能为空", groups = EditGroup.class)
private String eTag;
}
}
1.初始化分片上传任务
前端传递文件名和文件的md5值
- 检查缓存:通过
md5Digest
作为 key 查询 Redis,判断是否已有上传任务(避免重复上传)。 - 如果缓存存在且未超时:
- 直接返回已存在的
uploadId
和已上传分片信息,支持断点续传。
- 直接返回已存在的
- 如果缓存不存在或超时:
- 生成新的
uploadId
并创建分片上传任务。 - 将任务信息存入 Redis 以支持续传。
- 返回上传任务信息给前端。
- 生成新的
@Override
public MultipartVo initiateMultipart(MultipartBo multipartBo) {
OssClient storage = OssFactory.instance();
String md5Digest = multipartBo.getMd5Digest();
String osskey = GlobalConstants.OSS_CONTINUATION + LoginHelper.getUserId() + md5Digest;
MultipartVo multipartVo = new MultipartVo();
// 检查缓存是否存在并且在 2 小时内有效
if (RedisUtils.getTimeToLive(osskey) > 60 * 60 * 2 * 1000) {
multipartVo = RedisUtils.getCacheObject(osskey);
// 获取已上传的分片列表
List<PartUploadResult> listParts = storage.listParts(multipartVo.getFilename(), multipartVo.getUploadId(), null, null);
multipartVo.setPartUploadList(listParts.stream()
.map(x -> new MultipartVo.PartUploadResult(x.getPartNumber(), x.getETag()))
.collect(Collectors.toList()));
} else {
// 生成新的上传任务
String originalName = multipartBo.getOriginalName();
String suffix = StringUtils.substring(originalName, originalName.lastIndexOf("."), originalName.length());
UploadResult uploadResult = storage.initiateMultipart(suffix);
multipartVo.setFilename(uploadResult.getFilename());
multipartVo.setUploadId(uploadResult.getUploadId());
multipartVo.setMd5Digest(md5Digest);
multipartVo.setOriginalName(originalName);
multipartVo.setSuffix(suffix);
// 将任务信息存入 Redis,设置 72 小时有效期
RedisUtils.setCacheObject(osskey, multipartVo, Duration.ofMillis(60 * 60 * 72));
RedisUtils.setCacheObject(GlobalConstants.OSS_MULTIPART + multipartVo.getUploadId(), multipartVo, Duration.ofMillis(60 * 60 * 72));
}
return multipartVo;
}
//初始化上传任务关键代码
public UploadResult initiateMultipartUpload(String key) {
try {
// 异步创建分片上传任务
String uploadId = client.createMultipartUpload(
x -> x.bucket(properties.getBucketName()) // 设置 S3 存储桶名称
.key(key) // 设置对象键(文件名或路径)
.build() // 构建请求
).join() // 阻塞直到获取结果
.uploadId(); // 获取上传 ID
// 返回包含上传任务信息的 UploadResult 对象
return UploadResult.builder()
.filename(key) // 文件名
.uploadId(uploadId) // 上传 ID
.build();
} catch (Exception e) {
// 捕获异常并抛出自定义异常,表示上传任务创建失败
throw new OssException("创建分片上传任务失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
2.上传分片
/**
* 生成预签名的分片上传 URL
*
* @param key 在 Amazon S3 中的对象键
* @param uploadId 分片上传任务的 Upload ID
* @param partNumber 分片编号(从1开始递增)
* @param md5Digest 内容的 MD5 摘要,如果有的话
* @param second 签名持续时间(秒)
* @return 预签名 URL 字符串
*/
public String uploadPartFutures(String key, String uploadId, Integer partNumber, String md5Digest, Integer second) {
URL url = presigner.presignUploadPart(
x -> x.signatureDuration(Duration.ofSeconds(second))
.uploadPartRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.uploadId(uploadId)
.partNumber(partNumber)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.build()
).build()
).url();
return url.toString();
}
/**
* 上传文件的分段(分片上传)
*
* @param multipartBo 分段上传的参数对象
* @return 分片上传成功后的对象信息
*/
@Override
public MultipartVo uploadPart(MultipartBo multipartBo) {
String uploadId = multipartBo.getUploadId();
Integer partNumber = multipartBo.getPartNumber();
MultipartVo multipartVo = RedisUtils.getCacheObject(GlobalConstants.OSS_MULTIPART + uploadId);
if (ObjectUtil.isNull(multipartVo)) {
throw new ServiceException("该分片任务不存在!");
}
OssClient storage = OssFactory.instance();
String privateUrl = storage.uploadPartFutures(multipartVo.getFilename(), uploadId, partNumber, multipartBo.getMd5Digest(), 60 * 60 * 72);
multipartVo.setPrivateUrl(privateUrl);
multipartVo.setPartNumber(partNumber);
return multipartVo;
}
xception(“该分片任务不存在!”);
}
OssClient storage = OssFactory.instance();
String privateUrl = storage.uploadPartFutures(multipartVo.getFilename(), uploadId, partNumber, multipartBo.getMd5Digest(), 60 * 60 * 72);
multipartVo.setPrivateUrl(privateUrl);
multipartVo.setPartNumber(partNumber);
return multipartVo;
}