Jelajahi Sumber

Merge remote-tracking branch 'refs/remotes/yudao/develop' into develop

puhui999 10 bulan lalu
induk
melakukan
cd03ddddb3
46 mengubah file dengan 908 tambahan dan 364 penghapusan
  1. 1 1
      pom.xml
  2. 1 23
      yudao-dependencies/pom.xml
  3. 20 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java
  4. 5 0
      yudao-framework/yudao-spring-boot-starter-web/pom.xml
  5. 19 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/handler/DesensitizationHandler.java
  6. 8 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/EmailDesensitize.java
  7. 8 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/RegexDesensitize.java
  8. 8 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java
  9. 6 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java
  10. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/BankCardDesensitize.java
  11. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java
  12. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java
  13. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java
  14. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/IdCardDesensitize.java
  15. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/MobileDesensitize.java
  16. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/PasswordDesensitize.java
  17. 8 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/SliderDesensitize.java
  18. 8 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java
  19. 5 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/BankCardDesensitization.java
  20. 7 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java
  21. 2 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java
  22. 2 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java
  23. 1 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/IdCardDesensitization.java
  24. 1 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/MobileDesensitization.java
  25. 1 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/PasswordDesensitization.java
  26. 1 1
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java
  27. 4 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  28. 6 6
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java
  29. 2 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  30. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/message/TradeMessageServiceImpl.java
  31. 4 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  32. 3 3
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java
  33. 1 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java
  34. 1 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java
  35. 2 2
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java
  36. 2 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
  37. 0 13
      yudao-module-system/yudao-module-system-biz/pom.xml
  38. 8 7
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
  39. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java
  40. 124 112
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
  41. 349 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient_old.java
  42. 83 76
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
  43. 5 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
  44. 83 103
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java
  45. 66 1
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
  46. 1 1
      yudao-server/src/main/resources/application-local.yaml

+ 1 - 1
pom.xml

@@ -31,7 +31,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>2.1.0-snapshot</revision>
+        <revision>2.2.0-snapshot</revision>
         <!-- Maven 相关 -->
         <java.version>17</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>

+ 1 - 23
yudao-dependencies/pom.xml

@@ -14,7 +14,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>2.1.0-snapshot</revision>
+        <revision>2.2.0-snapshot</revision>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 统一依赖管理 -->
         <spring.boot.version>3.3.1</spring.boot.version>
@@ -69,8 +69,6 @@
         <okhttp3.version>4.11.0</okhttp3.version>
         <commons-io.version>2.15.1</commons-io.version>
         <minio.version>8.5.7</minio.version>
-        <aliyun-java-sdk-core.version>4.6.4</aliyun-java-sdk-core.version>
-        <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
         <tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version>
         <justauth.version>2.0.5</justauth.version>
         <jimureport.version>1.7.8</jimureport.version>
@@ -548,26 +546,6 @@
             </dependency>
 
             <!-- SMS SDK begin -->
-            <dependency>
-                <groupId>com.aliyun</groupId>
-                <artifactId>aliyun-java-sdk-core</artifactId>
-                <version>${aliyun-java-sdk-core.version}</version>
-                <exclusions>
-                    <exclusion>
-                        <artifactId>opentracing-api</artifactId>
-                        <groupId>io.opentracing</groupId>
-                    </exclusion>
-                    <exclusion>
-                        <artifactId>opentracing-util</artifactId>
-                        <groupId>io.opentracing</groupId>
-                    </exclusion>
-                </exclusions>
-            </dependency>
-            <dependency>
-                <groupId>com.aliyun</groupId>
-                <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
-                <version>${aliyun-java-sdk-dysmsapi.version}</version>
-            </dependency>
             <dependency>
                 <groupId>com.tencentcloudapi</groupId>
                 <artifactId>tencentcloud-sdk-java-sms</artifactId>

+ 20 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java

@@ -3,11 +3,15 @@ package cn.iocoder.yudao.framework.common.util.spring;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.context.expression.BeanFactoryResolver;
 import org.springframework.core.DefaultParameterNameDiscoverer;
 import org.springframework.core.ParameterNameDiscoverer;
 import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -86,4 +90,20 @@ public class SpringExpressionUtils {
         return result;
     }
 
+    /**
+     * 从 Bean 工厂,解析 EL 表达式的结果
+     *
+     * @param expressionString EL 表达式
+     * @return 执行界面
+     */
+    public static Object parseExpression(String expressionString) {
+        if (StrUtil.isBlank(expressionString)) {
+            return null;
+        }
+        Expression expression = EXPRESSION_PARSER.parseExpression(expressionString);
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext()));
+        return expression.getValue(context);
+    }
+
 }

+ 5 - 0
yudao-framework/yudao-spring-boot-starter-web/pom.xml

@@ -32,6 +32,11 @@
             <artifactId>spring-boot-configuration-processor</artifactId>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <scope>provided</scope> <!-- 解决工具类 SpringExpressionUtils 加载的时候访问不到 org.aspectj.lang.JoinPoint 问题 -->
+        </dependency>
 
         <dependency>
             <groupId>com.github.xiaoymin</groupId>

+ 19 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/handler/DesensitizationHandler.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.desensitize.core.base.handler;
 
+import cn.hutool.core.util.ReflectUtil;
+
 import java.lang.annotation.Annotation;
 
 /**
@@ -18,4 +20,21 @@ public interface DesensitizationHandler<T extends Annotation> {
      */
     String desensitize(String origin, T annotation);
 
+    /**
+     * 是否禁用脱敏的 Spring EL 表达式
+     *
+     * 如果返回 true 则跳过脱敏
+     *
+     * @param annotation 注解信息
+     * @return 是否禁用脱敏的 Spring EL 表达式
+     */
+    default String getDisable(T annotation) {
+        // 约定:默认就是 enable() 属性。如果不符合,子类重写
+        try {
+            return (String) ReflectUtil.invoke(annotation, "disable");
+        } catch (Exception ex) {
+            return "";
+        }
+    }
+
 }

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/EmailDesensitize.java

