Browse Source

【代码评审】SYSTEM:腾讯云短信客户端的 review

YunaiV 7 months ago
parent
commit
c2a50c4d9c

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

@@ -102,8 +102,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
         queryParam.put("TemplateCode", apiTemplateId);
         JSONObject response = request("QuerySmsTemplate", queryParam);
 
-        System.out.println("getSmsTemplate response is =====" + response.toString());
-
         // 2.1 请求失败
         String code = response.getStr("Code");
         if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
@@ -170,7 +168,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
         // 4. 构建 Authorization 签名
         String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
         String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
-
         String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
         String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
         headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
-
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 
@@ -31,13 +30,12 @@ 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;
 
-
+// todo @scholar:参考阿里云在优化下
 /**
  * 华为短信客户端的实现类
  *
@@ -56,7 +54,6 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-
     }
 
     public HuaweiSmsClient(SmsChannelProperties properties) {
@@ -68,6 +65,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
                                   List<KeyValue<String, Object>> templateParams) throws Throwable {
+        // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
         // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
         // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
         String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号

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

@@ -25,7 +25,6 @@ import java.util.*;
 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
-
 /**
  * 腾讯云短信功能实现
  *
@@ -35,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  */
 public class TencentSmsClient extends AbstractSmsClient {
 
+    private static final String VERSION = "2021-01-11";
+    private static final String REGION = "ap-guangzhou";
+
     /**
      * 调用成功 code
      */
@@ -48,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
      */
     private static final long INTERNATIONAL_CHINA = 0L;
 
-
     public TencentSmsClient(SmsChannelProperties properties) {
         super(properties);
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
@@ -57,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-
     }
 
     /**
@@ -87,32 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
     @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<>();
-        String[] phones = {mobile};
-        body.put("PhoneNumberSet",phones);
-        body.put("SmsSdkAppId",getSdkAppId());
-        body.put("SignName",properties.getSignature());
+        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, e -> String.valueOf(e.getValue())));
+        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 responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
+        return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code")))
+                .setApiRequestId(responseResult.getStr("RequestId"))
+                .setSerialNo(responseData.getStr("SerialNo"))
+                .setApiMsg(responseData.getStr("Message"));
+    }
+
+    @Override
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
+        JSONArray statuses = JSONUtil.parseArray(text);
+        // 字段参考
+        return convertList(statuses, status -> {
+            JSONObject statusObj = (JSONObject) status;
+            return new SmsReceiveRespDTO()
+                    .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
+                    .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
+                    .setMobile(statusObj.getStr("mobile")) // 手机号
+                    .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
+                    .setSerialNo(statusObj.getStr("sid")); // 发送序列号
+        });
+    }
 
-        JSONObject JsonResponse = request(body,"SendSms","2021-01-11","ap-guangzhou");
+    @Override
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+        // 1. 构建请求
+        // 参考链接 https://cloud.tencent.com/document/product/382/52067
+        TreeMap<String, Object> body = new TreeMap<>();
+        body.put("International", INTERNATIONAL_CHINA);
+        body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
+        JSONObject response = request("DescribeSmsTemplateList", body);
 
-        return new SmsSendRespDTO().setSuccess(API_CODE_SUCCESS.equals(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Code")))
-                .setApiRequestId(JsonResponse.getJSONObject("Response").getStr("RequestId"))
-                .setSerialNo(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("SerialNo"))
-                .setApiMsg(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Message"));
+        // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)
+        JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
+        String content = TemplateStatusSet.get("TemplateContent").toString();
+        int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
+        String auditReason = TemplateStatusSet.get("ReviewReply").toString();
+
+        return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
+                .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
     }
 
-    JSONObject request(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
+    @VisibleForTesting
+    Integer convertSmsTemplateAuditStatus(int templateStatus) {
+        switch (templateStatus) {
+            case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+            case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+            case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
+        }
+    }
 
+    /**
+     * 请求腾讯云短信
+     *
+     * @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
+     *
+     * @param action 请求的 API 名称
+     * @param body 请求参数
+     * @return 请求结果
+     */
+    private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
         String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+        // TODO @scholar:这个 format,看看怎么写的可以简化点
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
         // 注意时区,否则容易出错
         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
         String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
 
+        // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
         // ************* 步骤 1:拼接规范请求串 *************
+        // TODO @scholar:这个 hsot 枚举下;
         String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
         String httpMethod = "POST"; // 请求方式
         String canonicalUri = "/";
@@ -122,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
                 + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
         String signedHeaders = "content-type;host;x-tc-action";
         String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
+        // TODO @scholar:换行下,不然单行太长了
         String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
 
         // ************* 步骤 2:拼接待签名字符串 *************
@@ -146,65 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
         headers.put("Host", host);
         headers.put("X-TC-Action", action);
         headers.put("X-TC-Timestamp", timestamp);
-        headers.put("X-TC-Version", version);
-        headers.put("X-TC-Region", region);
+        headers.put("X-TC-Version", VERSION);
+        headers.put("X-TC-Region", REGION);
 
-        String responseBody = HttpUtils.post("https://"+host, headers, JSONUtil.toJsonStr(body));
+        String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
 
         return JSONUtil.parseObj(responseBody);
     }
 
