SpringBoot一键切换文件上传方式(巧用策略模式)

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

da5d6bb6575d9b5e4fd83765281809f5.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

来源:juejin.cn/post/
7221565450249568313


本文目的:将策略模式的思想融入到java编码中,更加便捷的实现文件上传方式的切换。阿里云Oss对象存储、腾讯云Cos对象存储、七牛云Kodo对象存储以及本地文件存储 之间的快速切换。

1 什么是策略模式

策略模式是指有一定行动内容的相对稳定的策略名称。策略模式在古代中又称“计策”,简称“计”,如《汉书·高帝纪上》:“汉王从其计”。

这里的“计”指的就是计谋、策略。策略模式具有相对稳定的形式,如“避实就虚”、“出奇制胜”等。一定的策略模式,既可应用于战略决策,也可应用于战术决策;既可实施于大系统的全局性行动,也可实施于大系统的局部性行动。

上面的概述可能大家会看的摸不着头脑,简单来说就是:

  • 我们定义一个接口(就比如接下来要实现的文件上传接口)

  • 我们定义所需要实现的策略实现类 A、B、C、D(也就是项目中所使用的四种策略阿里云Oss上传、腾讯云Cos上传、七牛云Kodo上传、本地上传)

  • 我们通过策略上下文来调用策略接口,并选择所需要使用的策略

上面就是策略模式的简单概述,光说不练假本事,那么我们接下来就新建一个项目来实际演练一下。

4abc2002e4fa8537f558f0a552d1a492.jpeg

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

2 策略模式的具体实现

2-1、SpringBoot项目的基本搭建

关于SpringBoot项目的搭建这里就不做过多的概述,我这里已经新建了一个基础的SpringBoot项目。

7178292e64e179af212862462f7a2d67.jpeg

2-2、策略接口的编写

首先我们新建一个名称为 strategy 的文件夹(在代码规范中,使用设计模式要明确的体现出来,便于后期维护)

d9501488bdf2fe060bbb6f86a7668c5f.jpeg

如下就是我们的策略接口了,接下来我们去编写对应的实现类。

public interface UploadStrategy {

    /**
     * 上传文件
     *
     * @param file        文件
     * @param filePath    文件上传露肩
     * @return {@link String} 文件上传的全路径
     */
    String uploadFile(MultipartFile file, final String filePath);

}

2-3、完善配置文件

在编写对象存储实现类之前,我门会发现一个问题。我们需要去对应的云服务厂商开通对象存储服务,然后获取到accessKey、accessKeySecret、endpoint、bucket、domainUrl等必须的参数。

因为这些信息基本是不会发生改变,所以我们可以将这些信息存储在配置文件中。

除此之外我们还需要对文件上传进行配置,设置为最大文件为100MB

server:
  port: 8080

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

application:
  store:
    oss:
      domain-url: *********
      access-key: *********
      access-key-secret: *******
      endpoint: *******
      bucket: *******
    cos:
      domain-url: *******
      access-key: *******
      access-key-secret: *******
      endpoint: *******
      bucket: *******
    kodo:
      domain-url: *******
      access-key: *******
      access-key-secret: *******
      endpoint: *******
      bucket: *******
    local:
      domain-url: *******
      access-key: *******
      access-key-secret: *******
      endpoint: *******
      bucket: *******

配置文件的格式如上,我们获取配置文件的时候可以使用@Value的注解进行获取。

嗯?不会吧不会吧?不会真的有人用@Value去一个一个获取吧?

优秀的我们肯定不会用这么Low的方式,在这里呢我们使用@ConfigurationProperties()的方式来获取配置文件的内容。

首先我们引入自定义配置依赖 以及 云服务依赖

<!--==============  项目版本号规定 ===============-->
<properties>
    <!--==============  对象存储依赖  ==================-->
    <cos.version>5.6.89</cos.version>
    <kodo.version>[7.7.0, 7.10.99]</kodo.version>
    <oss.version>3.15.1</oss.version>
</properties> 
  