@@ -33,4 +33,12 @@ public @interface EmailDesensitize {
      * 比如:example@gmail.com 脱敏之后为 e****@gmail.com
      */
     String replacer() default "$1****$2";
+
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/RegexDesensitize.java

@@ -35,4 +35,12 @@ public @interface RegexDesensitize {
      * 脱敏后字符串 ******456789
      */
     String replacer() default "******";
+
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.desensitize.core.regex.handler;
 
+import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
 import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
 
 import java.lang.annotation.Annotation;
@@ -14,6 +15,13 @@ public abstract class AbstractRegexDesensitizationHandler<T extends Annotation>
 
     @Override
     public String desensitize(String origin, T annotation) {
+        // 1. 判断是否禁用脱敏
+        Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
+        if (Boolean.TRUE.equals(disable)) {
+            return origin;
+        }
+
+        // 2. 执行脱敏
         String regex = getRegex(annotation);
         String replacer = getReplacer(annotation);
         return origin.replaceAll(regex, replacer);

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java

@@ -18,4 +18,10 @@ public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitiza
     String getReplacer(RegexDesensitize annotation) {
         return annotation.replacer();
     }
+
+    @Override
+    public String getDisable(RegexDesensitize annotation) {
+        return annotation.disable();
+    }
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/BankCardDesensitize.java

@@ -37,4 +37,11 @@ public @interface BankCardDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java

@@ -37,4 +37,11 @@ public @interface CarLicenseDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java

@@ -37,4 +37,11 @@ public @interface ChineseNameDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java

@@ -37,4 +37,11 @@ public @interface FixedPhoneDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/IdCardDesensitize.java

@@ -37,4 +37,11 @@ public @interface IdCardDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/MobileDesensitize.java

@@ -37,4 +37,11 @@ public @interface MobileDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/PasswordDesensitize.java

@@ -39,4 +39,11 @@ public @interface PasswordDesensitize {
      */
     String replacer() default "*";
 
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/SliderDesensitize.java

@@ -40,4 +40,12 @@ public @interface SliderDesensitize {
      * 前缀保留长度
      */
     int prefixKeep() default 0;
+
+    /**
+     * 是否禁用脱敏
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
+     */
+    String disable() default "";
+
 }

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
 
+import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
 import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
 
 import java.lang.annotation.Annotation;
@@ -14,6 +15,13 @@ public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
 
     @Override
     public String desensitize(String origin, T annotation) {
+        // 1. 判断是否禁用脱敏
+        Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
+        if (Boolean.FALSE.equals(disable)) {
+            return origin;
+        }
+
+        // 2. 执行脱敏
         int prefixKeep = getPrefixKeep(annotation);
         int suffixKeep = getSuffixKeep(annotation);
         String replacer = getReplacer(annotation);

+ 5 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/BankCardDesensitization.java

@@ -24,4 +24,9 @@ public class BankCardDesensitization extends AbstractSliderDesensitizationHandle
         return annotation.replacer();
     }
 
+    @Override
+    public String getDisable(BankCardDesensitize annotation) {
+        return "";
+    }
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.CarLicenseD
  * @author gaibu
  */
 public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler<CarLicenseDesensitize> {
+
     @Override
     Integer getPrefixKeep(CarLicenseDesensitize annotation) {
         return annotation.prefixKeep();
@@ -22,4 +23,10 @@ public class CarLicenseDesensitization extends AbstractSliderDesensitizationHand
     String getReplacer(CarLicenseDesensitize annotation) {
         return annotation.replacer();
     }
+
+    @Override
+    public String getDisable(CarLicenseDesensitize annotation) {
+        return annotation.disable();
+    }
+
 }

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesen
  * @author gaibu
  */
 public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler<SliderDesensitize> {
+
     @Override
     Integer getPrefixKeep(SliderDesensitize annotation) {
         return annotation.prefixKeep();
@@ -22,4 +23,5 @@ public class DefaultDesensitizationHandler extends AbstractSliderDesensitization
     String getReplacer(SliderDesensitize annotation) {
         return annotation.replacer();
     }
+
 }

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.FixedPhoneD
  * @author gaibu
  */
 public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler<FixedPhoneDesensitize> {
+
     @Override
     Integer getPrefixKeep(FixedPhoneDesensitize annotation) {
         return annotation.prefixKeep();
@@ -22,4 +23,5 @@ public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHand
     String getReplacer(FixedPhoneDesensitize annotation) {
         return annotation.replacer();
     }
+
 }

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/IdCardDesensitization.java

@@ -22,4 +22,5 @@ public class IdCardDesensitization extends AbstractSliderDesensitizationHandler<
     String getReplacer(IdCardDesensitize annotation) {
         return annotation.replacer();
     }
+
 }

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/MobileDesensitization.java

@@ -23,4 +23,5 @@ public class MobileDesensitization extends AbstractSliderDesensitizationHandler<
     String getReplacer(MobileDesensitize annotation) {
         return annotation.replacer();
     }
+
 }

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/PasswordDesensitization.java

@@ -22,4 +22,5 @@ public class PasswordDesensitization extends AbstractSliderDesensitizationHandle
     String getReplacer(PasswordDesensitize annotation) {
         return annotation.replacer();
     }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java

@@ -9,6 +9,6 @@ public interface MessageTemplateConstants {
 
     //======================= 小程序订阅消息模版 =======================
 
-    String COMBINATION_RESULT = "拼团结果通知";
+    String COMBINATION_SUCCESS = "拼团结果通知";
 
 }

+ 4 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java

@@ -44,7 +44,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.afterNow;
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeNow;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.module.promotion.enums.MessageTemplateConstants.COMBINATION_RESULT;
+import static cn.iocoder.yudao.module.promotion.enums.MessageTemplateConstants.COMBINATION_SUCCESS;
 
 // TODO 芋艿:等拼团记录做完,完整 review 下
 
@@ -212,10 +212,10 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
             }
             updateRecords.add(updateRecord);
         });
-        Boolean result = combinationRecordMapper.updateBatch(updateRecords);
+        Boolean updateSuccess = combinationRecordMapper.updateBatch(updateRecords);
 
         // 3. 拼团成功发送订阅消息
-        if (result && isFull) {
+        if (updateSuccess && isFull) {
             records.forEach(item -> {
                 getSelf().sendCombinationResultMessage(item);
             });
@@ -227,7 +227,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         // 构建并发送模版消息
         socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
                 .setUserId(record.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
-                .setTemplateTitle(COMBINATION_RESULT)
+                .setTemplateTitle(COMBINATION_SUCCESS)
                 .setPage("pages/order/detail?id=" + record.getOrderId()) // 订单详情页
                 .addMessage("thing1", "商品拼团活动") // 活动标题
                 .addMessage("thing2", "恭喜您拼团成功!我们将尽快为您发货。")); // 温馨提示

+ 6 - 6
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java

@@ -7,15 +7,15 @@ package cn.iocoder.yudao.module.trade.enums;
  */
 public interface MessageTemplateConstants {
 
-    //======================= 短信消息模版 =======================
+    // ======================= 短信消息模版 =======================
 
-    String ORDER_DELIVERY = "order_delivery"; // 短信模版编号
+    String SMS_ORDER_DELIVERY = "order_delivery"; // 短信模版编号
 
-    String BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现(审核通过)
-    String BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现(审核不通过)
+    String SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现(审核通过)
+    String SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现(审核不通过)
 
-    //======================= 小程序订阅消息模版 =======================
+    // ======================= 小程序订阅消息模版 =======================
 
-    String DELIVERY_ORDER = "订单发货通知";
+    String WXA_ORDER_DELIVERY = "订单发货通知";
 
 }

+ 2 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java

@@ -77,14 +77,14 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
         String templateCode;
         if (BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.equals(status)) {
-            templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_APPROVE;
+            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE;
             // 3.1 通过时佣金转余额
             if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
                 // todo 疯狂:
             }
             // TODO 疯狂:调用转账接口
         } else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) {
-            templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_REJECT;
+            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT;
             // 3.2 驳回时需要退还用户佣金
             brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
                     String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/message/TradeMessageServiceImpl.java

@@ -37,7 +37,7 @@ public class TradeMessageServiceImpl implements TradeMessageService {
         notifyMessageSendApi.sendSingleMessageToMember(
                 new NotifySendSingleToUserReqDTO()
                         .setUserId(reqBO.getUserId())
-                        .setTemplateCode(MessageTemplateConstants.ORDER_DELIVERY)
+                        .setTemplateCode(MessageTemplateConstants.SMS_ORDER_DELIVERY)
                         .setTemplateParams(msgMap));
     }
 

+ 4 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -7,6 +7,7 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
@@ -71,7 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.min
 import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getTerminal;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants.DELIVERY_ORDER;
+import static cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants.WXA_ORDER_DELIVERY;
 
 /**
  * 交易订单【写】Service 实现类
@@ -384,12 +385,12 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         Long orderId = order.getId();
         socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
                 .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
-                .setTemplateTitle(DELIVERY_ORDER)
+                .setTemplateTitle(WXA_ORDER_DELIVERY)
                 .setPage("pages/order/detail?id=" + orderId) // 订单详情页
                 .addMessage("character_string3", String.valueOf(orderId)) // 订单编号
                 .addMessage("phrase6", TradeOrderStatusEnum.DELIVERED.getName()) // 订单状态
                 .addMessage("date4", LocalDateTimeUtil.formatNormal(LocalDateTime.now()))// 发货时间
-                .addMessage("character_string5", deliveryReqVO.getLogisticsNo()) // 快递单号
+                .addMessage("character_string5", StrUtil.blankToDefault(deliveryReqVO.getLogisticsNo(), "-")) // 快递单号
                 .addMessage("thing9", order.getReceiverDetailAddress())); // 收货地址
     }
 

+ 3 - 3
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java

@@ -63,16 +63,16 @@ public class AppSocialUserController {
 
     @PostMapping("/wxa-qrcode")
     @Operation(summary = "获得微信小程序码(base64 image)")
-    public CommonResult<String> getWxaQrcode(@RequestBody @Valid AppSocialWxQrcodeReqVO reqVO) {
+    public CommonResult<String> getWxaQrcode(@RequestBody @Valid AppSocialWxaQrcodeReqVO reqVO) {
         byte[] wxQrcode = socialClientApi.getWxaQrcode(BeanUtils.toBean(reqVO, SocialWxQrcodeReqDTO.class));
         return success(Base64.encode(wxQrcode));
     }
 
     @GetMapping("/get-subscribe-template-list")
     @Operation(summary = "获得微信小程订阅模板列表")
-    public CommonResult<List<AppSocialWxSubscribeTemplateRespVO>> getSubscribeTemplateList() {
+    public CommonResult<List<AppSocialWxaSubscribeTemplateRespVO>> getSubscribeTemplateList() {
         List<SocialWxaSubscribeTemplateRespDTO> template = socialClientApi.getWxaSubscribeTemplateList(UserTypeEnum.MEMBER.getValue());
-        return success(BeanUtils.toBean(template, AppSocialWxSubscribeTemplateRespVO.class));
+        return success(BeanUtils.toBean(template, AppSocialWxaSubscribeTemplateRespVO.class));
     }
 
 }

+ 1 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxQrcodeReqVO.java → yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java

@@ -7,7 +7,7 @@ import lombok.Data;
 
 @Schema(description = "用户 APP - 获得获取小程序码 Request VO")
 @Data
-public class AppSocialWxQrcodeReqVO {
+public class AppSocialWxaQrcodeReqVO {
 
     /**
      * 页面路径不能携带参数(参数请放在scene字段里)

+ 1 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxSubscribeTemplateRespVO.java → yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java

@@ -5,7 +5,7 @@ import lombok.Data;
 
 @Schema(description = "用户 APP - 获得小程序订阅模版 Response VO")
 @Data
-public class AppSocialWxSubscribeTemplateRespVO {
+public class AppSocialWxaSubscribeTemplateRespVO {
 
     @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED,
             example = "9Aw5ZV1j9xdWTFEkqCpZ7mIBbSC34khK55OtzUPl0rU")

+ 2 - 2
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java

@@ -7,8 +7,8 @@ package cn.iocoder.yudao.module.pay.enums;
  */
 public interface MessageTemplateConstants {
 
-    //======================= 小程序订阅消息 =======================
+    // ======================= 小程序订阅消息 =======================
 
-    String WALLET_RECHARGER_PAID = "充值成功通知";
+    String WXA_WALLET_RECHARGER_PAID = "充值成功通知";
 
 }

+ 2 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java

@@ -39,7 +39,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
 import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.fenToYuanStr;
 import static cn.iocoder.yudao.module.pay.convert.wallet.PayWalletRechargeConvert.INSTANCE;
 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.module.pay.enums.MessageTemplateConstants.WALLET_RECHARGER_PAID;
+import static cn.iocoder.yudao.module.pay.enums.MessageTemplateConstants.WXA_WALLET_RECHARGER_PAID;
 import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
 
 /**
@@ -147,7 +147,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
         // 2. 构建并发送模版消息
         socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
                 .setUserId(wallet.getUserId()).setUserType(wallet.getUserType())
-                .setTemplateTitle(WALLET_RECHARGER_PAID)
+                .setTemplateTitle(WXA_WALLET_RECHARGER_PAID)
                 .setPage("pages/user/wallet/money") // 钱包详情界面
                 .addMessage("character_string1", String.valueOf(payOrderId)) // 支付单编号
                 .addMessage("amount2", fenToYuanStr(walletRecharge.getTotalPrice())) // 充值金额

+ 0 - 13
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -110,19 +110,6 @@
             <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>  <!-- 微信登录(小程序) -->
         </dependency>
 
-        <dependency>
-            <groupId>com.aliyun</groupId>
-            <artifactId>aliyun-java-sdk-core</artifactId> <!-- 短信(阿里云) -->
-        </dependency>
-        <dependency>
-            <groupId>com.aliyun</groupId>
-            <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <!-- 短信(阿里云) -->
-        </dependency>
-        <dependency>
-            <groupId>com.tencentcloudapi</groupId>
-            <artifactId>tencentcloud-sdk-java-sms</artifactId> <!-- 短信(腾讯云) -->
-        </dependency>
-
         <dependency>
             <groupId>com.xingyuv</groupId>
             <artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 -->

+ 8 - 7
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.system.api.social.dto.*;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import cn.iocoder.yudao.module.system.service.social.SocialClientService;
+import cn.iocoder.yudao.module.system.service.social.SocialUserService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.bean.WxJsapiSignature;
@@ -33,7 +34,7 @@ public class SocialClientApiImpl implements SocialClientApi {
     @Resource
     private SocialClientService socialClientService;
     @Resource
-    public SocialUserApi socialUserApi;
+    private SocialUserService socialUserService;
 
     @Override
     public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
@@ -68,29 +69,29 @@ public class SocialClientApiImpl implements SocialClientApi {
     @Override
     public void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO) {
         // 1.1 获得订阅模版列表
-        List<SocialWxaSubscribeTemplateRespDTO> templateList = getWxaSubscribeTemplateList(reqDTO.getUserType());
+        List<TemplateInfo> templateList = socialClientService.getSubscribeTemplateList(reqDTO.getUserType());
         if (CollUtil.isEmpty(templateList)) {
             log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
             return;
         }
         // 1.2 获得需要使用的模版
-        SocialWxaSubscribeTemplateRespDTO template = findOne(templateList, item ->
+        TemplateInfo template = findOne(templateList, item ->
                 ObjUtil.equal(item.getTitle(), reqDTO.getTemplateTitle()));
         if (template == null) {
-            log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
+            log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
             return;
         }
 
         // 2. 获得社交用户
-        SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
+        SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
                 SocialTypeEnum.WECHAT_MINI_APP.getType());
         if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
-            log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
+            log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
             return;
         }
 
         // 3. 发送订阅消息
-        socialClientService.sendSubscribeMessage(reqDTO, template.getId(), socialUser.getOpenid());
+        socialClientService.sendSubscribeMessage(reqDTO, template.getPriTmplId(), socialUser.getOpenid());
     }
 
 }

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java

@@ -46,6 +46,8 @@ public interface SmsClient {
     /**
      * 查询指定的短信模板
      *
+     * 如果查询失败,则返回 null 空
+     *
      * @param apiTemplateId 短信 API 的模板编号
      * @return 短信模板
      */

+ 124 - 112
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java

@@ -1,6 +1,17 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
+import cn.hutool.core.date.format.FastDateFormat;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@@ -9,27 +20,13 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
-import com.aliyuncs.DefaultAcsClient;
-import com.aliyuncs.IAcsClient;
-import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
-import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
-import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
-import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
-import com.aliyuncs.profile.DefaultProfile;
-import com.aliyuncs.profile.IClientProfile;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.annotations.VisibleForTesting;
-import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
 /**
  * 阿里短信客户端的实现类
@@ -40,20 +37,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
 @Slf4j
 public class AliyunSmsClient extends AbstractSmsClient {
 
-    /**
-     * 调用成功 code
-     */
-    public static final String API_CODE_SUCCESS = "OK";
+    private static final String URL = "https://dysmsapi.aliyuncs.com";
+    private static final String HOST = "dysmsapi.aliyuncs.com";
+    private static final String VERSION = "2017-05-25";
 
-    /**
-     * REGION, 使用杭州
-     */
-    private static final String ENDPOINT = "cn-hangzhou";
-
-    /**
-     * 阿里云客户端
-     */
-    private volatile IAcsClient client;
+    private static final String RESPONSE_CODE_SUCCESS = "OK";
 
     public AliyunSmsClient(SmsChannelProperties properties) {
         super(properties);
@@ -63,47 +51,66 @@ public class AliyunSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-        IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
-        client = new DefaultAcsClient(profile);
     }
 
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
                                   List<KeyValue<String, Object>> templateParams) throws Throwable {
-        // 构建请求
-        SendSmsRequest request = new SendSmsRequest();
-        request.setPhoneNumbers(mobile);
-        request.setSignName(properties.getSignature());
-        request.setTemplateCode(apiTemplateId);
-        request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
-        request.setOutId(String.valueOf(sendLogId));
-        // 执行请求
-        SendSmsResponse response = client.getAcsResponse(request);
-        return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId())
-                .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage());
+        // 1. 执行请求
+        // 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms
+        TreeMap<String, Object> queryParam = new TreeMap<>();
+        queryParam.put("PhoneNumbers",mobile);
+        queryParam.put("SignName", properties.getSignature());
+        queryParam.put("TemplateCode", apiTemplateId);
+        queryParam.put("TemplateParam", JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
+        queryParam.put("OutId", sendLogId);
+        JSONObject response = request("sendSms", queryParam);
+
+        // 2. 解析请求
+        return new SmsSendRespDTO()
+                .setSuccess(Objects.equals(response.getStr("Code"), RESPONSE_CODE_SUCCESS))
+                .setSerialNo(response.getStr("BizId"))
+                .setApiRequestId(response.getStr("RequestId"))
+                .setApiCode(response.getStr("Code"))
+                .setApiMsg(response.getStr("Message"));
     }
 
     @Override
     public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
-        List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess())
-                .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg())
-                .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime())
-                .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())));
+        JSONArray statuses = JSONUtil.parseArray(text);
+        // 字段参考
+        return convertList(statuses, status -> {
+            JSONObject statusObj = (JSONObject) status;
+            return new SmsReceiveRespDTO()
+                    .setSuccess(statusObj.getBool("success")) // 是否接收成功
+                    .setErrorCode(statusObj.getStr("err_code")) // 状态报告编码
+                    .setErrorMsg(statusObj.getStr("err_msg")) // 状态报告说明
+                    .setMobile(statusObj.getStr("phone_number")) // 手机号
+                    .setReceiveTime(statusObj.getLocalDateTime("report_time", null)) // 状态报告时间
+                    .setSerialNo(statusObj.getStr("biz_id")) // 发送序列号
+                    .setLogId(statusObj.getLong("out_id")); // 用户序列号
+        });
     }
 
     @Override
     public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
