Commit 5b7e637ebf9c0419ffd0516043af33210c3ed0b3

Authored by YunaiV
1 parent 8cce7375

【功能优化】短信:华为云的实现优化

yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; 4 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
5 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; 5 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
6 import cn.iocoder.yudao.module.system.service.sms.SmsSendService; 6 import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
7 -import com.xingyuv.captcha.util.StreamUtils;  
8 import io.swagger.v3.oas.annotations.Operation; 7 import io.swagger.v3.oas.annotations.Operation;
9 import io.swagger.v3.oas.annotations.tags.Tag; 8 import io.swagger.v3.oas.annotations.tags.Tag;
10 import org.springframework.web.bind.annotation.*; 9 import org.springframework.web.bind.annotation.*;
@@ -13,8 +12,6 @@ import jakarta.annotation.Resource; @@ -13,8 +12,6 @@ import jakarta.annotation.Resource;
13 import jakarta.annotation.security.PermitAll; 12 import jakarta.annotation.security.PermitAll;
14 import jakarta.servlet.http.HttpServletRequest; 13 import jakarta.servlet.http.HttpServletRequest;
15 14
16 -import java.nio.charset.Charset;  
17 -  
18 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 15 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
19 16
20 @Tag(name = "管理后台 - 短信回调") 17 @Tag(name = "管理后台 - 短信回调")
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java
@@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient { @@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient {
26 * 初始化 26 * 初始化
27 */ 27 */
28 public final void init() { 28 public final void init() {
29 - doInit();  
30 log.debug("[init][配置({}) 初始化完成]", properties); 29 log.debug("[init][配置({}) 初始化完成]", properties);
31 } 30 }
32 31
33 - /**  
34 - * 自定义初始化  
35 - */  
36 - protected abstract void doInit();  
37 -  
38 public final void refresh(SmsChannelProperties properties) { 32 public final void refresh(SmsChannelProperties properties) {
39 // 判断是否更新 33 // 判断是否更新
40 if (properties.equals(this.properties)) { 34 if (properties.equals(this.properties)) {
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
@@ -51,10 +51,6 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -51,10 +51,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
51 } 51 }
52 52
53 @Override 53 @Override
54 - protected void doInit() {  
55 - }  
56 -  
57 - @Override  
58 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, 54 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
59 List<KeyValue<String, Object>> templateParams) throws Throwable { 55 List<KeyValue<String, Object>> templateParams) throws Throwable {
60 Assert.notBlank(properties.getSignature(), "短信签名不能为空"); 56 Assert.notBlank(properties.getSignature(), "短信签名不能为空");
@@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
80 @Override 76 @Override
81 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { 77 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
82 JSONArray statuses = JSONUtil.parseArray(text); 78 JSONArray statuses = JSONUtil.parseArray(text);
83 - // 字段参考 79 + // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2
84 return convertList(statuses, status -> { 80 return convertList(statuses, status -> {
85 JSONObject statusObj = (JSONObject) status; 81 JSONObject statusObj = (JSONObject) status;
86 return new SmsReceiveRespDTO() 82 return new SmsReceiveRespDTO()
@@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient { @@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
166 String hashedRequestBody = DigestUtil.sha256Hex(requestBody); 162 String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
167 163
168 // 4. 构建 Authorization 签名 164 // 4. 构建 Authorization 签名
169 - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; 165 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
  166 + + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
170 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); 167 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
171 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; 168 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
172 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 169 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
@@ -37,10 +37,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { @@ -37,10 +37,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
37 } 37 }
38 38
39 @Override 39 @Override
40 - protected void doInit() {  
41 - }  
42 -  
43 - @Override  
44 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, 40 public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
45 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable { 41 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
46 // 构建请求 42 // 构建请求
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.hutool.core.date.format.FastDateFormat;
3 import cn.hutool.core.lang.Assert; 5 import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.CharsetUtil;
4 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
5 -  
6 import cn.hutool.crypto.SecureUtil; 8 import cn.hutool.crypto.SecureUtil;
  9 +import cn.hutool.http.HttpUtil;
7 import cn.hutool.json.JSONObject; 10 import cn.hutool.json.JSONObject;
8 import cn.hutool.json.JSONUtil; 11 import cn.hutool.json.JSONUtil;
9 import cn.iocoder.yudao.framework.common.core.KeyValue; 12 import cn.iocoder.yudao.framework.common.core.KeyValue;
10 -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;  
11 import cn.iocoder.yudao.framework.common.util.http.HttpUtils; 13 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
12 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
13 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; 15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
@@ -15,23 +17,19 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD @@ -15,23 +17,19 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD
15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; 17 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
16 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
17 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
18 -  
19 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
20 21
21 import java.io.UnsupportedEncodingException; 22 import java.io.UnsupportedEncodingException;
22 -import java.net.URLDecoder;  
23 import java.net.URLEncoder; 23 import java.net.URLEncoder;
24 -import java.text.SimpleDateFormat; 24 +import java.nio.charset.StandardCharsets;
25 import java.time.Instant; 25 import java.time.Instant;
  26 +import java.time.LocalDateTime;
26 import java.time.ZoneId; 27 import java.time.ZoneId;
27 import java.util.*; 28 import java.util.*;
28 29
29 -  
30 -import java.time.LocalDateTime;  
31 -  
32 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; 30 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
  31 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
33 32
34 -// todo @scholar:参考阿里云在优化下  
35 /** 33 /**
36 * 华为短信客户端的实现类 34 * 华为短信客户端的实现类
37 * 35 *
@@ -41,13 +39,11 @@ import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; @@ -41,13 +39,11 @@ import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
41 @Slf4j 39 @Slf4j
42 public class HuaweiSmsClient extends AbstractSmsClient { 40 public class HuaweiSmsClient extends AbstractSmsClient {
43 41
44 - public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI  
45 - public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";  
46 - public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date"; 42 + private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
  43 + private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
  44 + private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
47 45
48 - @Override  
49 - protected void doInit() {  
50 - } 46 + private static final String RESPONSE_CODE_SUCCESS = "000000";
51 47
52 public HuaweiSmsClient(SmsChannelProperties properties) { 48 public HuaweiSmsClient(SmsChannelProperties properties) {
53 super(properties); 49 super(properties);
@@ -58,139 +54,96 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -58,139 +54,96 @@ public class HuaweiSmsClient extends AbstractSmsClient {
58 @Override 54 @Override
59 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, 55 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
60 List<KeyValue<String, Object>> templateParams) throws Throwable { 56 List<KeyValue<String, Object>> templateParams) throws Throwable {
  57 + // 1. 执行请求
61 // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html 58 // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
62 - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构  
63 - // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。  
64 - String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号  
65 - String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID  
66 -  
67 - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告 59 + // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构,
  60 + // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符)
  61 + String sender = apiTemplateId.split(" ")[1]; // 中国大陆短信签名通道号或全球短信通道号
  62 + String templateId = apiTemplateId.split(" ")[0]; //模板ID
68 String statusCallBack = properties.getCallbackUrl(); 63 String statusCallBack = properties.getCallbackUrl();
69 -  
70 - List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));  
71 -  
72 - JSONObject JsonResponse = request(sendLogId,sender,mobile,templateId,templateParas,statusCallBack);  
73 -  
74 - return new SmsSendRespDTO().setSuccess("000000".equals(JsonResponse.getStr("code")))  
75 - .setSerialNo(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("smsMsgId"))  
76 - .setApiCode(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("status"));  
77 - }  
78 -  
79 - JSONObject request(Long sendLogId,String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {  
80 -  
81 - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);  
82 - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));  
83 - String sdkDate = sdf.format(new Date());  
84 -  
85 - // ************* 步骤 1:拼接规范请求串 *************  
86 - String httpRequestMethod = "POST";  
87 - String canonicalUri = "/sms/batchSendSms/v1/";  
88 - String canonicalQueryString = "";//查询参数为空  
89 - String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"  
90 - + "host:"+ HOST +"\n"  
91 - + "x-sdk-date:" + sdkDate + "\n";  
92 - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, sendLogId);  
93 - if (null == body || body.isEmpty()) {  
94 - return null; 64 + StringBuilder requestBody = new StringBuilder();
  65 + appendToBody(requestBody, "from=", sender);
  66 + appendToBody(requestBody, "&to=", mobile);
  67 + appendToBody(requestBody, "&templateId=", templateId);
  68 + appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString(
  69 + convertList(templateParams, kv -> String.valueOf(kv.getValue()))));
  70 + appendToBody(requestBody, "&statusCallback=", statusCallBack);
  71 + appendToBody(requestBody, "&extend=", String.valueOf(sendLogId));
  72 + JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString());
  73 +
  74 + // 2. 解析请求
  75 + if (!response.containsKey("result")) { // 例如说:密钥不正确
  76 + return new SmsSendRespDTO().setSuccess(false)
  77 + .setApiCode(response.getStr("code"))
  78 + .setApiMsg(response.getStr("description"));
95 } 79 }
96 - String hashedRequestBody = sha256Hex(body);  
97 - String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"  
98 - + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;  
99 -  
100 - // ************* 步骤 2:拼接待签名字符串 *************  
101 - String hashedCanonicalRequest = sha256Hex(canonicalRequest);  
102 - String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest;  
103 -  
104 - // ************* 步骤 3:计算签名 *************  
105 - String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);  
106 -  
107 - // ************* 步骤 4:拼接 Authorization *************  
108 - String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "  
109 - + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature; 80 + JSONObject sendResult = response.getJSONArray("result").getJSONObject(0);
  81 + return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code")))
  82 + .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status"));
  83 + }
110 84
111 - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* 85 + /**
  86 + * 请求华为云短信
  87 + *
  88 + * @see <a href="认证鉴权">https://support.huaweicloud.com/api-msgsms/sms_05_0046.html</a>
  89 + * @param uri 请求 URI
  90 + * @param method 请求 Method
  91 + * @param requestBody 请求 Body
  92 + * @return 请求结果
  93 + */
  94 + private JSONObject request(String uri, String method, String requestBody) {
  95 + // 1.1 请求 Header
112 TreeMap<String, String> headers = new TreeMap<>(); 96 TreeMap<String, String> headers = new TreeMap<>();
113 headers.put("Content-Type", "application/x-www-form-urlencoded"); 97 headers.put("Content-Type", "application/x-www-form-urlencoded");
  98 + String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date());
114 headers.put("X-Sdk-Date", sdkDate); 99 headers.put("X-Sdk-Date", sdkDate);
115 headers.put("host", HOST); 100 headers.put("host", HOST);
116 - headers.put("Authorization", authorization);  
117 101
118 - String responseBody = HttpUtils.post(URL, headers, body); 102 + // 1.2 构建签名 Header
  103 + String canonicalQueryString = ""; // 查询参数为空
  104 + String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
  105 + + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n";
  106 + String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n"
  107 + + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody);
  108 + String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest);
  109 + String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
  110 + headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey()
  111 + + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature);
  112 +
  113 + // 2. 发起请求
  114 + String responseBody = HttpUtils.post(URL, headers, requestBody);
119 return JSONUtil.parseObj(responseBody); 115 return JSONUtil.parseObj(responseBody);
120 -//  
121 -//  
122 -// HttpResponse response = HttpRequest.post(URL)  
123 -// .header("Content-Type", "application/x-www-form-urlencoded")  
124 -// .header("X-Sdk-Date", sdkDate)  
125 -// .header("host",HOST)  
126 -// .header("Authorization", authorization)  
127 -// .body(body)  
128 -// .execute();  
129 -//  
130 -// return JSONUtil.parseObj(response.body());  
131 - }  
132 -  
133 - static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,  
134 - String statusCallBack, Long sendLogId) throws UnsupportedEncodingException {  
135 - if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()  
136 - || templateId.isEmpty()) {  
137 - System.out.println("buildRequestBody(): sender, receiver or templateId is null.");  
138 - return null;  
139 - }  
140 -  
141 - StringBuilder body = new StringBuilder();  
142 - appendToBody(body, "from=", sender);  
143 - appendToBody(body, "&to=", receiver);  
144 - appendToBody(body, "&templateId=", templateId);  
145 - appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));  
146 - appendToBody(body, "&statusCallback=", statusCallBack);  
147 - appendToBody(body, "&signature=", null);  
148 - appendToBody(body, "&extend=", String.valueOf(sendLogId));  
149 -  
150 - return body.toString();  
151 } 116 }
152 117
153 - private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {  
154 - if (null != val && !val.isEmpty()) {  
155 - body.append(key).append(URLEncoder.encode(val, "UTF-8"));  
156 - }  
157 - }  
158 @Override 118 @Override
159 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) { 119 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
160 -  
161 - System.out.println("text in parseSmsReceiveStatus===== " + requestBody);  
162 -  
163 - Map<String, String> params = new HashMap<>();  
164 - try {  
165 - String[] pairs = requestBody.split("&");  
166 - for (String pair : pairs) {  
167 - int idx = pair.indexOf("=");  
168 - String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8");  
169 - String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8");  
170 - params.put(key, value);  
171 - }  
172 - } catch (Exception e) {  
173 - e.printStackTrace();  
174 - }  
175 -  
176 - List<SmsReceiveRespDTO> respDTOS = new ArrayList<>();  
177 - respDTOS.add(new SmsReceiveRespDTO()  
178 - .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功  
179 - .setErrorCode(params.get("status")) // 状态报告编码  
180 - .setErrorMsg(params.get("statusDesc"))  
181 - .setMobile(params.get("to")) // 手机号  
182 - .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间  
183 - .setSerialNo(params.get("smsMsgId")) // 发送序列号  
184 - .setLogId(Long.valueOf(params.get("extend")))//logId  
185 - );  
186 -  
187 - return respDTOS; 120 + Map<String, String> params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8);
  121 + // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html
  122 + return ListUtil.of(new SmsReceiveRespDTO()
  123 + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
  124 + .setErrorCode(params.get("status")) // 状态报告编码
  125 + .setErrorMsg(params.get("statusDesc"))
  126 + .setMobile(params.get("to")) // 手机号
  127 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
  128 + .setSerialNo(params.get("smsMsgId")) // 发送序列号
  129 + .setLogId(Long.valueOf(params.get("extend")))); // 用户序列号
