From 4f7ac969feec4fc61f403f3aa2e605473900e544 Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Mon, 26 Aug 2024 15:52:41 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=83=E7=89=9B?= =?UTF-8?q?=E4=BA=91=E7=9F=AD=E4=BF=A1=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/http/HttpUtils.java | 42 ++++- .../admin/sms/SmsCallbackController.java | 17 +- .../sms/core/client/impl/QiniuSmsClient.java | 172 ++++++++++++++++++ .../sms/core/enums/SmsChannelEnum.java | 2 + .../core/client/impl/QiniuSmsClientTest.java | 128 +++++++++++++ .../sms/core/client/impl/SmsClientTests.java | 138 +++++++------- 6 files changed, 428 insertions(+), 71 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 9a39a7a4e3..1697d097ff 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -5,6 +5,8 @@ import cn.hutool.core.map.TableMap; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -109,7 +111,7 @@ public class HttpUtils { authorization = Base64.decodeStr(authorization); clientId = StrUtil.subBefore(authorization, ":", false); clientSecret = StrUtil.subAfter(authorization, ":", false); - // 再从 Param 中获取 + // 再从 Param 中获取 } else { clientId = request.getParameter("client_id"); clientSecret = request.getParameter("client_secret"); @@ -122,5 +124,43 @@ public class HttpUtils { return null; } + /** + * HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @param requestBody 请求体 + * @return 请求结果 + */ + public static String post(String url, Map headers, String requestBody) { + + try (HttpResponse response = HttpRequest.post(url) + .addHeaders(headers) + .body(requestBody) + .execute()) { + return response.body(); + } + } + + /** + * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @return 请求结果 + */ + public static String get(String url, Map headers) { + + try (HttpResponse response = HttpRequest.get(url) + .addHeaders(headers) + .execute()) { + return response.body(); + } + } } + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java index 90cb763cc3..f4712f0abb 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -2,10 +2,8 @@ package cn.iocoder.yudao.module.system.controller.admin.sms; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; -import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; -import com.xingyuv.captcha.util.StreamUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @@ -14,8 +12,6 @@ import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; -import java.nio.charset.Charset; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信回调") @@ -29,7 +25,6 @@ public class SmsCallbackController { @PostMapping("/aliyun") @PermitAll @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档") - @OperateLog(enable = false) public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); @@ -39,7 +34,6 @@ public class SmsCallbackController { @PostMapping("/tencent") @PermitAll @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档") - @OperateLog(enable = false) public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); @@ -50,9 +44,18 @@ public class SmsCallbackController { @PostMapping("/huawei") @PermitAll @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") - @OperateLog(enable = false) public CommonResult receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable { smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody); return success(true); } + + @PostMapping("/qiniu") + @PermitAll + @Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档") + public CommonResult receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable { + smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody); + return success(true); + } + } + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java new file mode 100644 index 0000000000..c0a2b60ac8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -0,0 +1,172 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +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.http.HttpUtils; +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.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 七牛云短信客户端的实现类 + * + * @author scholar + * @since 2024/08/26 15:35 + */ +@Slf4j +public class QiniuSmsClient extends AbstractSmsClient { + + private static final String HOST = "sms.qiniuapi.com"; + + private static final String PATH = "/v1/message/single"; + + private static final String TEMPLATE_PATH = "/v1/template"; + + public QiniuSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable { + + // 1. 执行请求 + // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages + LinkedHashMap body = new LinkedHashMap<>(); + Map paramsMap = templateParams.stream() + .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); + + body.put("template_id", apiTemplateId); + body.put("mobile", mobile); + body.put("parameters", paramsMap); + body.put("seq", Long.toString(sendLogId)); + + JSONObject response = request("POST", body, null); + // 2. 解析请求 + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) + .setSerialNo(response.getStr("message_id")); + } + + + /** + * 请求七牛云短信 + * + * @see + * @param httpMethod http请求方法 + * @param queryParams 请求参数 + * @return 请求结果 + */ + private JSONObject request(String httpMethod, LinkedHashMap body, Map queryParams) { + + String signature = ""; + String templateIdPath = ""; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String signDate = dateFormat.format(new Date()); + + //请求头 + Map header = new HashMap<>(4); + header.put("HOST", HOST); + header.put("Authorization", signature); + header.put("Content-Type", "application/json"); + header.put("X-Qiniu-Date", signDate); + + String responseBody =""; + if(Objects.equals(httpMethod, "POST")){ + header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate)); + responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body)); + }else { // GET + templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id"); + header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate)); + responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header); + } + return JSONUtil.parseObj(responseBody); + } + + public String getSignature(String method, String host, String path, String body, String signDate) { + + StringBuilder dataToSign = new StringBuilder(); + dataToSign.append(method.toUpperCase()).append(" ").append(path); + dataToSign.append("\nHost: ").append(host); + dataToSign.append("\n").append("Content-Type").append(": ").append("application/json"); + dataToSign.append("\n").append("X-Qiniu-Date").append(": ").append(signDate); + dataToSign.append("\n\n"); + if (ObjectUtil.isNotEmpty(body)) { + dataToSign.append(body); + } + HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8)); + byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8)); + String encodedSignature = Base64.getEncoder().encodeToString(signData); + + return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; + } + + @Override + public List parseSmsReceiveStatus(String text) { + + JSONObject status = JSONUtil.parseObj(text); + //字段参考 https://developer.qiniu.com/sms/5910/message-push + return ListUtil.of(new SmsReceiveRespDTO() + .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 + .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) + .setMobile(status.getJSONArray("items").getJSONObject(0).getStr("mobile")) // 手机号 + .setReceiveTime(LocalDateTimeUtil.of(status.getJSONArray("items").getJSONObject(0).getLong("delivrd_at")*1000L)) + .setSerialNo(status.getJSONArray("items").getJSONObject(0).getStr("message_id")) // 发送序列号 + .setLogId(Long.valueOf(status.getJSONArray("items").getJSONObject(0).getStr("seq")))); // logId + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 1. 执行请求 + // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template + HashMap queryParam = new HashMap<>(); + queryParam.put("template_id", apiTemplateId); + JSONObject response = request("GET", null, queryParam); + + // 2.1 请求失败 + String status = response.getStr("audit_status"); + if (!Objects.equals(status, "passed")) { + log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); + return null; + } + // 2.2 请求成功 + return new SmsTemplateRespDTO() + .setId(response.getStr("id")) + .setContent(response.getStr("template")) + .setAuditStatus(convertSmsTemplateAuditStatus(response.getStr("audit_status"))) + .setAuditReason(response.getStr("reject_reason")); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(String templateStatus) { + + if(Objects.equals(templateStatus, "passed")){ + return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + }else { + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); + } + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java index 88f578a18a..cbbde696b6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java @@ -18,6 +18,7 @@ public enum SmsChannelEnum { ALIYUN("ALIYUN", "阿里云"), TENCENT("TENCENT", "腾讯云"), HUAWEI("HUAWEI", "华为云"), + QINIU("QINIU", "七牛云"), ; /** @@ -34,3 +35,4 @@ public enum SmsChannelEnum { } } + diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java new file mode 100644 index 0000000000..c64c39470f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +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.google.common.collect.Lists; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; + +/** + * {@link QiniuSmsClient} 的单元测试 + * + * @author scholar + */ +public class QiniuSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private QiniuSmsClient smsClient = new QiniuSmsClient(properties); + + @Test + public void testDoInit() { + // 调用 + smsClient.doInit(); + } + + @Test + public void testDoSendSms_success() throws Throwable { + + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"message_id\":\"17245678901\"}" + ); + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertTrue(result.getSuccess()); + assertEquals("17245678901", result.getSerialNo()); + } + } + + @Test + public void testDoSendSms_fail() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}" + ); + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + } + } + + @Test + public void testGetSmsTemplate() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.get(anyString(), anyMap())) + .thenReturn("{\"audit_status\":\"passed\",\"created_at\":1724231187,\"description\":\"\",\"disable_broadcast\":false,\"disable_broadcast_reason\":\"\",\"disable_reason\":\"\",\"disabled\":false,\"id\":\"1826184073773596672\",\"is_oversea\":false,\"name\":\"dd\",\"parameters\":[\"code\"],\"reject_reason\":\"\",\"signature_id\":\"1826099896017498112\",\"signature_text\":\"yudao\",\"template\":\"您的验证码为:${code}\",\"type\":\"verification\",\"uid\":1383022432,\"updated_at\":1724288561,\"variable_count\":0}"); + // 调用 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); + // 断言 + assertEquals("1826184073773596672", result.getId()); + assertEquals("您的验证码为:${code}", result.getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); + assertEquals("", result.getAuditReason()); + } + } + + @Test + public void testParseSmsReceiveStatus() { + // 准备参数 + String text = "{\"items\":[{\"mobile\":\"18881234567\",\"message_id\":\"10135515063508004167\",\"status\":\"DELIVRD\",\"delivrd_at\":1724591666,\"error\":\"DELIVRD\",\"seq\":\"123\"}]}"; + // 调用 + List statuses = smsClient.parseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.getFirst().getSuccess()); + assertEquals("DELIVRD", statuses.getFirst().getErrorMsg()); + assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), statuses.getFirst().getReceiveTime()); + assertEquals("18881234567", statuses.getFirst().getMobile()); + assertEquals("10135515063508004167", statuses.getFirst().getSerialNo()); + assertEquals(123, statuses.getFirst().getLogId()); + } + +} \ 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/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 f1db141e80..3752e5763c 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 @@ -1,7 +1,7 @@ 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.SmsClient; 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; @@ -11,39 +11,19 @@ import org.junit.jupiter.api.Test; import java.util.List; /** - * 各种 {@link SmsClientTests 集成测试 + * 各种 {@link SmsClient} 的集成测试 * * @author 芋道源码 */ 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 @Disabled public void testAliyunSmsClient_getSmsTemplate() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 String apiTemplateId = "SMS_207945135"; @@ -57,9 +37,9 @@ public class SmsClientTests { @Disabled public void testAliyunSmsClient_sendSms() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") - .setSignature("runpu"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")) + .setSignature("Ballcat"); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); @@ -71,49 +51,21 @@ public class SmsClientTests { 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 statuses = client.parseSmsReceiveStatus(text); - // 打印结果 - System.out.println(statuses); - } - // ========== 腾讯云 ========== @Test @Disabled public void testTencentSmsClient_sendSms() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); // 打印结果 @@ -123,17 +75,77 @@ public class SmsClientTests { @Test @Disabled public void testTencentSmsClient_getSmsTemplate() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); // 打印结果 System.out.println(template); } -} + // ========== 华为云 ========== + + @Test + @Disabled + public void testHuaweiSmsClient_sendSms() throws Throwable { + String sender = "x8824060312575"; + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender) + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY")) + .setSignature("runpu"); + HuaweiSmsClient client = new HuaweiSmsClient(properties); + // 准备参数 + Long sendLogId = System.currentTimeMillis(); + String mobile = "17321315478"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + List> templateParams = List.of(new KeyValue<>("code", "1024")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + + // ========== 七牛云 ========== + + @Test + @Disabled + public void testQiniuSmsClient_sendSms() throws Throwable { + + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("SMS_QINIU_ACCESS_KEY") + .setApiSecret("SMS_QINIU_SECRET_KEY"); + QiniuSmsClient client = new QiniuSmsClient(properties); + // 准备参数 + Long sendLogId = System.currentTimeMillis(); + String mobile = "17321315478"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + List> templateParams = List.of(new KeyValue<>("code", "1122")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + + @Test + @Disabled + public void testQiniuSmsClient_getSmsTemplate() throws Throwable { + + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("SMS_QINIU_ACCESS_KEY") + .setApiSecret("SMS_QINIU_SECRET_KEY"); + QiniuSmsClient client = new QiniuSmsClient(properties); + // 准备参数 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + // 调用 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); + // 打印结果 + System.out.println(template); + } + +} \ No newline at end of file From 1c1abae5bbf8357fb32044e766cb92b06603dcfe Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Wed, 28 Aug 2024 10:51:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=B8=83=E7=89=9B=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=AE=9E=E7=8E=B0=EF=BC=8C=E8=AF=84=E5=AE=A1=E6=84=8F?= =?UTF-8?q?=E8=A7=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/http/HttpUtils.java | 2 - .../sms/core/client/impl/QiniuSmsClient.java | 85 ++++++++----------- .../core/client/impl/QiniuSmsClientTest.java | 25 ++++-- .../sms/core/client/impl/SmsClientTests.java | 2 - 4 files changed, 51 insertions(+), 63 deletions(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 1697d097ff..456b4007ed 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -135,7 +135,6 @@ public class HttpUtils { * @return 请求结果 */ public static String post(String url, Map headers, String requestBody) { - try (HttpResponse response = HttpRequest.post(url) .addHeaders(headers) .body(requestBody) @@ -154,7 +153,6 @@ public class HttpUtils { * @return 请求结果 */ public static String get(String url, Map headers) { - try (HttpResponse response = HttpRequest.get(url) .addHeaders(headers) .execute()) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java index c0a2b60ac8..4fbb8649de 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; +import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -18,11 +21,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProp import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; - import java.util.*; -import java.util.stream.Collectors; /** * 七牛云短信客户端的实现类 @@ -45,69 +44,60 @@ public class QiniuSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - @Override protected void doInit() { } - @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { - // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages LinkedHashMap body = new LinkedHashMap<>(); - Map paramsMap = templateParams.stream() - .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); - body.put("template_id", apiTemplateId); body.put("mobile", mobile); - body.put("parameters", paramsMap); + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue)); body.put("seq", Long.toString(sendLogId)); - JSONObject response = request("POST", body, null); + JSONObject response = request("POST", body, PATH); // 2. 解析请求 + if (ObjectUtil.isNotEmpty(response.getStr("error"))){//短信请求失败 + return new SmsSendRespDTO().setSuccess(false) + .setApiCode(response.getStr("error")) + .setApiRequestId(response.getStr("request_id")) + .setApiMsg(response.getStr("message")); + } + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) .setSerialNo(response.getStr("message_id")); } - /** * 请求七牛云短信 * * @see * @param httpMethod http请求方法 - * @param queryParams 请求参数 + * @param body http请求消息体 + * @param path URL path * @return 请求结果 */ - private JSONObject request(String httpMethod, LinkedHashMap body, Map queryParams) { - - String signature = ""; - String templateIdPath = ""; - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - String signDate = dateFormat.format(new Date()); - + private JSONObject request(String httpMethod, LinkedHashMap body, String path) { + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'"); //请求头 Map header = new HashMap<>(4); header.put("HOST", HOST); - header.put("Authorization", signature); + header.put("Authorization", getSignature(httpMethod, HOST, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate)); header.put("Content-Type", "application/json"); header.put("X-Qiniu-Date", signDate); String responseBody =""; - if(Objects.equals(httpMethod, "POST")){ - header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate)); - responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body)); - }else { // GET - templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id"); - header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate)); - responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header); + if (Objects.equals(httpMethod, "POST")){// POST 发送短消息用POST请求 + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body)); + }else { // GET 查询template状态用GET请求 + responseBody = HttpUtils.get("https://" + HOST + path, header); } return JSONUtil.parseObj(responseBody); } public String getSignature(String method, String host, String path, String body, String signDate) { - StringBuilder dataToSign = new StringBuilder(); dataToSign.append(method.toUpperCase()).append(" ").append(path); dataToSign.append("\nHost: ").append(host); @@ -117,18 +107,15 @@ public class QiniuSmsClient extends AbstractSmsClient { if (ObjectUtil.isNotEmpty(body)) { dataToSign.append(body); } - HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8)); - byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8)); - String encodedSignature = Base64.getEncoder().encodeToString(signData); + String encodedSignature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()).digestBase64(dataToSign.toString(), true); return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; } @Override public List parseSmsReceiveStatus(String text) { - JSONObject status = JSONUtil.parseObj(text); - //字段参考 https://developer.qiniu.com/sms/5910/message-push + // 字段参考 https://developer.qiniu.com/sms/5910/message-push return ListUtil.of(new SmsReceiveRespDTO() .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) @@ -142,16 +129,13 @@ public class QiniuSmsClient extends AbstractSmsClient { public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template - HashMap queryParam = new HashMap<>(); - queryParam.put("template_id", apiTemplateId); - JSONObject response = request("GET", null, queryParam); - + JSONObject response = request("GET", null, TEMPLATE_PATH + "/" + apiTemplateId); // 2.1 请求失败 - String status = response.getStr("audit_status"); - if (!Objects.equals(status, "passed")) { + if (ObjUtil.notEqual(response.getStr("audit_status"), "passed")) { log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); return null; } + // 2.2 请求成功 return new SmsTemplateRespDTO() .setId(response.getStr("id")) @@ -162,11 +146,12 @@ public class QiniuSmsClient extends AbstractSmsClient { @VisibleForTesting Integer convertSmsTemplateAuditStatus(String templateStatus) { - - if(Objects.equals(templateStatus, "passed")){ - return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); - }else { - throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); - } + return switch (templateStatus) { + case "passed" -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "reviewing" -> SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "rejected" -> SmsTemplateAuditStatusEnum.FAIL.getStatus(); + case null, default -> + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); + }; } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java index c64c39470f..c3e8966952 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.mockStatic; * @author scholar */ public class QiniuSmsClientTest extends BaseMockitoUnitTest { - private final SmsChannelProperties properties = new SmsChannelProperties() .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 @@ -46,7 +45,6 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { @Test public void testDoSendSms_success() throws Throwable { - try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -56,9 +54,7 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"message_id\":\"17245678901\"}" - ); + .thenReturn("{\"message_id\":\"17245678901\"}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); @@ -77,17 +73,17 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { String apiTemplateId = randomString() + " " + randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); - // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}" - ); + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); + assertEquals("BadToken", result.getApiCode()); + assertEquals("Your authorization token is invalid", result.getApiMsg()); + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId()); } } @@ -125,4 +121,15 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { assertEquals(123, statuses.getFirst().getLogId()); } + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus("passed")); + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus("reviewing")); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus("rejected")); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"), + "未知审核状态(3)"); + } } \ 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/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 3752e5763c..4f003ebaf0 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 @@ -116,7 +116,6 @@ public class SmsClientTests { @Test @Disabled public void testQiniuSmsClient_sendSms() throws Throwable { - SmsChannelProperties properties = new SmsChannelProperties() .setApiKey("SMS_QINIU_ACCESS_KEY") .setApiSecret("SMS_QINIU_SECRET_KEY"); @@ -135,7 +134,6 @@ public class SmsClientTests { @Test @Disabled public void testQiniuSmsClient_getSmsTemplate() throws Throwable { - SmsChannelProperties properties = new SmsChannelProperties() .setApiKey("SMS_QINIU_ACCESS_KEY") .setApiSecret("SMS_QINIU_SECRET_KEY");