Selaa lähdekoodia

feat(system): 集成腾讯云短信服务

- 添加腾讯云短信 SDK 依赖
- 实现腾讯云短信发送功能
- 更新短信服务配置
- 重构短信发送逻辑,支持腾讯云短信
zrd 2 kuukautta sitten
vanhempi
sitoutus
41557a2962

+ 6 - 1
yudao-dependencies/pom.xml

@@ -122,7 +122,12 @@
                 <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
                 <version>${revision}</version>
             </dependency>
-
+            <dependency>
+                <groupId>com.tencentcloudapi</groupId>
+                <artifactId>tencentcloud-sdk-java</artifactId>
+                <!-- 注:这里可选择指定版本号,最新版本请参考官网 -->
+                <version>3.1.563</version>
+            </dependency>
             <!-- Spring 核心 -->
             <dependency>
                 <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->

+ 20 - 2
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -64,13 +64,21 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-redis</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java</artifactId>
+            <!-- 注:这里可选择指定版本号,最新版本请参考官网 -->
+        </dependency>
         <!-- Job 定时任务相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-job</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-stdlib-jdk8</artifactId>
+            <version>1.8.22</version> <!-- 使用最新稳定版本 -->
+        </dependency>
         <!-- 消息队列相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
@@ -119,6 +127,16 @@
             <groupId>org.dromara.hutool</groupId>
             <artifactId>hutool-extra</artifactId> <!-- 邮件 -->
         </dependency>
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java-common</artifactId>
+            <version>3.1.880</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java-sms</artifactId>
+            <version>3.1.880</version>
+        </dependency>
 
     </dependencies>
 

+ 100 - 35
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java

@@ -18,6 +18,13 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
 import com.google.common.annotations.VisibleForTesting;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.sms.v20210111.SmsClient;
+import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
+import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
+import com.tencentcloudapi.sms.v20210111.models.SendStatus;
 
 import java.nio.charset.StandardCharsets;
 import java.util.*;