-    public static byte[] hmac256(byte[] key, String msg) throws Exception {
+    // TODO @scholar:使用 hutool 简化下
+    private static byte[] hmac256(byte[] key, String msg) throws Exception {
         Mac mac = Mac.getInstance("HmacSHA256");
         SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
         mac.init(secretKeySpec);
         return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
     }
-
-    @Override
-    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
-
-        JSONArray statuses = JSONUtil.parseArray(text);
-        // 字段参考
-        return convertList(statuses, status -> {
-            JSONObject statusObj = (JSONObject) status;
-            return new SmsReceiveRespDTO()
-                    .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
-                    .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
-                    .setMobile(statusObj.getStr("mobile")) // 手机号
-                    .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
-                    .setSerialNo(statusObj.getStr("sid")); // 发送序列号
-        });
-    }
-
-    @Override
-    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
-
-        // 构建请求
-        TreeMap<String, Object> body = new TreeMap<>();
-        body.put("International",INTERNATIONAL_CHINA);
-        Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
-        body.put("TemplateIdSet",templateIds);
-
-        JSONObject JsonResponse = request(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
-        System.out.println("JsonResponse======"+JsonResponse);
-
-        JSONObject TemplateStatusSet = JsonResponse.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
-        String content = TemplateStatusSet.get("TemplateContent").toString();
-        int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
-        String auditReason = TemplateStatusSet.get("ReviewReply").toString();
-
-        return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
-                .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
-    }
-
-    @VisibleForTesting
-    Integer convertSmsTemplateAuditStatus(int templateStatus) {
-        switch (templateStatus) {
-            case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
-            case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
-            case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
-            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
-        }
-    }
 }

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

@@ -38,15 +38,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
     @InjectMocks
     private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
 
-    @Test
-    public void testDoInit() {
-        // 准备参数
-        // mock 方法
-
-        // 调用
-        smsClient.doInit();
-    }
-
     @Test
     public void tesSendSms_success() throws Throwable {
         try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {

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

@@ -17,25 +17,6 @@ import java.util.List;
  */
 public class SmsClientTests {
 
-    @Test
-    @Disabled
-    public void testHuaweiSmsClient_sendSms() throws Throwable {
-        SmsChannelProperties properties = new SmsChannelProperties()
-                .setApiKey("123")
-                .setApiSecret("456")
-                .setSignature("runpu");
-        HuaweiSmsClient client = new HuaweiSmsClient(properties);
-        // 准备参数
-        Long sendLogId = System.currentTimeMillis();
-        String mobile = "15601691323";
-        String apiTemplateId = "xx test01";
-        List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
-        // 调用
-        SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
-        // 打印结果
-        System.out.println(smsSendRespDTO);
-    }
-
     // ========== 阿里云 ==========
 
     @Test
@@ -135,5 +116,27 @@ public class SmsClientTests {
         // 打印结果
         System.out.println(template);
     }
+
+    // ========== 华为云 ==========
+
+    @Test
+    @Disabled
+    public void testHuaweiSmsClient_sendSms() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("123")
+                .setApiSecret("456")
+                .setSignature("runpu");
+        HuaweiSmsClient client = new HuaweiSmsClient(properties);
+        // 准备参数
+        Long sendLogId = System.currentTimeMillis();
+        String mobile = "15601691323";
+        String apiTemplateId = "xx test01";
+        List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
+        // 调用
+        SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+        // 打印结果
+        System.out.println(smsSendRespDTO);
+    }
+
 }
 

+ 4 - 22
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java

@@ -38,18 +38,8 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
     @InjectMocks
     private TencentSmsClient smsClient = new TencentSmsClient(properties);
 
-    @Test
-    public void testDoInit() {
-        // 准备参数
-        // mock 方法
-
-        // 调用
-        smsClient.doInit();
-    }
-
     @Test
     public void testDoSendSms_success() throws Throwable {
-
         try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
             // 准备参数
             Long sendLogId = randomLongId();
@@ -57,11 +47,9 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
             String apiTemplateId = randomString();
             List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
                     new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
-
             // mock 方法
             httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
-                    .thenReturn(
-                            "{\n" +
+                    .thenReturn("{\n" +
                                     "    \"Response\": {\n" +
                                     "        \"SendStatusSet\": [\n" +
                                     "            {\n" +
@@ -76,8 +64,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
                                     "        ],\n" +
                                     "        \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
                                     "    }\n" +
-                                    "}"
-                    );
+                                    "}");
 
             // 调用
             SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
@@ -87,7 +74,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
             assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
             assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
             assertEquals("send success", result.getApiMsg());
-
         }
     }
 
@@ -103,8 +89,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
 
             // mock 方法
             httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
-                    .thenReturn(
-                            "{\n" +
+                    .thenReturn("{\n" +
                                     "    \"Response\": {\n" +
                                     "        \"SendStatusSet\": [\n" +
                                     "            {\n" +
@@ -119,8 +104,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
                                     "        ],\n" +
                                     "        \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
                                     "    }\n" +
-                                    "}"
-                    );
+                                    "}");
 
             // 调用
             SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
@@ -162,9 +146,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
 
     @Test
     public void testGetSmsTemplate() throws Throwable {
-
         try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
-
             // 准备参数
             String apiTemplateId = "1122";