-        // 构建请求
-        QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
-        request.setTemplateCode(apiTemplateId);
-        // 执行请求
-        QuerySmsTemplateResponse response = client.getAcsResponse(request);
-        if (response.getTemplateStatus() == null) {
+        // 1. 执行请求
+        // 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplate
+        TreeMap<String, Object> queryParam = new TreeMap<>();
+        queryParam.put("TemplateCode", apiTemplateId);
+        JSONObject response = request("QuerySmsTemplate", queryParam);
+
+        // 2.1 请求失败
+        String code = response.getStr("Code");
+        if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
+            log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response);
             return null;
         }
-        return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent())
-                .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
+        // 2.2 请求成功
+        return new SmsTemplateRespDTO().setId(apiTemplateId)
+                .setContent(response.getStr("TemplateContent"))
+                .setAuditStatus(convertSmsTemplateAuditStatus(response.getInt("TemplateStatus")))
+                .setAuditReason(response.getStr("Reason"));
     }
 
     @VisibleForTesting
@@ -117,66 +124,71 @@ public class AliyunSmsClient extends AbstractSmsClient {
     }
 
     /**
-     * 短信接收状态
+     * 请求阿里云短信
      *
-     * 参见 <a href="https://help.aliyun.com/document_detail/101867.html">文档</a>
-     *
-     * @author 芋道源码
+     * @see <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">V3 版本请求体&签名机制</>
+     * @param apiName 请求的 API 名称
+     * @param queryParams 请求参数
+     * @return 请求结果
      */
-    @Data
-    public static class SmsReceiveStatus {
-
-        /**
-         * 手机号
-         */
-        @JsonProperty("phone_number")
-        private String phoneNumber;
-        /**
-         * 发送时间
-         */
-        @JsonProperty("send_time")
-        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
-        private LocalDateTime sendTime;
-        /**
-         * 状态报告时间
-         */
-        @JsonProperty("report_time")
-        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
-        private LocalDateTime reportTime;
-        /**
-         * 是否接收成功
-         */
-        private Boolean success;
-        /**
-         * 状态报告说明
-         */
-        @JsonProperty("err_msg")
-        private String errMsg;
-        /**
-         * 状态报告编码
-         */
-        @JsonProperty("err_code")
-        private String errCode;
-        /**
-         * 发送序列号
-         */
-        @JsonProperty("biz_id")
-        private String bizId;
-        /**
-         * 用户序列号
-         *
-         * 这里我们传递的是 SysSmsLogDO 的日志编号
-         */
-        @JsonProperty("out_id")
-        private String outId;
-        /**
-         * 短信长度,例如说 1、2、3
-         *
-         * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
-         */
-        @JsonProperty("sms_size")
-        private Integer smsSize;
+    private JSONObject request(String apiName, TreeMap<String, Object> queryParams) {
+        // 1. 请求参数
+        String queryString = queryParams.entrySet().stream()
+                .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue())))
+                .collect(Collectors.joining("&"));
+
+        // 2.1 请求 Header
+        TreeMap<String, String> headers = new TreeMap<>();
+        headers.put("host", HOST);
+        headers.put("x-acs-version", VERSION);
+        headers.put("x-acs-action", apiName);
+        headers.put("x-acs-date", FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT")).format(new Date()));
+        headers.put("x-acs-signature-nonce", IdUtil.randomUUID());
+        // 2.2 构建签名 Header
+        StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
+        StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
+        headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-")
+                        || entry.getKey().equalsIgnoreCase("host")
+                        || entry.getKey().equalsIgnoreCase("content-type"))
+                .sorted(Map.Entry.comparingByKey()).forEach(entry -> {
+                    String lowerKey = entry.getKey().toLowerCase();
+                    canonicalHeaders.append(lowerKey).append(":").append(String.valueOf(entry.getValue()).trim()).append("\n");
+                    signedHeadersBuilder.append(lowerKey).append(";");
+                });
+        String signedHeaders = signedHeadersBuilder.substring(0, signedHeadersBuilder.length() - 1);
+
+        // 3. 请求 Body
+        String requestBody = ""; // 短信 API 为 RPC 接口,query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空。
+        String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
+
+        // 4. 构建 Authorization 签名
+        String hashedCanonicalRequest = DigestUtil.sha256Hex("POST" // httpMethod
+                + "\n" + "/" // canonicalUri
+                + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody);
+        String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
+        String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
+        headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()
+                + ", " + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature);
+
+        // 5. 发起请求
+        String urlWithParams = URL + "?" + URLUtil.buildQuery(queryParams, null);
+        try (HttpResponse response = HttpRequest.post(urlWithParams).addHeaders(headers).body(requestBody).execute()) {
+            return JSONUtil.parseObj(response.body());
+        }
+    }
 