<dependencies>
   <!-- 自定义配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
    <!--================== 对象存储依赖 =======================-->
    <!-- 腾讯云Cos对象存储 -->
    <dependency>
        <groupId>com.qcloud</groupId>
        <artifactId>cos_api</artifactId>
        <version>${cos.version}</version>
    </dependency>
    <!-- 七牛云Kodo对象存储 -->
    <dependency>
        <groupId>com.qiniu</groupId>
        <artifactId>qiniu-java-sdk</artifactId>
        <version>${kodo.version}</version>
    </dependency>
    <!--阿里云Oss对象存储-->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>${oss.version}</version>
    </dependency> 
</dependencies>

我们编写properties实体类,通过@ConfigurationProperties()注解可以将配置文件中的内容读取到实体类中。

实体类中由于类继承关系不要使用@Data注解,而要使用@Getter@Setter,某则可能会出现问题。

除此之外还要注意配置目录的对应关系。

6c91f26c9ec53350b7d7fed833760baa.jpeg
@Getter
@Setter
@Component
@ConfigurationProperties("application.store")
public class ObjectStoreProperties {

    /**
     * Oss
     */
    private ConfigEntity oss;

    /**
     * Cos
     */
    private ConfigEntity cos;

    /**
     * Kodo
     */
    private ConfigEntity kodo;

    /**
     * local
     */
    private ConfigEntity local;

    @Getter
    @Setter
    public static class ConfigEntity {

        /**
         * 访问域名
         */
        private String domainUrl;

        /**
         * key
         */
        private String accessKey;

        /**
         * 密钥
         */
        private String accessKeySecret;


        /**
         * 地域节点
         */
        private String endpoint;


        /**
         * 存储桶名称
         */
        private String bucket;

    }
}
2-3-1、阿里云Oss配制信息完善

首先我们去阿里云开通对象存储服务 我这里已经创建好了一个存储桶

  • endpoint:  oss-cn-hangzhou.aliyuncs.com

  • bucket: muzinan-blog

  • domainUrl: muzinan-blog.oss-cn-hangzhou.aliyuncs.com

e7254d762ec8ad39ec3168980ecc25e7.jpeg

接下来我们需要去获取 accessKeyaccessKeySecret,大家可以直接使用主账户的key和密钥,但是由于主账户是拥有所有权限的,所以不要把key和密钥泄露出去。如果发现泄露,第一时间通过阿里云更换key和密钥,及时止损。

0fb79c735630507045c23953b940129d.jpeg

点击创建accessKey,进行验证之后就会生成accessKeyaccessKeySecret,大家将其复制下来配置 入项目即可。

80db5003c37705894d4e66cab76039c8.jpeg 476b57c3bc3441a4af972bdb74e40cbd.jpeg

完成之后创建之后呢我们的配置文件应该是这样的

6af4f64779a7ea7fe1069fddebcea6a7.jpeg
2-3-2、腾讯云Cos配制信息完善

首先我们去腾讯云开通对象存储服务 我这里已经创建好了一个存储桶

  • endpoint:  ap-shanghai

  • bucket: muzinan-blog-1314779712

  • domainUrl: https://muzinan-blog-1314779712.cos.ap-shanghai.myqcloud.com

eb0a306a1a7f161fcf9c4b26b26c26a9.jpeg

接下来我们需要去获取 accessKeyaccessKeySecret (注意事项同阿里云Oss)

这里我们就不使用主账户了,我们来新建一个子用户,并且只给子用户分配对象存储的权限,这样即使key和秘钥被泄露,最多只有对象存储服务可能会被别人恶意使用,其他服务不会收到影响。

  • 点击新增用户

  • 点击 自定义创建

  • 选择  可访问资源井接收消息

  • 访问方式 选择编程访问

  • 主账户验证

  • 搜索Cos 选择红框内的权限赋予当前用户

  • 标签可不设置

  • 新建成功之后就会展示秘钥信息

f768d80fab98076fa86e9c5eb4e5aef7.jpeg c2ce3d97faaa0b84a11387be2a4af532.jpeg 878cfb4ae325343d9b0cd45b991ad3dc.jpeg 2b59fbbac45952db48c25b021a1eb22d.jpeg 60e563c87742fefe3efbd25ec7b53d3c.jpeg 7270b574896aa35584c29e28fdff0411.jpeg ddabffa5193d28becdcdc11423275d09.jpeg

