woody пре 9 месеци
родитељ
комит
83dd018f22
26 измењених фајлова са 542 додато и 267 уклоњено
  1. 40 0
      framework-aliyun/pom.xml
  2. 25 0
      framework-aliyun/src/main/java/com/chelvc/framework/aliyun/AliyunHandler.java
  3. 94 0
      framework-aliyun/src/main/java/com/chelvc/framework/aliyun/DefaultAliyunHandler.java
  4. 22 0
      framework-aliyun/src/main/java/com/chelvc/framework/aliyun/config/AliyunConfigurer.java
  5. 42 0
      framework-aliyun/src/main/java/com/chelvc/framework/aliyun/config/AliyunProperties.java
  6. 32 0
      framework-base/src/main/java/com/chelvc/framework/base/util/HttpUtils.java
  7. 5 1
      framework-common/src/main/java/com/chelvc/framework/common/util/FileUtils.java
  8. 3 3
      framework-jpush/src/main/java/com/chelvc/framework/jpush/config/JPushConfigurer.java
  9. 5 5
      framework-location/src/main/java/com/chelvc/framework/location/Address.java
  10. 9 0
      framework-location/src/main/java/com/chelvc/framework/location/LocationHandler.java
  11. 20 12
      framework-location/src/main/java/com/chelvc/framework/location/support/CacheableLocationHandler.java
  12. 14 8
      framework-location/src/main/java/com/chelvc/framework/location/support/DelegatingLocationHandler.java
  13. 12 6
      framework-location/src/main/java/com/chelvc/framework/location/support/JuheLocationHandler.java
  14. 37 0
      framework-location/src/main/java/com/chelvc/framework/location/support/TencentAddressResponse.java
  15. 40 5
      framework-location/src/main/java/com/chelvc/framework/location/support/TencentLocationHandler.java
  16. 1 1
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java
  17. 2 2
      framework-sms/pom.xml
  18. 1 1
      framework-sms/src/main/java/com/chelvc/framework/sms/support/DelegatingNormalSmsHandler.java
  19. 8 2
      framework-upload/pom.xml
  20. 27 8
      framework-upload/src/main/java/com/chelvc/framework/upload/UploadHandler.java
  21. 6 28
      framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadConfigurer.java
  22. 27 41
      framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadProperties.java
  23. 14 13
      framework-upload/src/main/java/com/chelvc/framework/upload/support/DefaultUploadHandler.java
  24. 0 93
      framework-upload/src/main/java/com/chelvc/framework/upload/support/DelegatingUploadHandler.java
  25. 55 38
      framework-upload/src/main/java/com/chelvc/framework/upload/support/TencentUploadHandler.java
  26. 1 0
      pom.xml

+ 40 - 0
framework-aliyun/pom.xml