+    /**
+     * 对指定的字符串进行 URL 编码,并对特定的字符进行替换,以符合URL编码规范
+     *
+     * @param str 需要进行URL编码的字符串
+     * @return 编码后的字符串
+     */
+    private static String percentCode(String str) {
+        Assert.notNull(str, "str 不能为空");
+        return URLUtil.encode(str)
+                .replace("+", "%20") // 加号 "+" 被替换为 "%20"
+                .replace("*", "%2A") // 星号 "*" 被替换为 "%2A"
+                .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
     }
 
 }

+ 349 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient_old.java

@@ -0,0 +1,349 @@
+package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.HexUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
+import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+/**
+ * 阿里短信客户端的实现类
+ *
+ * @author zzf
+ * @since 2021/1/25 14:17
+ */
+@Slf4j
+public class AliyunSmsClient_old extends AbstractSmsClient {
+
+    public AliyunSmsClient_old(SmsChannelProperties properties) {
+        super(properties);
+        Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
+        Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+    }
+
+    @Override
+    protected void doInit() {
+//        IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
+//        client = new DefaultAcsClient(profile);
+    }
+
+    @Override
+    public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
+                                  List<KeyValue<String, Object>> templateParams) throws Throwable {
+
+        TreeMap<String, Object> queryParam = new TreeMap<>();
+        queryParam.put("PhoneNumbers",mobile);
+        queryParam.put("SignName",properties.getSignature());
+        queryParam.put("TemplateCode",apiTemplateId);
+        queryParam.put("TemplateParam",JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
+
+        JSONObject response = sendSmsRequest(queryParam,"sendSms");
+        SmsResponse smsResponse = getSmsSendResponse(response);
+
+        return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
+    }
+
+    JSONObject sendSmsRequest(TreeMap<String, Object> queryParam,String apiName) throws IOException, URISyntaxException {
+
+        // ************* 步骤 1:拼接规范请求串 *************
+        String url = "https://dysmsapi.aliyuncs.com"; //APP接入地址+接口访问URI
+        String httpMethod = "POST"; // 请求方式
+        String canonicalUri = "/";
+        // 请求参数,当请求的查询字符串为空时,使用空字符串作为规范化查询字符串
+        StringBuilder canonicalQueryString = new StringBuilder();
+        queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
+            // 如果canonicalQueryString已经不是空的,则在查询参数前添加"&"
+            if (!canonicalQueryString.isEmpty()) {
+                canonicalQueryString.append("&");
+            }
+            canonicalQueryString.append(queryPart);
+            System.out.println("canonicalQueryString=========>\n" + canonicalQueryString);
+        });
+
+        SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        SDF.setTimeZone(new SimpleTimeZone(0, "GMT"));
+        String SdfTime = SDF.format(new Date());
+        String randomUUID = UUID.randomUUID().toString();
+
+        TreeMap<String, String> headers = new TreeMap<>();
+        headers.put("host", "dysmsapi.aliyuncs.com");
+        headers.put("x-acs-action", apiName);
+        headers.put("x-acs-version", "2017-05-25");
+        headers.put("x-acs-date", SdfTime);
+        headers.put("x-acs-signature-nonce", randomUUID);
+//        headers.put("content-type", "application/json;charset=utf-8");
+
+        // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
+        StringBuilder canonicalHeaders = new StringBuilder();
+        // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
+        StringBuilder signedHeadersSb = new StringBuilder();
+        headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || entry.getKey().equalsIgnoreCase("host") || entry.getKey().equalsIgnoreCase("content-type")).sorted(Map.Entry.comparingByKey()).forEach(entry -> {
+            String lowerKey = entry.getKey().toLowerCase();
+            String value = String.valueOf(entry.getValue()).trim();
+            canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
+            signedHeadersSb.append(lowerKey).append(";");
+        });
+        String signedHeaders = signedHeadersSb.substring(0, signedHeadersSb.length() - 1);
+
+        String body = "";//短信API为RPC接口,query parameters在uri中拼接,因此request body如果没有特殊要求,设置为空。
+        String hashedRequestBody = HexUtil.encodeHexStr(DigestUtil.sha256(body));
+
+
+        String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
+        System.out.println("canonicalRequest=========>\n" + canonicalRequest);
+
+        // ************* 步骤 2:拼接待签名字符串 *************
+        String hashedCanonicalRequest = HexUtil.encodeHexStr(DigestUtil.sha256(canonicalRequest));
+        String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
+
+        // ************* 步骤 3:计算签名 *************
+        String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);
+
+        // ************* 步骤 4:拼接 Authorization *************
+        String authorization = "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey() + ", "
+                + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
+        headers.put("Authorization", authorization);
+
+        // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
+//        url = url + canonicalUri;
+        String urlWithParams = url + "?" + URLUtil.buildQuery(queryParam, null);
+
+        HttpResponse response = HttpRequest.post(urlWithParams)
+                .addHeaders(headers)
+                .body(body)
+                .execute();
+//        URIBuilder uriBuilder = new URIBuilder(url);
+//        // 添加请求参数
+//        for (Map.Entry<String, Object> entry : queryParam.entrySet()) {
+//            uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
+//        }
+//        HttpUriRequest httpRequest = new HttpPost(uriBuilder.build());
+////        HttpPost httpPost = new HttpPost(uriBuilder.build());
+////        httpRequest = httpPost;
+//
+//        // 添加http请求头
+//        for (Map.Entry<String, Object> entry : headers.entrySet()) {
+//            httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
+//        }
+//
+//        // 发送请求
+//        CloseableHttpClient httpClient = HttpClients.createDefault();
+//        CloseableHttpResponse response = httpClient.execute(httpRequest);
+        System.out.println("getEntity====="+response.body());
+        System.out.println("response====="+response);
+
+        return JSONUtil.parseObj(response.body());
+    }
+
+    @Override
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
+        List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
+        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess())
+                .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg())
+                .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime())
+                .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())));
+    }
+
+    @Override
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+
+        TreeMap<String, Object> queryParam = new TreeMap<>();
+        queryParam.put("TemplateCode",apiTemplateId);
+
+        JSONObject response = sendSmsRequest(queryParam,"QuerySmsTemplate");
+        QuerySmsTemplateResponse smsTemplateResponse = getSmsTemplateResponse(response);
+        return new SmsTemplateRespDTO().setId(smsTemplateResponse.getTemplateCode()).setContent(smsTemplateResponse.getTemplateContent())
+                .setAuditStatus(convertSmsTemplateAuditStatus(smsTemplateResponse.getTemplateStatus())).setAuditReason(smsTemplateResponse.getReason());
+
+    }
+
+    @VisibleForTesting
+    Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
+        switch (templateStatus) {
+            case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+            case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+            case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
+        }
+    }
+
+
+    /**
+     * 对指定的字符串进行URL编码。
+     * 使用UTF-8编码字符集对字符串进行编码,并对特定的字符进行替换,以符合URL编码规范。
+     *
+     * @param str 需要进行URL编码的字符串。
+     * @return 编码后的字符串。其中,加号"+"被替换为"%20",星号"*"被替换为"%2A",波浪号"%7E"被替换为"~"。
+     */
+    public static String percentCode(String str) {
+        if (str == null) {
+            throw new IllegalArgumentException("输入字符串不可为null");
+        }
+        try {
+            return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("UTF-8编码不被支持", e);
+        }
+    }
+
+    private SmsResponse getSmsSendResponse(JSONObject resJson) {
+        SmsResponse smsResponse = new SmsResponse();
+        smsResponse.setSuccess("OK".equals(resJson.getStr("Code")));
+        smsResponse.setData(resJson);
+//        smsResponse.setConfigId(getConfigId());
+        return smsResponse;
+    }
+
+    private QuerySmsTemplateResponse getSmsTemplateResponse(JSONObject resJson) {
+
+        QuerySmsTemplateResponse smsTemplateResponse = new QuerySmsTemplateResponse();
+
+        smsTemplateResponse.setRequestId(resJson.getStr("RequestId"));
+        smsTemplateResponse.setTemplateContent(resJson.getStr("TemplateContent"));
+        smsTemplateResponse.setReason(resJson.getStr("Reason"));
+        smsTemplateResponse.setTemplateStatus(resJson.getInt("TemplateStatus"));
+
+        return smsTemplateResponse;
+    }
+
+    /**
+     * <p>类名: SmsResponse
+     * <p>说明:  发送短信返回信息
+     *
+     * @author :scholar
+     * 2024/07/17  0:25
+     **/
+    @Data
+    public static class SmsResponse {
+
+        /**
+         * 是否成功
+         */
+        private boolean success;
+
+        /**
+         * 厂商原返回体
+         */
+        private Object data;
+
+        /**
+         * 配置标识名 如未配置取对应渠道名例如 Alibaba
+         */
+        private String configId;
+    }
+
+
+    /**
+     * <p>类名: QuerySmsTemplateResponse
+     * <p>说明:  sms模板查询返回信息
+     *
+     * @author :scholar
+     * 2024/07/17  0:25
+     **/
+    @Data
+    public static class QuerySmsTemplateResponse {
+        private String requestId;
+        private String code;
+        private String message;
+        private Integer templateStatus;
+        private String reason;
+        private String templateCode;
+        private Integer templateType;
+        private String templateName;
+        private String templateContent;
+        private String createDate;
+    }
+
+    /**
+     * 短信接收状态
+     *
+     * 参见 <a href="https://help.aliyun.com/document_detail/101867.html">文档</a>
+     *
+     * @author 润普源码
+     */
+    @Data
+    public static class SmsReceiveStatus {
+
+        /**
+         * 手机号
+         */
+        @JsonProperty("phone_number")
+        private String phoneNumber;
+        /**
+         * 发送时间
+         */
+        @JsonProperty("send_time")
+        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+        private LocalDateTime sendTime;
+        /**
+         * 状态报告时间
+         */
+        @JsonProperty("report_time")
+        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+        private LocalDateTime reportTime;
+        /**
+         * 是否接收成功
+         */
+        private Boolean success;
+        /**
+         * 状态报告说明
+         */
+        @JsonProperty("err_msg")
+        private String errMsg;
+        /**
+         * 状态报告编码
+         */
+        @JsonProperty("err_code")
+        private String errCode;
+        /**
+         * 发送序列号
+         */
+        @JsonProperty("biz_id")
+        private String bizId;
+        /**
+         * 用户序列号
+         *
+         * 这里我们传递的是 SysSmsLogDO 的日志编号
+         */
+        @JsonProperty("out_id")
+        private String outId;
+        /**
+         * 短信长度,例如说 1、2、3
+         *
+         * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
+         */
+        @JsonProperty("sms_size")
+        private Integer smsSize;
+
+    }
+
+}