@@ -33,16 +40,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author shiwp
  */
 public class TencentSmsClient extends AbstractSmsClient {
-
-    private static final String HOST = "sms.tencentcloudapi.com";
-    private static final String VERSION = "2021-01-11";
-    private static final String REGION = "ap-guangzhou";
-
+    
     /**
      * 调用成功 code
      */
     public static final String API_CODE_SUCCESS = "Ok";
-
+    private static final String HOST = "sms.tencentcloudapi.com";
+    private static final String VERSION = "2021-01-11";
+    private static final String REGION = "ap-guangzhou";
     /**
      * 是否国际/港澳台短信:
      *
@@ -72,42 +77,106 @@ public class TencentSmsClient extends AbstractSmsClient {
         String[] keys = combineKey.trim().split(" ");
         Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
     }
-
+    
+    private static byte[] hmac256(byte[] key, String msg) {
+        return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
+    }
+    
     private String getSdkAppId() {
         return StrUtil.subAfter(properties.getApiKey(), " ", true);
     }
-
+    
     private String getApiKey() {
         return StrUtil.subBefore(properties.getApiKey(), " ", true);
     }
-
+    
+    //    @Override
+//    public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
+//                                  String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws
+//                                  Throwable {
+//        // 1. 执行请求
+//        // 参考链接 https://cloud.tencent.com/document/product/382/55981
+//        TreeMap<String, Object> body = new TreeMap<>();
+//        body.put("PhoneNumberSet", new String[]{mobile});
+//        body.put("SmsSdkAppId", getSdkAppId());
+//        body.put("SignName", properties.getSignature());
+//        body.put("TemplateId", apiTemplateId);
+//        body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
+//        JSONObject response = request("SendSms", body);
+//
+//        // 2. 解析请求
+//        JSONObject responseResult = response.getJSONObject("Response");
+//        JSONObject error = responseResult.getJSONObject("Error");
+//        if (error != null) {
+//            return new SmsSendRespDTO().setSuccess(false)
+//                    .setApiRequestId(responseResult.getStr("RequestId"))
+//                    .setApiCode(error.getStr("Code"))
+//                    .setApiMsg(error.getStr("Message"));
+//        }
+//        JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
+//        return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
+//                .setApiRequestId(responseResult.getStr("RequestId"))
+//                .setSerialNo(sendResult.getStr("SerialNo"))
+//                .setApiMsg(sendResult.getStr("Message"));
+//    }
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
                                   String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
         // 1. 执行请求
-        // 参考链接 https://cloud.tencent.com/document/product/382/55981
-        TreeMap<String, Object> body = new TreeMap<>();
-        body.put("PhoneNumberSet", new String[]{mobile});
-        body.put("SmsSdkAppId", getSdkAppId());
-        body.put("SignName", properties.getSignature());
-        body.put("TemplateId", apiTemplateId);
-        body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
-        JSONObject response = request("SendSms", body);
-
-        // 2. 解析请求
-        JSONObject responseResult = response.getJSONObject("Response");
-        JSONObject error = responseResult.getJSONObject("Error");
-        if (error != null) {
-            return new SmsSendRespDTO().setSuccess(false)
-                    .setApiRequestId(responseResult.getStr("RequestId"))
-                    .setApiCode(error.getStr("Code"))
-                    .setApiMsg(error.getStr("Message"));
+        Credential cred = new Credential(
+                getApiKey(),
+                properties.getApiSecret()
+        );
+        
+        // 实例化一个http选项,可选的,没有特殊需求可以跳过
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint("sms.tencentcloudapi.com");
+        
+        // 实例化一个client选项,可选的,没有特殊需求可以跳过
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        
+        // 实例化要请求产品的client对象,clientProfile是可选的
+        SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
+        
+        // 实例化一个请求对象,每个接口都会对应一个request对象
+        SendSmsRequest req = new SendSmsRequest();
+        
+        // 设置请求参数
+        String[] phoneNumberSet = {"+86" + mobile};
+        req.setPhoneNumberSet(phoneNumberSet);
+        
+        // 短信应用ID
+        req.setSmsSdkAppId(getSdkAppId());
+        
+        // 短信签名内容
+        req.setSignName(properties.getSignature());
+        
+        // 模板ID
+        req.setTemplateId(apiTemplateId);
+        
+        // 模板参数
+        req.setTemplateParamSet(ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
+        
+        // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
+        SendSmsResponse resp = client.SendSms(req);
+        SmsSendRespDTO smsSendRespDTO = new SmsSendRespDTO();
+        SendStatus[] status = resp.getSendStatusSet();
+        if (status.length > 0) {
+            SendStatus firstStatus = status[0];
+            // 使用firstStatus做后续处理
+            smsSendRespDTO.setSuccess(Objects.equals(API_CODE_SUCCESS, firstStatus.getCode()));
+            smsSendRespDTO.setApiRequestId(resp.getRequestId());
+            smsSendRespDTO.setSerialNo(firstStatus.getSerialNo());
+            smsSendRespDTO.setApiCode(firstStatus.getIsoCode());
+            smsSendRespDTO.setApiMsg(firstStatus.getMessage());
+            
+            return smsSendRespDTO;
+        } else {
+            // 处理数组为空的情况,避免ArrayIndexOutOfBoundsException
+            return null;
         }
-        JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
-        return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
-                .setApiRequestId(responseResult.getStr("RequestId"))
-                .setSerialNo(sendResult.getStr("SerialNo"))
-                .setApiMsg(sendResult.getStr("Message"));
+        
     }
 
     @Override
@@ -194,8 +263,4 @@ public class TencentSmsClient extends AbstractSmsClient {
         return JSONUtil.parseObj(responseBody);
     }
 
-    private static byte[] hmac256(byte[] key, String msg) {
-        return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
-    }
-
 }

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

@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
+
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.sms.v20210111.SmsClient;
+import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
+import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
+
+public class TencentSmsSender {
+    
+    /**
+     * 发送腾讯云短信
+     *
+     * @param phoneNumber    接收短信的手机号,格式如: "+8613800138000"
+     * @param templateId     短信模板ID
+     * @param templateParams 模板参数数组,如 ["验证码", "有效时间"]
+     * @param signName       短信签名
+     * @return 发送结果
+     */
+    public static String sendSms(String phoneNumber, String templateId,
+                                 String[] templateParams, String signName) {
+        try {
+            // 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey
+          /*  Credential cred = new Credential(
+                    System.getenv("TENCENTCLOUD_SECRET_ID"),
+                    System.getenv("TENCENTCLOUD_SECRET_KEY")
+            );*/
+            Credential cred = new Credential(
+                    "AKIDra7RXP9sT5Yqxtb7PWP6arwoeP2g9PSH",
+                    "iCUdjeL3qiU9ct0xkMnAQar20kHahfS3"
+            );
+            
+            // 实例化一个http选项,可选的,没有特殊需求可以跳过
+            HttpProfile httpProfile = new HttpProfile();
+            httpProfile.setEndpoint("sms.tencentcloudapi.com");
+            
+            // 实例化一个client选项,可选的,没有特殊需求可以跳过
+            ClientProfile clientProfile = new ClientProfile();
+            clientProfile.setHttpProfile(httpProfile);
+            
+            // 实例化要请求产品的client对象,clientProfile是可选的
+            SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
+            
+            // 实例化一个请求对象,每个接口都会对应一个request对象
+            SendSmsRequest req = new SendSmsRequest();
+            
+            // 设置请求参数
+            String[] phoneNumberSet = {phoneNumber};
+            req.setPhoneNumberSet(phoneNumberSet);
+            
+            // 短信应用ID
+            req.setSmsSdkAppId("1400984843");
+            
+            // 短信签名内容
+            req.setSignName(signName);
+            
+            // 模板ID
+            req.setTemplateId(templateId);
+            
+            // 模板参数
+            req.setTemplateParamSet(templateParams);
+            
+            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
+            SendSmsResponse resp = client.SendSms(req);
+            
+            // 输出json格式的字符串回包
+            System.out.println(SendSmsResponse.toJsonString(resp));
+            return SendSmsResponse.toJsonString(resp);
+            
+        } catch (TencentCloudSDKException e) {
+            System.err.println("短信发送失败: " + e.toString());
+            return null;
+        }
+    }
+    
+    public static void main(String[] args) {
+        // 设置环境变量或直接替换下面的值
+        // System.setProperty("TENCENTCLOUD_SECRET_ID", "你的SecretId");
+        // System.setProperty("TENCENTCLOUD_SECRET_KEY", "你的SecretKey");
+        // System.setProperty("TENCENTCLOUD_SMS_APPID", "你的SMS应用ID");
+        
+        // 发送短信
+        sendSms(
+                "+8618864472866",    // 接收短信的手机号
+                "2427515",            // 短信模板ID
+                new String[]{"127556"}, // 模板参数:验证码和有效时间
+                "江苏赢伟达资产管理"             // 短信签名
+        );
+    }
+}

