From c2a50c4d9c00b760aeef35063d0a20bc8302d5f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 14 Aug 2024 23:52:19 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E3=80=91SYSTEM=EF=BC=9A=E8=85=BE=E8=AE=AF=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=AE=A2=E6=88=B7=E7=AB=AF=E7=9A=84=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/AliyunSmsClient.java | 3 - .../sms/core/client/impl/HuaweiSmsClient.java | 6 +- .../core/client/impl/TencentSmsClient.java | 153 ++++++++++-------- .../core/client/impl/AliyunSmsClientTest.java | 9 -- .../sms/core/client/impl/SmsClientTests.java | 41 ++--- .../client/impl/TencentSmsClientTest.java | 26 +-- 6 files changed, 114 insertions(+), 124 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index ed6dd7a8d..f8158cdf2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/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() diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 4df820861..fdf2faa1a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/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> templateParams) throws Throwable { + // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 91f4c3f6b..23a01db24 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/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> templateParams) throws Throwable { - // 构建请求 + // 1. 执行请求 + // 参考链接 https://cloud.tencent.com/document/product/382/55981 TreeMap 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); - JSONObject JsonResponse = request(body,"SendSms","2021-01-11","ap-guangzhou"); - - 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")); + // 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")); } - JSONObject request(TreeMap body,String action,String version,String region) throws Exception { + @Override + public List 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 { + // 1. 构建请求 + // 参考链接 https://cloud.tencent.com/document/product/382/52067 + TreeMap body = new TreeMap<>(); + body.put("International", INTERNATIONAL_CHINA); + body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); + JSONObject response = request("DescribeSmsTemplateList", body); + + // 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); + } + + @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 签名方法 v3 + * + * @param action 请求的 API 名称 + * @param body 请求参数 + * @return 请求结果 + */ + private JSONObject request(String action, TreeMap 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 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 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)); - } - } } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java index c6e015d81..093060e84 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java +++ b/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 httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index b22f0f3f0..6eb22af1b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/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> 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> templateParams = List.of(new KeyValue<>("code", "1024")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index 66cb8250e..b25540b44 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/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 httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -57,11 +47,9 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { String apiTemplateId = randomString(); List> 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 httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { - // 准备参数 String apiTemplateId = "1122";