+ 83 - 76
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java

@@ -1,39 +1,43 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
+
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.HexUtil;
 import cn.hutool.core.util.StrUtil;
+
 import cn.hutool.crypto.SecureUtil;
-import cn.hutool.crypto.digest.DigestUtil;
-import cn.hutool.json.JSONArray;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
+
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.methods.RequestBuilder;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
 
+import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
-import java.time.LocalDateTime;
 import java.util.*;
 
+
+import java.time.LocalDateTime;
+
+
+import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
+
 /**
  * 华为短信客户端的实现类
  *
@@ -46,7 +50,14 @@ public class HuaweiSmsClient extends AbstractSmsClient {
     /**
      * 调用成功 code
      */
-    public static final String API_CODE_SUCCESS = "OK";
+    public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
+    public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
+    public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
+
+    @Override
+    protected void doInit() {
+
+    }
 
     public HuaweiSmsClient(SmsChannelProperties properties) {
         super(properties);
@@ -54,96 +65,79 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
     }
 
-    @Override
-    protected void doInit() {
-    }
-
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
                                   List<KeyValue<String, Object>> templateParams) throws Throwable {
-        // TODO @scholar:https://smsapi.cn-north-4.myhuaweicloud.com:443 是不是枚举成静态变量
-        String url = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1"; //APP接入地址+接口访问URI
         // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
         // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
-        // TODO @scholar:暂时只考虑中国大陆,所以不需要 sender 哈
         String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
         String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID
 
-        // 选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
+        //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
         String statusCallBack = properties.getCallbackUrl();
 
-        // TODO @scholar:1)是不是用  LocalDateTimeUtil.format();这样 3 行变成一行
-        // TODO @scholar:singerDate 叫 sdkDate 会更合适哈,这样理解起来简单。另外,singer 应该是 signed 么?
+        List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
+
+        JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
+        SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
+
+        return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
+    }
+
+    JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
+
         SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
-        String singerDate = sdf.format(new Date());
+        String sdkDate = sdf.format(new Date());
 
-        // TODO @scholar:整个处理加密的过程,是不是应该抽成一个 private 方法哈。这样整个调用的主干更清晰。
         // ************* 步骤 1:拼接规范请求串 *************
         String httpRequestMethod = "POST";
         String canonicalUri = "/sms/batchSendSms/v1/";
-        String canonicalQueryString = ""; // 查询参数为空
+        String canonicalQueryString = "";//查询参数为空
         String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
-                + "host:smsapi.cn-north-4.myhuaweicloud.com:443\n"
-                + "x-sdk-date:" + singerDate + "\n";
-        // TODO @scholar:静态枚举了
-        String signedHeaders = "content-type;host;x-sdk-date";
-        // TODO @scholar:下面的注释,可以考虑去掉
-        /*
-         * 选填,使用无变量模板时请赋空值 String templateParas = "";
-         * 单变量模板示例:模板内容为"您的验证码是${NUM_6}"时,templateParas可填写为"[\"111111\"]"
-         * 双变量模板示例:模板内容为"您有${NUM_2}件快递请到${TXT_20}领取"时,templateParas可填写为"[\"3\",\"人民公园正门\"]"
-         */
-        // TODO @scholar:CollectionUtils.convertList 可以把 4 行变成 1 行。
-        // TODO @scholar:templateParams 拼写错误哈
-        List<String> templateParas = new ArrayList<>();
-        for (KeyValue<String, Object> kv : templateParams) {
-            templateParas.add(String.valueOf(kv.getValue()));
-        }
-
-        // 请求Body,不携带签名名称时,signature请填null
+                + "host:"+ HOST +"\n"
+                + "x-sdk-date:" + sdkDate + "\n";
+        //请求Body,不携带签名名称时,signature请填null
         String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