+ 2 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java

@@ -10,10 +10,10 @@ import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO;
 import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsCodeMapper;
 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
 import cn.iocoder.yudao.module.system.framework.sms.config.SmsCodeProperties;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 import java.time.LocalDateTime;
 
 import static cn.hutool.core.util.RandomUtil.randomInt;
@@ -73,6 +73,7 @@ public class SmsCodeServiceImpl implements SmsCodeService {
                 .todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1)
                 .createIp(ip).used(false).build();
         smsCodeMapper.insert(newSmsCode);
+        //发送短信
         return code;
     }
 

+ 2 - 2
yudao-server/src/main/resources/application-dev.yaml

@@ -155,8 +155,8 @@ wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-sta
       key-prefix: wx # Redis Key 的前缀
       http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
   miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
-    appid: wx63c280fe3248a3e7
-    secret: 6f270509224a7ae1296bbf1c8cb97aed
+    appid: wxa93e976708131efe
+    secret: 2a60a659508cb2a0d13661bb67ea95ee
     config-storage:
       type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
       key-prefix: wa # Redis Key 的前缀

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

@@ -341,7 +341,7 @@ yudao:
     expire-times: 10m
     send-frequency: 1m
     send-maximum-quantity-per-day: 10
-    begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
+    begin-code: 1000 # 这里配置 9999 的原因是,测试方便。
     end-code: 9999 # 这里配置 9999 的原因是,测试方便。
   trade:
     order: