Commit c2a50c4d9c00b760aeef35063d0a20bc8302d5f1

Authored by YunaiV
1 parent 5f6bcc4a

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

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 {
102 102 queryParam.put("TemplateCode", apiTemplateId);
103 103 JSONObject response = request("QuerySmsTemplate", queryParam);
104 104  
105   - System.out.println("getSmsTemplate response is =====" + response.toString());
106   -
107 105 // 2.1 请求失败
108 106 String code = response.getStr("Code");
109 107 if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
... ... @@ -170,7 +168,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
170 168 // 4. 构建 Authorization 签名
171 169 String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
172 170 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
173   -
174 171 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
175 172 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
176 173 headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
3   -
4 3 import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.util.StrUtil;
6 5  
... ... @@ -31,13 +30,12 @@ import java.util.*;
31 30  
32 31 import java.time.LocalDateTime;
33 32  
34   -
35 33 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
36 34 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
37 35 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
38 36 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
39 37  
40   -
  38 +// todo @scholar:参考阿里云在优化下
41 39 /**
42 40 * 华为短信客户端的实现类
43 41 *
... ... @@ -56,7 +54,6 @@ public class HuaweiSmsClient extends AbstractSmsClient {
56 54  
57 55 @Override
58 56 protected void doInit() {
59   -
60 57 }
61 58  
62 59 public HuaweiSmsClient(SmsChannelProperties properties) {
... ... @@ -68,6 +65,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
68 65 @Override
69 66 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
70 67 List<KeyValue<String, Object>> templateParams) throws Throwable {
  68 + // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
71 69 // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
72 70 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
73 71 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
... ...
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.*;
25 25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
26 26 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
27 27  
28   -
29 28 /**
30 29 * 腾讯云短信功能实现
31 30 *
... ... @@ -35,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
35 34 */
36 35 public class TencentSmsClient extends AbstractSmsClient {
37 36  
  37 + private static final String VERSION = "2021-01-11";
  38 + private static final String REGION = "ap-guangzhou";
  39 +
38 40 /**
39 41 * 调用成功 code
40 42 */
... ... @@ -48,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
48 50 */
49 51 private static final long INTERNATIONAL_CHINA = 0L;
50 52  
51   -
52 53 public TencentSmsClient(SmsChannelProperties properties) {
53 54 super(properties);
54 55 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
... ... @@ -57,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
57 58  
58 59 @Override
59 60 protected void doInit() {
60   -
61 61 }
62 62  
63 63 /**
... ... @@ -87,32 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
87 87 @Override
88 88 public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
89 89 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
90   - // 构建请求
  90 + // 1. 执行请求
  91 + // 参考链接 https://cloud.tencent.com/document/product/382/55981
91 92 TreeMap<String, Object> body = new TreeMap<>();
92   - String[] phones = {mobile};
93   - body.put("PhoneNumberSet",phones);
94   - body.put("SmsSdkAppId",getSdkAppId());
95   - body.put("SignName",properties.getSignature());
  93 + body.put("PhoneNumberSet", new String[]{mobile});
  94 + body.put("SmsSdkAppId", getSdkAppId());
  95 + body.put("SignName", properties.getSignature());
96 96 body.put("TemplateId",apiTemplateId);
97   - body.put("TemplateParamSet",ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
  97 + body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
  98 + JSONObject response = request("SendSms", body);
  99 +
  100 + // 2. 解析请求
  101 + JSONObject responseResult = response.getJSONObject("Response");
  102 + JSONObject error = responseResult.getJSONObject("Error");
  103 + if (error != null) {
  104 + return new SmsSendRespDTO().setSuccess(false)
  105 + .setApiRequestId(responseResult.getStr("RequestId"))
  106 + .setApiCode(error.getStr("Code"))
  107 + .setApiMsg(error.getStr("Message"));
  108 + }
  109 + JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
  110 + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code")))
  111 + .setApiRequestId(responseResult.getStr("RequestId"))
  112 + .setSerialNo(responseData.getStr("SerialNo"))
  113 + .setApiMsg(responseData.getStr("Message"));
  114 + }
  115 +
  116 + @Override
  117 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
  118 + JSONArray statuses = JSONUtil.parseArray(text);
  119 + // 字段参考
  120 + return convertList(statuses, status -> {
  121 + JSONObject statusObj = (JSONObject) status;
  122 + return new SmsReceiveRespDTO()
  123 + .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
  124 + .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
  125 + .setMobile(statusObj.getStr("mobile")) // 手机号
  126 + .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
  127 + .setSerialNo(statusObj.getStr("sid")); // 发送序列号
  128 + });
  129 + }
98 130  
99   - JSONObject JsonResponse = request(body,"SendSms","2021-01-11","ap-guangzhou");
  131 + @Override
  132 + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
  133 + // 1. 构建请求
  134 + // 参考链接 https://cloud.tencent.com/document/product/382/52067
  135 + TreeMap<String, Object> body = new TreeMap<>();
  136 + body.put("International", INTERNATIONAL_CHINA);
  137 + body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
  138 + JSONObject response = request("DescribeSmsTemplateList", body);
100 139  
101   - return new SmsSendRespDTO().setSuccess(API_CODE_SUCCESS.equals(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Code")))
102   - .setApiRequestId(JsonResponse.getJSONObject("Response").getStr("RequestId"))
103   - .setSerialNo(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("SerialNo"))
104   - .setApiMsg(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Message"));
  140 + // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)
  141 + JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
  142 + String content = TemplateStatusSet.get("TemplateContent").toString();
  143 + int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
  144 + String auditReason = TemplateStatusSet.get("ReviewReply").toString();
  145 +
  146 + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
  147 + .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
105 148 }
106 149  
107   - JSONObject request(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
  150 + @VisibleForTesting
  151 + Integer convertSmsTemplateAuditStatus(int templateStatus) {
  152 + switch (templateStatus) {
  153 + case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  154 + case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  155 + case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  156 + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
  157 + }
  158 + }
108 159  
  160 + /**
  161 + * 请求腾讯云短信
  162 + *
  163 + * @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
  164 + *
  165 + * @param action 请求的 API 名称
  166 + * @param body 请求参数
  167 + * @return 请求结果
  168 + */
  169 + private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
109 170 String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
  171 + // TODO @scholar:这个 format,看看怎么写的可以简化点
110 172 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
111 173 // 注意时区,否则容易出错
112 174 sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
113 175 String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
114 176  
  177 + // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
115 178 // ************* 步骤 1:拼接规范请求串 *************
  179 + // TODO @scholar:这个 hsot 枚举下;
116 180 String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
117 181 String httpMethod = "POST"; // 请求方式
118 182 String canonicalUri = "/";
... ... @@ -122,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
122 186 + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
123 187 String signedHeaders = "content-type;host;x-tc-action";
124 188 String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
  189 + // TODO @scholar:换行下,不然单行太长了
125 190 String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
126 191  
127 192 // ************* 步骤 2:拼接待签名字符串 *************
... ... @@ -146,65 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
146 211 headers.put("Host", host);
147 212 headers.put("X-TC-Action", action);
148 213 headers.put("X-TC-Timestamp", timestamp);
149   - headers.put("X-TC-Version", version);
150   - headers.put("X-TC-Region", region);
  214 + headers.put("X-TC-Version", VERSION);
  215 + headers.put("X-TC-Region", REGION);
151 216  
152   - String responseBody = HttpUtils.post("https://"+host, headers, JSONUtil.toJsonStr(body));
  217 + String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
153 218  
154 219 return JSONUtil.parseObj(responseBody);
155 220 }
156 221  
157   - public static byte[] hmac256(byte[] key, String msg) throws Exception {
  222 + // TODO @scholar:使用 hutool 简化下
  223 + private static byte[] hmac256(byte[] key, String msg) throws Exception {
158 224 Mac mac = Mac.getInstance("HmacSHA256");
159 225 SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
160 226 mac.init(secretKeySpec);
161 227 return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
162 228 }
163   -
164   - @Override
165   - public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
166   -
167   - JSONArray statuses = JSONUtil.parseArray(text);
168   - // 字段参考
169   - return convertList(statuses, status -> {
170   - JSONObject statusObj = (JSONObject) status;
171   - return new SmsReceiveRespDTO()
172   - .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
173   - .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
174   - .setMobile(statusObj.getStr("mobile")) // 手机号
175   - .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
176   - .setSerialNo(statusObj.getStr("sid")); // 发送序列号
177   - });
178   - }
179   -
180   - @Override
181   - public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
182   -
183   - // 构建请求
184   - TreeMap<String, Object> body = new TreeMap<>();
185   - body.put("International",INTERNATIONAL_CHINA);
186   - Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
187   - body.put("TemplateIdSet",templateIds);
188   -
189   - JSONObject JsonResponse = request(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
190   - System.out.println("JsonResponse======"+JsonResponse);
191   -
192   - JSONObject TemplateStatusSet = JsonResponse.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
193   - String content = TemplateStatusSet.get("TemplateContent").toString();
194   - int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
195   - String auditReason = TemplateStatusSet.get("ReviewReply").toString();
196   -
197   - return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
198   - .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
199   - }
200   -
201   - @VisibleForTesting
202   - Integer convertSmsTemplateAuditStatus(int templateStatus) {
203   - switch (templateStatus) {
204   - case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
205   - case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
206   - case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
207   - default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
208   - }
209   - }
210 229 }
211 230 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java
... ... @@ -39,15 +39,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
39 39 private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
40 40  
41 41 @Test
42   - public void testDoInit() {
43   - // 准备参数
44   - // mock 方法
45   -
46   - // 调用
47   - smsClient.doInit();
48   - }
49   -
50   - @Test
51 42 public void tesSendSms_success() throws Throwable {
52 43 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
53 44 // 准备参数
... ...
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;
17 17 */
18 18 public class SmsClientTests {
19 19  
20   - @Test
21   - @Disabled
22   - public void testHuaweiSmsClient_sendSms() throws Throwable {
23   - SmsChannelProperties properties = new SmsChannelProperties()
24   - .setApiKey("123")
25   - .setApiSecret("456")
26   - .setSignature("runpu");
27   - HuaweiSmsClient client = new HuaweiSmsClient(properties);
28   - // 准备参数
29   - Long sendLogId = System.currentTimeMillis();
30   - String mobile = "15601691323";
31   - String apiTemplateId = "xx test01";
32   - List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
33   - // 调用
34   - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
35   - // 打印结果
36   - System.out.println(smsSendRespDTO);
37   - }
38   -
39 20 // ========== 阿里云 ==========
40 21  
41 22 @Test
... ... @@ -135,5 +116,27 @@ public class SmsClientTests {
135 116 // 打印结果
136 117 System.out.println(template);
137 118 }
  119 +
  120 + // ========== 华为云 ==========
  121 +
  122 + @Test
  123 + @Disabled
  124 + public void testHuaweiSmsClient_sendSms() throws Throwable {
  125 + SmsChannelProperties properties = new SmsChannelProperties()
  126 + .setApiKey("123")
  127 + .setApiSecret("456")
  128 + .setSignature("runpu");
  129 + HuaweiSmsClient client = new HuaweiSmsClient(properties);
  130 + // 准备参数
  131 + Long sendLogId = System.currentTimeMillis();
  132 + String mobile = "15601691323";
  133 + String apiTemplateId = "xx test01";
  134 + List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
  135 + // 调用
  136 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
  137 + // 打印结果
  138 + System.out.println(smsSendRespDTO);
  139 + }
  140 +
138 141 }
139 142  
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java
... ... @@ -39,17 +39,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
39 39 private TencentSmsClient smsClient = new TencentSmsClient(properties);
40 40  
41 41 @Test
42   - public void testDoInit() {
43   - // 准备参数
44   - // mock 方法
45   -
46   - // 调用
47   - smsClient.doInit();
48   - }
49   -
50   - @Test
51 42 public void testDoSendSms_success() throws Throwable {
52   -
53 43 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
54 44 // 准备参数
55 45 Long sendLogId = randomLongId();
... ... @@ -57,11 +47,9 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
57 47 String apiTemplateId = randomString();
58 48 List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
59 49 new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
60   -
61 50 // mock 方法
62 51 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
63   - .thenReturn(
64   - "{\n" +
  52 + .thenReturn("{\n" +
65 53 " \"Response\": {\n" +
66 54 " \"SendStatusSet\": [\n" +
67 55 " {\n" +
... ... @@ -76,8 +64,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
76 64 " ],\n" +
77 65 " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
78 66 " }\n" +
79   - "}"
80   - );
  67 + "}");
81 68  
82 69 // 调用
83 70 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
... ... @@ -87,7 +74,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
87 74 assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
88 75 assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
89 76 assertEquals("send success", result.getApiMsg());
90   -
91 77 }
92 78 }
93 79  
... ... @@ -103,8 +89,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
103 89  
104 90 // mock 方法
105 91 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
106   - .thenReturn(
107   - "{\n" +
  92 + .thenReturn("{\n" +
108 93 " \"Response\": {\n" +
109 94 " \"SendStatusSet\": [\n" +
110 95 " {\n" +
... ... @@ -119,8 +104,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
119 104 " ],\n" +
120 105 " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
121 106 " }\n" +
122   - "}"
123   - );
  107 + "}");
124 108  
125 109 // 调用
126 110 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
... ... @@ -162,9 +146,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
162 146  
163 147 @Test
164 148 public void testGetSmsTemplate() throws Throwable {
165   -
166 149 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
167   -
168 150 // 准备参数
169 151 String apiTemplateId = "1122";
170 152  
... ...