-        // TODO @scholar:Assert 断言,抛出异常
         if (null == body || body.isEmpty()) {
             return null;
         }
-        String hashedRequestBody = HexUtil.encodeHexStr(DigestUtil.sha256(body));
+        String hashedRequestBody = sha256Hex(body);
         String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
-                + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
+                + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;
 
         // ************* 步骤 2:拼接待签名字符串 *************
-        // TODO @scholar:sha256Hex 是不是更简洁哈
-        String hashedCanonicalRequest = HexUtil.encodeHexStr(DigestUtil.sha256(canonicalRequest));
-        String stringToSign = "SDK-HMAC-SHA256" + "\n" + singerDate + "\n" + hashedCanonicalRequest;
+        String hashedCanonicalRequest = sha256Hex(canonicalRequest);
+        String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest;
 
         // ************* 步骤 3:计算签名 *************
         String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);
 
         // ************* 步骤 4:拼接 Authorization *************
         String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "
-                + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
+                + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
 
         // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
-        // TODO @scholar:考虑了下,还是换 hutool 的 httpUtils。因为未来 httpclient 我们可能会移除掉
-        HttpUriRequest postMethod = RequestBuilder.post()
-                .setUri(url)
-                .setEntity(new StringEntity(body, StandardCharsets.UTF_8))
-                .setHeader("Content-Type","application/x-www-form-urlencoded")
-                .setHeader("X-Sdk-Date", singerDate)
-                .setHeader("Authorization", authorization)
-                .build();
-        // TODO @scholar:这种不太适合一直 new 的哈
-        CloseableHttpClient client = HttpClientBuilder.create().build();
-        HttpResponse response = client.execute(postMethod);
-        // TODO @scholar:失败的情况下的处理
-        // TODO @scholar:setSerialNo(Integer.toString(response.getStatusLine().getStatusCode())) 这部分,空一行。一行代码太多了,阅读性不太好哈
-        return new SmsSendRespDTO().setSuccess(Objects.equals(response.getStatusLine().getReasonPhrase(), API_CODE_SUCCESS)).setSerialNo(Integer.toString(response.getStatusLine().getStatusCode()))
-                .setApiRequestId(null).setApiCode(null).setApiMsg(null);
+        HttpResponse response = HttpRequest.post(URL)
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .header("X-Sdk-Date", sdkDate)
+                .header("host",HOST)
+                .header("Authorization", authorization)
+                .body(body)
+                .execute();
+
+        return JSONUtil.parseObj(response.body());
+    }
+
+    private SmsResponse getSmsSendResponse(JSONObject resJson) {
+        SmsResponse smsResponse = new SmsResponse();
+        smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
+        smsResponse.setData(resJson);
+        return smsResponse;
     }
 
     static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
-                                   String statusCallBack, @SuppressWarnings("SameParameterValue") String signature) {
-        // TODO @scholar:参数不满足,是不是抛出异常更好哈;通过 hutool 的 Assert 去断言
+                                   String statusCallBack, String signature) throws UnsupportedEncodingException {
         if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
                 || templateId.isEmpty()) {
             System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
@@ -154,20 +148,17 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         appendToBody(body, "from=", sender);
         appendToBody(body, "&to=", receiver);
         appendToBody(body, "&templateId=", templateId);
-        // TODO @scholar:new JSONArray(templateParas).toString(),是不是 JsonUtils.toString 呀?
-        appendToBody(body, "&templateParas=", new JSONArray(templateParas).toString());
+        appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
         appendToBody(body, "&statusCallback=", statusCallBack);
         appendToBody(body, "&signature=", signature);
         return body.toString();
     }
 
-    private static void appendToBody(StringBuilder body, String key, String val) {
-        // TODO @scholar:StrUtils.isNotEmpty(val),是不是更简洁哈
+    private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
         if (null != val && !val.isEmpty()) {
-            body.append(key).append(URLEncoder.encode(val, StandardCharsets.UTF_8));
+            body.append(key).append(URLEncoder.encode(val, "UTF-8"));
         }
     }
-
     @Override
     public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
         List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
@@ -179,12 +170,28 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
     @Override
     public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
-        // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现
-        // 对应文档 https://support.huaweicloud.com/api-msgsms/sms_05_0040.html
-        return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
+        //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。
+        return new SmsTemplateRespDTO().setId(null).setContent(null)
                 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
+
+    }
+
+    @Data
+    public static class SmsResponse {
+
+        /**
+         * 是否成功
+         */
+        private boolean success;
+
+        /**
+         * 厂商原返回体
+         */
+        private Object data;
+
     }
 
+
     /**
      * 短信接收状态
      *

+ 5 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java

@@ -82,7 +82,11 @@ public class SocialClientServiceImpl implements SocialClientService {
     @Value("${yudao.wxa-code.env-version:release}")
     public String envVersion;
     /**
-     * 订阅消息跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;
+     * 订阅消息跳转小程序类型
+     *
+     * 1. developer:开发版
+     * 2. trial:体验版
+     * 3. formal:正式版
      */
     @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
     public String miniprogramState;

+ 83 - 103
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java

@@ -1,36 +1,21 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
-import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
-import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
-import com.aliyuncs.IAcsClient;
-import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
-import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
-import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
-import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
-import com.google.common.collect.Lists;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentMatcher;
 import org.mockito.InjectMocks;
-import org.mockito.Mock;
 
 import java.time.LocalDateTime;
 import java.util.List;
 
-import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.when;
 
+// TODO 芋艿:需要优化
 /**
- * {@link AliyunSmsClient} 的单元测试
+ * {@link cn.iocoder.yudao.module.system.framework.sms.core.client.impl.AliyunSmsClient_old} 的单元测试
  *
  * @author 芋道源码
  */
@@ -44,9 +29,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
     @InjectMocks
     private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
 
-    @Mock
-    private IAcsClient client;
-
     @Test
     public void testDoInit() {
         // 准备参数
@@ -54,68 +36,66 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
 
         // 调用
         smsClient.doInit();
-        // 断言
-        assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient"));
-    }
-
-    @Test
-    public void tesSendSms_success() throws Throwable {
-        // 准备参数
-        Long sendLogId = randomLongId();
-        String mobile = randomString();
-        String apiTemplateId = randomString();
-        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
-                new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
-        // mock 方法
-        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK"));
-        when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
-            assertEquals(mobile, acsRequest.getPhoneNumbers());
-            assertEquals(properties.getSignature(), acsRequest.getSignName());
-            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
-            assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
-            assertEquals(sendLogId.toString(), acsRequest.getOutId());
-            return true;
-        }))).thenReturn(response);
-
-        // 调用
-        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
-                apiTemplateId, templateParams);
-        // 断言
-        assertTrue(result.getSuccess());
-        assertEquals(response.getRequestId(), result.getApiRequestId());
-        assertEquals(response.getCode(), result.getApiCode());
-        assertEquals(response.getMessage(), result.getApiMsg());
-        assertEquals(response.getBizId(), result.getSerialNo());
     }
 