188 } 130 }
189 131
190 @Override 132 @Override
191 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { 133 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
192 - //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。  
193 - return new SmsTemplateRespDTO().setId(null).setContent(null) 134 + // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构,
  135 + // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符)
  136 + String[] strs = apiTemplateId.split(" ");
  137 + Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender");
  138 + return new SmsTemplateRespDTO().setId(strs[0]).setContent(null)
194 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); 139 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
195 } 140 }
  141 +
  142 + @SuppressWarnings("CharsetObjectCanBeUsed")
  143 + private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException {
  144 + if (StrUtil.isNotEmpty(value)) {
  145 + body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name()));
  146 + }
  147 + }
  148 +
196 } 149 }
197 \ No newline at end of file 150 \ No newline at end of file
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
@@ -56,10 +56,6 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -56,10 +56,6 @@ public class TencentSmsClient extends AbstractSmsClient {
56 validateSdkAppId(properties); 56 validateSdkAppId(properties);
57 } 57 }
58 58
59 - @Override  
60 - protected void doInit() {  
61 - }  
62 -  
63 /** 59 /**
64 * 参数校验腾讯云的 SDK AppId 60 * 参数校验腾讯云的 SDK AppId
65 * 61 *
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java
@@ -37,14 +37,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @@ -37,14 +37,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
37 private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties); 37 private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
38 38
39 @Test 39 @Test
40 - public void testDoInit() {  
41 - // 调用  
42 - smsClient.doInit();  
43 - }  
44 -  
45 - @Test  
46 public void testDoSendSms_success() throws Throwable { 40 public void testDoSendSms_success() throws Throwable {
47 -  
48 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { 41 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
49 // 准备参数 42 // 准备参数
50 Long sendLogId = randomLongId(); 43 Long sendLogId = randomLongId();
@@ -55,9 +48,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @@ -55,9 +48,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
55 48
56 // mock 方法 49 // mock 方法
57 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) 50 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 - ); 51 + .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");
61 52
62 // 调用 53 // 调用
63 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, 54 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
@@ -66,12 +57,35 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @@ -66,12 +57,35 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
66 assertTrue(result.getSuccess()); 57 assertTrue(result.getSuccess());
67 assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo()); 58 assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
68 assertEquals("000000", result.getApiCode()); 59 assertEquals("000000", result.getApiCode());
  60 + }
  61 + }
69 62
  63 + @Test
  64 + public void testDoSendSms_fail_01() throws Throwable {
  65 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  66 + // 准备参数
  67 + Long sendLogId = randomLongId();
  68 + String mobile = randomString();
  69 + String apiTemplateId = randomString() + " " + randomString();
  70 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  71 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  72 +
  73 + // mock 方法
  74 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  75 + .thenReturn("{\"result\":[{\"total\":1,\"originTo\":\"17321315478\",\"createTime\":\"2024-08-18T11:32:20Z\",\"from\":\"x8824060312575\",\"smsMsgId\":\"06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461\",\"countryId\":\"CN\",\"status\":\"E200033\"}],\"code\":\"E000510\",\"description\":\"The SMS fails to be sent. For details, see status.\"}");
  76 +
  77 + // 调用
  78 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  79 + apiTemplateId, templateParams);
  80 + // 断言
  81 + assertFalse(result.getSuccess());
  82 + assertEquals("06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461", result.getSerialNo());
  83 + assertEquals("E200033", result.getApiCode());
70 } 84 }
71 } 85 }
72 86
73 @Test 87 @Test
74 - public void testDoSendSms_fail() throws Throwable { 88 + public void testDoSendSms_fail_02() throws Throwable {
75 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { 89 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
76 // 准备参数 90 // 准备参数
77 Long sendLogId = randomLongId(); 91 Long sendLogId = randomLongId();
@@ -82,17 +96,15 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @@ -82,17 +96,15 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
82 96
83 // mock 方法 97 // mock 方法
84 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) 98 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 - ); 99 + .thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}");
88 100
89 // 调用 101 // 调用
90 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, 102 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
91 apiTemplateId, templateParams); 103 apiTemplateId, templateParams);
92 // 断言 104 // 断言
93 assertFalse(result.getSuccess()); 105 assertFalse(result.getSuccess());
94 - assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());  
95 - assertEquals("E200015", result.getApiCode()); 106 + assertEquals("E000102", result.getApiCode());
  107 + assertEquals("Invalid app_key.", result.getApiMsg());
96 } 108 }
97 } 109 }
98 110
@@ -105,10 +117,11 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @@ -105,10 +117,11 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
105 List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text); 117 List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
106 // 断言 118 // 断言
107 assertEquals(1, statuses.size()); 119 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()); 120 + SmsReceiveRespDTO status = statuses.get(0);
  121 + assertTrue(status.getSuccess());
  122 + assertEquals("DELIVRD", status.getErrorCode());
  123 + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), status.getReceiveTime());
  124 + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", status.getSerialNo());
112 } 125 }
113 126
114 } 127 }
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
3 import cn.iocoder.yudao.framework.common.core.KeyValue; 3 import cn.iocoder.yudao.framework.common.core.KeyValue;
4 -import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; 4 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
5 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; 5 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; 6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
7 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 7 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
@@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test;
11 import java.util.List; 11 import java.util.List;
12 12
13 /** 13 /**
14 - * 各种 {@link SmsClientTests 集成测试 14 + * 各种 {@link SmsClient} 的集成测试
15 * 15 *
16 * @author 芋道源码 16 * @author 芋道源码
17 */ 17 */
@@ -23,8 +23,8 @@ public class SmsClientTests { @@ -23,8 +23,8 @@ public class SmsClientTests {
23 @Disabled 23 @Disabled
24 public void testAliyunSmsClient_getSmsTemplate() throws Throwable { 24 public void testAliyunSmsClient_getSmsTemplate() throws Throwable {
25 SmsChannelProperties properties = new SmsChannelProperties() 25 SmsChannelProperties properties = new SmsChannelProperties()
26 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
27 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); 26 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  27 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"));
28 AliyunSmsClient client = new AliyunSmsClient(properties); 28 AliyunSmsClient client = new AliyunSmsClient(properties);
29 // 准备参数 29 // 准备参数
30 String apiTemplateId = "SMS_207945135"; 30 String apiTemplateId = "SMS_207945135";
@@ -38,9 +38,9 @@ public class SmsClientTests { @@ -38,9 +38,9 @@ public class SmsClientTests {
38 @Disabled 38 @Disabled
39 public void testAliyunSmsClient_sendSms() throws Throwable { 39 public void testAliyunSmsClient_sendSms() throws Throwable {
40 SmsChannelProperties properties = new SmsChannelProperties() 40 SmsChannelProperties properties = new SmsChannelProperties()
41 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
42 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")  
43 - .setSignature("runpu"); 41 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  42 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"))
  43 + .setSignature("Ballcat");
44 AliyunSmsClient client = new AliyunSmsClient(properties); 44 AliyunSmsClient client = new AliyunSmsClient(properties);
45 // 准备参数 45 // 准备参数
46 Long sendLogId = System.currentTimeMillis(); 46 Long sendLogId = System.currentTimeMillis();
@@ -52,35 +52,6 @@ public class SmsClientTests { @@ -52,35 +52,6 @@ public class SmsClientTests {
52 System.out.println(sendRespDTO); 52 System.out.println(sendRespDTO);
53 } 53 }
54 54
55 - @Test  
56 - @Disabled  
57 - public void testAliyunSmsClient_parseSmsReceiveStatus() {  
58 - SmsChannelProperties properties = new SmsChannelProperties()  
59 - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")  
60 - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");  
61 - AliyunSmsClient client = new AliyunSmsClient(properties);  
62 - // 准备参数  
63 - String text = "[\n" +  
64 - " {\n" +  
65 - " \"phone_number\" : \"13900000001\",\n" +  
66 - " \"send_time\" : \"2017-01-01 11:12:13\",\n" +  
67 - " \"report_time\" : \"2017-02-02 22:23:24\",\n" +  
68 - " \"success\" : true,\n" +  
69 - " \"err_code\" : \"DELIVERED\",\n" +  
70 - " \"err_msg\" : \"用户接收成功\",\n" +  
71 - " \"sms_size\" : \"1\",\n" +  
72 - " \"biz_id\" : \"12345\",\n" +  
73 - " \"out_id\" : \"67890\"\n" +  
74 - " }\n" +  
75 - "]";  
76 - // mock 方法  
77 -  
78 - // 调用  
79 - List<SmsReceiveRespDTO> statuses = client.parseSmsReceiveStatus(text);  
80 - // 打印结果  
81 - System.out.println(statuses);  
82 - }  
83 -  
84 // ========== 腾讯云 ========== 55 // ========== 腾讯云 ==========
85 56
86 @Test 57 @Test
@@ -123,14 +94,14 @@ public class SmsClientTests { @@ -123,14 +94,14 @@ public class SmsClientTests {
123 @Disabled 94 @Disabled
124 public void testHuaweiSmsClient_sendSms() throws Throwable { 95 public void testHuaweiSmsClient_sendSms() throws Throwable {
125 SmsChannelProperties properties = new SmsChannelProperties() 96 SmsChannelProperties properties = new SmsChannelProperties()
126 - .setApiKey("123")  
127 - .setApiSecret("456") 97 + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY"))
  98 + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY"))
128 .setSignature("runpu"); 99 .setSignature("runpu");
129 HuaweiSmsClient client = new HuaweiSmsClient(properties); 100 HuaweiSmsClient client = new HuaweiSmsClient(properties);
130 // 准备参数 101 // 准备参数
131 Long sendLogId = System.currentTimeMillis(); 102 Long sendLogId = System.currentTimeMillis();
132 - String mobile = "15601691323";  
133 - String apiTemplateId = "xx test01"; 103 + String mobile = "17321315478";
  104 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef x8824060312575";
134 List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024")); 105 List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
135 // 调用 106 // 调用
136 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); 107 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);