Commit efc5ea23bfcc6fb5ddc211e9a45f824ed751fc8f

Authored by scholar
1 parent 8242735d

完成todo部分

1,华为云短信实现优化,去除不必要VO;
2,增加华为云短信功能单测;
3,fix华为云短信接收状态回调的bug;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
... ... @@ -5,16 +5,17 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
5 5 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
6 6 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
7 7 import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
  8 +import com.xingyuv.captcha.util.StreamUtils;
8 9 import io.swagger.v3.oas.annotations.Operation;
9 10 import io.swagger.v3.oas.annotations.tags.Tag;
10   -import org.springframework.web.bind.annotation.PostMapping;
11   -import org.springframework.web.bind.annotation.RequestMapping;
12   -import org.springframework.web.bind.annotation.RestController;
  11 +import org.springframework.web.bind.annotation.*;
13 12  
14 13 import jakarta.annotation.Resource;
15 14 import jakarta.annotation.security.PermitAll;
16 15 import jakarta.servlet.http.HttpServletRequest;
17 16  
  17 +import java.nio.charset.Charset;
  18 +
18 19 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
19 20  
20 21 @Tag(name = "管理后台 - 短信回调")
... ... @@ -50,10 +51,8 @@ public class SmsCallbackController {
50 51 @PermitAll
51 52 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
52 53 @OperateLog(enable = false)
53   - public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
54   - String text = ServletUtils.getBody(request);
55   - smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
  54 + public CommonResult<Boolean> receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
  55 + smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
56 56 return success(true);
57 57 }
58   -
59 58 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
... ... @@ -5,12 +5,11 @@ import cn.hutool.core.lang.Assert;
5 5 import cn.hutool.core.util.StrUtil;
6 6  
7 7 import cn.hutool.crypto.SecureUtil;
8   -import cn.hutool.http.HttpRequest;
9   -import cn.hutool.http.HttpResponse;
10 8 import cn.hutool.json.JSONObject;
11 9 import cn.hutool.json.JSONUtil;
12 10 import cn.iocoder.yudao.framework.common.core.KeyValue;
13 11 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  12 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
14 13 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
15 14 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
16 15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
... ... @@ -18,14 +17,14 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
18 17 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
19 18 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
20 19  
21   -import com.fasterxml.jackson.annotation.JsonFormat;
22   -import com.fasterxml.jackson.annotation.JsonProperty;
23   -import lombok.Data;
24 20 import lombok.extern.slf4j.Slf4j;
25 21  
26 22 import java.io.UnsupportedEncodingException;
  23 +import java.net.URLDecoder;
27 24 import java.net.URLEncoder;
28 25 import java.text.SimpleDateFormat;
  26 +import java.time.Instant;
  27 +import java.time.ZoneId;
29 28 import java.util.*;
30 29  
31 30  
... ... @@ -33,9 +32,6 @@ import java.time.LocalDateTime;
33 32  
34 33  
35 34 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
36   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
37   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
38   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
39 35  
40 36  
41 37 /**
... ... @@ -47,9 +43,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
47 43 @Slf4j
48 44 public class HuaweiSmsClient extends AbstractSmsClient {
49 45  
50   - /**
51   - * 调用成功 code
52   - */
53 46 public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
54 47 public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
55 48 public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
... ... @@ -78,13 +71,14 @@ public class HuaweiSmsClient extends AbstractSmsClient {
78 71  
79 72 List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
80 73  
81   - JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
82   - SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
  74 + JSONObject JsonResponse = request(sendLogId,sender,mobile,templateId,templateParas,statusCallBack);
83 75  
84   - return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
  76 + return new SmsSendRespDTO().setSuccess("000000".equals(JsonResponse.getStr("code")))
  77 + .setSerialNo(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("smsMsgId"))
  78 + .setApiCode(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("status"));
85 79 }
86 80  
87   - JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
  81 + JSONObject request(Long sendLogId,String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
88 82  
89 83 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
90 84 sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
... ... @@ -97,8 +91,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
97 91 String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
98 92 + "host:"+ HOST +"\n"
99 93 + "x-sdk-date:" + sdkDate + "\n";
100   - //请求Body,不携带签名名称时,signature请填null
101   - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
  94 + String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, sendLogId);