-    @Test
-    public void tesSendSms_fail() throws Throwable {
-        // 准备参数
-        Long sendLogId = randomLongId();
-        String mobile = randomString();
-        String apiTemplateId = randomString();
-        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
-                new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
-        // mock 方法
-        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR"));
-        when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
-            assertEquals(mobile, acsRequest.getPhoneNumbers());
-            assertEquals(properties.getSignature(), acsRequest.getSignName());
-            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
-            assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
-            assertEquals(sendLogId.toString(), acsRequest.getOutId());
-            return true;
-        }))).thenReturn(response);
-
-        // 调用
-        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
-        // 断言
-        assertFalse(result.getSuccess());
-        assertEquals(response.getRequestId(), result.getApiRequestId());
-        assertEquals(response.getCode(), result.getApiCode());
-        assertEquals(response.getMessage(), result.getApiMsg());
-        assertEquals(response.getBizId(), result.getSerialNo());
-    }
+//    @Test
+//    public void tesSendSms_success() throws Throwable {
+//        // 准备参数
+//        Long sendLogId = randomLongId();
+//        String mobile = randomString();
+//        String apiTemplateId = randomString();
+//        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+//                new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
+//        // mock 方法
+//        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK"));
+//        when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
+//            assertEquals(mobile, acsRequest.getPhoneNumbers());
+//            assertEquals(properties.getSignature(), acsRequest.getSignName());
+//            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
+//            assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
+//            assertEquals(sendLogId.toString(), acsRequest.getOutId());
+//            return true;
+//        }))).thenReturn(response);
+//
+//        // 调用
+//        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
+//                apiTemplateId, templateParams);
+//        // 断言
+//        assertTrue(result.getSuccess());
+//        assertEquals(response.getRequestId(), result.getApiRequestId());
+//        assertEquals(response.getCode(), result.getApiCode());
+//        assertEquals(response.getMessage(), result.getApiMsg());
+//        assertEquals(response.getBizId(), result.getSerialNo());
+//    }
+
+//    @Test
+//    public void tesSendSms_fail() throws Throwable {
+//        // 准备参数
+//        Long sendLogId = randomLongId();
+//        String mobile = randomString();
+//        String apiTemplateId = randomString();
+//        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+//                new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
+//        // mock 方法
+//        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR"));
+//        when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
+//            assertEquals(mobile, acsRequest.getPhoneNumbers());
+//            assertEquals(properties.getSignature(), acsRequest.getSignName());
+//            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
+//            assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
+//            assertEquals(sendLogId.toString(), acsRequest.getOutId());
+//            return true;
+//        }))).thenReturn(response);
+//
+//        // 调用
+//        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+//        // 断言
+//        assertFalse(result.getSuccess());
+//        assertEquals(response.getRequestId(), result.getApiRequestId());
+//        assertEquals(response.getCode(), result.getApiCode());
+//        assertEquals(response.getMessage(), result.getApiMsg());
+//        assertEquals(response.getBizId(), result.getSerialNo());
+//    }
 
     @Test
     public void testParseSmsReceiveStatus() {
@@ -149,28 +129,28 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
         assertEquals(67890L, statuses.get(0).getLogId());
     }
 
-    @Test
-    public void testGetSmsTemplate() throws Throwable {
-        // 准备参数
-        String apiTemplateId = randomString();
-        // mock 方法
-        QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
-            o.setCode("OK");
-            o.setTemplateStatus(1); // 设置模板通过
-        });
-        when(client.getAcsResponse(argThat((ArgumentMatcher<QuerySmsTemplateRequest>) acsRequest -> {
-            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
-            return true;
-        }))).thenReturn(response);
-
-        // 调用
-        SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
-        // 断言
-        assertEquals(response.getTemplateCode(), result.getId());
-        assertEquals(response.getTemplateContent(), result.getContent());
-        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
-        assertEquals(response.getReason(), result.getAuditReason());
-    }
+//    @Test
+//    public void testGetSmsTemplate() throws Throwable {
+//        // 准备参数
+//        String apiTemplateId = randomString();
+//        // mock 方法
+//        QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
+//            o.setCode("OK");
+//            o.setTemplateStatus(1); // 设置模板通过
+//        });
+//        when(client.getAcsResponse(argThat((ArgumentMatcher<QuerySmsTemplateRequest>) acsRequest -> {
+//            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
+//            return true;
+//        }))).thenReturn(response);
+//
+//        // 调用
+//        SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
+//        // 断言
+//        assertEquals(response.getTemplateCode(), result.getId());
+//        assertEquals(response.getTemplateContent(), result.getContent());
+//        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+//        assertEquals(response.getReason(), result.getAuditReason());
+//    }
 
     @Test
     public void testConvertSmsTemplateAuditStatus() {

+ 66 - 1
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
 import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -17,7 +19,7 @@ public class SmsClientTests {
 
     @Test
     @Disabled
-    public void testHuaweiSmsClient() throws Throwable {
+    public void testHuaweiSmsClient_sendSms() throws Throwable {
         SmsChannelProperties properties = new SmsChannelProperties()
                 .setApiKey("123")
                 .setApiSecret("456");
@@ -33,4 +35,67 @@ public class SmsClientTests {
         System.out.println(smsSendRespDTO);
     }
 
+    // ========== 阿里云 ==========
+
+    @Test
+    @Disabled
+    public void testAliyunSmsClient_getSmsTemplate() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
+                .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
+        AliyunSmsClient client = new AliyunSmsClient(properties);
+        // 准备参数
+        String apiTemplateId = "SMS_207945135";
+        // 调用
+        SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
+        // 打印结果
+        System.out.println(template);
+    }
+
+    @Test
+    @Disabled
+    public void testAliyunSmsClient_sendSms() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
+                .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
+        AliyunSmsClient client = new AliyunSmsClient(properties);
+        // 准备参数
+        Long sendLogId = System.currentTimeMillis();
+        String mobile = "17321315478";
+        String apiTemplateId = "SMS_207945135";
+        // 调用
+        SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
+        // 打印结果
+        System.out.println(sendRespDTO);
+    }
+
+    @Test
+    @Disabled
+    public void testAliyunSmsClient_parseSmsReceiveStatus() {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
+                .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
+        AliyunSmsClient client = new AliyunSmsClient(properties);
+        // 准备参数
+        String text = "[\n" +
+                "  {\n" +
+                "    \"phone_number\" : \"13900000001\",\n" +
+                "    \"send_time\" : \"2017-01-01 11:12:13\",\n" +
+                "    \"report_time\" : \"2017-02-02 22:23:24\",\n" +
+                "    \"success\" : true,\n" +
+                "    \"err_code\" : \"DELIVERED\",\n" +
+                "    \"err_msg\" : \"用户接收成功\",\n" +
+                "    \"sms_size\" : \"1\",\n" +
+                "    \"biz_id\" : \"12345\",\n" +
+                "    \"out_id\" : \"67890\"\n" +
+                "  }\n" +
+                "]";
+        // mock 方法
+
+        // 调用
+        List<SmsReceiveRespDTO> statuses = client.parseSmsReceiveStatus(text);
+        // 打印结果
+        System.out.println(statuses);
+    }
+
 }

+ 1 - 1
yudao-server/src/main/resources/application-local.yaml

@@ -224,7 +224,7 @@ yudao:
   wxa-code:
     env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
   wxa-subscribe-message:
-    miniprogram-state: developer # 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;
+    miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
   tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
 
 justauth: