Browse Source

封装 yudao-spring-boot-starter-file 组件,初步实现 S3 对接云存储的能力

YunaiV 3 years ago
parent
commit
ed53ca3de9
15 changed files with 498 additions and 13 deletions
  1. 1 0
      yudao-framework/pom.xml
  2. 2 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
  3. 4 11
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  4. 69 0
      yudao-framework/yudao-spring-boot-starter-file/pom.xml
  5. 41 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java
  6. 16 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientConfig.java
  7. 58 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/AbstractFileClient.java
  8. 95 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3FileClient.java
  9. 83 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3FileClientConfig.java
  10. 36 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3ModifyPathInterceptor.java
  11. 4 0
      yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/config/package-info.java
  12. 4 0
      yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/package-info.java
  13. 81 0
      yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java
  14. 4 0
      yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/enums/package-info.java
  15. BIN
      yudao-framework/yudao-spring-boot-starter-file/src/test/resources/file/erweima.jpg

+ 1 - 0
yudao-framework/pom.xml

@@ -16,6 +16,7 @@
         <module>yudao-spring-boot-starter-web</module>
         <module>yudao-spring-boot-starter-security</module>
 
+        <module>yudao-spring-boot-starter-file</module>
         <module>yudao-spring-boot-starter-monitor</module>
         <module>yudao-spring-boot-starter-protection</module>
         <module>yudao-spring-boot-starter-config</module>

+ 2 - 2
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java

@@ -39,8 +39,8 @@ public class ValidationUtils {
                 && PATTERN_XML_NCNAME.matcher(str).matches();
     }
 
-    public static void validate(Validator validator, Object reqVO, Class<?>... groups) {
-        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(reqVO, groups);
+    public static void validate(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
         if (CollUtil.isNotEmpty(constraintViolations)) {
             throw new ConstraintViolationException(constraintViolations);
         }

+ 4 - 11
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl;
 
 import cn.hutool.extra.validation.ValidationUtil;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
@@ -11,7 +10,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
 import lombok.extern.slf4j.Slf4j;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
@@ -26,7 +24,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
      * 渠道编号
      */
     private final Long channelId;
-
     /**
      * 渠道编码
      */
@@ -40,10 +37,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
      */
     protected Config config;
 
-    protected Double calculateAmount(Long amount) {
-        return amount / 100.0;
-    }
-
     public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
         this.channelId = channelId;
         this.channelCode = channelCode;
@@ -75,6 +68,10 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         this.init();
     }
 
+    protected Double calculateAmount(Long amount) {
+        return amount / 100.0;
+    }
+
     @Override
     public Long getId() {
         return channelId;
@@ -96,12 +93,9 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         return result;
     }
 
-
-
     protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
             throws Throwable;
 
-
     @Override
     public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
         PayCommonResult<PayRefundUnifiedRespDTO> resp;
@@ -115,7 +109,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         return resp;
     }
 
-
     protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
 
 }

+ 69 - 0
yudao-framework/yudao-spring-boot-starter-file/pom.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-file</artifactId>
+
+    <name>${project.artifactId}</name>
+    <description>文件客户端,支持多种存储器
+        1. file:本地磁盘
+        2. ftp:FTP 服务器
+        3. db:数据库
+        4. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等
+    </description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+
+        <!-- 三方云服务相关 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <version>2.17.147</version>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 41 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.framework.file.core.client;
+
+/**
+ * 文件客户端
+ *
+ * @author 芋道源码
+ */
+public interface FileClient {
+
+    /**
+     * 获得客户端编号
+     *
+     * @return 客户端编号
+     */
+    Long getId();
+
+    /**
+     * 上传文件
+     *
+     * @param content 文件流
+     * @param path 相对路径
+     * @return 完整路径,即 HTTP 访问地址
+     */
+    String upload(byte[] content, String path);
+
+    /**
+     * 删除文件
+     *
+     * @param path 相对路径
+     */
+    void delete(String path);
+
+    /**
+     * 获得文件的内容
+     *
+     * @param path 相对路径
+     * @return 文件的内容
+     */
+    byte[] getContent(String path);
+
+}

+ 16 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientConfig.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.framework.file.core.client;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * 文件客户端的配置
+ * 不同实现的客户端,需要不同的配置,通过子类来定义
+ *
+ * @author 芋道源码
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+// @JsonTypeInfo 注解的作用,Jackson 多态
+// 1. 序列化到时数据库时,增加 @class 属性。
+// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
+public interface FileClientConfig {
+}

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/AbstractFileClient.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.file.core.client.impl;
+
+import cn.iocoder.yudao.framework.file.core.client.FileClient;
+import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {
+
+    /**
+     * 配置编号
+     */
+    private final Long id;
+    /**
+     * 文件配置
+     */
+    protected Config config;
+
+    public AbstractFileClient(Long id, Config config) {
+        this.id = id;
+        this.config = config;
+    }
+
+    /**
+     * 初始化
+     */
+    public final void init() {
+        doInit();
+        log.info("[init][配置({}) 初始化完成]", config);
+    }
+
+    /**
+     * 自定义初始化
+     */
+    protected abstract void doInit();
+
+    public final void refresh(Config config) {
+        // 判断是否更新
+        if (config.equals(this.config)) {
+            return;
+        }
+        log.info("[refresh][配置({})发生变化,重新初始化]", config);
+        this.config = config;
+        // 初始化
+        this.init();
+    }
+
+    @Override
+    public Long getId() {
+        return id;
+    }
+
+}

+ 95 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3FileClient.java

@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.framework.file.core.client.impl.s3;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.file.core.client.impl.AbstractFileClient;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+import java.net.URI;
+
+import static cn.iocoder.yudao.framework.file.core.client.impl.s3.S3FileClientConfig.ENDPOINT_QINIU;
+
+/**
+ * 基于 S3 协议,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
+ *
+ * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
+ *
+ * @author 芋道源码
+ */
+public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
+
+    private S3Client client;
+
+    public S3FileClient(Long id, S3FileClientConfig config) {
+        super(id, config);
+    }
+
+    @Override
+    protected void doInit() {
+        // 补全 domain
+        if (StrUtil.isEmpty(config.getDomain())) {
+            config.setDomain(createDomain());
+        }
+        // 初始化客户端
+        client = S3Client.builder()
+                .serviceConfiguration(sb -> sb.pathStyleAccessEnabled(false) // 关闭路径风格
+                .chunkedEncodingEnabled(false)) // 禁用 chunk
+                .endpointOverride(createURI()) // 上传地址
+                .region(Region.of(config.getRegion())) // Region
+                .credentialsProvider(StaticCredentialsProvider.create( // 认证密钥
+                        AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())))
+                .overrideConfiguration(cb -> cb.addExecutionInterceptor(new S3ModifyPathInterceptor(config.getBucket())))
+                .build();
+    }
+
+    /**
+     * 基于 endpoint 构建调用云服务的 URI 地址
+     *
+     * @return URI 地址
+     */
+    private URI createURI() {
+        String uri;
+        // 如果是七牛,无需拼接 bucket
+        if (config.getEndpoint().contains(ENDPOINT_QINIU)) {
+            uri = StrUtil.format("https://{}", config.getEndpoint());
+        } else {
+            uri = StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
+        }
+        return URI.create(uri);
+    }
+
+    /**
+     * 基于 bucket + endpoint 构建访问的 Domain 地址
+     *
+     * @return Domain 地址
+     */
+    private String createDomain() {
+        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
+    }
+
+    @Override
+    public String upload(byte[] content, String path) {
+        // 执行上传
+        PutObjectRequest.Builder request = PutObjectRequest.builder()
+                .bucket(config.getBucket()) // bucket 必须传递
+                .key(path); // 相对路径作为 key
+        client.putObject(request.build(), RequestBody.fromBytes(content));
+        // 拼接返回路径
+        return config.getDomain() + "/" + path;
+    }
+
+    @Override
+    public void delete(String path) {
+
+    }
+
+    @Override
+    public byte[] getContent(String path) {
+        return new byte[0];
+    }
+
+}