完成之后创建之后呢我们的配置文件应该是这样的

4aac51a00e9638d772fb1b1b90b5146d.jpeg
2-2-3、七牛云Kodo配制信息完善

首先我们去腾讯云开通对象存储服务 我这里已经创建好了一个存储桶,七牛云不需要配制 地域节点

  • bucket: muzinan-blog

  • domainUrl: http://rlqlffbtr.hd-bkt.clouddn.com

2dc831acecdd26ef089e1c88f040623f.jpeg

接下来我们需要去获取 accessKeyaccessKeySecret,七牛云没有子用户的概念,所以我们可以直接创建秘钥。(注意点同阿里云Oss)

b8a59062b890ac12bf3f50c06fef8b31.jpeg b43af1fce5e7665aab63afcd4e160c74.jpeg 8f5104576e492bdd5045d376818942ba.jpeg

完成之后创建之后呢我们的配置文件应该是这样的

76f20541e6216837b329fcecd23f98ca.jpeg
2-2-4、本地上传配制信息完善

本地上传目前不需要进行配置,项目上线可以进行域名配置域名配制

2-3、策略实现类内部实现

我们在进行具体文件上传策略实现之前总结一下所涉及到的功能。

  • 上传对象初始化

  • 文件是否已经存在

  • 文件上传

  • 获取访问路径

我们会发现无论是通过哪个平台进行文件的上传,基本上都会使用到上述的步骤,也就是说都会使用到上述的方法。

所以在这里我们定义一个抽象类来规定具体所需要使用的方法,然后各个具体实现来继承我们的抽象类即可。

@Getter
@Setter
public abstract class AbstractUploadStrategyImpl implements UploadStrategy {
    
    @Override
    public String uploadFile(MultipartFile file, String filePath) {
        try {

            //region 获取文件md5值 -> 获取文件后缀名 -> 生成相对路径
            String fileMd5 = FileUtil.getMd5(file.getInputStream());
            String extName = FileUtil.getExtName(file.getOriginalFilename());
            String fileRelativePath = filePath + fileMd5 + extName;
            //endregion

            //region 初始化
            initClient();
            //endregion

            //region 检测文件是否已经存在,不存在则进行上传操作
            if (!checkFileIsExisted(fileRelativePath)) {
                executeUpload(file, fileRelativePath);
            }
            //endregion

            return getPublicNetworkAccessUrl(fileRelativePath);
        } catch (IOException e) {
            throw new BaseException("文件上传失败");
        }
    }

    /**
     * 初始化客户端
     */
    public abstract void initClient();

    /**
     * 检查文件是否已经存在(文件MD5值唯一)
     *
     * @param fileRelativePath 文件相对路径
     * @return true 已经存在  false 不存在
     */
    public abstract boolean checkFileIsExisted(String fileRelativePath);

    /**
     * 执行上传操作
     *
     * @param file             文件
     * @param fileRelativePath 文件相对路径
     * @throws IOException io异常信息
     */
    public abstract void executeUpload(MultipartFile file, String fileRelativePath) throws IOException;

    /**
     * 获取公网访问路径
     *
     * @param fileRelativePath 文件相对路径
     * @return 公网访问绝对路径
     */
    public abstract String getPublicNetworkAccessUrl(String fileRelativePath);

}
2-3-1、Oss上传策略具体实现

我们在OssUploadStrategyImpl实现文件上传至Oss平台,具体如何上传代码至阿里云Oss平台可以去看阿里云官方文档。

6e0d84b804d8448ab25cdcfb2c1d4b90.jpeg
/**
 * @author: MuZiNan
 * @createDate: 2022/11/25
 * @description: Oss上传策略实现类
 * @version: 1.0
 */
@Slf4j
@Getter
@Setter
@RequiredArgsConstructor
@Service("ossUploadServiceImpl")
public class OssUploadStrategyImpl extends AbstractUploadStrategyImpl {

    /**
     * 构造器注入bean
     */
    private final ObjectStoreProperties properties;

    /**
     * 当前类的属性
     */
    private OSS ossClient;


    @Override
    public void initClient() {
        ossClient = new OSSClientBuilder().build(properties.getOss().getEndpoint(), properties.getOss().getAccessKey(), properties.getOss().getAccessKeySecret());
        log.info("OssClient Init Success...");
    }

    @Override
    public boolean checkFileIsExisted(String fileRelativePath) {
        return ossClient.doesObjectExist(properties.getOss().getBucket(), fileRelativePath);
    }

    @Override
    public void executeUpload(MultipartFile file, String fileRelativePath) throws IOException {
        log.info("File Upload Starts...");
        ossClient.putObject(properties.getOss().getBucket(), fileRelativePath, file.getInputStream());
        log.info("File Upload Finish...");
    }

    @Override
    public String getPublicNetworkAccessUrl(String fileRelativePath) {
        return properties.getOss().getDomainUrl() + fileRelativePath;
    }
}
2-3-2、Cos上传策略具体实现

我们在CosUploadStrategyImpl实现文件上传至Cos平台,具体如何上传代码至腾讯云Cos平台可以去看腾讯云官方文档。

d699004313474a13d01dbb58afc49573.jpeg
/**
 * @author: MuZiNan
 * @createDate: 2022/11/25
 * @description: 腾讯云Cos文件上传策略实现类
 * @version: 1.0
 */
@Slf4j
@Getter
@Setter
@RequiredArgsConstructor
@Service("cssUploadServiceImpl")
public class CosUploadStrategyImpl extends AbstractUploadStrategyImpl {

    /**
     * 构造器注入
     */
    private final ObjectStoreProperties properties;

    /**
     * 属性
     */
    private COSClient cosClient;


    @Override
    public void initClient() {

        COSCredentials cred = new BasicCOSCredentials(properties.getCos().getAccessKey(), properties.getCos().getAccessKeySecret());
        //region ClientConfig 中包含了后续请求 COS 的客户端设置:
        ClientConfig clientConfig = new ClientConfig();
        clientConfig.setRegion(new Region(properties.getCos().getEndpoint()));
        clientConfig.setHttpProtocol(HttpProtocol.http);
        clientConfig.setSocketTimeout(30 * 1000);
        clientConfig.setConnectionTimeout(30 * 1000);
        //endregion

        // 生成 cos 客户端
        cosClient = new COSClient(cred, clientConfig);
        log.info("CosClient Init Success...");
    }

    @Override
    public boolean checkFileIsExisted(String fileRelativePath) {
        return cosClient.doesObjectExist(properties.getCos().getBucket(), fileRelativePath);
    }

    @Override
    public void executeUpload(MultipartFile file, String fileRelativePath) throws IOException {
        log.info("File Upload Starts...");
        cosClient.putObject(properties.getCos().getBucket(), fileRelativePath, file.getInputStream(), null);
        log.info("File Upload Finish...");
    }

    @Override
    public String getPublicNetworkAccessUrl(String fileRelativePath) {
        return properties.getCos().getDomainUrl() + fileRelativePath;
    }
}
2-3-3、Kodo上传策略具体实现

我们在KodoUploadStrategyImpl实现文件上传至七牛云平台,具体如何上传代码至七牛云Kodo平台可以去看七牛云官方文档。

189e5727f17b8a4ff056ee6a827ac23b.jpeg
/**
 * @author: MuZiNan
 * @createDate: 2022/11/25
 * @description: 七牛云Kodo上传策略实现类
 * @version: 1.0
 */
@Slf4j
@Getter
@Setter
@RequiredArgsConstructor
@Service("kodoUploadServiceImpl")
public class KodoUploadStrategyImpl extends AbstractUploadStrategyImpl {

    /**
     * 构造器注入Bean
     */
    private final ObjectStoreProperties properties;

    /**
     * upToken
     */
    private String upToken;

    /**
     * 上传Manger
     */
    private UploadManager uploadManager;

    /**
     * 存储桶Manger
     */
    private BucketManager bucketManager;