102 95 if (null == body || body.isEmpty()) {
103 96 return null;
104 97 }
... ... @@ -118,26 +111,29 @@ public class HuaweiSmsClient extends AbstractSmsClient {
118 111 + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
119 112  
120 113 // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
121   - HttpResponse response = HttpRequest.post(URL)
122   - .header("Content-Type", "application/x-www-form-urlencoded")
123   - .header("X-Sdk-Date", sdkDate)
124   - .header("host",HOST)
125   - .header("Authorization", authorization)
126   - .body(body)
127   - .execute();
128   -
129   - return JSONUtil.parseObj(response.body());
130   - }
131   -
132   - private SmsResponse getSmsSendResponse(JSONObject resJson) {
133   - SmsResponse smsResponse = new SmsResponse();
134   - smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
135   - smsResponse.setData(resJson);
136   - return smsResponse;
  114 + TreeMap<String, String> headers = new TreeMap<>();
  115 + headers.put("Content-Type", "application/x-www-form-urlencoded");
  116 + headers.put("X-Sdk-Date", sdkDate);
  117 + headers.put("host", HOST);
  118 + headers.put("Authorization", authorization);
  119 +
  120 + String responseBody = HttpUtils.post(URL, headers, body);
  121 + return JSONUtil.parseObj(responseBody);
  122 +//
  123 +//
  124 +// HttpResponse response = HttpRequest.post(URL)
  125 +// .header("Content-Type", "application/x-www-form-urlencoded")
  126 +// .header("X-Sdk-Date", sdkDate)
  127 +// .header("host",HOST)
  128 +// .header("Authorization", authorization)
  129 +// .body(body)
  130 +// .execute();
  131 +//
  132 +// return JSONUtil.parseObj(response.body());
137 133 }
138 134  
139 135 static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
140   - String statusCallBack, String signature) throws UnsupportedEncodingException {
  136 + String statusCallBack, Long sendLogId) throws UnsupportedEncodingException {
141 137 if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
142 138 || templateId.isEmpty()) {
143 139 System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
... ... @@ -150,7 +146,9 @@ public class HuaweiSmsClient extends AbstractSmsClient {
150 146 appendToBody(body, "&templateId=", templateId);
151 147 appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
152 148 appendToBody(body, "&statusCallback=", statusCallBack);
153   - appendToBody(body, "&signature=", signature);
  149 + appendToBody(body, "&signature=", null);
  150 + appendToBody(body, "&extend=", String.valueOf(sendLogId));
  151 +
154 152 return body.toString();
155 153 }
156 154  
... ... @@ -160,12 +158,35 @@ public class HuaweiSmsClient extends AbstractSmsClient {
160 158 }
161 159 }
162 160 @Override
163   - public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
164   - List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
165   - return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))
166   - .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())
167   - .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())
168   - .setSerialNo(status.getSmsMsgId()));
  161 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
  162 +
  163 + System.out.println("text in parseSmsReceiveStatus===== " + requestBody);
  164 +
  165 + Map<String, String> params = new HashMap<>();
  166 + try {
  167 + String[] pairs = requestBody.split("&");
  168 + for (String pair : pairs) {
  169 + int idx = pair.indexOf("=");
  170 + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8");
  171 + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8");
  172 + params.put(key, value);
  173 + }
  174 + } catch (Exception e) {
  175 + e.printStackTrace();
  176 + }
  177 +
  178 + List<SmsReceiveRespDTO> respDTOS = new ArrayList<>();
  179 + respDTOS.add(new SmsReceiveRespDTO()
  180 + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
  181 + .setErrorCode(params.get("status")) // 状态报告编码
  182 + .setErrorMsg(params.get("statusDesc"))
  183 + .setMobile(params.get("to")) // 手机号
  184 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
  185 + .setSerialNo(params.get("smsMsgId")) // 发送序列号
  186 + .setLogId(Long.valueOf(params.get("extend")))//logId
  187 + );
  188 +
  189 + return respDTOS;