@@ -0,0 +1,40 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.chelvc.framework</groupId>
+        <artifactId>framework-dependencies</artifactId>
+        <version>1.0.0-RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>framework-aliyun</artifactId>
+    <version>1.0.0-RELEASE</version>
+
+    <properties>
+        <aliyun-captcha.version>1.1.2</aliyun-captcha.version>
+        <aliyun-dypnsapi.version>1.2.1</aliyun-dypnsapi.version>
+        <framework-base.version>1.0.0-RELEASE</framework-base.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.chelvc.framework</groupId>
+            <artifactId>framework-base</artifactId>
+            <version>${framework-base.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>captcha20230305</artifactId>
+            <version>${aliyun-captcha.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dypnsapi20170525</artifactId>
+            <version>${aliyun-dypnsapi.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 25 - 0
framework-aliyun/src/main/java/com/chelvc/framework/aliyun/AliyunHandler.java

@@ -0,0 +1,25 @@
+package com.chelvc.framework.aliyun;
+
+/**
+ * 阿里云操作接口
+ *
+ * @author Woody
+ * @date 2024/8/12
+ */
+public interface AliyunHandler {
+    /**
+     * 根据临时令牌获取手机号
+     *
+     * @param token 临时令牌
+     * @return 手机号
+     */
+    String code2mobile(String token);
+
+    /**
+     * 校验智能验证码
+     *
+     * @param verification 验证信息
+     * @return true/false
+     */
+    boolean verifyCaptcha(String verification);
+}

+ 94 - 0
framework-aliyun/src/main/java/com/chelvc/framework/aliyun/DefaultAliyunHandler.java

@@ -0,0 +1,94 @@
+package com.chelvc.framework.aliyun;
+
+import java.util.Objects;
+
+import com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaRequest;
+import com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaResponse;
+import com.aliyun.dypnsapi20170525.models.GetMobileRequest;
+import com.aliyun.dypnsapi20170525.models.GetMobileResponse;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import com.chelvc.framework.aliyun.config.AliyunProperties;
+import lombok.NonNull;
+
+/**
+ * 阿里云操作接口默认实现
+ *
+ * @author Woody
+ * @date 2024/8/12
+ */
+public class DefaultAliyunHandler implements AliyunHandler {
+    private final AliyunProperties properties;
+    private final com.aliyun.captcha20230305.Client captcha;
+    private final com.aliyun.dypnsapi20170525.Client dypnsapi;
+
+    public DefaultAliyunHandler(@NonNull AliyunProperties properties) {
+        this.properties = properties;
+
+        try {
+            // 初始化captcha客户端
+            Config config = this.getConfig(properties);
+            config.endpoint = "captcha.cn-shanghai.aliyuncs.com";
+            this.captcha = new com.aliyun.captcha20230305.Client(config);
+
+            // 初始化dypnsapi客户端
+            config = this.getConfig(properties);
+            config.endpoint = "dypnsapi.aliyuncs.com";
+            this.dypnsapi = new com.aliyun.dypnsapi20170525.Client(config);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 获取配置对象
+     *
+     * @param properties 配置信息
+     * @return 配置对象实例
+     */
+    private Config getConfig(AliyunProperties properties) {
+        return new Config().setAccessKeyId(properties.getAppid()).setAccessKeySecret(properties.getSecret());
+    }
+
+    @Override
+    public String code2mobile(@NonNull String token) {
+        RuntimeOptions runtime = new RuntimeOptions();
+        GetMobileRequest request = new GetMobileRequest().setAccessToken(token);
+        GetMobileResponse response;
+        try {
+            response = this.dypnsapi.getMobileWithOptions(request, runtime);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        if (response == null || response.body == null) {
+            throw new RuntimeException("Invalid aliyun response");
+        }
+        if (!Objects.equals(response.body.code, "OK")) {
+            throw new RuntimeException(response.body.code + ", " + response.body.message);
+        }
+        return response.body.getGetMobileResultDTO().getMobile();
+    }
+
+    @Override
+    public boolean verifyCaptcha(@NonNull String verification) {
+        VerifyIntelligentCaptchaRequest request = new VerifyIntelligentCaptchaRequest();
+        request.captchaVerifyParam = verification;
+        request.sceneId = this.properties.getCaptcha().getScene();
+        VerifyIntelligentCaptchaResponse response;
+        try {
+            response = this.captcha.verifyIntelligentCaptcha(request);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        if (response == null || response.body == null || response.body.result == null) {
+            throw new RuntimeException("Invalid aliyun response");
+        }
+        if (!Boolean.TRUE.equals(response.body.success)) {
+            throw new RuntimeException(response.body.code + ", " + response.body.message);
+        }
+        if (!Boolean.TRUE.equals(response.body.result.verifyResult)) {
+            throw new RuntimeException(response.body.result.verifyCode);
+        }
+        return true;
+    }
+}

+ 22 - 0
framework-aliyun/src/main/java/com/chelvc/framework/aliyun/config/AliyunConfigurer.java

@@ -0,0 +1,22 @@
+package com.chelvc.framework.aliyun.config;
+
+import com.chelvc.framework.aliyun.AliyunHandler;
+import com.chelvc.framework.aliyun.DefaultAliyunHandler;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 阿里云配置类
+ *
+ * @author Woody
+ * @date 2024/3/11
+ */
+@Configuration
+public class AliyunConfigurer {
+    @Bean
+    @ConditionalOnMissingBean(AliyunHandler.class)
+    public AliyunHandler aliyunHandler(AliyunProperties properties) {
+        return new DefaultAliyunHandler(properties);
+    }
+}

+ 42 - 0
framework-aliyun/src/main/java/com/chelvc/framework/aliyun/config/AliyunProperties.java

@@ -0,0 +1,42 @@
+package com.chelvc.framework.aliyun.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 阿里云配置信息
+ *
+ * @author Woody
+ * @date 2024/3/11
+ */
+@Data
+@Configuration
+@ConfigurationProperties("aliyun")
+public class AliyunProperties {
+    /**
+     * 应用标识
+     */
+    private String appid;
+
+    /**
+     * 应用密钥
+     */
+    private String secret;
+
+    /**
+     * 智能验证码配置
+     */
+    private final Captcha captcha = new Captcha();
+
+    /**
+     * 智能验证码信息
+     */
+    @Data
+    public static class Captcha {
+        /**
+         * 场景标识
+         */
+        private String scene;
+    }
+}

+ 32 - 0
framework-base/src/main/java/com/chelvc/framework/base/util/HttpUtils.java

@@ -32,6 +32,7 @@ import com.google.common.collect.Maps;
 import lombok.NonNull;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.springframework.http.MediaType;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * Http工具类
@@ -417,4 +418,35 @@ public final class HttpUtils {
         Matcher matcher = CONTENT_TYPE_CHARSET_PATTERN.matcher(type);
         return matcher.find() ? Charset.forName(matcher.group(1)) : StandardCharsets.UTF_8;
     }
+
+    /**
+     * 获取文件后缀名
+     *
+     * @param file 文件上传对象
+     * @return 后缀名
+     */
+    public static String getSuffix(@NonNull MultipartFile file) {
+        return getSuffix(file, null);
+    }
+
+    /**
+     * 获取文件后缀名
+     *
+     * @param file          文件上传对象
+     * @param defaultSuffix 默认后缀名
+     * @return 后缀名
+     */
+    public static String getSuffix(@NonNull MultipartFile file, String defaultSuffix) {
+        return StringUtils.ifEmpty(ObjectUtils.ifNull(file.getOriginalFilename(), FileUtils::getSuffix), defaultSuffix);
+    }
+
+    /**
+     * 生成文件上传文件名
+     *
+     * @param file 文件上传对象
+     * @return 文件名
+     */
+    public static String generateUploadFilename(@NonNull MultipartFile file) {
+        return StringUtils.uuid() + StringUtils.ifEmpty(getSuffix(file), suffix -> "." + suffix, StringUtils.EMPTY);
+    }
 }

+ 5 - 1
framework-common/src/main/java/com/chelvc/framework/common/util/FileUtils.java

@@ -78,7 +78,11 @@ public final class FileUtils {
      */
     public static String getSuffix(@NonNull String filename) {
         int separator = filename.lastIndexOf('.');
-        return separator < 0 ? null : StringUtils.ifEmpty(filename.substring(separator + 1), (String) null);
+        if (separator < 0) {
+            return null;
+        }
+        String suffix = StringUtils.ifEmpty(filename.substring(separator + 1), (String) null);
+        return StringUtils.ifEmpty(suffix, String::toLowerCase);
     }
 
     /**

+ 3 - 3
framework-jpush/src/main/java/com/chelvc/framework/jpush/config/JPushConfigurer.java

@@ -34,7 +34,7 @@ public class JPushConfigurer {
 
     @Bean
     @ConditionalOnMissingBean(JPushClient.class)
-    public JPushClient pushClient() {
+    public JPushClient jpushClient() {
         return new JPushClient(this.properties.getSecret(), this.properties.getAppid()) {
             {
                 Field field = ObjectUtils.getField(JPushClient.class, "_pushClient");
@@ -56,7 +56,7 @@ public class JPushConfigurer {
 
     @Bean
     @ConditionalOnMissingBean(JMessageClient.class)
-    public JMessageClient messageClient() {
+    public JMessageClient jmessageClient() {
         JMessageClient client = new JMessageClient(this.properties.getAppid(), this.properties.getSecret());
         client.setHttpClient(new ApacheHttpClient(
                 ServiceHelper.getBasicAuthorization(this.properties.getAppid(), this.properties.getSecret()),
@@ -68,7 +68,7 @@ public class JPushConfigurer {
 
     @Bean
     @ConditionalOnMissingBean(JPushHandler.class)
-    public JPushHandler pushHandler(JPushClient pushClient, JMessageClient messageClient) {
+    public JPushHandler jpushHandler(JPushClient pushClient, JMessageClient messageClient) {
         return new DefaultJPushHandler(pushClient, messageClient, this.properties);
     }
 }

+ 5 - 5
framework-location/src/main/java/com/chelvc/framework/location/Address.java

@@ -30,6 +30,11 @@ public class Address implements Serializable {
      */
     private String name;
 
+    /**
+     * 地址全名
+     */
+    private String fullname;
+
     /**
      * 经度
      */
@@ -54,9 +59,4 @@ public class Address implements Serializable {
      * 区
      */
     private Area district;
-
-    /**
-     * 详细地址
-     */
-    private String address;
 }

+ 9 - 0
framework-location/src/main/java/com/chelvc/framework/location/LocationHandler.java

@@ -1,5 +1,7 @@
 package com.chelvc.framework.location;
 
+import java.util.List;
+
 /**
  * 地址定位处理接口
  *
@@ -7,6 +9,13 @@ package com.chelvc.framework.location;
  * @date 2024/1/30
  */
 public interface LocationHandler {
+    /**
+     * 获取所有地址
+     *
+     * @return 地址信息列表
+     */
+    List<Address> addresses();
+
     /**
      * 根据IP定位地址
      *

+ 20 - 12
framework-location/src/main/java/com/chelvc/framework/location/support/CacheableLocationHandler.java

@@ -1,6 +1,7 @@
 package com.chelvc.framework.location.support;
 
 import java.time.Duration;
+import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Supplier;
 
@@ -27,31 +28,38 @@ public class CacheableLocationHandler implements LocationHandler {
     }
 
     /**
-     * 执行地址定位解析
+     * 地址定位缓存处理
      *
-     * @param code     地址标识
-     * @param supplier 地址解析回调函数
-     * @return 地址信息
+     * @param code     缓存标识
+     * @param supplier 地址定位处理回调函数
+     * @param <T>      数据类型
+     * @return 结果数据
      */
-    private Address execute(String code, Supplier<Address> supplier) {
+    @SuppressWarnings("unchecked")
+    private <T> T execute(String code, Supplier<T> supplier) {
         String key = "location:" + code;
-        Address address = null;
+        T t = null;
         try {
-            address = (Address) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
+            t = (T) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
         } catch (Exception e) {
-            log.warn("Redis location address get failed: {}", e.getMessage());
+            log.warn("Location cache handle failed: {}", e.getMessage());
         }
-        if (address == null && (address = supplier.get()) != null) {
+        if (t == null && (t = supplier.get()) != null) {
             // 为避免缓存雪崩,缓存过期时间增加随机数
             int random = ThreadLocalRandom.current().nextInt(60);
             Duration duration = Duration.ofSeconds(this.expiration + random);
             try {
-                RedisContextHolder.getDefaultTemplate().opsForValue().set(key, address, duration);
+                RedisContextHolder.getDefaultTemplate().opsForValue().set(key, t, duration);
             } catch (Exception e) {
-                log.warn("Redis location address save failed: {}", e.getMessage());
+                log.warn("Location cache handle failed: {}", e.getMessage());
             }
         }
-        return address;
+        return t;
+    }
+
+    @Override
+    public List<Address> addresses() {
+        return this.handler.addresses();
     }
 
     @Override

+ 14 - 8
framework-location/src/main/java/com/chelvc/framework/location/support/DelegatingLocationHandler.java

@@ -26,33 +26,39 @@ public class DelegatingLocationHandler implements LocationHandler {
     }
 
     /**
-     * 遍历所有地址定位处理器直到定位成功
+     * 地址定位处理
      *
      * @param callback 地址定位处理回调函数
-     * @return 地址信息
+     * @param <T>      数据类型
+     * @return 结果数据
      */
-    private Address execute(Function<LocationHandler, Address> callback) {
+    private <T> T execute(Function<LocationHandler, T> callback) {
         Exception unsupported = null;
         for (LocationHandler handler : this.handlers) {
             try {
-                Address address = callback.apply(handler);
-                if (address != null) {
-                    return address;
+                T t = callback.apply(handler);
+                if (t != null) {
+                    return t;
                 }
             } catch (Exception e) {
                 if (e instanceof UnsupportedOperationException) {
                     unsupported = e;
                 } else {
-                    log.warn("Address locate failed: {}", e.getMessage());
+                    log.warn("Location handle failed: {}", e.getMessage());
                 }
             }
         }
         if (unsupported != null) {
-            log.warn("Address locate failed: {}", unsupported.getMessage());
+            log.warn("Location handle failed: {}", unsupported.getMessage());
         }
         return null;
     }
 
+    @Override
+    public List<Address> addresses() {
+        return this.execute(LocationHandler::addresses);
+    }
+
     @Override
     public Address ip2address(@NonNull String ip) {
         return this.execute(handler -> handler.ip2address(ip));

+ 12 - 6
framework-location/src/main/java/com/chelvc/framework/location/support/JuheLocationHandler.java

@@ -1,5 +1,6 @@
 package com.chelvc.framework.location.support;
 
+import java.util.List;
 import java.util.Objects;
 
 import com.chelvc.framework.base.context.RestContextHolder;
@@ -37,11 +38,11 @@ public class JuheLocationHandler implements LocationHandler {
      * @param parameter 定位参数
      * @return 地址信息
      */
-    private Address execute(String url, String parameter) {
+    private Address locate(String url, String parameter) {
         url = String.format(url, parameter, this.key);
         boolean debug = log.isDebugEnabled();
         if (debug) {
-            log.debug("Juhe location request: {}, {}", url, parameter);
+            log.debug("Juhe locate request: {}, {}", url, parameter);
         }
         JuheLocationResponse response;
         try {
@@ -63,12 +64,17 @@ public class JuheLocationHandler implements LocationHandler {
             return null;
         } else if (Objects.equals(province, city) || StringUtils.isEmpty(city)) {
             // 针对直辖市省、市名称相同的地址只返回市名称
-            return Address.builder().province(Area.builder().name(province).build()).address(province).build();
+            return Address.builder().province(Area.builder().name(province).build()).fullname(province).build();
         } else if (StringUtils.isEmpty(province)) {
-            return Address.builder().city(Area.builder().name(city).build()).address(city).build();
+            return Address.builder().city(Area.builder().name(city).build()).fullname(city).build();
         }
         return Address.builder().province(Area.builder().name(province).build())
-                .city(Area.builder().name(city).build()).address(province + "·" + city).build();
+                .city(Area.builder().name(city).build()).fullname(province + "·" + city).build();
+    }
+
+    @Override
+    public List<Address> addresses() {
+        throw new UnsupportedOperationException();
     }
 
     @Override
@@ -78,7 +84,7 @@ public class JuheLocationHandler implements LocationHandler {
 
     @Override
     public Address mobile2address(String mobile) {
-        return this.execute(MOBILE2ADDRESS_URL, mobile);
+        return this.locate(MOBILE2ADDRESS_URL, mobile);
     }
 
     @Override

+ 37 - 0
framework-location/src/main/java/com/chelvc/framework/location/support/TencentAddressResponse.java

@@ -0,0 +1,37 @@
+package com.chelvc.framework.location.support;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.chelvc.framework.location.Address;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * 腾讯地址响应信息
+ *
+ * @author Woody
+ * @date 2024/4/3
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TencentAddressResponse implements Serializable {
+    /**
+     * 响应状态
+     */
+    private Integer status;
+
+    /**
+     * 描述信息
+     */
+    private String message;
+
+    /**
+     * 地址列表
+     */
+    private List<List<Address>> result;
+}

+ 40 - 5
framework-location/src/main/java/com/chelvc/framework/location/support/TencentLocationHandler.java

@@ -1,5 +1,7 @@
 package com.chelvc.framework.location.support;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -22,6 +24,11 @@ import lombok.extern.slf4j.Slf4j;
  */
 @Slf4j
 public class TencentLocationHandler implements LocationHandler {
+    /**
+     * 获取行政区划接口地址
+     */
+    private static final String GET_ADDRESS_URL = "https://apis.map.qq.com/ws/district/v1/list?key=%s&output=JSON";
+
     /**
      * IP定位接口地址
      */
@@ -45,11 +52,11 @@ public class TencentLocationHandler implements LocationHandler {
      * @param parameter 定位参数
      * @return 地址信息
      */
-    private Address execute(String url, String parameter) {
+    private Address locate(String url, String parameter) {
         url = String.format(url, parameter, this.key);
         boolean debug = log.isDebugEnabled();
         if (debug) {
-            log.debug("Tencent location request: {}, {}", url, parameter);
+            log.debug("Tencent locate request: {}, {}", url, parameter);
         }
         TencentLocationResponse response;
         try {
@@ -77,7 +84,7 @@ public class TencentLocationHandler implements LocationHandler {
         Double longitude = ObjectUtils.ifNull(location, TencentLocationResponse.Location::getLongitude);
         Double latitude = ObjectUtils.ifNull(location, TencentLocationResponse.Location::getLatitude);
         Address address = Address.builder().id(region.getCode()).longitude(longitude).latitude(latitude)
-                .address(response.getResult().getAddress()).build();
+                .fullname(response.getResult().getAddress()).build();
         address.setName(Stream.of(position.getProvince(), position.getCity(), position.getDistrict())
                 .filter(StringUtils::notEmpty).collect(Collectors.joining("·")
                 ));
@@ -92,9 +99,37 @@ public class TencentLocationHandler implements LocationHandler {
         return address;
     }
 
+    @Override
+    public List<Address> addresses() {
+        String url = String.format(GET_ADDRESS_URL, this.key);
+        boolean debug = log.isDebugEnabled();
+        if (debug) {
+            log.debug("Tencent addressing request: {}", url);
+        }
+        TencentAddressResponse response;
+        try {
+            response = RestContextHolder.get(url, TencentAddressResponse.class);
+        } catch (Exception e) {
+            log.warn("Tencent addressing failed: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+        if (debug) {
+            log.debug("Tencent addressing response: {}", response);
+        }
+        if (response == null || response.getStatus() == null || response.getStatus() != 0) {
+            log.warn("Tencent addressing failed: {}", response);
+            return Collections.emptyList();
+        }
+        if (ObjectUtils.isEmpty(response.getResult())) {
+            return Collections.emptyList();
+        }
+        return response.getResult().stream().filter(ObjectUtils::notEmpty).flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
     @Override
     public Address ip2address(@NonNull String ip) {
-        return this.execute(IP2ADDRESS_URL, ip);
+        return this.locate(IP2ADDRESS_URL, ip);
     }
 
     @Override
@@ -104,7 +139,7 @@ public class TencentLocationHandler implements LocationHandler {
 
     @Override
     public Address location2address(@NonNull String location) {
-        return this.execute(LOCATION2ADDRESS_URL, location);
+        return this.locate(LOCATION2ADDRESS_URL, location);
     }
 
     @Override

+ 1 - 1
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java

@@ -1326,7 +1326,7 @@ public final class RedisContextHolder {
      */
     public static String random(@NonNull Supplier<String> supplier, @NonNull Duration duration) {
         RedisConnectionFactory factory = getDefaultConnectionFactory();
-        for (int i = 0; i < 10; i++) {
+        for (int i = 0; i < 3; i++) {
             String value = supplier.get();
             byte[] key = (RANDOM_REDIS_PREFIX + value).getBytes(StandardCharsets.UTF_8);
             Boolean success = execute(factory, connection -> connection.set(key, ObjectUtils.EMPTY,

+ 2 - 2
framework-sms/pom.xml

@@ -16,7 +16,7 @@
 
     <properties>
         <qcloudsms.version>1.0.5</qcloudsms.version>
-        <dysmsapi20170525.version>2.0.23</dysmsapi20170525.version>
+        <aliyun-dysmsapi.version>2.0.23</aliyun-dysmsapi.version>
         <framework-base.version>1.0.0-RELEASE</framework-base.version>
         <framework-redis.version>1.0.0-RELEASE</framework-redis.version>
     </properties>
@@ -35,7 +35,7 @@
         <dependency>
             <groupId>com.aliyun</groupId>
             <artifactId>dysmsapi20170525</artifactId>
-            <version>${dysmsapi20170525.version}</version>
+            <version>${aliyun-dysmsapi.version}</version>
         </dependency>
         <dependency>
             <groupId>com.github.qcloudsms</groupId>

+ 1 - 1
framework-sms/src/main/java/com/chelvc/framework/sms/support/DelegatingNormalSmsHandler.java

@@ -25,7 +25,7 @@ public class DelegatingNormalSmsHandler implements NormalSmsHandler {
     }
 
     /**
-     * 遍历所有短信处理器并执行短信发送处理,直到发送成功
+     * 执行短信发送处理
      *
      * @param callback 短信处理回调函数
      * @return true/false

+ 8 - 2
framework-upload/pom.xml

@@ -15,7 +15,8 @@
     <version>1.0.0-RELEASE</version>
 
     <properties>
-        <cos.version>5.6.8</cos.version>
+        <tencent-cos.version>5.6.8</tencent-cos.version>
+        <tencent-cdn.version>3.1.1069</tencent-cdn.version>
         <framework-base.version>1.0.0-RELEASE</framework-base.version>
     </properties>
 
@@ -28,7 +29,12 @@
         <dependency>
             <groupId>com.qcloud</groupId>
             <artifactId>cos_api</artifactId>
-            <version>${cos.version}</version>
+            <version>${tencent-cos.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java-cdn</artifactId>
+            <version>${tencent-cdn.version}</version>
         </dependency>
     </dependencies>
 </project>

+ 27 - 8
framework-upload/src/main/java/com/chelvc/framework/upload/UploadHandler.java

@@ -17,6 +17,13 @@ public interface UploadHandler {
     default void initialize() {
     }
 
+    /**
+     * 刷新CDN
+     *
+     * @param urls 资源地址数组
+     */
+    void refreshContentDeliveryNetwork(String... urls);
+
     /**
      * 文件上传
      *
@@ -24,28 +31,40 @@ public interface UploadHandler {
      * @return 文件访问地址
      * @throws IOException I/O异常
      */
-    String upload(File file) throws IOException;
+    default String upload(File file) throws IOException {
+        return this.upload(file, file.getName());
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param file     文件对象
+     * @param filename 文件名称
+     * @return 文件访问地址
+     * @throws IOException I/O异常
+     */
+    String upload(File file, String filename) throws IOException;
 
     /**
      * 文件上传
      *
-     * @param stream 文件流
-     * @param suffix 后缀名
+     * @param stream   文件流
+     * @param filename 文件名称
      * @return 文件访问地址
      * @throws IOException I/O异常
      */
-    String upload(InputStream stream, String suffix) throws IOException;
+    String upload(InputStream stream, String filename) throws IOException;
 
     /**
      * 文件上传
      *
-     * @param stream 文件流
-     * @param suffix 后缀名
-     * @param length 文件大小
+     * @param stream   文件流
+     * @param filename 文件名称
+     * @param length   文件大小
      * @return 文件访问地址
      * @throws IOException I/O异常
      */
-    String upload(InputStream stream, String suffix, long length) throws IOException;
+    String upload(InputStream stream, String filename, long length) throws IOException;
 
     /**
      * 销毁处理器

+ 6 - 28
framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadConfigurer.java

@@ -1,15 +1,8 @@
 package com.chelvc.framework.upload.config;
 
-import java.util.List;
-import java.util.stream.Collectors;
-
-import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.upload.UploadHandler;
 import com.chelvc.framework.upload.support.DefaultUploadHandler;
-import com.chelvc.framework.upload.support.DelegatingUploadHandler;
 import com.chelvc.framework.upload.support.TencentUploadHandler;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -21,31 +14,16 @@ import org.springframework.context.annotation.Configuration;
  * @date 2024/1/30
  */
 @Configuration
-@RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class UploadConfigurer {
-    private final UploadProperties properties;
-
-    /**
-     * 初始化文件上传处理器实例
-     *
-     * @param client 客户端配置
-     * @return 文件上传处理器实例
-     */
-    private UploadHandler initializeUploadHandler(UploadProperties.Client client) {
-        UploadProperties.Channel channel = client.getChannel();
+    @Bean(initMethod = "initialize", destroyMethod = "destroy")
+    @ConditionalOnMissingBean(UploadHandler.class)
+    public UploadHandler uploadHandler(UploadProperties properties) {
+        UploadProperties.Channel channel = properties.getChannel();
         if (channel == UploadProperties.Channel.DEFAULT) {
-            return new DefaultUploadHandler(client);
+            return new DefaultUploadHandler(properties);
         } else if (channel == UploadProperties.Channel.TENCENT) {
-            return new TencentUploadHandler(client, this.properties.getHttpclient());
+            return new TencentUploadHandler(properties);
         }
         throw new UnsupportedOperationException(String.valueOf(channel));
     }
-
-    @Bean(initMethod = "initialize", destroyMethod = "destroy")
-    @ConditionalOnMissingBean(UploadHandler.class)
-    public UploadHandler uploadHandler() {
-        List<UploadHandler> handlers = this.properties.getClients().stream()
-                .map(this::initializeUploadHandler).collect(Collectors.toList());
-        return ObjectUtils.isEmpty(handlers) ? null : new DelegatingUploadHandler(handlers);
-    }
 }

+ 27 - 41
framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadProperties.java

@@ -1,8 +1,5 @@
 package com.chelvc.framework.upload.config;
 
-import java.util.Collections;
-import java.util.List;
-
 import com.chelvc.framework.base.config.HttpClientProperties;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -18,16 +15,6 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 @ConfigurationProperties("upload")
 public class UploadProperties {
-    /**
-     * 文件上传客户端配置
-     */
-    private List<Client> clients = Collections.emptyList();
-
-    /**
-     * http链接配置
-     */
-    private final HttpClientProperties httpclient = new HttpClientProperties();
-
     /**
      * 文件上传渠道枚举
      */
@@ -44,38 +31,37 @@ public class UploadProperties {
     }
 
     /**
-     * 客户端配置
+     * 应用标识
      */
-    @Data
-    public static class Client {
-        /**
-         * 应用标识
-         */
-        private String appid;
+    private String appid;
 
-        /**
-         * 应用密钥
-         */
-        private String secret;
+    /**
+     * 应用密钥
+     */
+    private String secret;
 
-        /**
-         * 地区标识
-         */
-        private String region;
+    /**
+     * 地区标识
+     */
+    private String region;
 
-        /**
-         * 存储路径
-         */
-        private String path;
+    /**
+     * 访问域名
+     */
+    private String domain;
 
-        /**
-         * 文件访问域名
-         */
-        private String domain;
+    /**
+     * 存储目录
+     */
+    private String directory;
 
-        /**
-         * 文件上传渠道
-         */
-        private Channel channel;
-    }
+    /**
+     * 上传渠道
+     */
+    private Channel channel;
+
+    /**
+     * http链接配置
+     */
+    private final HttpClientProperties httpclient = new HttpClientProperties();
 }

+ 14 - 13
framework-upload/src/main/java/com/chelvc/framework/upload/support/DefaultUploadHandler.java

@@ -7,7 +7,6 @@ import java.io.InputStream;
 import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.util.AssertUtils;
 import com.chelvc.framework.common.util.FileUtils;
-import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.upload.UploadHandler;
 import com.chelvc.framework.upload.config.UploadProperties;
 import lombok.NonNull;
@@ -19,31 +18,33 @@ import lombok.NonNull;
  * @date 2024/1/30
  */
 public class DefaultUploadHandler implements UploadHandler {
-    private final String path;
     private final String domain;
+    private final String directory;
 
-    public DefaultUploadHandler(@NonNull UploadProperties.Client properties) {
-        this.path = AssertUtils.nonempty(properties.getPath(), () -> "Local upload path is missing");
+    public DefaultUploadHandler(@NonNull UploadProperties properties) {
         this.domain = AssertUtils.nonempty(properties.getDomain(), () -> "Local upload access domain is missing");
+        this.directory = AssertUtils.nonempty(properties.getDirectory(), () -> "Local upload directory is missing");
     }
 
     @Override
-    public String upload(@NonNull File file) throws IOException {
-        String suffix = FileUtils.getSuffix(file);
-        String filename = StringUtils.uuid() + StringUtils.ifEmpty(suffix, name -> "." + name, StringUtils.EMPTY);
-        FileUtils.write(new File(this.path, filename), file);
+    public void refreshContentDeliveryNetwork(String... urls) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String upload(@NonNull File file, @NonNull String filename) throws IOException {
+        FileUtils.write(new File(this.directory, filename), file);
         return HttpUtils.url(this.domain, filename);
     }
 
     @Override
-    public String upload(@NonNull InputStream stream, String suffix) throws IOException {
-        return this.upload(stream, suffix, stream.available());
+    public String upload(@NonNull InputStream stream, @NonNull String filename) throws IOException {
+        return this.upload(stream, filename, stream.available());
     }
 
     @Override
-    public String upload(@NonNull InputStream stream, String suffix, long length) throws IOException {
-        String filename = StringUtils.uuid() + StringUtils.ifEmpty(suffix, name -> "." + name, StringUtils.EMPTY);
-        FileUtils.write(new File(this.path, filename), stream);
+    public String upload(@NonNull InputStream stream, @NonNull String filename, long length) throws IOException {
+        FileUtils.write(new File(this.directory, filename), stream);
         return HttpUtils.url(this.domain, filename);
     }
 }

+ 0 - 93
framework-upload/src/main/java/com/chelvc/framework/upload/support/DelegatingUploadHandler.java

@@ -1,93 +0,0 @@
-package com.chelvc.framework.upload.support;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-import com.chelvc.framework.common.util.AssertUtils;
-import com.chelvc.framework.upload.UploadHandler;
-import com.google.common.collect.Lists;
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * 文件上传处理器委派实现
- *
- * @author Woody
- * @date 2024/1/30
- */
-@Slf4j
-public class DelegatingUploadHandler implements UploadHandler {
-    private final List<UploadHandler> handlers;
-
-    public DelegatingUploadHandler(@NonNull List<UploadHandler> handlers) {
-        AssertUtils.nonempty(handlers, () -> "Delegating handlers must not be empty");
-        this.handlers = Lists.newArrayList(handlers);
-    }
-
-    /**
-     * 文件上传执行器接口
-     */
-    private interface Executor {
-        /**
-         * 执行文件上传处理
-         *
-         * @param handler 文件上传处理器
-         * @return 文件路径
-         * @throws IOException I/O异常
-         */
-        String execute(UploadHandler handler) throws IOException;
-    }
-
-    /**
-     * 遍历所有短信处理器并执行文件上传处理,直到上传成功
-     *
-     * @param executor 文件上传执行器
-     */
-    private String execute(Executor executor) throws IOException {
-        RuntimeException unsupported = null;
-        for (int i = 0, last = this.handlers.size() - 1; i <= last; i++) {
-            try {
-                return executor.execute(this.handlers.get(i));
-            } catch (Exception e) {
-                if (e instanceof UnsupportedOperationException) {
-                    unsupported = (UnsupportedOperationException) e;
-                } else if (i < last) {
-                    log.warn("File upload failed: {}", e.getMessage());
-                } else {
-                    throw e;
-                }
-            }
-        }
-        if (unsupported != null) {
-            throw unsupported;
-        }
-        return null;
-    }
-
-    @Override
-    public void initialize() {
-        this.handlers.forEach(UploadHandler::initialize);
-    }
-
-    @Override
-    public String upload(@NonNull File file) throws IOException {
-        return this.execute(handler -> handler.upload(file));
-    }
-
-    @Override
-    public String upload(@NonNull InputStream stream, String suffix) throws IOException {
-        return this.execute(handler -> handler.upload(stream, suffix));
-    }
-
-    @Override
-    public String upload(@NonNull InputStream stream, String suffix, long length) throws IOException {
-        return this.execute(handler -> handler.upload(stream, suffix, length));
-    }
-
-    @Override
-    public void destroy() {
-        this.handlers.forEach(UploadHandler::destroy);
-    }
-}

+ 55 - 38
framework-upload/src/main/java/com/chelvc/framework/upload/support/TencentUploadHandler.java

@@ -4,9 +4,9 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 
-import com.chelvc.framework.base.config.HttpClientProperties;
+import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.util.AssertUtils;
-import com.chelvc.framework.common.util.FileUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.upload.UploadHandler;
 import com.chelvc.framework.upload.config.UploadProperties;
@@ -16,6 +16,12 @@ import com.qcloud.cos.auth.BasicCOSCredentials;
 import com.qcloud.cos.http.HttpProtocol;
 import com.qcloud.cos.model.ObjectMetadata;
 import com.qcloud.cos.region.Region;
+import com.tencentcloudapi.cdn.v20180606.CdnClient;
+import com.tencentcloudapi.cdn.v20180606.models.PurgeUrlsCacheRequest;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
 import lombok.NonNull;
 
 /**
@@ -25,63 +31,74 @@ import lombok.NonNull;
  * @date 2024/1/30
  */
 public class TencentUploadHandler implements UploadHandler {
-    private final String path;
-    private final String region;
-    private final COSClient client;
+    private final String domain;
+    private final String directory;
+    private final COSClient cos;
+    private final CdnClient cdn;
 
-    public TencentUploadHandler(@NonNull UploadProperties.Client properties, @NonNull HttpClientProperties httpClient) {
-        this.path = AssertUtils.nonempty(properties.getPath(), () -> "Tencent upload path is missing");
-        this.region = AssertUtils.nonempty(properties.getRegion(), () -> "Tencent upload region is missing");
+    public TencentUploadHandler(@NonNull UploadProperties properties) {
+        String appid = AssertUtils.nonempty(properties.getAppid(), () -> "Tencent upload appid is missing");
+        String secret = AssertUtils.nonempty(properties.getSecret(), () -> "Tencent upload secret is missing");
+        String region = AssertUtils.nonempty(properties.getRegion(), () -> "Tencent upload region is missing");
+        this.domain = AssertUtils.nonempty(properties.getDomain(), () -> "Tencent upload access domain is missing");
+        this.directory = AssertUtils.nonempty(properties.getDirectory(), () -> "Tencent upload directory is missing");
 
-        // 构建客户端配置
+        // 初始化COS客户端
         ClientConfig config = new ClientConfig();
-        config.setRegion(new Region(this.region));
+        config.setRegion(new Region(region));
         config.setHttpProtocol(HttpProtocol.https);
-        config.setMaxConnectionsCount(httpClient.getMaxTotal());
-        config.setConnectionTimeout(httpClient.getConnectTimeout());
-        config.setSocketTimeout(httpClient.getSocketTimeout());
-        config.setConnectionRequestTimeout(httpClient.getConnectionRequestTimeout());
+        config.setMaxConnectionsCount(properties.getHttpclient().getMaxTotal());
+        config.setConnectionTimeout(properties.getHttpclient().getConnectTimeout());
+        config.setSocketTimeout(properties.getHttpclient().getSocketTimeout());
+        config.setConnectionRequestTimeout(properties.getHttpclient().getConnectionRequestTimeout());
+        this.cos = new COSClient(new BasicCOSCredentials(appid, secret), config);
 
-        // 初始化客户端实例
-        String appid = AssertUtils.nonempty(properties.getAppid(), () -> "Tencent upload appid is missing");
-        String secret = AssertUtils.nonempty(properties.getSecret(), () -> "Tencent upload secret is missing");
-        this.client = new COSClient(new BasicCOSCredentials(appid, secret), config);
+        // 初始化CDN客户端
+        HttpProfile http = new HttpProfile();
+        http.setConnTimeout(properties.getHttpclient().getConnectTimeout());
+        http.setReadTimeout(properties.getHttpclient().getSocketTimeout());
+        ClientProfile profile = new ClientProfile();
+        profile.setHttpProfile(http);
+        Credential credential = new Credential(appid, secret);
+        this.cdn = new CdnClient(credential, StringUtils.EMPTY, profile);
     }
 
-    /**
-     * 将文件名称转换成URL地址
-     *
-     * @param filename 文件名称
-     * @return URL地址
-     */
-    private String name2url(String filename) {
-        return "https://" + this.path + ".cos." + this.region + ".myqcloud.com/" + filename;
+    @Override
+    public void refreshContentDeliveryNetwork(@NonNull String... urls) {
+        if (ObjectUtils.isEmpty(urls)) {
+            return;
+        }
+        PurgeUrlsCacheRequest request = new PurgeUrlsCacheRequest();
+        request.setUrls(urls);
+        request.setArea("mainland");
+        try {
+            this.cdn.PurgeUrlsCache(request);
+        } catch (TencentCloudSDKException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     @Override
-    public String upload(@NonNull File file) throws IOException {
-        String suffix = FileUtils.getSuffix(file);
-        String filename = StringUtils.uuid() + StringUtils.ifEmpty(suffix, name -> "." + name, StringUtils.EMPTY);
-        this.client.putObject(this.path, filename, file);
-        return this.name2url(filename);
+    public String upload(@NonNull File file, @NonNull String filename) throws IOException {
+        this.cos.putObject(this.directory, filename, file);
+        return HttpUtils.url(this.domain, filename);
     }
 
     @Override
-    public String upload(@NonNull InputStream stream, String suffix) throws IOException {
-        return this.upload(stream, suffix, stream.available());
+    public String upload(@NonNull InputStream stream, @NonNull String filename) throws IOException {
+        return this.upload(stream, filename, stream.available());
     }
 
     @Override
-    public String upload(@NonNull InputStream stream, String suffix, long length) throws IOException {
+    public String upload(@NonNull InputStream stream, @NonNull String filename, long length) throws IOException {
         ObjectMetadata metadata = new ObjectMetadata();
         metadata.setContentLength(length);
-        String filename = StringUtils.uuid() + StringUtils.ifEmpty(suffix, name -> "." + name, StringUtils.EMPTY);
-        this.client.putObject(this.path, filename, stream, metadata);
-        return this.name2url(filename);
+        this.cos.putObject(this.directory, filename, stream, metadata);
+        return HttpUtils.url(this.domain, filename);
     }
 
     @Override
     public void destroy() {
-        this.client.shutdown();
+        this.cos.shutdown();
     }
 }

+ 1 - 0
pom.xml

@@ -25,6 +25,7 @@
         <module>framework-rocketmq</module>
         <module>framework-security</module>
         <module>framework-sms</module>
+        <module>framework-aliyun</module>
         <module>framework-upload</module>
         <module>framework-wechat</module>
         <module>framework-download</module>