批量发送半成品

This commit is contained in:
zengzefeng 2021-03-08 09:15:52 +08:00
parent c3372d4bd2
commit 767cd90279
30 changed files with 735 additions and 230 deletions

16
pom.xml
View File

@ -183,12 +183,6 @@
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
@ -222,17 +216,25 @@
<version>${easyexcel.verion}</version>
</dependency>
<!-- SMS SDK begin -->
<dependency>
<groupId>com.yunpian.sdk</groupId>
<artifactId>yunpian-java-sdk</artifactId>
<version>1.2.7</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.18</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
<!-- SMS SDK end -->
</dependencies>

View File

@ -12,6 +12,8 @@ CREATE TABLE `sms_channel`
`code` varchar(50) NOT NULL COMMENT '编码(来自枚举类 阿里、华为、七牛等)',
`api_key` varchar(100) NOT NULL COMMENT '账号id',
`api_secret` varchar(100) NOT NULL COMMENT '账号秘钥',
`had_callback` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否拥有回调函数',
`callback_url` varchar(100) NOT NULL default '' COMMENT '回调请求路径',
`api_signature_id` varchar(100) NOT NULL COMMENT '实际渠道签名唯一标识',
`name` varchar(50) NOT NULL COMMENT '名称',
`signature` varchar(50) NOT NULL COMMENT '签名值',
@ -61,22 +63,46 @@ CREATE TABLE `sms_template`
DEFAULT CHARSET = utf8mb4 COMMENT ='短信模板';
-- ----------------------------
-- Table structure for sms_log
-- Table structure for sms_query_log
-- ----------------------------
DROP TABLE IF EXISTS `sms_log`;
CREATE TABLE `sms_log`
DROP TABLE IF EXISTS `sms_query_log`;
CREATE TABLE `sms_query_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号',
`api_id` varchar(100) NOT NULL COMMENT '第三方唯一标识',
`channel_code` varchar(50) NOT NULL COMMENT '短信渠道编码(来自枚举类)',
`channel_id` bigint(20) NOT NULL COMMENT '短信渠道id',
`template_code` varchar(50) NOT NULL COMMENT '渠道编码',
`phones` char(11) NOT NULL COMMENT '手机号(数组json字符串)',
`phones` varchar(2000) NOT NULL COMMENT '手机号(数组json字符串)',
`content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`send_status` tinyint(4) NOT NULL DEFAULT 2 COMMENT '发送状态1异步推送中 2发送中 3失败 4成功',
`send_result_param` varchar(200) NOT NULL DEFAULT '' COMMENT '查询短信发送结果的参数',
`send_status` tinyint(1) NOT NULL DEFAULT 2 COMMENT '发送状态0本地异步中 1发送请求失败 2发送请求成功',
`got_result` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否获取发送结果',
`had_callback` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否拥有回调函数',
`create_by` varchar(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='短信日志';
DEFAULT CHARSET = utf8mb4 COMMENT ='短信请求日志';
-- ----------------------------
-- Table structure for sms_log
-- ----------------------------
DROP TABLE IF EXISTS `sms_send_log`;
CREATE TABLE `sms_send_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号',
`channel_code` varchar(50) NOT NULL COMMENT '短信渠道编码(来自枚举类)',
`channel_id` bigint(20) NOT NULL COMMENT '短信渠道id',
`template_code` varchar(50) NOT NULL COMMENT '渠道编码',
`query_log_id` bigint(20) NOT NULL COMMENT '请求日志id',
`phone` char(11) NOT NULL COMMENT '手机号',
`content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`success` tinyint(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`send_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='短信发送日志';

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 通用状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum DefaultBitFieldEnum {
NO(0, ""),
YES(1, "");
/**
* 状态值
*/
private final Integer val;
/**
* 状态名
*/
private final String name;
}

View File

@ -58,7 +58,7 @@ public abstract class AbstractSmsClient implements SmsClient {
* @return 短信发送结果
* @throws Exception 调用发送失败抛出异常
*/
public abstract SmsResult doSend(String templateApiId, SmsBody smsBody, Collection<String> targets) throws Exception;
protected abstract SmsResult doSend(String templateApiId, SmsBody smsBody, Collection<String> targets) throws Exception;
protected void beforeSend(String templateApiId, SmsBody smsBody, Collection<String> targets) throws Exception {
}

View File

@ -2,7 +2,6 @@ package cn.iocoder.dashboard.framework.sms.client;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
@ -14,11 +13,11 @@ import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
@ -31,7 +30,7 @@ import java.util.List;
* @date 2021/1/25 14:17
*/
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {
public class AliyunSmsClient extends AbstractSmsClient implements NeedQuerySendResultSmsClient {
private static final String OK = "OK";
@ -70,35 +69,43 @@ public class AliyunSmsClient extends AbstractSmsClient {
request.setTemplateParam(smsBody.getParamsStr());
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
boolean result = OK.equals(sendSmsResponse.getCode());
if (!result) {
boolean success = OK.equals(sendSmsResponse.getCode());
if (!success) {
log.debug("send fail[code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
}
SmsResult resultBody = new SmsResult();
resultBody.setSuccess(result);
return new SmsResult()
.setSuccess(success)
.setMessage(sendSmsResponse.getMessage())
.setCode(sendSmsResponse.getCode())
.setApiId(sendSmsResponse.getBizId())
.setSendResultParam(sendSmsResponse.getBizId());
}
@Override
public List<SmsResultDetail> getSmsSendResult(String param) throws ClientException {
QuerySendDetailsRequest querySendDetailsRequest = new QuerySendDetailsRequest();
querySendDetailsRequest.setBizId(sendSmsResponse.getBizId());
// TODO FROM 芋艿 to zzf发送完之后基于短信平台回调去更新回执状态短信发送是否成功和最终用户收到是两个维度这块有困惑可以微信我给个截图哈
querySendDetailsRequest.setBizId(param);
// TODO FROM 芋艿 to zzf发送完之后基于短信平台回调去更新回执状态短信发送是否成功和最终用户收到是两个维度这块有困惑可以微信我给个截图哈 DONE
QuerySendDetailsResponse acsResponse = acsClient.getAcsResponse(querySendDetailsRequest);
List<SmsResultDetail> resultDetailList = new ArrayList<>(Integer.parseInt(acsResponse.getTotalCount()));
acsResponse.getSmsSendDetailDTOs().forEach(s -> {
SmsResultDetail resultDetail = new SmsResultDetail();
resultDetail.setCreateTime(DateUtil.parseDateTime(s.getSendDate()));
resultDetail.setSendTime(DateUtil.parseDateTime(s.getSendDate()));
resultDetail.setMessage(s.getContent());
resultDetail.setPhone(s.getPhoneNum());
resultDetail.setStatus(statusConvert(s.getSendStatus()));
resultDetail.setSendStatus(statusConvert(s.getSendStatus()));
resultDetailList.add(resultDetail);
});
resultBody.setResult(resultDetailList);
return resultBody;
return resultDetailList;
}
private int statusConvert(Long aliSendStatus) {
if (aliSendStatus == 1L) {
return SmsSendStatusEnum.SUCCESS.getStatus();
return SmsSendStatusEnum.SEND_SUCCESS.getStatus();
}
if (aliSendStatus == 2L) {
return SmsSendStatusEnum.FAIL.getStatus();
return SmsSendStatusEnum.SEND_FAIL.getStatus();
}
return SmsSendStatusEnum.WAITING.getStatus();
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.dashboard.framework.sms.client;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import javax.servlet.ServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.List;
/**
* 需要发送请求获取短信发送结果的短信客户端
*
* @author zzf
* @date 2021/3/4 17:20
*/
public interface HadCallbackSmsClient {
/**
* 获取短信发送结果
*
* @param request 请求
* @return 短信发送结果
*/
List<SmsResultDetail> getSmsSendResult(ServletRequest request) throws Exception;
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.dashboard.framework.sms.client;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import com.aliyuncs.exceptions.ClientException;
import java.util.List;
/**
* 需要发送请求获取短信发送结果的短信客户端
*
* @author zzf
* @date 2021/3/4 17:20
*/
public interface NeedQuerySendResultSmsClient {
/**
* 获取短信发送结果
*
* @param param 参数
* @return 短信发送结果
*/
List<SmsResultDetail> getSmsSendResult(String param) throws Exception;
}

View File

@ -2,8 +2,10 @@ package cn.iocoder.dashboard.framework.sms.client;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import java.util.Collection;
import java.util.List;
/**
* 短信父接口
@ -23,4 +25,7 @@ public interface SmsClient {
*/
SmsResult send(String templateApiId, SmsBody smsBody, Collection<String> targets);
//List<SmsResultDetail> getSmsSendResult(String jsonObjectParam);
}

View File

@ -0,0 +1,132 @@
package cn.iocoder.dashboard.framework.sms.client;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsConstants;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.yunpian.sdk.YunpianClient;
import com.yunpian.sdk.constant.Code;
import com.yunpian.sdk.constant.YunpianConstant;
import com.yunpian.sdk.model.Result;
import com.yunpian.sdk.model.SmsBatchSend;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* 云片短信实现类
*
* @author zzf
* @date 9:48 2021/3/5
*/
@Slf4j
public class YunpianSmsClient extends AbstractSmsClient implements HadCallbackSmsClient {
private final YunpianClient client;
private final TypeReference<List<Map<String, String>>> callbackType = new TypeReference<List<Map<String, String>>>() {
};
/**
* 构造云片短信发送处理
*
* @param channelVO 阿里云短信配置
*/
public YunpianSmsClient(SmsChannelProperty channelVO) {
super(channelVO);
client = new YunpianClient(channelVO.getApiKey());
}
@Override
public SmsResult doSend(String templateApiId, SmsBody smsBody, Collection<String> targets) {
Map<String, String> paramMap = new HashMap<>();
paramMap.put("apikey", getProperty().getApiKey());
paramMap.put("mobile", String.join(SmsConstants.COMMA, targets));
paramMap.put("text", formatContent(smsBody));
paramMap.put("callback", getProperty().getCallbackUrl());
Result<SmsBatchSend> sendResult = client.sms().batch_send(paramMap);
boolean success = sendResult.getCode().equals(Code.OK);
if (!success) {
log.debug("send fail[code={}, message={}]", sendResult.getCode(), sendResult.getDetail());
}
return new SmsResult()
.setSuccess(success)
.setMessage(sendResult.getDetail())
.setCode(sendResult.getCode().toString())
.setApiId(sendResult.getData().getData().get(0).getSid().toString());
}
/**
* 格式化短信内容将参数注入到模板中
*
* @param smsBody 短信信息
* @return 格式化后的短信内容
*/
private String formatContent(SmsBody smsBody) {
StringBuilder result = new StringBuilder(smsBody.getTemplateContent());
smsBody.getParams().forEach((key, val) -> {
String param = parseParamToPlaceholder(key);
result.replace(result.indexOf(param), result.indexOf(param + param.length()), val);
});
return result.toString();
}
/**
* 将指定参数改成对应的占位字符
* <p>
* 云片的是 #param# 的形式作为占位符
*
* @param key 参数名
* @return 对应的占位字符
*/
private String parseParamToPlaceholder(String key) {
return SmsConstants.JING_HAO + key + SmsConstants.JING_HAO;
}
@Override
public List<SmsResultDetail> getSmsSendResult(ServletRequest request) throws UnsupportedEncodingException {
List<Map<String, String>> stringStringMap = getSendResult(request);
List<SmsResultDetail> resultDetailList = new ArrayList<>(stringStringMap.size());
stringStringMap.forEach(map -> {
SmsResultDetail detail = new SmsResultDetail();
detail.setPhone(map.get("mobile"));
detail.setMessage(map.get("error_msg"));
detail.setSendTime(DateUtil.parseTime(map.get("user_receive_time")));
String reportStatus = map.get("report_status");
detail.setSendStatus(reportStatus.equals(SmsConstants.SUCCESS)
? SmsSendStatusEnum.SEND_SUCCESS.getStatus()
: SmsSendStatusEnum.SEND_FAIL.getStatus()
);
resultDetailList.add(detail);
});
return resultDetailList;
}
/**
* request 中获取请求中传入的短信发送结果信息
*
* @param request 回调请求
* @return 短信发送结果信息
* @throws UnsupportedEncodingException 解码异常
*/
private List<Map<String, String>> getSendResult(ServletRequest request) throws UnsupportedEncodingException {
Map<String, String[]> parameterMap = request.getParameterMap();
String[] smsStatuses = parameterMap.get(YunpianConstant.SMS_STATUS);
String encode = URLEncoder.encode(smsStatuses[0], CharsetUtil.UTF_8);
return JsonUtils.parseByType(encode, callbackType);
}
}

View File

@ -4,7 +4,6 @@ import cn.iocoder.dashboard.util.json.JsonUtils;
import lombok.Data;
import java.util.Map;
import java.util.UUID;
/**
* 消息内容实体类
@ -22,6 +21,11 @@ public class SmsBody {
*/
private String templateCode;
/**
* 模板编码
*/
private String templateContent;
/**
* 参数列表
*/

View File

@ -0,0 +1,18 @@
package cn.iocoder.dashboard.framework.sms.core;
/**
* 短信相关常量类
*
* @author zzf
* @date 2021/3/5 10:42
*/
public interface SmsConstants {
String OK = "OK";
String JING_HAO = "#";
String COMMA = ",";
String SUCCESS = "SUCCESS";
}

View File

@ -1,14 +1,15 @@
package cn.iocoder.dashboard.framework.sms.core;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
/**
* 消息内容实体类
*/
@Data
@Accessors(chain = true)
public class SmsResult implements Serializable {
/**
@ -16,6 +17,11 @@ public class SmsResult implements Serializable {
*/
private Boolean success;
/**
* 第三方唯一标识
*/
private String apiId;
/**
* 状态码
*/
@ -27,10 +33,9 @@ public class SmsResult implements Serializable {
private String message;
/**
* 返回值
* 用于查询发送结果的参数
*/
private List<SmsResultDetail> result;
private String sendResultParam;
public static SmsResult failResult(String message) {
SmsResult resultBody = new SmsResult();

View File

@ -14,7 +14,7 @@ public class SmsResultDetail implements Serializable {
/**
* 短信发送状态 {@link cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum}
*/
private Integer status;
private Integer sendStatus;
/**
* 接收手机号
@ -29,5 +29,5 @@ public class SmsResultDetail implements Serializable {
/**
* 时间
*/
private Date createTime;
private Date sendTime;
}

View File

@ -54,4 +54,15 @@ public class SmsChannelProperty implements Serializable {
@NotEmpty(message = "签名值不能为空")
private String signature;
/**
* 是否拥有回调函数0否 1是
*/
@NotNull(message = "是否拥有回调函数不能为空")
private Integer hadCallback;
/**
* 短信发送回调url
*/
private String callbackUrl;
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.modules.system.controller.sms;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
/**
* 短信默认回调接口
*
* @author zzf
* @date 2021/3/5 8:59
*/
@RestController("/sms/callback")
public class SmsDefaultCallbackController {
@Resource
private SysSmsService smsService;
@RequestMapping("/sms-send")
public Object sendSmsCallback(ServletRequest request){
return smsService.smsSendCallbackHandle(request);
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms;
import cn.iocoder.dashboard.common.enums.DefaultBitFieldEnum;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsQueryLogDO;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SysSmsQueryLogMapper extends BaseMapper<SysSmsQueryLogDO> {
/**
* 查询还没有获取发送结果的短信请求信息
*
* @return
*/
default List<SysSmsQueryLogDO> selectNoResultQueryLogList() {
return this.selectList(new LambdaQueryWrapper<SysSmsQueryLogDO>()
.eq(SysSmsQueryLogDO::getSendStatus, SmsSendStatusEnum.QUERY_SUCCESS)
.eq(SysSmsQueryLogDO::getGotResult, DefaultBitFieldEnum.NO)
.eq(SysSmsQueryLogDO::getHadCallback, DefaultBitFieldEnum.NO)
);
}
}

View File

@ -1,10 +1,10 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsLogDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsSendLogDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysSmsLogMapper extends BaseMapper<SysSmsLogDO> {
public interface SysSmsSendLogMapper extends BaseMapper<SysSmsSendLogDO> {
}

View File

@ -27,6 +27,16 @@ public class SysSmsChannelDO extends BaseDO {
*/
private String code;
/**
* 是否拥有回答0否 1是
*/
private Integer had_callback;
/**
* 短信发送回调url
*/
private String callback_url;
/**
* 渠道账号id
*/

View File

@ -0,0 +1,94 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 短信日志
*
* @author zzf
* @since 2021-01-25
*/
@Data
@EqualsAndHashCode
@Accessors(chain = true)
@TableName(value = "sms_query_log", autoResultMap = true)
public class SysSmsQueryLogDO implements Serializable {
/**
* 自增编号
*/
private Long id;
/**
* 短信渠道编码(来自枚举类)
*/
private String channelCode;
/**
* 短信渠道id
*/
private Long channelId;
/**
* 模板id
*/
private String templateCode;
/**
* 手机号
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> phones;
/**
* 内容
*/
private String content;
/**
* 发送状态
*
* @see cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum
*/
private Integer sendStatus;
/**
* 是否获取过结果[0否 1是]
*/
private Integer gotResult;
/**
* 是否拥有回调函数0否 1是
*/
private Integer hadCallback;
/**
* 结果(对象json字符串)
*/
private String sendResultParam;
/**
* 备注
*/
private String remark;
/**
* 创建人
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -17,8 +18,8 @@ import java.util.Date;
@Data
@EqualsAndHashCode
@Accessors(chain = true)
@TableName(value = "sms_log", autoResultMap = true)
public class SysSmsLogDO implements Serializable {
@TableName(value = "sms_send_log", autoResultMap = true)
public class SysSmsSendLogDO implements Serializable {
/**
* 自增编号
@ -41,14 +42,9 @@ public class SysSmsLogDO implements Serializable {
private String templateCode;
/**
* 手机号(数组json字符串)
* 手机号
*/
private String phones;
/**
* 内容
*/
private String content;
private String phone;
/**
* 备注
@ -56,18 +52,15 @@ public class SysSmsLogDO implements Serializable {
private String remark;
/**
* 发送状态1异步推送中 2发送中 3失败 4成功
* 发送状态
*
* @see SmsSendStatusEnum
*/
private Integer sendStatus;
/**
* 创建者
* 发送时间
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
private Date sendTime;
}

View File

@ -13,20 +13,26 @@ import lombok.Getter;
@AllArgsConstructor
public enum SmsSendStatusEnum {
//请求发送结果时失败
QUERY_SEND_FAIL(-3),
//短信发送失败
SEND_FAIL(-2),
//短信请求失败
QUERY_FAIL(-1),
//异步转发中
ASYNC(1),
ASYNC(0),
//发送中
SENDING(2),
//请求成功
QUERY_SUCCESS(1),
//失败
FAIL(3),
//短信成功
SEND_SUCCESS(2),
//等待回执
WAITING(4),
//成功
SUCCESS(5);
WAITING(3);
private final int status;

View File

@ -1,10 +1,12 @@
package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.modules.system.mq.message.dept.SysDeptRefreshMessage;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SmsSendMessage;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsQueryLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -20,12 +22,20 @@ import javax.annotation.Resource;
public class SmsSendConsumer extends AbstractChannelMessageListener<SmsSendMessage> {
@Resource
private SysSmsService sysSmsService;
private SysSmsChannelService smsChannelService;
@Resource
private SysSmsQueryLogService smsQueryLogService;
@Override
public void onMessage(SmsSendMessage message) {
log.info("[onMessage][收到 发送短信 消息], content: " + message.toString());
SmsResult send = sysSmsService.send(message.getSmsBody(), message.getTargetPhones());
AbstractSmsClient smsClient = smsChannelService.getSmsClient(message.getSmsBody().getTemplateCode());
String templateApiId = smsChannelService.getSmsTemplateApiIdByCode(message.getSmsBody().getTemplateCode());
SmsResult result = smsClient.send(templateApiId, message.getSmsBody(), message.getTargetPhones());
smsQueryLogService.afterSendLog(message.getSmsBody().getSmsLogId(), result);
}
}

View File

@ -7,26 +7,25 @@ import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import java.util.List;
/**
* 短信渠道Service接口
* 短信请求日志服务接口
*
* @author zzf
* @date 2021/1/25 9:24
*/
public interface SysSmsLogService {
public interface SysSmsQueryLogService {
/**
* 发送短信前的日志处理
*
* @param smsBody 短信内容
* @param targetPhones 发送对象手机号集合
* @param client 短信客户端
* @param isAsync 是否异步发送
* @return 生成的日志id
*/
// TODO FROM 芋艿 to ZZF: async 是针对发送的方式对于日志不一定需要关心这样短信日志实际就发送前插入发送后更新结果.
// 这里只用于记录状态毕竟异步可能推送失败此时日志可记录该状态
// TODO FROM 芋艿 to ZZF短信日志群发的情况应该是每个手机一条哈虽然是群发但是可能部分成功部分失败对应到短信平台实际也是多条
Long beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient client, Boolean isAsync);
void beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient client);
/**
* 发送消息后的日志处理

View File

@ -0,0 +1,13 @@
package cn.iocoder.dashboard.modules.system.service.sms;
/**
* 短信发送日志服务接口
*
* @author zzf
* @date 13:48 2021/3/2
*/
public interface SysSmsSendLogService {
void getAndSaveSmsSendLog();
}

View File

@ -1,15 +1,15 @@
package cn.iocoder.dashboard.modules.system.service.sms;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.ServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 短信Service接口
* 只支持异步因此没有返回值
*
* @author zzf
* @date 2021/1/25 9:24
@ -21,23 +21,17 @@ public interface SysSmsService {
*
* @param smsBody 消息内容
* @param targetPhones 发送对象手机号列表
* @return 是否发送成功
*/
SmsResult send(SmsBody smsBody, List<String> targetPhones);
void send(SmsBody smsBody, List<String> targetPhones);
/**
* 发送消息
*
* @param smsBody 消息内容
* @param targetPhone 发送对象手机号
* @return 是否发送成功
*/
default SmsResult send(SmsBody smsBody, String targetPhone) {
if (StringUtils.isBlank(targetPhone)) {
return failResult("targetPhone must not null.");
}
return send(smsBody, Collections.singletonList(targetPhone));
default void send(SmsBody smsBody, String targetPhone) {
send(smsBody, Collections.singletonList(targetPhone));
}
/**
@ -45,57 +39,16 @@ public interface SysSmsService {
*
* @param smsBody 消息内容
* @param targetPhones 发送对象手机号数组
* @return 是否发送成功
*/
default SmsResult send(SmsBody smsBody, String... targetPhones) {
if (targetPhones == null) {
return failResult("targetPhones must not null.");
}
return send(smsBody, Arrays.asList(targetPhones));
}
/**
* 异步发送消息
*
* @param msgBody 消息内容
* @param targetPhones 发送对象列表
*/
void sendAsync(SmsBody msgBody, List<String> targetPhones);
/**
* 异步发送消息
*
* @param msgBody 消息内容
* @param targetPhone 发送对象
*/
default void sendAsync(SmsBody msgBody, String targetPhone) {
if (StringUtils.isBlank(targetPhone)) {
return;
}
sendAsync(msgBody, Collections.singletonList(targetPhone));
default void send(SmsBody smsBody, String... targetPhones) {
send(smsBody, Arrays.asList(targetPhones));
}
/**
* 异步发送消息
* 处理短信发送回调函数
*
* @param msgBody 消息内容
* @param targetPhones 发送对象列表
* @param request 请求
* @return 响应数据
*/
default void sendAsync(SmsBody msgBody, String... targetPhones) {
if (targetPhones == null) {
return;
}
sendAsync(msgBody, Arrays.asList(targetPhones));
}
default SmsResult failResult(String message) {
SmsResult resultBody = new SmsResult();
resultBody.setSuccess(false);
resultBody.setMessage(message);
return resultBody;
}
Object smsSendCallbackHandle(ServletRequest request);
}

View File

@ -1,77 +0,0 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SysSmsLogMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsLogDO;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
import cn.iocoder.dashboard.util.json.JsonUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 短信日志Service实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Service
public class SysSmsLogServiceImpl implements SysSmsLogService {
@Resource
private SysSmsLogMapper logMapper;
@Override
public Long beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient client, Boolean isAsync) {
SysSmsLogDO smsLog = new SysSmsLogDO();
if (smsBody.getSmsLogId() != null) {
smsLog.setId(smsBody.getSmsLogId());
smsLog.setSendStatus(SmsSendStatusEnum.SENDING.getStatus());
logMapper.updateById(smsLog);
return smsBody.getSmsLogId();
} else {
SmsChannelProperty property = client.getProperty();
smsLog.setChannelCode(property.getCode())
.setChannelId(property.getId())
.setTemplateCode(smsBody.getTemplateCode())
.setPhones(JsonUtils.toJsonString(targetPhones))
.setContent(smsBody.getParams().toString());
if (isAsync) {
smsLog.setSendStatus(SmsSendStatusEnum.ASYNC.getStatus());
} else {
smsLog.setSendStatus(SmsSendStatusEnum.SENDING.getStatus());
}
logMapper.insert(smsLog);
return smsLog.getId();
}
}
@Override
public void afterSendLog(Long logId, SmsResult result) {
SysSmsLogDO smsLog = new SysSmsLogDO();
smsLog.setId(logId);
if (result.getSuccess()) {
smsLog.setSendStatus(SmsSendStatusEnum.SUCCESS.getStatus());
SysSmsLogDO smsLogDO = logMapper.selectById(logId);
result.getResult().forEach(s -> {
smsLogDO.setPhones(s.getPhone());
smsLogDO.setSendStatus(s.getStatus());
smsLogDO.setRemark(s.getMessage());
smsLogDO.setCreateTime(s.getCreateTime());
logMapper.insert(smsLogDO);
});
} else {
smsLog.setSendStatus(SmsSendStatusEnum.FAIL.getStatus());
smsLog.setRemark(result.getMessage() + JsonUtils.toJsonString(result.getResult()));
}
logMapper.updateById(smsLog);
}
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SysSmsQueryLogMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsQueryLogDO;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsQueryLogService;
import cn.iocoder.dashboard.util.json.JsonUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 短信请求日志服务实现类
*
* @author zzf
* @date 13:50 2021/3/2
*/
@Service
public class SysSmsQueryLogServiceImpl implements SysSmsQueryLogService {
@Resource
private SysSmsQueryLogMapper logMapper;
@Override
public void beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient client) {
SysSmsQueryLogDO smsLog = new SysSmsQueryLogDO();
SmsChannelProperty property = client.getProperty();
smsLog.setChannelCode(property.getCode())
.setChannelId(property.getId())
.setTemplateCode(smsBody.getTemplateCode())
.setPhones(targetPhones)
.setContent(smsBody.getParams().toString());
smsLog.setSendStatus(SmsSendStatusEnum.ASYNC.getStatus());
logMapper.insert(smsLog);
smsBody.setSmsLogId(smsLog.getId());
}
@Override
public void afterSendLog(Long logId, SmsResult result) {
SysSmsQueryLogDO smsLog = new SysSmsQueryLogDO();
smsLog.setId(logId);
if (result.getSuccess()) {
smsLog.setSendStatus(SmsSendStatusEnum.QUERY_SUCCESS.getStatus());
smsLog.setSendResultParam(result.getSendResultParam());
} else {
smsLog.setSendStatus(SmsSendStatusEnum.QUERY_FAIL.getStatus());
smsLog.setRemark(result.getMessage());
}
logMapper.updateById(smsLog);
}
}

View File

@ -0,0 +1,109 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.client.NeedQuerySendResultSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SysSmsQueryLogMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SysSmsSendLogMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsQueryLogDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SysSmsSendLogDO;
import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsSendLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 短信发送日志服务实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Slf4j
@Service
public class SysSmsSendLogServiceImpl implements SysSmsSendLogService {
@Resource
private SysSmsQueryLogMapper smsQueryLogMapper;
@Resource
private SysSmsSendLogMapper smsSendLogMapper;
@Resource
private SysSmsChannelService smsChannelService;
/**
* 定时执行 {@link #getSmsSendResultJob()} 的周期
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
@Override
public void getAndSaveSmsSendLog() {
List<SysSmsQueryLogDO> noResultQueryLogList = smsQueryLogMapper.selectNoResultQueryLogList();
if (CollectionUtil.isEmpty(noResultQueryLogList)) {
return;
}
//用于添加的发送日志对象
SysSmsSendLogDO insertSendLog = new SysSmsSendLogDO();
//用于修改状态的请求日志对象
SysSmsQueryLogDO updateQueryLog = new SysSmsQueryLogDO();
noResultQueryLogList.forEach(queryLog -> {
AbstractSmsClient smsClient = smsChannelService.getSmsClient(queryLog.getTemplateCode());
updateQueryLog.setId(queryLog.getId());
// 只处理实现了获取发送结果方法的短信客户端理论上这里都是满足条件的以防万一加个判断
if (smsClient instanceof NeedQuerySendResultSmsClient) {
//初始化点字段值
queryLog2SendLong(insertSendLog, queryLog);
NeedQuerySendResultSmsClient querySendResultSmsClient = (NeedQuerySendResultSmsClient) smsClient;
try {
List<SmsResultDetail> smsSendResult = querySendResultSmsClient.getSmsSendResult(queryLog.getRemark());
smsSendResult.forEach(resultDetail -> {
insertSendLog.setPhone(resultDetail.getPhone());
insertSendLog.setSendStatus(resultDetail.getSendStatus());
insertSendLog.setSendTime(resultDetail.getSendTime());
insertSendLog.setRemark(resultDetail.getMessage());
smsSendLogMapper.insert(insertSendLog);
});
} catch (Exception e) {
//exception handle
log.error("query send result fail, exception: " + e.getMessage());
updateQueryLog.setSendStatus(SmsSendStatusEnum.QUERY_SEND_FAIL.getStatus());
updateQueryLog.setRemark(e.getMessage());
smsQueryLogMapper.updateById(updateQueryLog);
return;
}
} else {
//理论上这里都是满足条件的以防万一加个判断
updateQueryLog.setSendStatus(SmsSendStatusEnum.QUERY_SEND_FAIL.getStatus());
smsQueryLogMapper.updateById(updateQueryLog);
}
updateQueryLog.setSendStatus(SmsSendStatusEnum.SEND_SUCCESS.getStatus());
updateQueryLog.setRemark(String.format("日志(id = %s)对应的客户端没有继承NeedQuerySendResultSmsClient, 不能获取短信结果。", queryLog.getId()));
smsQueryLogMapper.updateById(updateQueryLog);
});
}
private void queryLog2SendLong(SysSmsSendLogDO insertSendLog, SysSmsQueryLogDO queryLog) {
insertSendLog.setChannelCode(queryLog.getChannelCode());
insertSendLog.setChannelId(queryLog.getChannelId());
insertSendLog.setTemplateCode(queryLog.getTemplateCode());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void getSmsSendResultJob() {
getAndSaveSmsSendLog();
}
}

View File

@ -2,11 +2,11 @@ package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.modules.system.mq.producer.sms.SmsProducer;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsQueryLogService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@ -25,30 +25,18 @@ public class SysSmsServiceImpl implements SysSmsService {
private SysSmsChannelService channelService;
@Resource
private SysSmsLogService logService;
private SysSmsQueryLogService logService;
@Resource
private SmsProducer smsProducer;
@Override
public SmsResult send(SmsBody smsBody, List<String> targetPhones) {
public void send(SmsBody smsBody, List<String> targetPhones) {
AbstractSmsClient client = channelService.getSmsClient(smsBody.getTemplateCode());
String templateApiId = channelService.getSmsTemplateApiIdByCode(smsBody.getTemplateCode());
Long logId = logService.beforeSendLog(smsBody, targetPhones, client, false);
SmsResult result = client.send(templateApiId, smsBody, targetPhones);
logService.afterSendLog(logId, result);
return result;
}
// TODO FROM 芋艿 to ZZF可能要讨论下对于短信发送来说貌似只提供异步发送即可对于业务来说一定不能依赖短信的发送结果
// 我的想法是1很多短信比如验证码总还是需要知道是否发送成功的2别人可以不用我们不能没有3实现挺简单的个人觉得无需纠结
@Override
public void sendAsync(SmsBody smsBody, List<String> targetPhones) {
AbstractSmsClient client = channelService.getSmsClient(smsBody.getTemplateCode());
logService.beforeSendLog(smsBody, targetPhones, client, true);
logService.beforeSendLog(smsBody, targetPhones, client);
smsProducer.sendSmsSendMessage(smsBody, targetPhones);
}
// TODO FROM 芋艿 to ZZF可能要讨论下对于短信发送来说貌似只提供异步发送即可对于业务来说一定不能依赖短信的发送结果.
}

View File

@ -20,7 +20,7 @@ public class JsonUtils {
/**
* 初始化 objectMapper 属性
*
* <p>
* 通过这样的方式使用 Spring 创建的 ObjectMapper Bean
*
* @param objectMapper ObjectMapper 对象
@ -67,4 +67,12 @@ public class JsonUtils {
}
}
public static <T> T parseByType(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}