Browse Source

增加七牛云短信实现

scholar 6 months ago
parent
commit
4f7ac969fe

+ 41 - 1
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<String, String> 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<String, String> headers) {
+
+        try (HttpResponse response = HttpRequest.get(url)
+                .addHeaders(headers)
+                .execute()) {
+            return response.body();
+        }
+    }
 
 }
+

+ 10 - 7
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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable {
+        smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody);
+        return success(true);
+    }
+
 }
+

+ 172 - 0
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<KeyValue<String, Object>> templateParams) throws Throwable {
+
+        // 1. 执行请求
+        // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
+        LinkedHashMap<String, Object> body = new LinkedHashMap<>();
+        Map<String, Object> 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 <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</>
+     * @param httpMethod http请求方法
+     * @param queryParams 请求参数
+     * @return 请求结果
+     */
+    private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, Map<String, Object> 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<String, String> 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<SmsReceiveRespDTO> 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<String, Object> 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));
+        }
+    }
+}

+ 2 - 0
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 {
     }
 
 }
+

+ 128 - 0
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<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(
+                            "{\"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<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(
+                            "{\"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<HttpUtils> 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<SmsReceiveRespDTO> 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());
+    }
+
+}

+ 75 - 63
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<KeyValue<String, Object>> 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<SmsReceiveRespDTO> 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<KeyValue<String, Object>> 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<KeyValue<String, Object>> 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);
     }
-}
 
+}