Commit 1c1abae5bbf8357fb32044e766cb92b06603dcfe

Authored by scholar
1 parent 4f7ac969

七牛云短信实现,评审意见修改

yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
... ... @@ -135,7 +135,6 @@ public class HttpUtils {
135 135 * @return 请求结果
136 136 */
137 137 public static String post(String url, Map<String, String> headers, String requestBody) {
138   -
139 138 try (HttpResponse response = HttpRequest.post(url)
140 139 .addHeaders(headers)
141 140 .body(requestBody)
... ... @@ -154,7 +153,6 @@ public class HttpUtils {
154 153 * @return 请求结果
155 154 */
156 155 public static String get(String url, Map<String, String> headers) {
157   -
158 156 try (HttpResponse response = HttpRequest.get(url)
159 157 .addHeaders(headers)
160 158 .execute()) {
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
  3 +import cn.hutool.core.collection.CollStreamUtil;
3 4 import cn.hutool.core.collection.ListUtil;
  5 +import cn.hutool.core.date.DateUtil;
4 6 import cn.hutool.core.date.LocalDateTimeUtil;
5 7 import cn.hutool.core.lang.Assert;
  8 +import cn.hutool.core.util.ObjUtil;
6 9 import cn.hutool.core.util.ObjectUtil;
7   -import cn.hutool.crypto.digest.HMac;
  10 +import cn.hutool.crypto.SecureUtil;
8 11 import cn.hutool.crypto.digest.HmacAlgorithm;
9 12 import cn.hutool.json.JSONObject;
10 13 import cn.hutool.json.JSONUtil;
... ... @@ -18,11 +21,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProp
18 21 import com.google.common.annotations.VisibleForTesting;
19 22 import lombok.extern.slf4j.Slf4j;
20 23  
21   -import java.nio.charset.StandardCharsets;
22   -import java.text.SimpleDateFormat;
23   -
24 24 import java.util.*;
25   -import java.util.stream.Collectors;
26 25  
27 26 /**
28 27 * 七牛云短信客户端的实现类
... ... @@ -45,69 +44,60 @@ public class QiniuSmsClient extends AbstractSmsClient {
45 44 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
46 45 }
47 46  
48   - @Override
49 47 protected void doInit() {
50 48 }
51   - @Override
  49 +
52 50 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
53 51 List<KeyValue<String, Object>> templateParams) throws Throwable {
54   -
55 52 // 1. 执行请求
56 53 // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
57 54 LinkedHashMap<String, Object> body = new LinkedHashMap<>();
58   - Map<String, Object> paramsMap = templateParams.stream()
59   - .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue));
60   -
61 55 body.put("template_id", apiTemplateId);
62 56 body.put("mobile", mobile);
63   - body.put("parameters", paramsMap);
  57 + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue));
64 58 body.put("seq", Long.toString(sendLogId));
65 59  
66   - JSONObject response = request("POST", body, null);
  60 + JSONObject response = request("POST", body, PATH);
67 61 // 2. 解析请求
  62 + if (ObjectUtil.isNotEmpty(response.getStr("error"))){//短信请求失败
  63 + return new SmsSendRespDTO().setSuccess(false)
  64 + .setApiCode(response.getStr("error"))
  65 + .setApiRequestId(response.getStr("request_id"))
  66 + .setApiMsg(response.getStr("message"));
  67 + }
  68 +
68 69 return new SmsSendRespDTO().setSuccess(response.containsKey("message_id"))
69 70 .setSerialNo(response.getStr("message_id"));
70 71 }
71 72  
72   -
73 73 /**
74 74 * 请求七牛云短信
75 75 *
76 76 * @see <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</>
77 77 * @param httpMethod http请求方法
78   - * @param queryParams 请求参数
  78 + * @param body http请求消息体
  79 + * @param path URL path
79 80 * @return 请求结果
80 81 */
81   - private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, Map<String, Object> queryParams) {
82   -
83   - String signature = "";
84   - String templateIdPath = "";
85   -
86   - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
87   - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
88   - String signDate = dateFormat.format(new Date());
89   -
  82 + private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, String path) {
  83 + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'");
90 84 //请求头
91 85 Map<String, String> header = new HashMap<>(4);
92 86 header.put("HOST", HOST);
93   - header.put("Authorization", signature);
  87 + header.put("Authorization", getSignature(httpMethod, HOST, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate));
94 88 header.put("Content-Type", "application/json");
95 89 header.put("X-Qiniu-Date", signDate);
96 90  
97 91 String responseBody ="";
98   - if(Objects.equals(httpMethod, "POST")){
99   - header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate));
100   - responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body));
101   - }else { // GET
102   - templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id");
103   - header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate));
104   - responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header);
  92 + if (Objects.equals(httpMethod, "POST")){// POST 发送短消息用POST请求
  93 + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body));
  94 + }else { // GET 查询template状态用GET请求
  95 + responseBody = HttpUtils.get("https://" + HOST + path, header);
105 96 }
106 97 return JSONUtil.parseObj(responseBody);
107 98 }
108 99  
109 100 public String getSignature(String method, String host, String path, String body, String signDate) {
110   -
111 101 StringBuilder dataToSign = new StringBuilder();
112 102 dataToSign.append(method.toUpperCase()).append(" ").append(path);
113 103 dataToSign.append("\nHost: ").append(host);
... ... @@ -117,18 +107,15 @@ public class QiniuSmsClient extends AbstractSmsClient {
117 107 if (ObjectUtil.isNotEmpty(body)) {
118 108 dataToSign.append(body);
119 109 }
120   - HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8));
121   - byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8));
122   - String encodedSignature = Base64.getEncoder().encodeToString(signData);
  110 + String encodedSignature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()).digestBase64(dataToSign.toString(), true);
