短信发送的逻辑,迁移到 yudao-core-service 模块下

This commit is contained in:
YunaiV
2021-10-12 08:18:44 +08:00
parent d6040214b8
commit 8e2388d52d
77 changed files with 1008 additions and 630 deletions

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 短信渠道 DO
*
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "sys_sms_channel", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsChannelDO extends BaseDO {
/**
* 渠道编号
*/
private Long id;
/**
* 短信签名
*/
private String signature;
/**
* 渠道编码
*
* 枚举 {@link SmsChannelEnum}
*/
private String code;
/**
* 启用状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 短信 API 的账号
*/
private String apiKey;
/**
* 短信 API 的秘钥
*/
private String apiSecret;
/**
* 短信发送回调 URL
*/
private String callbackUrl;
}

View File

@@ -0,0 +1,173 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsReceiveStatusEnum;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.util.Date;
import java.util.Map;
/**
* 短信日志 DO
*
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "sys_sms_log", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SysSmsLogDO extends BaseDO {
/**
* 自增编号
*/
private Long id;
// ========= 渠道相关字段 =========
/**
* 短信渠道编号
*
* 关联 {@link SysSmsChannelDO#getId()}
*/
private Long channelId;
/**
* 短信渠道编码
*
* 冗余 {@link SysSmsChannelDO#getCode()}
*/
private String channelCode;
// ========= 模板相关字段 =========
/**
* 模板编号
*
* 关联 {@link SysSmsTemplateDO#getId()}
*/
private Long templateId;
/**
* 模板编码
*
* 冗余 {@link SysSmsTemplateDO#getCode()}
*/
private String templateCode;
/**
* 短信类型
*
* 冗余 {@link SysSmsTemplateDO#getType()}
*/
private Integer templateType;
/**
* 基于 {@link SysSmsTemplateDO#getContent()} 格式化后的内容
*/
private String templateContent;
/**
* 基于 {@link SysSmsTemplateDO#getParams()} 输入后的参数
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> templateParams;
/**
* 短信 API 的模板编号
*
* 冗余 {@link SysSmsTemplateDO#getApiTemplateId()}
*/
private String apiTemplateId;
// ========= 手机相关字段 =========
/**
* 手机号
*/
private String mobile;
/**
* 用户编号
*/
private Long userId;
/**
* 用户类型
*
* 枚举 {@link UserTypeEnum}
*/
private Integer userType;
// ========= 发送相关字段 =========
/**
* 发送状态
*
* 枚举 {@link SysSmsSendStatusEnum}
*/
private Integer sendStatus;
/**
* 发送时间
*/
private Date sendTime;
/**
* 发送结果的编码
*
* 枚举 {@link SmsFrameworkErrorCodeConstants}
*/
private Integer sendCode;
/**
* 发送结果的提示
*
* 一般情况下,使用 {@link SmsFrameworkErrorCodeConstants}
* 异常情况下,通过格式化 Exception 的提示存储
*/
private String sendMsg;
/**
* 短信 API 发送结果的编码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiSendCode;
/**
* 短信 API 发送失败的提示
*/
private String apiSendMsg;
/**
* 短信 API 发送返回的唯一请求 ID
*
* 用于和短信 API 进行定位于排错
*/
private String apiRequestId;
/**
* 短信 API 发送返回的序号
*
* 用于和短信 API 平台的发送记录关联
*/
private String apiSerialNo;
// ========= 接收相关字段 =========
/**
* 接收状态
*
* 枚举 {@link SysSmsReceiveStatusEnum}
*/
private Integer receiveStatus;
/**
* 接收时间
*/
private Date receiveTime;
/**
* 短信 API 接收结果的编码
*/
private String apiReceiveCode;
/**
* 短信 API 接收结果的提示
*/
private String apiReceiveMsg;
}

View File

@@ -0,0 +1,89 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsTemplateTypeEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
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.ToString;
import java.util.List;
/**
* 短信模板 DO
*
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "sys_sms_template", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsTemplateDO extends BaseDO {
/**
* 自增编号
*/
private Long id;
// ========= 模板相关字段 =========
/**
* 短信类型
*
* 枚举 {@link SysSmsTemplateTypeEnum}
*/
private Integer type;
/**
* 启用状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 模板编码,保证唯一
*/
private String code;
/**
* 模板名称
*/
private String name;
/**
* 模板内容
*
* 内容的参数,使用 {} 包括,例如说 {name}
*/
private String content;
/**
* 参数数组(自动根据内容生成)
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> params;
/**
* 备注
*/
private String remark;
/**
* 短信 API 的模板编号
*/
private String apiTemplateId;
// ========= 渠道相关字段 =========
/**
* 短信渠道编号
*
* 关联 {@link SysSmsChannelDO#getId()}
*/
private Long channelId;
/**
* 短信渠道编码
*
* 冗余 {@link SysSmsChannelDO#getCode()}
*/
private String channelCode;
}

View File

@@ -0,0 +1,94 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user;
import cn.iocoder.yudao.coreservice.modules.system.enums.common.SysSexEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Date;
import java.util.Set;
/**
* 管理后台的用户 DO
*
* @author 芋道源码
*/
@TableName(value = "sys_user", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysUserDO extends BaseDO {
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 用户账号
*/
private String username;
/**
* 加密后的密码
*
* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
*/
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 备注
*/
private String remark;
/**
* 部门ID
*/
private Long deptId;
/**
* 岗位编号数组
*/
@TableField(typeHandler = JsonLongSetTypeHandler.class)
private Set<Long> postIds;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String mobile;
/**
* 用户性别
*
* 枚举类 {@link SysSexEnum}
*/
private Integer sex;
/**
* 用户头像
*/
private String avatar;
/**
* 帐号状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private Date loginDate;
}

View File

@@ -0,0 +1,13 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SysSmsLogCoreMapper extends BaseMapperX<SysSmsLogDO> {
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
@Mapper
public interface SysSmsTemplateCoreMapper extends BaseMapperX<SysSmsTemplateDO> {
@Select("SELECT id FROM sys_sms_template WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@@ -0,0 +1,11 @@
package cn.iocoder.yudao.coreservice.modules.system.dal.mysql.user;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysUserCoreMapper extends BaseMapperX<SysUserDO> {
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.coreservice.modules.system.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* System 错误码枚举类
*
* system 系统,使用 1-006-000-000 段
*/
public interface SysErrorCodeConstants {
// ========== 短信发送 1006000000 ==========
ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1006000000, "手机号不存在");
ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1006000001, "模板参数({})缺失");
ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1006000000, "短信模板不存在");
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.coreservice.modules.system.enums.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 性别的枚举值
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum SysSexEnum {
/** 男 */
MALE(1),
/** 女 */
FEMALE(2),
/* 未知 */
UNKNOWN(3);
/**
* 性别
*/
private final Integer sex;
}

View File

@@ -1 +0,0 @@
package cn.iocoder.yudao.coreservice.modules.system.enums;

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.coreservice.modules.system.enums.sms;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信的接收状态枚举
*
* @author 芋道源码
* @date 2021/2/1 13:39
*/
@Getter
@AllArgsConstructor
public enum SysSmsReceiveStatusEnum {
INIT(0), // 初始化
SUCCESS(10), // 接收成功
FAILURE(20), // 接收失败
;
private final int status;
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.coreservice.modules.system.enums.sms;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信的发送状态枚举
*
* @author zzf
* @date 2021/2/1 13:39
*/
@Getter
@AllArgsConstructor
public enum SysSmsSendStatusEnum {
INIT(0), // 初始化
SUCCESS(10), // 发送成功
FAILURE(20), // 发送失败
IGNORE(30), // 忽略,即不发送
;
private final int status;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.coreservice.modules.system.enums.sms;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信的模板类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum SysSmsTemplateTypeEnum {
VERIFICATION_CODE(1), // 验证码
NOTICE(2), // 通知
PROMOTION(3), // 营销
;
/**
* 类型
*/
private final int type;
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package cn.iocoder.yudao.coreservice.modules.system.mq.message;

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.coreservice.modules.system.mq.message.sms;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.mq.core.stream.StreamMessage;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 短信发送消息
*
* @author 芋道源码
*/
@Data
public class SysSmsSendMessage implements StreamMessage {
/**
* 短信日志编号
*/
@NotNull(message = "短信日志编号不能为空")
private Long logId;
/**
* 手机号
*/
@NotNull(message = "手机号不能为空")
private String mobile;
/**
* 短信渠道编号
*/
@NotNull(message = "短信渠道编号不能为空")
private Long channelId;
/**
* 短信 API 的模板编号
*/
@NotNull(message = "短信 API 的模板编号不能为空")
private String apiTemplateId;
/**
* 短信模板参数
*/
private List<KeyValue<String, Object>> templateParams;
@Override
public String getStreamKey() {
return "system.sms.send";
}
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package cn.iocoder.yudao.coreservice.modules.system.mq.producer;

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.coreservice.modules.system.mq.producer.sms;
import cn.iocoder.yudao.coreservice.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.mq.core.util.RedisMessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* Sms 短信相关消息的 Core Producer
*
* @author zzf
* @date 2021/3/9 16:35
*/
@Slf4j
@Component
public class SysSmsCoreProducer {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 发送 {@link SysSmsSendMessage} 消息
*
* @param logId 短信日志编号
* @param mobile 手机号
* @param channelId 渠道编号
* @param apiTemplateId 短信模板编号
* @param templateParams 短信模板参数
*/
public void sendSmsSendMessage(Long logId, String mobile,
Long channelId, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
SysSmsSendMessage message = new SysSmsSendMessage().setLogId(logId).setMobile(mobile);
message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams);
RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
}
}

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.iocoder.yudao.coreservice.modules.system.mq.message.sms.SysSmsSendMessage;
import java.util.List;
import java.util.Map;
/**
* 短信 Service Core 接口
*
* 接入方,通过调用 send 开头的方法,创建发送短信的任务到 MQ 中
* 后续yudao-admin-server 监听 MQ执行真正的短信发送逻辑
*
* @author 芋道源码
*/
public interface SysSmsCoreService {
/**
* 发送单条短信给后台用户
*
* 在 mobile 为空时,使用 userId 加载对应管理员的手机号
*
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToAdmin(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条短信给前台用户
*
* 在 mobile 为空时,使用 userId 加载对应会员的手机号
*
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToMember(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
Long sendSingleSms(String mobile, Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams);
void sendBatchSms(List<String> mobiles, List<Long> userIds, Integer userType,
String templateCode, Map<String, Object> templateParams);
void doSendSms(SysSmsSendMessage message);
/**
* 接收短信的接收结果
*
* @param channelCode 渠道编码
* @param text 结果内容
* @throws Throwable 处理失败时,抛出异常
*/
void receiveSmsStatus(String channelCode, String text) throws Throwable;
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import java.util.Date;
import java.util.Map;
/**
* 短信日志 Core Service 接口
*
* @author zzf
* @date 13:48 2021/3/2
*/
public interface SysSmsLogCoreService {
/**
* 创建短信日志
*
* @param mobile 手机号
* @param userId 用户编号
* @param userType 用户类型
* @param isSend 是否发送
* @param template 短信模板
* @param templateContent 短信内容
* @param templateParams 短信参数
* @return 发送日志编号
*/
Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend,
SysSmsTemplateDO template, String templateContent, Map<String, Object> templateParams);
/**
* 更新日志的发送结果
*
* @param id 日志编号
* @param sendCode 发送结果的编码
* @param sendMsg 发送结果的提示
* @param apiSendCode 短信 API 发送结果的编码
* @param apiSendMsg 短信 API 发送失败的提示
* @param apiRequestId 短信 API 发送返回的唯一请求 ID
* @param apiSerialNo 短信 API 发送返回的序号
*/
void updateSmsSendResult(Long id, Integer sendCode, String sendMsg,
String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo);
/**
* 更新日志的接收结果
*
* @param id 日志编号
* @param success 是否接收成功
* @param receiveTime 用户接收时间
* @param apiReceiveCode API 接收结果的编码
* @param apiReceiveMsg API 接收结果的说明
*/
void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg);
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import java.util.Map;
/**
* 短信模板 Core Service 接口
*
* @author 芋道源码
*/
public interface SysSmsTemplateCoreService {
/**
* 初始化短信模板的本地缓存
*/
void initLocalCache();
/**
* 获得短信模板,从缓存中
*
* @param code 模板编码
* @return 短信模板
*/
SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code);
/**
* 格式化短信内容
*
* @param content 短信模板的内容
* @param params 内容的参数
* @return 格式化后的内容
*/
String formatSmsTemplateContent(String content, Map<String, Object> params);
}

View File

@@ -0,0 +1,169 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.yudao.coreservice.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.yudao.coreservice.modules.system.mq.producer.sms.SysSmsCoreProducer;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsLogCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsTemplateCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.user.SysUserCoreService;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.coreservice.modules.system.enums.SysErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 短信 Service Core 实现
*
* @author 芋道源码
*/
@Service
public class SysSmsCoreServiceImpl implements SysSmsCoreService {
@Resource
private SysUserCoreService sysUserCoreService;
@Resource
private SysSmsTemplateCoreService smsTemplateCoreService;
@Resource
private SysSmsLogCoreService smsLogCoreService;
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SysSmsCoreProducer smsCoreProducer;
@Override
public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StrUtil.isEmpty(mobile)) {
SysUserDO user = sysUserCoreService.getUser(userId);
if (user != null) {
mobile = user.getMobile();
}
}
// 执行发送
return this.sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
}
@Override
public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
}
@Override
public Long sendSingleSms(String mobile, Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams) {
// 校验短信模板是否合法
SysSmsTemplateDO template = this.checkSmsTemplateValid(templateCode);
// 校验手机号码是否存在
mobile = this.checkMobile(mobile);
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = this.buildTemplateParams(template, templateParams);
// 创建发送日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); // 如果模板被禁用,则不发送短信,只记录日志
String content = smsTemplateCoreService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogCoreService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsCoreProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}
return sendLogId;
}
@Override
public void sendBatchSms(List<String> mobiles, List<Long> userIds, Integer userType,
String templateCode, Map<String, Object> templateParams) {
throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
}
@VisibleForTesting
public SysSmsTemplateDO checkSmsTemplateValid(String templateCode) {
// 获得短信模板。考虑到效率,从缓存中获取
SysSmsTemplateDO template = smsTemplateCoreService.getSmsTemplateByCodeFromCache(templateCode);
// 短信模板不存在
if (template == null) {
throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS);
}
return template;
}
/**
* 将参数模板,处理成有序的 KeyValue 数组
*
* 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说腾讯云 https://cloud.tencent.com/document/product/382/39023
*
* @param template 短信模板
* @param templateParams 原始参数
* @return 处理后的参数
*/
@VisibleForTesting
public List<KeyValue<String, Object>> buildTemplateParams(SysSmsTemplateDO template, Map<String, Object> templateParams) {
return template.getParams().stream().map(key -> {
Object value = templateParams.get(key);
if (value == null) {
throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key);
}
return new KeyValue<>(key, value);
}).collect(Collectors.toList());
}
@VisibleForTesting
public String checkMobile(String mobile) {
if (StrUtil.isEmpty(mobile)) {
throw exception(SMS_SEND_MOBILE_NOT_EXISTS);
}
return mobile;
}
@Override
public void doSendSms(SysSmsSendMessage message) {
// 获得渠道对应的 SmsClient 客户端
SmsClient smsClient = smsClientFactory.getSmsClient(message.getChannelId());
Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", message.getChannelId()));
// 发送短信
SmsCommonResult<SmsSendRespDTO> sendResult = smsClient.sendSms(message.getLogId(), message.getMobile(),
message.getApiTemplateId(), message.getTemplateParams());
smsLogCoreService.updateSmsSendResult(message.getLogId(), sendResult.getCode(), sendResult.getMsg(),
sendResult.getApiCode(), sendResult.getApiMsg(), sendResult.getApiRequestId(),
sendResult.getData() != null ? sendResult.getData().getSerialNo() : null);
}
@Override
public void receiveSmsStatus(String channelCode, String text) throws Throwable {
// 获得渠道对应的 SmsClient 客户端
SmsClient smsClient = smsClientFactory.getSmsClient(channelCode);
Assert.notNull(smsClient, String.format("短信客户端(%s) 不存在", channelCode));
// 解析内容
List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text);
if (CollUtil.isEmpty(receiveResults)) {
return;
}
// 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新
receiveResults.forEach(result -> smsLogCoreService.updateSmsReceiveResult(result.getLogId(),
result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorCode()));
}
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms.impl;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms.SysSmsLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsReceiveStatusEnum;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsLogCoreService;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
/**
* 短信日志 Core Service 实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Slf4j
@Service
public class SysSmsLogCoreServiceImpl implements SysSmsLogCoreService {
@Resource
private SysSmsLogCoreMapper smsLogCoreMapper;
@Override
public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend,
SysSmsTemplateDO template, String templateContent, Map<String, Object> templateParams) {
SysSmsLogDO.SysSmsLogDOBuilder logBuilder = SysSmsLogDO.builder();
// 根据是否要发送,设置状态
logBuilder.sendStatus(Objects.equals(isSend, true) ? SysSmsSendStatusEnum.INIT.getStatus()
: SysSmsSendStatusEnum.IGNORE.getStatus());
// 设置手机相关字段
logBuilder.mobile(mobile).userId(userId).userType(userType);
// 设置模板相关字段
logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType());
logBuilder.templateContent(templateContent).templateParams(templateParams).apiTemplateId(template.getApiTemplateId());
// 设置渠道相关字段
logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode());
// 设置接收相关字段
logBuilder.receiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
// 插入数据库
SysSmsLogDO logDO = logBuilder.build();
smsLogCoreMapper.insert(logDO);
return logDO.getId();
}
@Override
public void updateSmsSendResult(Long id, Integer sendCode, String sendMsg,
String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) {
SysSmsSendStatusEnum sendStatus = CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS
: SysSmsSendStatusEnum.FAILURE;
smsLogCoreMapper.updateById(SysSmsLogDO.builder().id(id).sendStatus(sendStatus.getStatus()).sendTime(new Date())
.sendCode(sendCode).sendMsg(sendMsg).apiSendCode(apiSendCode).apiSendMsg(apiSendMsg)
.apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build());
}
@Override
public void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg) {
SysSmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SysSmsReceiveStatusEnum.SUCCESS
: SysSmsReceiveStatusEnum.FAILURE;
smsLogCoreMapper.updateById(SysSmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()).receiveTime(receiveTime)
.apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}
}

View File

@@ -0,0 +1,105 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms.SysSmsTemplateCoreMapper;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsTemplateCoreService;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 短信模板 Core Service 接口
*
* @author 芋道源码
*/
@Service
@Slf4j
public class SysSmsTemplateCoreServiceImpl implements SysSmsTemplateCoreService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 短信模板缓存
* key短信模板编码 {@link SysSmsTemplateDO#getCode()}
*
* 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
*/
private volatile Map<String, SysSmsTemplateDO> smsTemplateCache;
/**
* 缓存短信模板的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private SysSmsTemplateCoreMapper smsTemplateCoreMapper;
@Override
@PostConstruct
public void initLocalCache() {
// 获取短信模板列表,如果有更新
List<SysSmsTemplateDO> smsTemplateList = this.loadSmsTemplateIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(smsTemplateList)) {
return;
}
// 写入缓存
ImmutableMap.Builder<String, SysSmsTemplateDO> builder = ImmutableMap.builder();
smsTemplateList.forEach(sysSmsTemplateDO -> builder.put(sysSmsTemplateDO.getCode(), sysSmsTemplateDO));
smsTemplateCache = builder.build();
assert smsTemplateList.size() > 0; // 断言,避免告警
maxUpdateTime = smsTemplateList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size());
}
/**
* 如果短信模板发生变化,从数据库中获取最新的全量短信模板。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前短信模板的最大更新时间
* @return 短信模板列表
*/
private List<SysSmsTemplateDO> loadSmsTemplateIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadSmsTemplateIfUpdate][首次加载全量短信模板]");
} else { // 判断数据库中是否有更新的短信模板
if (smsTemplateCoreMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadSmsTemplateIfUpdate][增量加载全量短信模板]");
}
// 第二步,如果有更新,则从数据库加载所有短信模板
return smsTemplateCoreMapper.selectList();
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initLocalCache();
}
@Override
public SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code) {
return smsTemplateCache.get(code);
}
@Override
public String formatSmsTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
}
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.coreservice.modules.system.service.user;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
/**
* 后台用户 Service Core 接口
*
* @author 芋道源码
*/
public interface SysUserCoreService {
/**
* 通过用户 ID 查询用户
*
* @param id 用户ID
* @return 用户对象信息
*/
SysUserDO getUser(Long id);
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.coreservice.modules.system.service.user.impl;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.user.SysUserCoreMapper;
import cn.iocoder.yudao.coreservice.modules.system.service.user.SysUserCoreService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 后台用户 Service Core 实现
*
* @author 芋道源码
*/
@Service
public class SysUserCoreServiceImpl implements SysUserCoreService {
@Resource
private SysUserCoreMapper userCoreMapper;
@Override
public SysUserDO getUser(Long id) {
return userCoreMapper.selectById(id);
}
}

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.coreservice.modules.system.service.logger;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.logger.SysLoginLogDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.logger.SysLoginLogCoreMapper;
@@ -8,6 +7,7 @@ import cn.iocoder.yudao.coreservice.modules.system.enums.logger.SysLoginLogTypeE
import cn.iocoder.yudao.coreservice.modules.system.enums.logger.SysLoginResultEnum;
import cn.iocoder.yudao.coreservice.modules.system.service.logger.dto.SysLoginLogCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.system.service.logger.impl.SysLoginLogCoreServiceImpl;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
import org.junit.jupiter.api.Test;
@@ -15,6 +15,7 @@ import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
@Import(SysLoginLogCoreServiceImpl.class)
@@ -30,8 +31,9 @@ public class SysLoginLogServiceImplTest extends BaseDbUnitTest {
public void testCreateLoginLog() {
SysLoginLogCreateReqDTO reqDTO = RandomUtils.randomPojo(SysLoginLogCreateReqDTO.class, vo -> {
// 指定随机的范围,避免超出范围入库失败
vo.setLogType(RandomUtil.randomEle(SysLoginLogTypeEnum.values()).getType());
vo.setResult(RandomUtil.randomEle(SysLoginResultEnum.values()).getResult());
vo.setUserType(randomEle(UserTypeEnum.values()).getValue());
vo.setLogType(randomEle(SysLoginLogTypeEnum.values()).getType());
vo.setResult(randomEle(SysLoginResultEnum.values()).getResult());
vo.setTraceId(TracerUtils.getTraceId());
});

View File

@@ -0,0 +1,199 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.yudao.coreservice.modules.system.mq.producer.sms.SysSmsCoreProducer;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.impl.SysSmsCoreServiceImpl;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.iocoder.yudao.coreservice.modules.system.enums.SysErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link SysSmsCoreService} 的单元测试类
*
* @author 芋道源码
*/
public class SysSmsCoreServiceTest extends BaseMockitoUnitTest {
@InjectMocks
private SysSmsCoreServiceImpl smsCoreService;
@Mock
private SysSmsTemplateCoreService smsTemplateCoreService;
@Mock
private SysSmsLogCoreService smsLogCoreService;
@Mock
private SysSmsCoreProducer smsCoreProducer;
@Mock
private SmsClientFactory smsClientFactory;
/**
* 发送成功,当短信模板开启时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateEnable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
Integer userType = randomEle(UserTypeEnum.values()).getValue();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock SmsTemplateService 的方法
SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateCoreService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateCoreService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogCoreService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsCoreService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsCoreProducer, times(1)).sendSmsSendMessage(eq(smsLogId), eq(mobile),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
/**
* 发送成功,当短信模板关闭时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateDisable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
Integer userType = randomEle(UserTypeEnum.values()).getValue();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock SmsTemplateService 的方法
SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.DISABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateCoreService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateCoreService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogCoreService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsCoreService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsCoreProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(),
anyLong(), any(), anyList());
}
@Test
public void testCheckSmsTemplateValid_notExists() {
// 准备参数
String templateCode = randomString();
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsCoreService.checkSmsTemplateValid(templateCode),
SMS_SEND_TEMPLATE_NOT_EXISTS);
}
@Test
public void testBuildTemplateParams_paramMiss() {
// 准备参数
SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class,
o -> o.setParams(Lists.newArrayList("code")));
Map<String, Object> templateParams = new HashMap<>();
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsCoreService.buildTemplateParams(template, templateParams),
SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code");
}
@Test
public void testCheckMobile_notExists() {
// 准备参数
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsCoreService.checkMobile(null),
SMS_SEND_MOBILE_NOT_EXISTS);
}
@Test
@SuppressWarnings("unchecked")
public void testDoSendSms() {
// 准备参数
SysSmsSendMessage message = randomPojo(SysSmsSendMessage.class);
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient);
// mock SmsClient 的方法
SmsCommonResult<SmsSendRespDTO> sendResult = randomPojo(SmsCommonResult.class, SmsSendRespDTO.class);
when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()),
eq(message.getTemplateParams()))).thenReturn(sendResult);
// 调用
smsCoreService.doSendSms(message);
// 断言
verify(smsLogCoreService, times(1)).updateSmsSendResult(eq(message.getLogId()),
eq(sendResult.getCode()), eq(sendResult.getMsg()), eq(sendResult.getApiCode()),
eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getData().getSerialNo()));
}
@Test
public void testReceiveSmsStatus() throws Throwable {
// 准备参数
String channelCode = randomString();
String text = randomString();
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(channelCode))).thenReturn(smsClient);
// mock SmsClient 的方法
List<SmsReceiveRespDTO> receiveResults = randomPojoList(SmsReceiveRespDTO.class);
// 调用
smsCoreService.receiveSmsStatus(channelCode, text);
// 断言
receiveResults.forEach(result -> smsLogCoreService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode())));
}
}

View File

@@ -0,0 +1,149 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms.SysSmsLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsReceiveStatusEnum;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsTemplateTypeEnum;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.impl.SysSmsLogCoreServiceImpl;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.function.Consumer;
import static cn.hutool.core.util.RandomUtil.randomBoolean;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* {@link SysSmsLogCoreServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(SysSmsLogCoreServiceImpl.class)
public class SysSmsLogCoreServiceTest extends BaseDbUnitTest {
@Resource
private SysSmsLogCoreServiceImpl smsLogCoreService;
@Resource
private SysSmsLogCoreMapper smsLogCoreMapper;
@Test
public void testCreateSmsLog() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
Integer userType = randomEle(UserTypeEnum.values()).getValue();
Boolean isSend = randomBoolean();
SysSmsTemplateDO templateDO = randomPojo(SysSmsTemplateDO.class,
o -> o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()));
String templateContent = randomString();
Map<String, Object> templateParams = randomTemplateParams();
// mock 方法
// 调用
Long logId = smsLogCoreService.createSmsLog(mobile, userId, userType, isSend,
templateDO, templateContent, templateParams);
// 断言
SysSmsLogDO logDO = smsLogCoreMapper.selectById(logId);
assertEquals(isSend ? SysSmsSendStatusEnum.INIT.getStatus() : SysSmsSendStatusEnum.IGNORE.getStatus(),
logDO.getSendStatus());
assertEquals(mobile, logDO.getMobile());
assertEquals(userType, logDO.getUserType());
assertEquals(userId, logDO.getUserId());
assertEquals(templateDO.getId(), logDO.getTemplateId());
assertEquals(templateDO.getCode(), logDO.getTemplateCode());
assertEquals(templateDO.getType(), logDO.getTemplateType());
assertEquals(templateDO.getChannelId(), logDO.getChannelId());
assertEquals(templateDO.getChannelCode(), logDO.getChannelCode());
assertEquals(templateContent, logDO.getTemplateContent());
assertEquals(templateParams, logDO.getTemplateParams());
assertEquals(SysSmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus());
}
@Test
public void testUpdateSmsSendResult() {
// mock 数据
SysSmsLogDO dbSmsLog = randomSmsLogDO(
o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus()));
smsLogCoreMapper.insert(dbSmsLog);
// 准备参数
Long id = dbSmsLog.getId();
Integer sendCode = randomInteger();
String sendMsg = randomString();
String apiSendCode = randomString();
String apiSendMsg = randomString();
String apiRequestId = randomString();
String apiSerialNo = randomString();
// 调用
smsLogCoreService.updateSmsSendResult(id, sendCode, sendMsg,
apiSendCode, apiSendMsg, apiRequestId, apiSerialNo);
// 断言
dbSmsLog = smsLogCoreMapper.selectById(id);
assertEquals(CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS.getStatus()
: SysSmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus());
assertNotNull(dbSmsLog.getSendTime());
assertEquals(sendMsg, dbSmsLog.getSendMsg());
assertEquals(apiSendCode, dbSmsLog.getApiSendCode());
assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg());
assertEquals(apiRequestId, dbSmsLog.getApiRequestId());
assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo());
}
@Test
public void testUpdateSmsReceiveResult() {
// mock 数据
SysSmsLogDO dbSmsLog = randomSmsLogDO(
o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()));
smsLogCoreMapper.insert(dbSmsLog);
// 准备参数
Long id = dbSmsLog.getId();
Boolean success = randomBoolean();
Date receiveTime = randomDate();
String apiReceiveCode = randomString();
String apiReceiveMsg = randomString();
// 调用
smsLogCoreService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg);
// 断言
dbSmsLog = smsLogCoreMapper.selectById(id);
assertEquals(success ? SysSmsReceiveStatusEnum.SUCCESS.getStatus()
: SysSmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus());
assertEquals(receiveTime, dbSmsLog.getReceiveTime());
assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode());
assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg());
}
// ========== 随机对象 ==========
@SafeVarargs
private static SysSmsLogDO randomSmsLogDO(Consumer<SysSmsLogDO>... consumers) {
Consumer<SysSmsLogDO> consumer = (o) -> {
o.setTemplateParams(randomTemplateParams());
o.setTemplateType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围
o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围
o.setSendStatus(randomEle(SysSmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围
o.setReceiveStatus(randomEle(SysSmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围
};
return randomPojo(SysSmsLogDO.class, ArrayUtils.append(consumer, consumers));
}
private static Map<String, Object> randomTemplateParams() {
return MapUtil.<String, Object>builder().put(randomString(), randomString())
.put(randomString(), randomString()).build();
}
}

View File

@@ -0,0 +1,71 @@
package cn.iocoder.yudao.coreservice.modules.system.service.sms;
import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.sms.SysSmsTemplateCoreMapper;
import cn.iocoder.yudao.coreservice.modules.system.enums.sms.SysSmsTemplateTypeEnum;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.impl.SysSmsTemplateCoreServiceImpl;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.function.Consumer;
import static cn.hutool.core.bean.BeanUtil.getFieldValue;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link SysSmsTemplateCoreServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(SysSmsTemplateCoreServiceImpl.class)
public class SysSmsTemplateServiceTest extends BaseDbUnitTest {
@Resource
private SysSmsTemplateCoreServiceImpl smsTemplateCoreService;
@Resource
private SysSmsTemplateCoreMapper smsTemplateCoreMapper;
@Test
@SuppressWarnings("unchecked")
void testInitLocalCache() {
// mock 数据
SysSmsTemplateDO smsTemplate01 = randomSmsTemplateDO();
smsTemplateCoreMapper.insert(smsTemplate01);
SysSmsTemplateDO smsTemplate02 = randomSmsTemplateDO();
smsTemplateCoreMapper.insert(smsTemplate02);
// 调用
smsTemplateCoreService.initLocalCache();
// 断言 deptCache 缓存
Map<String, SysSmsTemplateDO> smsTemplateCache = (Map<String, SysSmsTemplateDO>) getFieldValue(smsTemplateCoreService, "smsTemplateCache");
assertEquals(2, smsTemplateCache.size());
assertPojoEquals(smsTemplate01, smsTemplateCache.get(smsTemplate01.getCode()));
assertPojoEquals(smsTemplate02, smsTemplateCache.get(smsTemplate02.getCode()));
// 断言 maxUpdateTime 缓存
Date maxUpdateTime = (Date) getFieldValue(smsTemplateCoreService, "maxUpdateTime");
assertEquals(max(smsTemplate01.getUpdateTime(), smsTemplate02.getUpdateTime()), maxUpdateTime);
}
// ========== 随机对象 ==========
@SafeVarargs
private static SysSmsTemplateDO randomSmsTemplateDO(Consumer<SysSmsTemplateDO>... consumers) {
Consumer<SysSmsTemplateDO> consumer = (o) -> {
o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围
};
return randomPojo(SysSmsTemplateDO.class, ArrayUtils.append(consumer, consumers));
}
}

View File

@@ -5,3 +5,5 @@ DELETE FROM "inf_api_error_log";
-- sys 开头的 DB
DELETE FROM "sys_user_session";
DELETE FROM "sys_dict_data";
DELETE FROM "sys_sms_template";
DELETE FROM "sys_sms_log";

View File

@@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS "inf_api_access_log" (
"update_time" timestamp not null default current_timestamp,
"deleted" bit not null default false,
primary key ("id")
) COMMENT 'API 访问日志表';
) COMMENT 'API 访问日志表';
CREATE TABLE IF NOT EXISTS "inf_api_error_log" (
"id" integer not null GENERATED BY DEFAULT AS IDENTITY,
@@ -87,4 +87,75 @@ CREATE TABLE IF NOT EXISTS "inf_api_error_log" (
"update_time" timestamp not null default current_timestamp,
"deleted" bit not null default false,
primary key ("id")
) COMMENT '系统异常日志';
) COMMENT '系统异常日志';
CREATE TABLE IF NOT EXISTS "sys_sms_template" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"type" tinyint NOT NULL,
"status" tinyint NOT NULL,
"code" varchar(63) NOT NULL,
"name" varchar(63) NOT NULL,
"content" varchar(255) NOT NULL,
"params" varchar(255) NOT NULL,
"remark" varchar(255) DEFAULT NULL,
"api_template_id" varchar(63) NOT NULL,
"channel_id" bigint NOT NULL,
"channel_code" varchar(63) NOT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '短信模板';
CREATE TABLE IF NOT EXISTS "sys_sms_log" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"channel_id" bigint NOT NULL,
"channel_code" varchar(63) NOT NULL,
"template_id" bigint NOT NULL,
"template_code" varchar(63) NOT NULL,
"template_type" tinyint NOT NULL,
"template_content" varchar(255) NOT NULL,
"template_params" varchar(255) NOT NULL,
"api_template_id" varchar(63) NOT NULL,
"mobile" varchar(11) NOT NULL,
"user_id" bigint DEFAULT '0',
"user_type" tinyint DEFAULT '0',
"send_status" tinyint NOT NULL DEFAULT '0',
"send_time" timestamp DEFAULT NULL,
"send_code" int DEFAULT NULL,
"send_msg" varchar(255) DEFAULT NULL,
"api_send_code" varchar(63) DEFAULT NULL,
"api_send_msg" varchar(255) DEFAULT NULL,
"api_request_id" varchar(255) DEFAULT NULL,
"api_serial_no" varchar(255) DEFAULT NULL,
"receive_status" tinyint NOT NULL DEFAULT '0',
"receive_time" timestamp DEFAULT NULL,
"api_receive_code" varchar(63) DEFAULT NULL,
"api_receive_msg" varchar(255) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '短信日志';
CREATE TABLE IF NOT EXISTS `sys_login_log` (
`id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
`log_type` bigint(4) NOT NULL,
"user_id" bigint not null default '0',
"user_type" tinyint NOT NULL,
`trace_id` varchar(64) NOT NULL DEFAULT '',
`username` varchar(50) NOT NULL DEFAULT '',
`result` tinyint(4) NOT NULL,
`user_ip` varchar(50) NOT NULL,
`user_agent` varchar(512) NOT NULL,
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) COMMENT ='系统访问记录';