    @Override
    public void initClient() {
        Auth auth = Auth.create(properties.getKodo().getAccessKey(), properties.getKodo().getAccessKeySecret());
        upToken = auth.uploadToken(properties.getKodo().getBucket());
        Configuration cfg = new Configuration(Region.region0());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
        uploadManager = new UploadManager(cfg);
        bucketManager = new BucketManager(auth, cfg);
        log.info("OssClient Init Success...");
    }

    @Override
    public boolean checkFileIsExisted(String fileRelativePath) {
        try {
            if (null == bucketManager.stat(properties.getKodo().getBucket(), fileRelativePath)) {
                return false;
            }
        } catch (QiniuException e) {
            return false;
        }
        return true;
    }

    @Override
    public void executeUpload(MultipartFile file, String fileRelativePath) throws IOException {
        try {
            uploadManager.put(file.getInputStream(), fileRelativePath, upToken, null, null);
        } catch (IOException e) {
            log.error("文件上传失败");
            throw new BaseException("文件上传失败");
        }
    }

    @Override
    public String getPublicNetworkAccessUrl(String fileRelativePath) {
        return properties.getKodo().getDomainUrl() + fileRelativePath;
    }
}
2-3-4、本地上传策略具体实现

我们在LocalUploadStrategyImpl实现文件上传至本地

d1be454ba8c01bab8b071c2ffb9ba714.jpeg
/**
 * @author: MuZiNan
 * @createDate: 2022/11/25
 * @description: 本地上传策略实现
 * @version: 1.0
 */
@Slf4j
@Getter
@Setter
@RequiredArgsConstructor
@Service("localUploadServiceImpl")
public class LocalUploadStrategyImpl extends AbstractUploadStrategyImpl {

    /**
     * 本地项目端口
     */
    @Value("${server.port}")
    private Integer port;

    /**
     * 前置路径 ip/域名
     */
    private String prefixUrl;

    /**
     * 构造器注入bean
     */
    private final ObjectStoreProperties properties;

    @Override
    public void initClient() {
        try {
            prefixUrl = ResourceUtils.getURL("classpath:").getPath() + "static/";
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new BaseException("文件不存在");
        }
        log.info("CosClient Init Success...");
    }

    @Override
    public boolean checkFileIsExisted(String fileRelativePath) {
        return new File(prefixUrl + fileRelativePath).exists();
    }

    @Override
    public void executeUpload(MultipartFile file, String fileRelativePath) throws IOException {
        File dest = checkFolderIsExisted(fileRelativePath);
        try {
            file.transferTo(dest);
        } catch (IOException e) {
            e.printStackTrace();
            throw new BaseException("文件上传失败");
        }
    }

    @Override
    public String getPublicNetworkAccessUrl(String fileRelativePath) {
        try {
            String host = InetAddress.getLocalHost().getHostAddress();
            if (StringUtils.isEmpty(properties.getLocal().getDomainUrl())) {
                return String.format("http://%s:%d%s", host, port, fileRelativePath);
            }
            return properties.getLocal().getDomainUrl() + fileRelativePath;
        } catch (UnknownHostException e) {
            throw new BaseException(HttpCodeEnum.UNKNOWN_ERROR);
        }
    }



    /**
     * 检查文件夹是否存在,若不存在则创建文件夹,最终返回上传文件
     *
     * @param fileRelativePath 文件相对路径
     * @return {@link  File} 文件
     */
    private File checkFolderIsExisted(String fileRelativePath) {
        File rootPath = new File(prefixUrl + fileRelativePath);
        if (!rootPath.exists()) {
            if (!rootPath.mkdirs()) {
                throw new BaseException("文件夹创建失败");
            }
        }
        return rootPath;
    }

}

2.4、策略上下文实现

我们通过策略上下文来选择使用哪种上传方式。

注意点:

当Map集合的Value为接口类型时,Spring会自动对Map集合进行注入。

  • 其中map集合的key为接口对应实现类的BeanName

  • 其中map集合的vlaue为接口对应实现类的实例

其中传入的uploadServiceName就是对应策略类所规定的的BeanName,这里的BeanName就作为选择的条件。

