Răsfoiți Sursa

完成todo部分
1,华为云短信实现优化,去除不必要VO;
2,增加华为云短信功能单测;
3,fix华为云短信接收状态回调的bug;

scholar 8 luni în urmă
părinte
comite
efc5ea23bf

+ 6 - 7
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;
 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.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 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 = "管理后台 - 短信回调")
@@ -50,10 +51,8 @@ public class SmsCallbackController {
     @PermitAll
     @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
     @OperateLog(enable = false)
-    public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
-        String text = ServletUtils.getBody(request);
-        smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
+    public CommonResult<Boolean> receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
+        smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
         return success(true);
     }
-
 }

+ 63 - 93
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;
 import cn.hutool.core.util.StrUtil;
 
 import cn.hutool.crypto.SecureUtil;
-import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
 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.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 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
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
 import java.util.*;
 
 
@@ -33,9 +32,6 @@ 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;
 
 
 /**
@@ -47,9 +43,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
 @Slf4j
 public class HuaweiSmsClient extends AbstractSmsClient {
 
-    /**
-     * 调用成功 code
-     */
     public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
     public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
     public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
@@ -78,13 +71,14 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
         List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
 
-        JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
-        SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
+        JSONObject JsonResponse = request(sendLogId,sender,mobile,templateId,templateParas,statusCallBack);
 
-        return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
+        return new SmsSendRespDTO().setSuccess("000000".equals(JsonResponse.getStr("code")))
+                .setSerialNo(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("smsMsgId"))
+                .setApiCode(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("status"));
     }
 
-    JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
+    JSONObject request(Long sendLogId,String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
 
         SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -97,8 +91,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
                 + "host:"+ HOST +"\n"
                 + "x-sdk-date:" + sdkDate + "\n";
-        //请求Body,不携带签名名称时,signature请填null
-        String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
+        String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, sendLogId);
         if (null == body || body.isEmpty()) {
             return null;
         }
@@ -118,26 +111,29 @@ public class HuaweiSmsClient extends AbstractSmsClient {
                 + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
 
         // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
-        HttpResponse response = HttpRequest.post(URL)
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .header("X-Sdk-Date", sdkDate)
-                .header("host",HOST)
-                .header("Authorization", authorization)
-                .body(body)
-                .execute();
-
-        return JSONUtil.parseObj(response.body());
-    }
-
-    private SmsResponse getSmsSendResponse(JSONObject resJson) {
-        SmsResponse smsResponse = new SmsResponse();
-        smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
-        smsResponse.setData(resJson);
-        return smsResponse;
+        TreeMap<String, String> headers = new TreeMap<>();
+        headers.put("Content-Type", "application/x-www-form-urlencoded");
+        headers.put("X-Sdk-Date", sdkDate);
+        headers.put("host", HOST);
+        headers.put("Authorization", authorization);
+
+        String responseBody = HttpUtils.post(URL, headers, body);
+        return JSONUtil.parseObj(responseBody);
+//
+//
+//        HttpResponse response = HttpRequest.post(URL)
+//                .header("Content-Type", "application/x-www-form-urlencoded")
+//                .header("X-Sdk-Date", sdkDate)
+//                .header("host",HOST)
+//                .header("Authorization", authorization)
+//                .body(body)
+//                .execute();
+//
+//        return JSONUtil.parseObj(response.body());
     }
 
     static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
-                                   String statusCallBack, String signature) throws UnsupportedEncodingException {
+                                   String statusCallBack, Long sendLogId) throws UnsupportedEncodingException {
         if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
                 || templateId.isEmpty()) {
             System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
@@ -150,7 +146,9 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         appendToBody(body, "&templateId=", templateId);
         appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
         appendToBody(body, "&statusCallback=", statusCallBack);
-        appendToBody(body, "&signature=", signature);
+        appendToBody(body, "&signature=", null);
+        appendToBody(body, "&extend=", String.valueOf(sendLogId));
+
         return body.toString();
     }
 
@@ -160,12 +158,35 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         }
     }
     @Override
-    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
-        List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))
-                .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())
-                .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())
-                .setSerialNo(status.getSmsMsgId()));
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
+
+        System.out.println("text in parseSmsReceiveStatus===== " + requestBody);
+
+        Map<String, String> params = new HashMap<>();
+        try {
+            String[] pairs = requestBody.split("&");
+            for (String pair : pairs) {
+                int idx = pair.indexOf("=");
+                String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8");
+                String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8");
+                params.put(key, value);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        List<SmsReceiveRespDTO> respDTOS = new ArrayList<>();
+        respDTOS.add(new SmsReceiveRespDTO()
+                        .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
+                        .setErrorCode(params.get("status")) // 状态报告编码
+                        .setErrorMsg(params.get("statusDesc"))
+                        .setMobile(params.get("to")) // 手机号
+                        .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
+                        .setSerialNo(params.get("smsMsgId")) // 发送序列号
+                        .setLogId(Long.valueOf(params.get("extend")))//logId
+        );
+
+        return respDTOS;
     }
 
     @Override
@@ -173,56 +194,5 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。
         return new SmsTemplateRespDTO().setId(null).setContent(null)
                 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
-
     }
-
-    @Data
-    public static class SmsResponse {
-
-        /**
-         * 是否成功
-         */
-        private boolean success;
-
-        /**
-         * 厂商原返回体
-         */
-        private Object data;
-
-    }
-
-
-    /**
-     * 短信接收状态
-     *
-     * 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>
-     *
-     * @author scholar
-     */
-    @Data
-    public static class SmsReceiveStatus {
-
-        /**
-         * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数
-         */
-        @JsonProperty("to")
-        private String phoneNumber;
-
-        /**
-         * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间
-         */
-        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
-        private LocalDateTime updateTime;
-
-        /**
-         * 短信状态报告枚举值
-         */
-        private String status;
-
-        /**
-         * 发送短信成功时返回的短信唯一标识。
-         */
-        private String smsMsgId;
-    }
-
-}
+}

+ 114 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java

@@ -0,0 +1,114 @@
+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.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 HuaweiSmsClient} 的单元测试
+ *
+ * @author scholar
+ */
+public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
+
+    private final SmsChannelProperties properties = new SmsChannelProperties()
+            .setApiKey(randomString())// 随机一个 apiKey,避免构建报错
+            .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
+            .setSignature("芋道源码");
+
+    @InjectMocks
+    private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
+
+    @Test
+    public void testDoInit() {
+        // 调用
+        smsClient.doInit();
+    }
+
+    @Test
+    public void testDoSendSms_success() throws Throwable {
+
+        try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
+            // 准备参数
+            Long sendLogId = randomLongId();
+            String mobile = randomString();
+            String apiTemplateId = randomString() + " " + randomString();
+            List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                    new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+
+            // mock 方法
+            httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
+                    .thenReturn(
+                            "{\"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"
+                    );
+
+            // 调用
+            SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
+                    apiTemplateId, templateParams);
+            // 断言
+            assertTrue(result.getSuccess());
+            assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
+            assertEquals("000000", result.getApiCode());
+
+        }
+    }
+
+    @Test
+    public void testDoSendSms_fail() throws Throwable {
+        try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
+            // 准备参数
+            Long sendLogId = randomLongId();
+            String mobile = randomString();
+            String apiTemplateId = randomString() + " " + randomString();
+            List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                    new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+
+            // mock 方法
+            httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
+                    .thenReturn(
+                            "{\"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"
+                    );
+
+            // 调用
+            SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
+                    apiTemplateId, templateParams);
+            // 断言
+            assertFalse(result.getSuccess());
+            assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
+            assertEquals("E200015", result.getApiCode());
+        }
+    }
+
+    @Test
+    public void testParseSmsReceiveStatus() {
+        // 准备参数
+        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";
+
+        // 调用
+        List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
+        // 断言
+        assertEquals(1, statuses.size());
+        assertTrue(statuses.getFirst().getSuccess());
+        assertEquals("DELIVRD", statuses.getFirst().getErrorCode());
+        assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), statuses.getFirst().getReceiveTime());
+        assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", statuses.getFirst().getSerialNo());
+    }
+
+}