123 111  
124 112 return "Qiniu " + properties.getApiKey() + ":" + encodedSignature;
125 113 }
126 114  
127 115 @Override
128 116 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
129   -
130 117 JSONObject status = JSONUtil.parseObj(text);
131   - //字段参考 https://developer.qiniu.com/sms/5910/message-push
  118 + // 字段参考 https://developer.qiniu.com/sms/5910/message-push
132 119 return ListUtil.of(new SmsReceiveRespDTO()
133 120 .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功
134 121 .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status"))
... ... @@ -142,16 +129,13 @@ public class QiniuSmsClient extends AbstractSmsClient {
142 129 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
143 130 // 1. 执行请求
144 131 // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template
145   - HashMap<String, Object> queryParam = new HashMap<>();
146   - queryParam.put("template_id", apiTemplateId);
147   - JSONObject response = request("GET", null, queryParam);
148   -
  132 + JSONObject response = request("GET", null, TEMPLATE_PATH + "/" + apiTemplateId);
149 133 // 2.1 请求失败
150   - String status = response.getStr("audit_status");
151   - if (!Objects.equals(status, "passed")) {
  134 + if (ObjUtil.notEqual(response.getStr("audit_status"), "passed")) {
152 135 log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response);
153 136 return null;
154 137 }
  138 +
155 139 // 2.2 请求成功
156 140 return new SmsTemplateRespDTO()
157 141 .setId(response.getStr("id"))
... ... @@ -162,11 +146,12 @@ public class QiniuSmsClient extends AbstractSmsClient {
162 146  
163 147 @VisibleForTesting
164 148 Integer convertSmsTemplateAuditStatus(String templateStatus) {
165   -
166   - if(Objects.equals(templateStatus, "passed")){
167   - return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
168   - }else {
169   - throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus));
170   - }
  149 + return switch (templateStatus) {
  150 + case "passed" -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  151 + case "reviewing" -> SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  152 + case "rejected" -> SmsTemplateAuditStatusEnum.FAIL.getStatus();
  153 + case null, default ->
  154 + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus));
  155 + };
171 156 }
172 157 }
... ...
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;
29 29 * @author scholar
30 30 */
31 31 public class QiniuSmsClientTest extends BaseMockitoUnitTest {
32   -
33 32 private final SmsChannelProperties properties = new SmsChannelProperties()
34 33 .setApiKey(randomString())// 随机一个 apiKey,避免构建报错
35 34 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
... ... @@ -46,7 +45,6 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
46 45  
47 46 @Test
48 47 public void testDoSendSms_success() throws Throwable {
49   -
50 48 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
51 49 // 准备参数
52 50 Long sendLogId = randomLongId();
... ... @@ -56,9 +54,7 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
56 54 new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
57 55 // mock 方法
58 56 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
59   - .thenReturn(
60   - "{\"message_id\":\"17245678901\"}"
61   - );
  57 + .thenReturn("{\"message_id\":\"17245678901\"}");
62 58 // 调用
63 59 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
64 60 apiTemplateId, templateParams);
... ... @@ -77,17 +73,17 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
77 73 String apiTemplateId = randomString() + " " + randomString();
78 74 List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
79 75 new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
80   -
81 76 // mock 方法
82 77 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
83   - .thenReturn(
84   - "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}"
85   - );
  78 + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}");
86 79 // 调用
87 80 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
88 81 apiTemplateId, templateParams);
89 82 // 断言
90 83 assertFalse(result.getSuccess());
  84 + assertEquals("BadToken", result.getApiCode());
  85 + assertEquals("Your authorization token is invalid", result.getApiMsg());
  86 + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId());
91 87 }
92 88 }
93 89  
... ... @@ -125,4 +121,15 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
125 121 assertEquals(123, statuses.getFirst().getLogId());
126 122 }
127 123  
  124 + @Test
  125 + public void testConvertSmsTemplateAuditStatus() {
  126 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
  127 + smsClient.convertSmsTemplateAuditStatus("passed"));
  128 + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
  129 + smsClient.convertSmsTemplateAuditStatus("reviewing"));
  130 + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
  131 + smsClient.convertSmsTemplateAuditStatus("rejected"));
  132 + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"),
  133 + "未知审核状态(3)");
  134 + }
128 135 }
129 136 \ 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/SmsClientTests.java
... ... @@ -116,7 +116,6 @@ public class SmsClientTests {
116 116 @Test
117 117 @Disabled
118 118 public void testQiniuSmsClient_sendSms() throws Throwable {
119   -
120 119 SmsChannelProperties properties = new SmsChannelProperties()
121 120 .setApiKey("SMS_QINIU_ACCESS_KEY")
122 121 .setApiSecret("SMS_QINIU_SECRET_KEY");
... ... @@ -135,7 +134,6 @@ public class SmsClientTests {
135 134 @Test
136 135 @Disabled
137 136 public void testQiniuSmsClient_getSmsTemplate() throws Throwable {
138   -
139 137 SmsChannelProperties properties = new SmsChannelProperties()
140 138 .setApiKey("SMS_QINIU_ACCESS_KEY")
141 139 .setApiSecret("SMS_QINIU_SECRET_KEY");
... ...