+ 83 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3FileClientConfig.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.framework.file.core.client.impl.s3;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.NotNull;
+
+/**
+ * S3 文件客户端的配置类
+ *
+ * @author 芋道源码
+ */
+@Data
+public class S3FileClientConfig implements FileClientConfig {
+
+    public static final String ENDPOINT_QINIU = "qiniucs.com";
+
+    /**
+     * 节点地址
+     * 1. MinIO:
+     * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
+     * 3. 腾讯云:
+     * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
+     * 5. 华为云:
+     */
+    @NotNull(message = "endpoint 不能为空")
+    private String endpoint;
+    /**
+     * 自定义域名
+     * 1. MinIO:
+     * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html
+     * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142
+     * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
+     * 5. 华为云:
+     */
+    @URL(message = "domain 必须是 URL 格式")
+    private String domain;
+    /**
+     * 区域
+     * 1. MinIO:
+     * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
+     * 3. 腾讯云:
+     * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
+     * 5. 华为云:
+     */
+    @NotNull(message = "region 不能为空")
+    private String region;
+    /**
+     * 存储 Bucket
+     */
+    @NotNull(message = "bucket 不能为空")
+    private String bucket;
+
+    /**
+     * 访问 Key
+     * 1. MinIO:
+     * 2. 阿里云:
+     * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
+     * 4. 七牛云:https://portal.qiniu.com/user/key
+     * 5. 华为云:
+     */
+    @NotNull(message = "accessKey 不能为空")
+    private String accessKey;
+    /**
+     * 访问 Secret
+     */
+    @NotNull(message = "accessSecret 不能为空")
+    private String accessSecret;
+
+    @AssertTrue(message = "domain 不能为空")
+    @SuppressWarnings("RedundantIfStatement")
+    public boolean isDomainValid() {
+        // 如果是七牛,必须带有 domain
+        if (endpoint.contains(ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {
+            return false;
+        }
+        return true;
+    }
+
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/impl/s3/S3ModifyPathInterceptor.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.file.core.client.impl.s3;
+
+import software.amazon.awssdk.core.interceptor.Context;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.http.SdkHttpRequest;
+
+/**
+ * S3 修改路径的拦截器,移除多余的 Bucket 前缀。
+ * 如果不使用该拦截器,希望上传的路径是 /tudou.jpg 时,会被添加成 /bucket/tudou.jpg
+ *
+ * @author 芋道源码
+ */
+public class S3ModifyPathInterceptor implements ExecutionInterceptor {
+
+	private final String bucket;
+
+	public S3ModifyPathInterceptor(String bucket) {
+		this.bucket = "/" + bucket;
+	}
+
+	@Override
+	public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
+		SdkHttpRequest request = context.httpRequest();
+		SdkHttpRequest.Builder rb = SdkHttpRequest.builder().protocol(request.protocol()).host(request.host()).port(request.port())
+				.method(request.method()).headers(request.headers()).rawQueryParameters(request.rawQueryParameters());
+		// 移除 path 前的 bucket 路径
+		if (request.encodedPath().startsWith(bucket)) {
+			rb.encodedPath(request.encodedPath().substring(bucket.length()));
+		} else {
+			rb.encodedPath(request.encodedPath());
+		}
+		return rb.build();
+	}
+
+}

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/config/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位,避免 package 无法提交到 Git 仓库
+ */
+package cn.iocoder.yudao.framework.file.config;

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位,避免 package 无法提交到 Git 仓库
+ */
+package cn.iocoder.yudao.framework.file.core.client;

+ 81 - 0
yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java

@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.framework.file.core.client.s3;
+
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+import cn.iocoder.yudao.framework.file.core.client.impl.s3.S3FileClient;
+import cn.iocoder.yudao.framework.file.core.client.impl.s3.S3FileClientConfig;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import javax.validation.Validation;
+
+public class S3FileClientTest {
+
+    @Test
+    @Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
+    public void testAliyun() {
+        S3FileClientConfig config = new S3FileClientConfig();
+        // 配置成你自己的
+        config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
+        config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY"));
+        config.setBucket("yunai-aoteman");
+        config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn
+        // 默认北京的 endpoint
+        config.setEndpoint("oss-cn-beijing.aliyuncs.com");
+
+        // 执行上传
+        testExecuteUpload(config);
+    }
+
+    @Test
+    @Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
+    public void testQCloud() {
+        S3FileClientConfig config = new S3FileClientConfig();
+        // 配置成你自己的
+        config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
+        config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY"));
+        config.setBucket("aoteman-1255880240");
+        config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
+        // 默认上海的 endpoint
+        config.setEndpoint("cos.ap-shanghai.myqcloud.com");
+        config.setRegion("ap-shanghai");
+
+        // 执行上传
+        testExecuteUpload(config);
+    }
+
+    @Test
+    @Disabled // 七牛云存储,如果要集成测试,可以注释本行
+    public void testQiniu() {
+        S3FileClientConfig config = new S3FileClientConfig();
+        // 配置成你自己的
+        config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
+        config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
+        config.setBucket("s3-test-yudao");
+        config.setDomain("http://r8oo8po1q.hn-bkt.clouddn.com"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn
+        // 默认上海的 endpoint
+        config.setEndpoint("s3-cn-south-1.qiniucs.com");
+
+        // 执行上传
+        testExecuteUpload(config);
+    }
+
+    private void testExecuteUpload(S3FileClientConfig config) {
+        // 补全配置
+        if (config.getRegion() == null) {
+            config.setRegion(StrUtil.subBefore(config.getEndpoint(), '.', false));
+        }
+        ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
+        // 创建 Client
+        S3FileClient client = new S3FileClient(0L, config);
+        client.init();
+        // 上传文件
+        String path = IdUtil.fastSimpleUUID() + ".jpg";
+        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+        String fullPath = client.upload(content, path);
+        System.out.println("访问地址:" + fullPath);
+    }
+
+}

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/enums/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位,避免 package 无法提交到 Git 仓库
+ */
+package cn.iocoder.yudao.framework.file.core.enums;

BIN
yudao-framework/yudao-spring-boot-starter-file/src/test/resources/file/erweima.jpg