若依(RuoYi)OSS上传学习记录总结

以下是关于若依(RuoYi)OSS上传学习记录的总结博客内容,你可以根据需要进一步修改和完善。


若依(RuoYi)OSS上传学习记录总结

在学习若依(RuoYi)框架的OSS(对象存储服务)上传功能过程中,我深入研究了其整合AWS S3协议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_keysecret_key:分别对应云厂商提供的访问密钥和访问秘钥,用于身份验证和授权,确保对OSS资源的安全访问。
  • bucket_name:存储桶名称,是OSS中存储对象的容器,每个对象都必须存储在某个存储桶中。
  • region:存储区域,虽然可以通过endpoint推导,但在配置中明确指定可以更清晰地表明存储桶所在的地理位置,有助于优化性能和满足合规性要求。
  • endpoint:访问地址,用于访问OSS服务的入口地址,不同的云厂商和区域可能有不同的endpoint
  • domain:自定义域名,允许用户为存储桶设置自定义的访问域名,提升访问的灵活性和用户体验。
  • prefix:存储路径前缀,可以为存储在存储桶中的对象设置统一的路径前缀,方便对对象进行分类和管理。
  • is_https:标识是否启用HTTPS,Y表示启用,N表示不启用,启用HTTPS可以提高数据传输的安全性。
  • access_policy:权限策略,0表示私有,只有拥有者可以访问;1表示公开,任何人都可以访问;2表示自定义,可以根据具体需求灵活设置访问权限。
  • create_timeupdate_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值

  1. 检查缓存:通过 md5Digest 作为 key 查询 Redis,判断是否已有上传任务(避免重复上传)。
  2. 如果缓存存在且未超时
    • 直接返回已存在的 uploadId 和已上传分片信息,支持断点续传。
  3. 如果缓存不存在或超时
    • 生成新的 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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值