169 190 }
170 191  
171 192 @Override
... ... @@ -173,56 +194,5 @@ public class HuaweiSmsClient extends AbstractSmsClient {
173 194 //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。
174 195 return new SmsTemplateRespDTO().setId(null).setContent(null)
175 196 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
176   -
177 197 }
178   -
179   - @Data
180   - public static class SmsResponse {
181   -
182   - /**
183   - * 是否成功
184   - */
185   - private boolean success;
186   -
187   - /**
188   - * 厂商原返回体
189   - */
190   - private Object data;
191   -
192   - }
193   -
194   -
195   - /**
196   - * 短信接收状态
197   - *
198   - * 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>
199   - *
200   - * @author scholar
201   - */
202   - @Data
203   - public static class SmsReceiveStatus {
204   -
205   - /**
206   - * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数
207   - */
208   - @JsonProperty("to")
209   - private String phoneNumber;
210   -
211   - /**
212   - * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间
213   - */
214   - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
215   - private LocalDateTime updateTime;
216   -
217   - /**
218   - * 短信状态报告枚举值
219   - */
220   - private String status;
221   -
222   - /**
223   - * 发送短信成功时返回的短信唯一标识。
224   - */
225   - private String smsMsgId;
226   - }
227   -
228   -}
  198 +}
229 199 \ 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/HuaweiSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  9 +import com.google.common.collect.Lists;
  10 +
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.MockedStatic;
  14 +
  15 +import java.time.LocalDateTime;
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.ArgumentMatchers.*;
  21 +import static org.mockito.ArgumentMatchers.anyString;
  22 +import static org.mockito.Mockito.mockStatic;
  23 +
  24 +/**
  25 + * {@link HuaweiSmsClient} 的单元测试
  26 + *
  27 + * @author scholar
  28 + */
  29 +public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
  30 +
  31 + private final SmsChannelProperties properties = new SmsChannelProperties()
  32 + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错
  33 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  34 + .setSignature("芋道源码");
  35 +
  36 + @InjectMocks
  37 + private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
  38 +
  39 + @Test
  40 + public void testDoInit() {
  41 + // 调用
  42 + smsClient.doInit();
  43 + }
  44 +
  45 + @Test
  46 + public void testDoSendSms_success() throws Throwable {
  47 +
  48 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  49 + // 准备参数
  50 + Long sendLogId = randomLongId();
  51 + String mobile = randomString();
  52 + String apiTemplateId = randomString() + " " + randomString();
  53 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  54 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  55 +
  56 + // mock 方法
  57 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  58 + .thenReturn(
  59 + "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n"
  60 + );
  61 +
  62 + // 调用
  63 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  64 + apiTemplateId, templateParams);
  65 + // 断言
  66 + assertTrue(result.getSuccess());
  67 + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
  68 + assertEquals("000000", result.getApiCode());
  69 +
  70 + }
  71 + }
  72 +
  73 + @Test
  74 + public void testDoSendSms_fail() throws Throwable {
  75 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  76 + // 准备参数
  77 + Long sendLogId = randomLongId();
  78 + String mobile = randomString();
  79 + String apiTemplateId = randomString() + " " + randomString();
  80 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  81 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  82 +
  83 + // mock 方法
  84 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  85 + .thenReturn(
  86 + "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"E200015\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"E000000\",\"description\":\"Success\"}\n"
  87 + );
  88 +
  89 + // 调用
  90 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  91 + apiTemplateId, templateParams);
  92 + // 断言
  93 + assertFalse(result.getSuccess());
  94 + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
  95 + assertEquals("E200015", result.getApiCode());
  96 + }
  97 + }
  98 +
  99 + @Test
  100 + public void testParseSmsReceiveStatus() {
  101 + // 准备参数
  102 + String text = "sequence=1&total=1&statusDesc=%E7%94%A8%E6%88%B7%E5%B7%B2%E6%88%90%E5%8A%9F%E6%94%B6%E5%88%B0%E7%9F%AD%E4%BF%A1&updateTime=2024-08-15T03%3A00%3A34Z&source=2&smsMsgId=70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459&status=DELIVRD&extend=176";
  103 +
  104 + // 调用
  105 + List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
  106 + // 断言
  107 + assertEquals(1, statuses.size());
  108 + assertTrue(statuses.getFirst().getSuccess());
  109 + assertEquals("DELIVRD", statuses.getFirst().getErrorCode());
  110 + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), statuses.getFirst().getReceiveTime());
  111 + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", statuses.getFirst().getSerialNo());
  112 + }
  113 +
  114 +}
... ...