Merge branch 'develop' of https://gitee.com/scholarli/ruoyi-vue-pro_1 into develop

# Conflicts:
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
This commit is contained in:
YunaiV 2024-08-18 17:42:31 +08:00
commit 8cce737523
3 changed files with 184 additions and 100 deletions

View File

@ -4,16 +4,17 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
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 = "管理后台 - 短信回调")
@ -26,7 +27,7 @@ public class SmsCallbackController {
@PostMapping("/aliyun")
@PermitAll
@Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档")
@Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
@ -35,7 +36,7 @@ public class SmsCallbackController {
@PostMapping("/tencent")
@PermitAll
@Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档")
@Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);
@ -46,9 +47,8 @@ public class SmsCallbackController {
@PostMapping("/huawei")
@PermitAll
@Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
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);
}

View File

@ -4,12 +4,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;
@ -17,23 +16,20 @@ 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.*;
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;
// todo @scholar参考阿里云在优化下
/**
@ -45,9 +41,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";
@ -76,13 +69,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"));
@ -95,8 +89,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;
}
@ -116,26 +109,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();
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);
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;
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.");
@ -148,7 +144,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();
}
@ -158,12 +156,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
@ -171,56 +192,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;
}
}

View File

@ -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());
}
}