/**
 * @author: MuZiNan
 * @createDate: 2022/11/25
 * @description: 上传策略上下文
 * @version: 1.0
 */
@Component
@RequiredArgsConstructor
public class UploadStrategyContext {

    private final Map<String, UploadStrategy> uploadStrategyMap;
    
    /**
     * 执行上传策略
     *
     * @param file     文件
     * @param filePath 文件上传路径前缀
     * @return {@link String} 文件上传全路径
     */
    public String executeUploadStrategy(MultipartFile file, final String filePath, String uploadServiceName) {
        // 执行特点的上传策略
        return uploadStrategyMap.get(uploadServiceName).uploadFile(file, filePath);
    }

}

2.5、不同策略上传测试

本文章中的项目测试使用的IDEA 插件 Fast Request,确实很好用,但是插件是需要付费的插件,大家可以先免费试用30天。

24756eb1c5bc0b191ef968aac7b193ff.jpeg c8a2d97ad7100f2b7911aaa3f838fa68.jpeg
2.5.1、上传测试controller代码
@RestController
@RequiredArgsConstructor
public class UploadController {

    private final UploadStrategyContext uploadStrategyContext;

    @PostMapping("/upload")
    public ResponseResult<?> upload(MultipartFile file) {
        return ResponseResult.success("文件上传成功!",uploadStrategyContext.executeUploadStrategy(file,"/blog/avatar","cosUploadServiceImpl"));
    }
}
2.5.2、Oss上传测试

目前我们的库中是没有任何文件的,接下来我们进行上传测试。 我们需要第三个参数更换为具体所需要执行的策略实现类的BeanName

5d51ab30f3294a9c22bde548da80ee20.jpeg 1ca0d81fd7c383b2b47348157800ff51.jpeg 78c4a095f8267450ddce91aee85671e3.jpeg a4e9a54fb02bc5a61ccc8550fbfd6760.jpeg 304c2dedc123c6a25105287d076b5608.jpeg
2.5.3、Cos上传测试

目前我们的库中是没有任何文件的,接下来我们进行上传测试。 我们需要第三个参数更换为具体所需要执行的策略实现类的BeanName

49c411e9af17a3479a3aa59bf655b16d.jpeg 4f7984ddedafbe7800d7e80737fecbc5.jpeg 84430735f34e13758562907cc4d7f80a.jpeg deacdf7164461283a9d854aef2472016.jpeg 4ef8043e912d8e5cffbe08b08f32dc28.jpeg
2.5.4、Kodo上传测试

目前我们的库中是没有任何文件的,接下来我们进行上传测试。 我们需要第三个参数更换为具体所需要执行的策略实现类的BeanName

07c0ab8cca19829aba33e756f9b4f1dd.jpeg 3e511cda98f4450f3def39a4163a2047.jpeg 50b60681002708f7d33d702256ba3f5b.jpeg 10110044393d63bfcf039fdac3e5a866.jpeg 691b1393f22dbac5ceed95bf22811ccd.jpeg
2.5.5、本地上传测试

目前我们的库中是没有任何文件的,接下来我们进行上传测试。 我们需要第三个参数更换为具体所需要执行的策略实现类的BeanName

74a0036a35cfa43c6c8e302eb5d46f5d.jpeg 8fe6a09541f1c684a935e2d3e7174989.jpeg 3b76613ab6318e47ac07d0775bbc4f14.jpeg

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

3 总结

上述只是对于策略模式的简单实践。 我们可以通过网站全局配制结合前端界面来完成选择使用哪个平台来进行文件的上传。 当我们选中哪种上传模式,那么后台则会执行该上传方式

d6b96e4e50bbf95a31b03ed6595df96c.jpeg

上述案例代码已经存在在GitCode平台当中

https://gitcode.net/nanshen__/store-object


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

4ee0b8d5cc9777c1aa9919acbe7e2d01.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

97aabd2ec0568f5647fd2a61ff5b00cc.png

53076b99021ba661a37609dbdb912521.pngfead869f828779877b16eab6799c6e7f.png69a854ccec169b4461d9cce9c301ee45.png07f0c2cf39c6b1c35166f5d90c41ef16.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值