Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/multi-datasource

 Conflicts:
	pom.xml
	src/main/resources/application-local.yaml
This commit is contained in:
YunaiV
2021-04-18 14:00:51 +08:00
158 changed files with 8756 additions and 287 deletions

View File

@@ -0,0 +1,20 @@
package cn.iocoder.dashboard.common.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Key Value 的键值对
*
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyValue<K, V> {
private K key;
private V value;
}

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

@@ -1,12 +1,13 @@
package cn.iocoder.dashboard.common.exception;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
/**
* 错误码对象
*
* 全局错误码,占用 [0, 999]参见 {@link GlobalException}
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
*
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
@@ -21,11 +22,11 @@ public class ErrorCode {
/**
* 错误提示
*/
private final String message;
private final String msg;
public ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
this.msg = message;
}
}

View File

@@ -1,41 +0,0 @@
package cn.iocoder.dashboard.common.exception;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 全局异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GlobalException extends RuntimeException {
/**
* 全局错误码
*
* @see GlobalErrorCodeConstants
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 空构造方法,避免反序列化问题
*/
public GlobalException() {
}
public GlobalException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
public GlobalException(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@@ -30,7 +30,7 @@ public final class ServiceException extends RuntimeException {
public ServiceException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
this.message = errorCode.getMsg();
}
public ServiceException(Integer code, String message) {

View File

@@ -32,6 +32,7 @@ public interface GlobalErrorCodeConstants {
// ========== 自定义错误段 ==========
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");

View File

@@ -47,12 +47,12 @@ public class ServiceExceptionUtil {
// ========== 和 ServiceException 的集成 ==========
public static ServiceException exception(ErrorCode errorCode) {
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
return exception0(errorCode.getCode(), messagePattern);
}
public static ServiceException exception(ErrorCode errorCode, Object... params) {
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
return exception0(errorCode.getCode(), messagePattern, params);
}

View File

@@ -1,7 +1,6 @@
package cn.iocoder.dashboard.common.pojo;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -9,6 +8,7 @@ import lombok.Data;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Objects;
/**
* 通用返回
@@ -16,7 +16,7 @@ import java.io.Serializable;
* @param <T> 数据泛型
*/
@Data
public final class CommonResult<T> implements Serializable {
public class CommonResult<T> implements Serializable {
/**
* 错误码
@@ -31,7 +31,7 @@ public final class CommonResult<T> implements Serializable {
/**
* 错误提示,用户可阅读
*
* @see ErrorCode#getMessage() ()
* @see ErrorCode#getMsg() ()
*/
private String msg;
@@ -57,7 +57,7 @@ public final class CommonResult<T> implements Serializable {
}
public static <T> CommonResult<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMessage());
return error(errorCode.getCode(), errorCode.getMsg());
}
public static <T> CommonResult<T> success(T data) {
@@ -68,9 +68,13 @@ public final class CommonResult<T> implements Serializable {
return result;
}
public static boolean isSuccess(Integer code) {
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
}
@JsonIgnore // 避免 jackson 序列化
public boolean isSuccess() {
return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code);
return isSuccess(code);
}
@JsonIgnore // 避免 jackson 序列化
@@ -81,16 +85,12 @@ public final class CommonResult<T> implements Serializable {
// ========= 和 Exception 异常体系集成 =========
/**
* 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
*/
public void checkError() throws GlobalException, ServiceException {
public void checkError() throws ServiceException {
if (isSuccess()) {
return;
}
// 全局异常
if (GlobalErrorCodeConstants.isMatch(code)) {
throw new GlobalException(code, msg);
}
// 业务异常
throw new ServiceException(code, msg);
}
@@ -99,8 +99,4 @@ public final class CommonResult<T> implements Serializable {
return error(serviceException.getCode(), serviceException.getMessage());
}
public static <T> CommonResult<T> error(GlobalException globalException) {
return error(globalException.getCode(), globalException.getMessage());
}
}

View File

@@ -13,6 +13,11 @@ import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
* Excel {@link SysDictDataDO} 数据字典转换器
*
* @author 芋道源码
*/
@Slf4j
public class DictConvert implements Converter<Object> {

View File

@@ -0,0 +1,39 @@
package cn.iocoder.dashboard.framework.excel.core.convert;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* Excel Json 转换器
*
* @author 芋道源码
*/
public class JsonConvert implements Converter<Object> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 生成 Excel 小表格
return new CellData<>(JsonUtils.toJsonString(value));
}
}

View File

@@ -48,8 +48,8 @@ public class RedisConfig {
* 创建 Redis Pub/Sub 广播消费的容器
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
List<AbstractChannelMessageListener<?>> listeners) {
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory factory, List<AbstractChannelMessageListener<?>> listeners) {
// 创建 RedisMessageListenerContainer 对象
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置 RedisConnection 工厂。
@@ -69,8 +69,8 @@ public class RedisConfig {
* Redis Stream 的 xreadgroup 命令https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
*/
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,
List<AbstractStreamMessageListener<?>> listeners) {
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
RedisTemplate<String, Object> redisTemplate, List<AbstractStreamMessageListener<?>> listeners) {
// 第一步,创建 StreamMessageListenerContainer 容器
// 创建 options 配置
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =

View File

@@ -128,13 +128,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 设置每个请求的权限
.authorizeRequests()
// 登陆的接口,可匿名访问
.antMatchers(webProperties.getApiPrefix() + "/login").anonymous()
.antMatchers(api("/login")).anonymous()
// 通用的接口,可匿名访问
.antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous()
.antMatchers(api("/system/captcha/**")).anonymous()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
// 文件的获取接口,可匿名访问
.antMatchers(webProperties.getApiPrefix() + "/infra/file/get/**").anonymous()
.antMatchers(api("/infra/file/get/**")).anonymous()
// Swagger 接口文档
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
@@ -148,13 +148,19 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers("/actuator/**").anonymous()
// Druid 监控
.antMatchers("/druid/**").anonymous()
// 短信回调 API
.antMatchers(api("/system/sms/callback/**")).anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler);
httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler);
// 添加 JWT Filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
private String api(String url) {
return webProperties.getApiPrefix() + url;
}
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.framework.sms.config;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 短信配置类
*
* @author 芋道源码
*/
@Configuration
public class SmsConfiguration {
@Bean
public SmsClientFactory smsClientFactory() {
return new SmsClientFactoryImpl();
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import java.util.List;
/**
* 短信客户端接口
*
* @author zzf
* @date 2021/1/25 14:14
*/
public interface SmsClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 发送消息
*
* @param logId 日志编号
* @param mobile 手机号
* @param apiTemplateId 短信 API 的模板编号
* @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
* @return 短信发送结果
*/
SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams);
/**
* 解析接收短信的接收结果
*
* @param text 结果
* @return 结果内容
* @throws Throwable 当解析 text 发生异常时,则会抛出异常
*/
List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable;
/**
* 查询指定的短信模板
*
* @param apiTemplateId 短信 API 的模板编号
* @return 短信模板
*/
SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
/**
* 短信客户端工厂接口
*
* @author zzf
* @date 2021/1/28 14:01
*/
public interface SmsClientFactory {
/**
* 获得短信 Client
*
* @param channelId 渠道编号
* @return 短信 Client
*/
SmsClient getSmsClient(Long channelId);
/**
* 获得短信 Client
*
* @param channelCode 渠道编码
* @return 短信 Client
*/
SmsClient getSmsClient(String channelCode);
/**
* 创建短信 Client
*
* @param properties 配置对象
*/
void createOrUpdateSmsClient(SmsChannelProperties properties);
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import java.util.function.Function;
/**
* 将 API 的错误码,转换为通用的错误码
*
* @see SmsCommonResult
* @see SmsFrameworkErrorCodeConstants
*
* @author 芋道源码
*/
public interface SmsCodeMapping extends Function<String, ErrorCode> {
}

View File

@@ -0,0 +1,68 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 短信的 CommonResult 拓展类
*
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
*
* 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SmsCommonResult<T> extends CommonResult<T> {
/**
* API 返回错误码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiCode;
/**
* API 返回提示
*/
private String apiMsg;
/**
* API 请求编号
*/
private String apiRequestId;
private SmsCommonResult() {
}
public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
T data, SmsCodeMapping codeMapping) {
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
SmsCommonResult<T> result = new SmsCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
result.setData(data);
// 翻译错误码
if (codeMapping != null) {
ErrorCode errorCode = codeMapping.apply(apiCode);
if (errorCode == null) {
errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
}
return result;
}
public static <T> SmsCommonResult<T> error(Throwable ex) {
SmsCommonResult<T> result = new SmsCommonResult<>();
result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
return result;
}
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import lombok.Data;
import java.util.Date;
/**
* 消息接收 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsReceiveRespDTO {
/**
* 是否接收成功
*/
private Boolean success;
/**
* API 接收结果的编码
*/
private String errorCode;
/**
* API 接收结果的说明
*/
private String errorMsg;
/**
* 手机号
*/
private String mobile;
/**
* 用户接收时间
*/
private Date receiveTime;
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
/**
* 短信日志编号
*
* 对应 SysSmsLogDO 的编号
*/
private Long logId;
}

View File

@@ -0,0 +1,18 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import lombok.Data;
/**
* 短信发送 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsSendRespDTO {
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import lombok.Data;
/**
* 短信模板 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsTemplateRespDTO {
/**
* 模板编号
*/
private String id;
/**
* 短信内容
*/
private String content;
/**
* 审核状态
*
* 枚举 {@link SmsTemplateAuditStatusEnum}
*/
private Integer auditStatus;
/**
* 审核未通过的理由
*/
private String auditReason;
}

View File

@@ -0,0 +1,122 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 短信客户端抽象类
*
* @author zzf
* @date 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
/**
* 短信渠道配置
*/
protected volatile SmsChannelProperties properties;
/**
* 错误码枚举类
*/
protected final SmsCodeMapping codeMapping;
/**
* 短信客户端有参构造函数
*
* @param properties 短信配置
*/
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
this.properties = properties;
this.codeMapping = codeMapping;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", properties);
}
public final void refresh(SmsChannelProperties properties) {
// 判断是否更新
if (properties.equals(this.properties)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
this.properties = properties;
// 初始化
this.init();
}
/**
* 自定义初始化
*/
protected abstract void doInit();
@Override
public Long getId() {
return properties.getId();
}
@Override
public final SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 执行短信发送
SmsCommonResult<SmsSendRespDTO> result;
try {
result = doSendSms(logId, mobile, apiTemplateId, templateParams);
} catch (Throwable ex) {
// 打印异常日志
log.error("[sendSms][发送短信异常sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
logId, mobile, apiTemplateId, templateParams, ex);
// 封装返回
return SmsCommonResult.error(ex);
}
return result;
}
protected abstract SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams)
throws Throwable;
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
try {
return doParseSmsReceiveStatus(text);
} catch (Throwable ex) {
log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
throw ex;
}
}
protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
@Override
public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
// 执行短信发送
SmsCommonResult<SmsTemplateRespDTO> result;
try {
result = doGetSmsTemplate(apiTemplateId);
} catch (Throwable ex) {
// 打印异常日志
log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
// 封装返回
return SmsCommonResult.error(ex);
}
return result;
}
protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
}

View File

@@ -0,0 +1,90 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 短信客户端工厂接口
*
* @author zzf
*/
@Validated
@Slf4j
public class SmsClientFactoryImpl implements SmsClientFactory {
/**
* 短信客户端 Map
* key渠道编号使用 {@link SmsChannelProperties#getId()}
*/
private final ConcurrentMap<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
/**
* 短信客户端 Map
* key渠道编码使用 {@link SmsChannelProperties#getCode()} ()}
*
* 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。
* 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients}
*/
private final ConcurrentMap<String, AbstractSmsClient> channelCodeClients = new ConcurrentHashMap<>();
public SmsClientFactoryImpl() {
// 初始化 channelCodeClients 集合
Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
// 创建一个空的 SmsChannelProperties 对象
SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
.setApiKey("default").setApiSecret("default");
// 创建 Sms 客户端
AbstractSmsClient smsClient = createSmsClient(properties);
channelCodeClients.put(channel.getCode(), smsClient);
});
}
@Override
public SmsClient getSmsClient(Long channelId) {
return channelIdClients.get(channelId);
}
@Override
public SmsClient getSmsClient(String channelCode) {
return channelCodeClients.get(channelCode);
}
@Override
public void createOrUpdateSmsClient(SmsChannelProperties properties) {
AbstractSmsClient client = channelIdClients.get(properties.getId());
if (client == null) {
client = this.createSmsClient(properties);
client.init();
channelIdClients.put(client.getId(), client);
} else {
client.refresh(properties);
}
}
private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode());
Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum));
// 创建客户端
switch (channelEnum) {
case ALIYUN: return new AliyunSmsClient(properties);
case YUN_PIAN: return new YunpianSmsClient(properties);
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
}
}

View File

@@ -0,0 +1,212 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.collection.MapUtils;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.aliyuncs.AcsRequest;
import com.aliyuncs.AcsResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 阿里短信客户端的实现类
*
* @author zzf
* @date 2021/1/25 14:17
*/
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {
/**
* REGION, 使用杭州
*/
private static final String ENDPOINT = "cn-hangzhou";
/**
* 阿里云客户端
*/
private volatile IAcsClient client;
public AliyunSmsClient(SmsChannelProperties properties) {
super(properties, new AliyunSmsCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
protected void doInit() {
IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
client = new DefaultAcsClient(profile);
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 构建参数
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(mobile);
request.setSignName(properties.getSignature());
request.setTemplateCode(apiTemplateId);
request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
request.setOutId(String.valueOf(sendLogId));
// 执行请求
return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
return statuses.stream().map(status -> {
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
resp.setSuccess(status.getSuccess());
resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
return resp;
}).collect(Collectors.toList());
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
// 构建参数
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
request.setTemplateCode(apiTemplateId);
// 执行请求
return invoke(request, response -> {
SmsTemplateRespDTO data = new SmsTemplateRespDTO();
data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
return data;
});
}
@VisibleForTesting
Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
switch (templateStatus) {
case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
}
}
@VisibleForTesting
<T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> responseConsumer) {
try {
// 执行发送. 由于阿里云 sms 短信没有统一的 Response但是有统一的 code、message、requestId 属性,所以只好反射
T sendResult = client.getAcsResponse(request);
String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
// 解析结果
R data = null;
if (Objects.equals(code, "OK")) { // 请求成功的情况下
data = responseConsumer.apply(sendResult);
}
// 拼接结果
return SmsCommonResult.build(code, message, requestId, data, codeMapping);
} catch (ClientException ex) {
return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
}
}
private static String formatResultMsg(ClientException ex) {
if (StrUtil.isEmpty(ex.getErrorDescription())) {
return ex.getErrMsg();
}
return ex.getErrMsg() + " => " + ex.getErrorDescription();
}
/**
* 短信接收状态
*
* 参见 https://help.aliyun.com/document_detail/101867.html 文档
*
* @author 芋道源码
*/
@Data
public static class SmsReceiveStatus {
/**
* 手机号
*/
@JsonProperty("phone_number")
private String phoneNumber;
/**
* 发送时间
*/
@JsonProperty("send_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date sendTime;
/**
* 状态报告时间
*/
@JsonProperty("report_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date reportTime;
/**
* 是否接收成功
*/
private Boolean success;
/**
* 状态报告说明
*/
@JsonProperty("err_msg")
private String errMsg;
/**
* 状态报告编码
*/
@JsonProperty("err_code")
private String errCode;
/**
* 发送序列号
*/
@JsonProperty("biz_id")
private String bizId;
/**
* 用户序列号
*
* 这里我们传递的是 SysSmsLogDO 的日志编号
*/
@JsonProperty("out_id")
private String outId;
/**
* 短信长度,例如说 1、2、3
*
* 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
*/
@JsonProperty("sms_size")
private Integer smsSize;
}
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
/**
* 阿里云的 SmsCodeMapping 实现类
*
* 参见 https://help.aliyun.com/document_detail/101346.htm 文档
*
* @author 芋道源码
*/
public class AliyunSmsCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
switch (apiCode) {
case "OK": return GlobalErrorCodeConstants.SUCCESS;
case "isv.ACCOUNT_NOT_EXISTS":
case "isv.ACCOUNT_ABNORMAL":
case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID;
case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY;
case "isv.INVALID_JSON_PARAM":
case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR;
case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL;
case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID;
case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID;
case "isv.SMS_SIGNATURE_ILLEGAL":
case "isv.SIGN_NAME_ILLEGAL":
case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID;
case "isv.AMOUNT_NOT_ENOUGH":
case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID;
case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR;
}
return SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import java.util.Objects;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
/**
* 钉钉的 SmsCodeMapping 实现类
*
* @author 芋道源码
*/
public class DebugDingTalkCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,96 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.http.HttpUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.collection.MapUtils;
import cn.iocoder.dashboard.util.json.JsonUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 基于钉钉 WebHook 实现的调试的短信客户端实现类
*
* 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。
*
* @author 芋道源码
*/
public class DebugDingTalkSmsClient extends AbstractSmsClient {
public DebugDingTalkSmsClient(SmsChannelProperties properties) {
super(properties, new DebugDingTalkCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
protected void doInit() {
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
// 构建请求
String url = buildUrl("robot/send");
Map<String, Object> params = new HashMap<>();
params.put("msgtype", "text");
String content = String.format("【模拟短信】\n手机号%s\n短信日志编号%d\n模板参数%s",
mobile, sendLogId, MapUtils.convertMap(templateParams));
params.put("text", MapUtil.builder().put("content", content).build());
// 执行请求
String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
// 解析结果
Map<?, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
}
/**
* 构建请求地址
*
* 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
*
* @param path 请求路径
* @return 请求地址
*/
@SuppressWarnings("SameParameterValue")
private String buildUrl(String path) {
// 生成 timestamp
long timestamp = System.currentTimeMillis();
// 生成 sign
String secret = properties.getApiSecret();
String stringToSign = timestamp + "\n" + secret;
byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign);
String sign = Base64.encode(signData);
// 构建最终 URL
return String.format("https://oapi.dingtalk.com/%s?access_token=%s&timestamp=%d&sign=%s",
path, properties.getApiKey(), timestamp, sign);
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
return SmsCommonResult.build("0", "success", null, data, codeMapping);
}
}

View File

@@ -0,0 +1,204 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.yunpian.sdk.YunpianClient;
import com.yunpian.sdk.constant.YunpianConstant;
import com.yunpian.sdk.model.Result;
import com.yunpian.sdk.model.Template;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 云片短信客户端的实现类
*
* @author zzf
* @date 9:48 2021/3/5
*/
@Slf4j
public class YunpianSmsClient extends AbstractSmsClient {
/**
* 云信短信客户端
*/
private volatile YunpianClient client;
public YunpianSmsClient(SmsChannelProperties properties) {
super(properties, new YunpianSmsCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
}
@Override
public void doInit() {
YunpianClient oldClient = client;
// 初始化新的客户端
YunpianClient newClient = new YunpianClient(properties.getApiKey());
newClient.init();
this.client = newClient;
// 销毁老的客户端
if (oldClient != null) {
oldClient.close();
}
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
return invoke(() -> {
Map<String, String> request = new HashMap<>();
request.put(YunpianConstant.MOBILE, mobile);
request.put(YunpianConstant.TPL_ID, apiTemplateId);
request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams));
request.put(YunpianConstant.UID, String.valueOf(sendLogId));
request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
return client.sms().tpl_single_send(request);
}, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid())));
}
private static String formatTplValue(List<KeyValue<String, Object>> templateParams) {
if (CollUtil.isEmpty(templateParams)) {
return "";
}
// 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化
StringJoiner joiner = new StringJoiner("&");
templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue())))));
return joiner.toString();
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
return statuses.stream().map(status -> {
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS"));
resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail());
resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime());
resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid());
return resp;
}).collect(Collectors.toList());
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
return invoke(() -> {
Map<String, String> request = new HashMap<>();
request.put(YunpianConstant.APIKEY, properties.getApiKey());
request.put(YunpianConstant.TPL_ID, apiTemplateId);
return client.tpl().get(request);
}, response -> {
Template template = response.get(0);
return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content())
.setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason());
});
}
@VisibleForTesting
Integer convertSmsTemplateAuditStatus(String checkStatus) {
switch (checkStatus) {
case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus));
}
}
@VisibleForTesting
<T, R> SmsCommonResult<R> invoke(Supplier<Result<T>> requestConsumer, Function<T, R> responseConsumer) throws Throwable {
// 执行请求
Result<T> result = requestConsumer.get();
if (result.getThrowable() != null) {
throw result.getThrowable();
}
// 解析结果
R data = null;
if (result.getData() != null) {
data = responseConsumer.apply(result.getData());
}
// 拼接结果
return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping);
}
private static String formatResultMsg(Result<?> sendResult) {
if (StrUtil.isEmpty(sendResult.getDetail())) {
return sendResult.getMsg();
}
return sendResult.getMsg() + " => " + sendResult.getDetail();
}
/**
* 短信接收状态
*
* 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档
*
* @author 芋道源码
*/
@Data
public static class SmsReceiveStatus {
/**
* 接收状态
*
* 目前仅有 SUCCESS / FAIL所以使用 Boolean 接收
*/
@JsonProperty("report_status")
private String reportStatus;
/**
* 接收手机号
*/
private String mobile;
/**
* 运营商返回的代码,如:"DB:0103"
*
* 由于不同运营商信息不同,此字段仅供参考;
*/
@JsonProperty("error_msg")
private String errorMsg;
/**
* 运营商反馈代码的中文解释
*
* 默认不推送此字段,如需推送,请联系客服
*/
@JsonProperty("error_detail")
private String errorDetail;
/**
* 短信编号
*/
private Long sid;
/**
* 用户自定义 id
*
* 这里我们传递的是 SysSmsLogDO 的日志编号
*/
private Long uid;
/**
* 用户接收时间
*/
@JsonProperty("user_receive_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date userReceiveTime;
}
}

View File

@@ -0,0 +1,45 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
import static com.yunpian.sdk.constant.Code.*;
/**
* 云片的 SmsCodeMapping 实现类
*
* 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档
*
* @author 芋道源码
*/
public class YunpianSmsCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
int code = Integer.parseInt(apiCode);
switch (code) {
case OK: return SUCCESS;
case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR;
case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR;
case TPL_NOT_FOUND:
case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID;
case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case BLACK_WORD: return SMS_SEND_CONTENT_INVALID;
case DUP_IN_SHORT_TIME:
case TOO_MANY_TIME_IN_5:
case DAY_LIMIT_PER_MOBILE:
case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL;
case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK;
case SIGN_NOT_MATCH:
case BAD_SIGN_FORMAT:
case SIGN_NOT_VALID: return SMS_SIGN_INVALID;
case BAD_API_KEY: return SMS_ACCOUNT_INVALID;
case API_NOT_ALLOWED: return SMS_PERMISSION_DENY;
case IP_NOT_ALLOWED: return SMS_IP_DENY;
}
return SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信渠道枚举
*
* @author zzf
* @date 2021/1/25 10:56
*/
@Getter
@AllArgsConstructor
public enum SmsChannelEnum {
DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
YUN_PIAN("YUN_PIAN", "云片"),
ALIYUN("ALIYUN", "阿里云"),
// TENCENT("TENCENT", "腾讯云"),
// HUA_WEI("HUA_WEI", "华为云"),
;
/**
* 编码
*/
private final String code;
/**
* 名字
*/
private final String name;
public static SmsChannelEnum getByCode(String code) {
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
}
}

View File

@@ -0,0 +1,47 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import cn.iocoder.dashboard.common.exception.ErrorCode;
/**
* 短信框架的错误码枚举
*
* 短信框架,使用 2-001-000-000 段
*
* @author 芋道源码
*/
public interface SmsFrameworkErrorCodeConstants {
ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
// ========== 权限 / 限流等相关 2001000100 ==========
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
// 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信
ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
// 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟5 条 / 小时,累计 10 条 / 天。
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
// 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
// ========== 模板相关 2001000200 ==========
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
// ========== 签名相关 2001000300 ==========
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
// ========== 账户相关 2001000400 ==========
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
// ========== 其它相关 2001000900 开头 ==========
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信模板的审核状态枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum SmsTemplateAuditStatusEnum {
CHECKING(1),
SUCCESS(2),
FAIL(3);
private final Integer status;
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.dashboard.framework.sms.core.property;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 短信渠道配置类
*
* @author zzf
* @date 2021/1/25 17:01
*/
@Data
@Validated
public class SmsChannelProperties {
/**
* 渠道编号
*/
@NotNull(message = "短信渠道 ID 不能为空")
private Long id;
/**
* 短信签名
*/
@NotEmpty(message = "短信签名不能为空")
private String signature;
/**
* 渠道编码
*
* 枚举 {@link SmsChannelEnum}
*/
@NotEmpty(message = "渠道编码不能为空")
private String code;
/**
* 短信 API 的账号
*/
@NotEmpty(message = "短信 API 的账号不能为空")
private String apiKey;
/**
* 短信 API 的秘钥
*/
@NotEmpty(message = "短信 API 的秘钥不能为空")
private String apiSecret;
/**
* 短信发送回调 URL
*/
private String callbackUrl;
}

View File

@@ -2,7 +2,9 @@ package cn.iocoder.dashboard.framework.web.config;
import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
import cn.iocoder.dashboard.framework.web.core.filter.CacheRequestBodyFilter;
import cn.iocoder.dashboard.framework.web.core.filter.DemoFilter;
import cn.iocoder.dashboard.framework.web.core.filter.XssFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@@ -68,6 +70,15 @@ public class WebConfiguration implements WebMvcConfigurer {
return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER);
}
/**
* 创建 DemoFilter Bean演示模式
*/
@Bean
@ConditionalOnProperty(value = "yudao.demo", havingValue = "true")
public FilterRegistrationBean<DemoFilter> demoFilter() {
return createFilterBean(new DemoFilter(), FilterOrderEnum.DEMO_FILTER);
}
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);

View File

@@ -9,6 +9,7 @@ public interface FilterOrderEnum {
int CORS_FILTER = Integer.MIN_VALUE;
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
// OrderedRequestContextFilter 默认为 -105用于国际化上下文等等
@@ -19,4 +20,6 @@ public interface FilterOrderEnum {
// Spring Security Filter 默认为 -100可见 SecurityProperties 配置属性类
int DEMO_FILTER = Integer.MAX_VALUE;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.dashboard.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
/**
* 演示 Filter禁止用户发起写操作避免影响测试数据
*
* @author 芋道源码
*/
public class DemoFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String method = request.getMethod();
return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率
|| SecurityFrameworkUtils.getLoginUser() == null; // 非登陆用户时,不进行过滤
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
// 直接返回 DEMO_DENY 的结果。即,请求不继续
ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
}
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.dashboard.framework.web.core.handler;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService;
@@ -96,9 +95,6 @@ public class GlobalExceptionHandler {
if (ex instanceof AccessDeniedException) {
return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
}
if (ex instanceof GlobalException) {
return globalExceptionHandler(request, (GlobalException) ex);
}
return defaultExceptionHandler(request, ex);
}
@@ -222,25 +218,6 @@ public class GlobalExceptionHandler {
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理全局异常 ServiceException
*
* 例如说Dubbo 请求超时,调用的 Dubbo 服务系统异常
*/
@ExceptionHandler(value = GlobalException.class)
public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {
// 系统异常时,才打印异常日志
if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {
// 插入异常日志
this.createExceptionLog(req, ex);
// 普通全局异常,打印 info 日志即可
} else {
log.info("[globalExceptionHandler]", ex);
}
// 返回 ERROR CommonResult
return CommonResult.error(ex);
}
/**
* 处理系统异常,兜底处理所有的一切
*/
@@ -250,7 +227,7 @@ public class GlobalExceptionHandler {
// 插入异常日志
this.createExceptionLog(req, ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
private void createExceptionLog(HttpServletRequest req, Throwable e) {

View File

@@ -61,7 +61,7 @@ public class SysDictDataController {
@GetMapping("/list-all-simple")
@ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地")
// 无需添加权限认证,因为前端全局都需要
public CommonResult<List<SysDictDataSimpleVO>> getSimpleDictDatas() {
public CommonResult<List<SysDictDataSimpleRespVO>> getSimpleDictDatas() {
List<SysDictDataDO> list = dictDataService.getDictDatas();
return success(SysDictDataConvert.INSTANCE.convertList(list));
}

View File

@@ -41,7 +41,7 @@ public class SysDictTypeController {
return success(dictTypeId);
}
@PostMapping("update")
@PutMapping("/update")
@ApiOperation("修改字典类型")
@PreAuthorize("@ss.hasPermission('system:dict:update')")
public CommonResult<Boolean> updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) {

View File

@@ -4,9 +4,9 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel("数据字典精简 VO")
@ApiModel("数据字典精简 Response VO")
@Data
public class SysDictDataSimpleVO {
public class SysDictDataSimpleRespVO {
@ApiModelProperty(value = "字典类型", required = true, example = "gender")
private String dictType;

View File

@@ -0,0 +1,49 @@
package cn.iocoder.dashboard.modules.system.controller.sms;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api(tags = "短信回调")
@RestController
@RequestMapping("/system/sms/callback")
public class SmsCallbackController {
@Resource
private SysSmsService smsService;
@PostMapping("/sms/yunpian")
@ApiOperation(value = "云片短信的回调", notes = "参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档")
@ApiImplicitParam(name = "sms_status", value = "发送状态", required = true, example = "[{具体内容}]", dataTypeClass = Long.class)
@OperateLog(enable = false)
public String receiveYunpianSmsStatus(@RequestParam("sms_status") String smsStatus) throws Throwable {
String text = URLUtil.decode(smsStatus); // decode 解码参数,因为它被 encode
smsService.receiveSmsStatus(SmsChannelEnum.YUN_PIAN.getCode(), text);
return "SUCCESS"; // 约定返回 SUCCESS 为成功
}
@PostMapping("/sms/aliyun")
@ApiOperation(value = "阿里云短信的回调", notes = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
@OperateLog(enable = false)
public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtil.getBody(request);
smsService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
return success(true);
}
}

View File

@@ -0,0 +1,80 @@
package cn.iocoder.dashboard.modules.system.controller.sms;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.*;
import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Comparator;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api(tags = "短信渠道")
@RestController
@RequestMapping("system/sms-channel")
public class SysSmsChannelController {
@Resource
private SysSmsChannelService smsChannelService;
@PostMapping("/create")
@ApiOperation("创建短信渠道")
@PreAuthorize("@ss.hasPermission('system:sms-channel:create')")
public CommonResult<Long> createSmsChannel(@Valid @RequestBody SysSmsChannelCreateReqVO createReqVO) {
return success(smsChannelService.createSmsChannel(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新短信渠道")
@PreAuthorize("@ss.hasPermission('system:sms-channel:update')")
public CommonResult<Boolean> updateSmsChannel(@Valid @RequestBody SysSmsChannelUpdateReqVO updateReqVO) {
smsChannelService.updateSmsChannel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除短信渠道")
@ApiImplicitParam(name = "id", value = "编号", required = true)
@PreAuthorize("@ss.hasPermission('system:sms-channel:delete')")
public CommonResult<Boolean> deleteSmsChannel(@RequestParam("id") Long id) {
smsChannelService.deleteSmsChannel(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得短信渠道")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
public CommonResult<SysSmsChannelRespVO> getSmsChannel(@RequestParam("id") Long id) {
SysSmsChannelDO smsChannel = smsChannelService.getSmsChannel(id);
return success(SysSmsChannelConvert.INSTANCE.convert(smsChannel));
}
@GetMapping("/page")
@ApiOperation("获得短信渠道分页")
@PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
public CommonResult<PageResult<SysSmsChannelRespVO>> getSmsChannelPage(@Valid SysSmsChannelPageReqVO pageVO) {
PageResult<SysSmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(pageVO);
return success(SysSmsChannelConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/list-all-simple")
@ApiOperation(value = "获得短信渠道精简列表", notes = "包含被禁用的短信渠道")
public CommonResult<List<SysSmsChannelSimpleRespVO>> getSimpleSmsChannels() {
List<SysSmsChannelDO> list = smsChannelService.getSmsChannelList();
// 排序后,返回给前端
list.sort(Comparator.comparing(SysSmsChannelDO::getId));
return success(SysSmsChannelConvert.INSTANCE.convertList03(list));
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.dashboard.modules.system.controller.sms;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO;
import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsLogConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "短信日志")
@RestController
@RequestMapping("/system/sms-log")
@Validated
public class SysSmsLogController {
@Resource
private SysSmsLogService smsLogService;
@GetMapping("/page")
@ApiOperation("获得短信日志分页")
@PreAuthorize("@ss.hasPermission('system:sms-log:query')")
public CommonResult<PageResult<SysSmsLogRespVO>> getSmsLogPage(@Valid SysSmsLogPageReqVO pageVO) {
PageResult<SysSmsLogDO> pageResult = smsLogService.getSmsLogPage(pageVO);
return success(SysSmsLogConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/export-excel")
@ApiOperation("导出短信日志 Excel")
@PreAuthorize("@ss.hasPermission('system:sms-log:export')")
@OperateLog(type = EXPORT)
public void exportSmsLogExcel(@Valid SysSmsLogExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<SysSmsLogDO> list = smsLogService.getSmsLogList(exportReqVO);
// 导出 Excel
List<SysSmsLogExcelVO> datas = SysSmsLogConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "短信日志.xls", "数据", SysSmsLogExcelVO.class, datas);
}
}

View File

@@ -0,0 +1,12 @@
### 请求 /menu/list 接口 => 成功
POST {{baseUrl}}/system/sms-template/send-sms
Authorization: Bearer {{token}}
Content-Type: application/json
{
"code": "test_01",
"params": {
"key01": "value01",
"key02": "value02"
}
}

View File

@@ -0,0 +1,98 @@
package cn.iocoder.dashboard.modules.system.controller.sms;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.*;
import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api("短信模板")
@RestController
@RequestMapping("/system/sms-template")
public class SysSmsTemplateController {
@Resource
private SysSmsTemplateService smsTemplateService;
@Resource
private SysSmsService smsService;
@PostMapping("/create")
@ApiOperation("创建短信模板")
@PreAuthorize("@ss.hasPermission('system:sms-template:create')")
public CommonResult<Long> createSmsTemplate(@Valid @RequestBody SysSmsTemplateCreateReqVO createReqVO) {
return success(smsTemplateService.createSmsTemplate(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新短信模板")
@PreAuthorize("@ss.hasPermission('system:sms-template:update')")
public CommonResult<Boolean> updateSmsTemplate(@Valid @RequestBody SysSmsTemplateUpdateReqVO updateReqVO) {
smsTemplateService.updateSmsTemplate(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除短信模板")
@ApiImplicitParam(name = "id", value = "编号", required = true)
@PreAuthorize("@ss.hasPermission('system:sms-template:delete')")
public CommonResult<Boolean> deleteSmsTemplate(@RequestParam("id") Long id) {
smsTemplateService.deleteSmsTemplate(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得短信模板")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:sms-template:query')")
public CommonResult<SysSmsTemplateRespVO> getSmsTemplate(@RequestParam("id") Long id) {
SysSmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id);
return success(SysSmsTemplateConvert.INSTANCE.convert(smsTemplate));
}
@GetMapping("/page")
@ApiOperation("获得短信模板分页")
@PreAuthorize("@ss.hasPermission('system:sms-template:query')")
public CommonResult<PageResult<SysSmsTemplateRespVO>> getSmsTemplatePage(@Valid SysSmsTemplatePageReqVO pageVO) {
PageResult<SysSmsTemplateDO> pageResult = smsTemplateService.getSmsTemplatePage(pageVO);
return success(SysSmsTemplateConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/export-excel")
@ApiOperation("导出短信模板 Excel")
@PreAuthorize("@ss.hasPermission('system:sms-template:export')")
@OperateLog(type = EXPORT)
public void exportSmsTemplateExcel(@Valid SysSmsTemplateExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<SysSmsTemplateDO> list = smsTemplateService.getSmsTemplateList(exportReqVO);
// 导出 Excel
List<SysSmsTemplateExcelVO> datas = SysSmsTemplateConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "短信模板.xls", "数据", SysSmsTemplateExcelVO.class, datas);
}
@PostMapping("/send-sms")
@ApiOperation("发送短信")
@PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')")
public CommonResult<Long> sendSms(@Valid @RequestBody SysSmsTemplateSendReqVO sendReqVO) {
return success(smsService.sendSingleSms(sendReqVO.getMobile(), null, null,
sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
}
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotNull;
/**
* 短信渠道 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class SysSmsChannelBaseVO {
@ApiModelProperty(value = "短信签名", required = true, example = "芋道源码")
@NotNull(message = "短信签名不能为空")
private String signature;
@ApiModelProperty(value = "启用状态", required = true, example = "1")
@NotNull(message = "启用状态不能为空")
private Integer status;
@ApiModelProperty(value = "备注", example = "好吃!")
private String remark;
@ApiModelProperty(value = "短信 API 的账号", required = true, example = "yudao")
@NotNull(message = "短信 API 的账号不能为空")
private String apiKey;
@ApiModelProperty(value = "短信 API 的秘钥", example = "yuanma")
private String apiSecret;
@ApiModelProperty(value = "短信发送回调 URL", example = "http://www.iocoder.cn")
@URL(message = "回调 URL 格式不正确")
private String callbackUrl;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel("短信渠道创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsChannelCreateReqVO extends SysSmsChannelBaseVO {
@ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
@NotNull(message = "渠道编码不能为空")
private String code;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("短信渠道分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsChannelPageReqVO extends PageParam {
@ApiModelProperty(value = "任务状态", example = "1")
private Integer status;
@ApiModelProperty(value = "短信签名", example = "芋道源码", notes = "模糊匹配")
private String signature;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("短信渠道 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsChannelRespVO extends SysSmsChannelBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
private String code;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@ApiModel("短信渠道精简 Response VO")
@Data
public class SysSmsChannelSimpleRespVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
@NotNull(message = "编号不能为空")
private Long id;
@ApiModelProperty(value = "短信签名", required = true, example = "芋道源码")
@NotNull(message = "短信签名不能为空")
private String signature;
@ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
private String code;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel("短信渠道更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsChannelUpdateReqVO extends SysSmsChannelBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,101 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert;
import cn.iocoder.dashboard.framework.excel.core.convert.JsonConvert;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
import java.util.Map;
import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*;
/**
* 短信日志 Excel VO
*
* @author 芋道源码
*/
@Data
public class SysSmsLogExcelVO {
@ExcelProperty("编号")
private Long id;
@ExcelProperty("短信渠道编号")
private Long channelId;
@ExcelProperty("短信渠道编码")
private String channelCode;
@ExcelProperty("模板编号")
private Long templateId;
@ExcelProperty("模板编码")
private String templateCode;
@ExcelProperty(value = "短信类型", converter = DictConvert.class)
@DictFormat(SYS_SMS_TEMPLATE_TYPE)
private Integer templateType;
@ExcelProperty("短信内容")
private String templateContent;
@ExcelProperty(value = "短信参数", converter = JsonConvert.class)
private Map<String, Object> templateParams;
@ExcelProperty("短信 API 的模板编号")
private String apiTemplateId;
@ExcelProperty("手机号")
private String mobile;
@ExcelProperty("用户编号")
private Long userId;
@ExcelProperty(value = "用户类型", converter = DictConvert.class)
@DictFormat(USER_TYPE)
private Integer userType;
@ExcelProperty(value = "发送状态", converter = DictConvert.class)
@DictFormat(SYS_SMS_SEND_STATUS)
private Integer sendStatus;
@ExcelProperty("发送时间")
private Date sendTime;
@ExcelProperty("发送结果的编码")
private Integer sendCode;
@ExcelProperty("发送结果的提示")
private String sendMsg;
@ExcelProperty("短信 API 发送结果的编码")
private String apiSendCode;
@ExcelProperty("短信 API 发送失败的提示")
private String apiSendMsg;
@ExcelProperty("短信 API 发送返回的唯一请求 ID")
private String apiRequestId;
@ExcelProperty("短信 API 发送返回的序号")
private String apiSerialNo;
@ExcelProperty(value = "接收状态", converter = DictConvert.class)
@DictFormat(SYS_SMS_RECEIVE_STATUS)
private Integer receiveStatus;
@ExcelProperty("接收时间")
private Date receiveTime;
@ExcelProperty("API 接收结果的编码")
private String apiReceiveCode;
@ExcelProperty("API 接收结果的说明")
private String apiReceiveMsg;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,47 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "短信日志 Excel 导出 Request VO", description = "参数和 SysSmsLogPageReqVO 是一致的")
@Data
public class SysSmsLogExportReqVO {
@ApiModelProperty(value = "短信渠道编号", example = "10")
private Long channelId;
@ApiModelProperty(value = "模板编号", example = "20")
private Long templateId;
@ApiModelProperty(value = "手机号", example = "15601691300")
private String mobile;
@ApiModelProperty(value = "发送状态", example = "1")
private Integer sendStatus;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始发送时间")
private Date beginSendTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束发送时间")
private Date endSendTime;
@ApiModelProperty(value = "接收状态", example = "0")
private Integer receiveStatus;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始接收时间")
private Date beginReceiveTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束接收时间")
private Date endReceiveTime;
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("短信日志分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsLogPageReqVO extends PageParam {
@ApiModelProperty(value = "短信渠道编号", example = "10")
private Long channelId;
@ApiModelProperty(value = "模板编号", example = "20")
private Long templateId;
@ApiModelProperty(value = "手机号", example = "15601691300")
private String mobile;
@ApiModelProperty(value = "发送状态", example = "1", notes = "参见 SysSmsSendStatusEnum 枚举类")
private Integer sendStatus;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始发送时间")
private Date beginSendTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束发送时间")
private Date endSendTime;
@ApiModelProperty(value = "接收状态", example = "0", notes = "参见 SysSmsReceiveStatusEnum 枚举类")
private Integer receiveStatus;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始接收时间")
private Date beginReceiveTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束接收时间")
private Date endReceiveTime;
}

View File

@@ -0,0 +1,89 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@ApiModel("短信日志 Response VO")
@Data
public class SysSmsLogRespVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "短信渠道编号", required = true, example = "10")
private Long channelId;
@ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN")
private String channelCode;
@ApiModelProperty(value = "模板编号", required = true, example = "20")
private Long templateId;
@ApiModelProperty(value = "模板编码", required = true, example = "test-01")
private String templateCode;
@ApiModelProperty(value = "短信类型", required = true, example = "1")
private Integer templateType;
@ApiModelProperty(value = "短信内容", required = true, example = "你好,你的验证码是 1024")
private String templateContent;
@ApiModelProperty(value = "短信参数", required = true, example = "name,code")
private Map<String, Object> templateParams;
@ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "SMS_207945135")
private String apiTemplateId;
@ApiModelProperty(value = "手机号", required = true, example = "15601691300")
private String mobile;
@ApiModelProperty(value = "用户编号", example = "10")
private Long userId;
@ApiModelProperty(value = "用户类型", example = "1")
private Integer userType;
@ApiModelProperty(value = "发送状态", required = true, example = "1")
private Integer sendStatus;
@ApiModelProperty(value = "发送时间")
private Date sendTime;
@ApiModelProperty(value = "发送结果的编码", example = "0")
private Integer sendCode;
@ApiModelProperty(value = "发送结果的提示", example = "成功")
private String sendMsg;
@ApiModelProperty(value = "短信 API 发送结果的编码", example = "SUCCESS")
private String apiSendCode;
@ApiModelProperty(value = "短信 API 发送失败的提示", example = "成功")
private String apiSendMsg;
@ApiModelProperty(value = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99")
private String apiRequestId;
@ApiModelProperty(value = "短信 API 发送返回的序号", example = "62923244790")
private String apiSerialNo;
@ApiModelProperty(value = "接收状态", required = true, example = "0")
private Integer receiveStatus;
@ApiModelProperty(value = "接收时间")
private Date receiveTime;
@ApiModelProperty(value = "API 接收结果的编码", example = "DELIVRD")
private String apiReceiveCode;
@ApiModelProperty(value = "API 接收结果的说明", example = "用户接收成功")
private String apiReceiveMsg;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 短信模板 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class SysSmsTemplateBaseVO {
@ApiModelProperty(value = "短信类型", required = true, example = "1", notes = "参见 SysSmsTemplateTypeEnum 枚举类")
@NotNull(message = "短信类型不能为空")
private Integer type;
@ApiModelProperty(value = "开启状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类")
@NotNull(message = "开启状态不能为空")
private Integer status;
@ApiModelProperty(value = "模板编码", required = true, example = "test_01")
@NotNull(message = "模板编码不能为空")
private String code;
@ApiModelProperty(value = "模板名称", required = true, example = "yudao")
@NotNull(message = "模板名称不能为空")
private String name;
@ApiModelProperty(value = "模板内容", required = true, example = "你好,{name}。你长的太{like}啦!")
@NotNull(message = "模板内容不能为空")
private String content;
@ApiModelProperty(value = "备注", example = "哈哈哈")
private String remark;
@ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "4383920")
@NotNull(message = "短信 API 的模板编号不能为空")
private String apiTemplateId;
@ApiModelProperty(value = "短信渠道编号", required = true, example = "10")
@NotNull(message = "短信渠道编号不能为空")
private Long channelId;
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ApiModel("短信模板创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsTemplateCreateReqVO extends SysSmsTemplateBaseVO {
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*;
/**
* 短信模板 Excel VO
*
* @author 芋道源码
*/
@Data
public class SysSmsTemplateExcelVO {
@ExcelProperty("编号")
private Long id;
@ExcelProperty(value = "短信签名", converter = DictConvert.class)
@DictFormat(SYS_SMS_TEMPLATE_TYPE)
private Integer type;
@ExcelProperty(value = "开启状态", converter = DictConvert.class)
@DictFormat(SYS_COMMON_STATUS)
private Integer status;
@ExcelProperty("模板编码")
private String code;
@ExcelProperty("模板名称")
private String name;
@ExcelProperty("模板内容")
private String content;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("短信 API 的模板编号")
private String apiTemplateId;
@ExcelProperty("短信渠道编号")
private Long channelId;
@ExcelProperty(value = "短信渠道编码", converter = DictConvert.class)
@DictFormat(SYS_SMS_CHANNEL_CODE)
private String channelCode;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "短信模板 Excel 导出 Request VO", description = "参数和 SysSmsTemplatePageReqVO 是一致的")
@Data
public class SysSmsTemplateExportReqVO {
@ApiModelProperty(value = "短信签名", example = "1")
private Integer type;
@ApiModelProperty(value = "开启状态", example = "1")
private Integer status;
@ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配")
private String code;
@ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配")
private String content;
@ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配")
private String apiTemplateId;
@ApiModelProperty(value = "短信渠道编号", example = "10")
private Long channelId;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,47 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("短信模板分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsTemplatePageReqVO extends PageParam {
@ApiModelProperty(value = "短信签名", example = "1")
private Integer type;
@ApiModelProperty(value = "开启状态", example = "1")
private Integer status;
@ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配")
private String code;
@ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配")
private String content;
@ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配")
private String apiTemplateId;
@ApiModelProperty(value = "短信渠道编号", example = "10")
private Long channelId;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
import java.util.List;
@ApiModel("短信模板 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsTemplateRespVO extends SysSmsTemplateBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN")
private String channelCode;
@ApiModelProperty(value = "参数数组", example = "name,code")
private List<String> params;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.Map;
@ApiModel("短信模板的发送 Request VO")
@Data
public class SysSmsTemplateSendReqVO {
@ApiModelProperty(value = "手机号", required = true, example = "15601691300")
@NotNull(message = "手机号不能为空")
private String mobile;
@ApiModelProperty(value = "模板编码", required = true, example = "test_01")
@NotNull(message = "模板编码不能为空")
private String templateCode;
@ApiModelProperty(value = "模板参数")
private Map<String, Object> templateParams;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel("短信模板更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SysSmsTemplateUpdateReqVO extends SysSmsTemplateBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1024")
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -149,9 +149,9 @@ public class SysUserController {
// 手动创建导出 demo
List<SysUserImportExcelVO> list = Arrays.asList(
SysUserImportExcelVO.builder().username("yudao").deptId(1L).email("yudao@iocoder.cn").mobile("15601691300")
.nickname("芋道").status(CommonStatusEnum.ENABLE.getStatus()).sex(SysSexEnum.MALE.getSEX()).build(),
.nickname("芋道").status(CommonStatusEnum.ENABLE.getStatus()).sex(SysSexEnum.MALE.getSex()).build(),
SysUserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300")
.nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SysSexEnum.FEMALE.getSEX()).build()
.nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SysSexEnum.FEMALE.getSex()).build()
);
// 输出

View File

@@ -3,9 +3,9 @@ package cn.iocoder.dashboard.modules.system.controller.user.vo.profile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@ApiModel("用户个人信息更新 Request VO")
@@ -22,7 +22,7 @@ public class SysUserProfileUpdateReqVO {
private String email;
@ApiModelProperty(value = "手机号码", example = "15601691300")
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@Length(min = 11, max = 11, message = "手机号长度必须 11 位")
private String mobile;
@ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SysSexEnum 枚举类")

View File

@@ -2,10 +2,10 @@ package cn.iocoder.dashboard.modules.system.controller.user.vo.user;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.Set;
@@ -40,7 +40,7 @@ public class SysUserBaseVO {
private String email;
@ApiModelProperty(value = "手机号码", example = "15601691300")
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@Length(min = 11, max = 11, message = "手机号长度必须 11 位")
private String mobile;
@ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SysSexEnum 枚举类")

View File

@@ -13,7 +13,7 @@ public interface SysDictDataConvert {
SysDictDataConvert INSTANCE = Mappers.getMapper(SysDictDataConvert.class);
List<SysDictDataSimpleVO> convertList(List<SysDictDataDO> list);
List<SysDictDataSimpleRespVO> convertList(List<SysDictDataDO> list);
SysDictDataRespVO convert(SysDictDataDO bean);

View File

@@ -0,0 +1,39 @@
package cn.iocoder.dashboard.modules.system.convert.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelRespVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelSimpleRespVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 短信渠道 Convert
*
* @author 芋道源码
*/
@Mapper
public interface SysSmsChannelConvert {
SysSmsChannelConvert INSTANCE = Mappers.getMapper(SysSmsChannelConvert.class);
SysSmsChannelDO convert(SysSmsChannelCreateReqVO bean);
SysSmsChannelDO convert(SysSmsChannelUpdateReqVO bean);
SysSmsChannelRespVO convert(SysSmsChannelDO bean);
List<SysSmsChannelRespVO> convertList(List<SysSmsChannelDO> list);
PageResult<SysSmsChannelRespVO> convertPage(PageResult<SysSmsChannelDO> page);
List<SmsChannelProperties> convertList02(List<SysSmsChannelDO> list);
List<SysSmsChannelSimpleRespVO> convertList03(List<SysSmsChannelDO> list);
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.dashboard.modules.system.convert.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 短信日志 Convert
*
* @author 芋道源码
*/
@Mapper
public interface SysSmsLogConvert {
SysSmsLogConvert INSTANCE = Mappers.getMapper(SysSmsLogConvert.class);
SysSmsLogRespVO convert(SysSmsLogDO bean);
List<SysSmsLogRespVO> convertList(List<SysSmsLogDO> list);
PageResult<SysSmsLogRespVO> convertPage(PageResult<SysSmsLogDO> page);
List<SysSmsLogExcelVO> convertList02(List<SysSmsLogDO> list);
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.dashboard.modules.system.convert.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExcelVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateRespVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface SysSmsTemplateConvert {
SysSmsTemplateConvert INSTANCE = Mappers.getMapper(SysSmsTemplateConvert.class);
SysSmsTemplateDO convert(SysSmsTemplateCreateReqVO bean);
SysSmsTemplateDO convert(SysSmsTemplateUpdateReqVO bean);
SysSmsTemplateRespVO convert(SysSmsTemplateDO bean);
List<SysSmsTemplateRespVO> convertList(List<SysSmsTemplateDO> list);
PageResult<SysSmsTemplateRespVO> convertPage(PageResult<SysSmsTemplateDO> page);
List<SysSmsTemplateExcelVO> convertList02(List<SysSmsTemplateDO> list);
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.dashboard.modules.system.dal.dataobject.sms;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.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.dashboard.modules.system.dal.dataobject.sms;
import cn.iocoder.dashboard.common.enums.UserTypeEnum;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
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.dashboard.modules.system.dal.dataobject.sms;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum;
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,29 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
import java.util.List;
@Mapper
public interface SysSmsChannelMapper extends BaseMapperX<SysSmsChannelDO> {
default PageResult<SysSmsChannelDO> selectPage(SysSmsChannelPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<SysSmsChannelDO>()
.likeIfPresent("signature", reqVO.getSignature())
.eqIfPresent("status", reqVO.getStatus())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
@Select("SELECT id FROM sys_sms_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SysSmsLogMapper extends BaseMapperX<SysSmsLogDO> {
default PageResult<SysSmsLogDO> selectPage(SysSmsLogPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<SysSmsLogDO>()
.eqIfPresent("channel_id", reqVO.getChannelId())
.eqIfPresent("template_id", reqVO.getTemplateId())
.likeIfPresent("mobile", reqVO.getMobile())
.eqIfPresent("send_status", reqVO.getSendStatus())
.betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime())
.eqIfPresent("receive_status", reqVO.getReceiveStatus())
.betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime())
.orderByDesc("id"));
}
default List<SysSmsLogDO> selectList(SysSmsLogExportReqVO reqVO) {
return selectList(new QueryWrapperX<SysSmsLogDO>()
.eqIfPresent("channel_id", reqVO.getChannelId())
.eqIfPresent("template_id", reqVO.getTemplateId())
.likeIfPresent("mobile", reqVO.getMobile())
.eqIfPresent("send_status", reqVO.getSendStatus())
.betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime())
.eqIfPresent("receive_status", reqVO.getReceiveStatus())
.betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime())
.orderByDesc("id"));
}
}

View File

@@ -0,0 +1,53 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
import java.util.List;
@Mapper
public interface SysSmsTemplateMapper extends BaseMapperX<SysSmsTemplateDO> {
default SysSmsTemplateDO selectByCode(String code) {
return selectOne("code", code);
}
default PageResult<SysSmsTemplateDO> selectPage(SysSmsTemplatePageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<SysSmsTemplateDO>()
.eqIfPresent("type", reqVO.getType())
.eqIfPresent("status", reqVO.getStatus())
.likeIfPresent("code", reqVO.getCode())
.likeIfPresent("content", reqVO.getContent())
.likeIfPresent("api_template_id", reqVO.getApiTemplateId())
.eqIfPresent("channel_id", reqVO.getChannelId())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<SysSmsTemplateDO> selectList(SysSmsTemplateExportReqVO reqVO) {
return selectList(new QueryWrapperX<SysSmsTemplateDO>()
.eqIfPresent("type", reqVO.getType())
.eqIfPresent("status", reqVO.getStatus())
.likeIfPresent("code", reqVO.getCode())
.likeIfPresent("content", reqVO.getContent())
.likeIfPresent("api_template_id", reqVO.getApiTemplateId())
.eqIfPresent("channel_id", reqVO.getChannelId())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default Integer selectCountByChannelId(Long channelId) {
return selectCount("channel_id", channelId);
}
@Select("SELECT id FROM sys_sms_template WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@@ -78,4 +78,18 @@ public interface SysErrorCodeConstants {
ErrorCode FILE_UPLOAD_FAILED = new ErrorCode(1002009002, "文件上传失败");
ErrorCode FILE_IS_EMPTY= new ErrorCode(1002009003, "文件为空");
// ========== 短信渠道 1002011000 ==========
ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1002011000, "短信渠道不存在");
ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1002011001, "短信渠道不处于开启状态,不允许选择");
ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1002011002, "无法删除,该短信渠道还有短信模板");
// ========== 短信模板 1002011000 ==========
ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1002011000, "短信模板不存在");
ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002011001, "已经存在编码为【{}】的短信模板");
// ========== 短信发送 1002012000 ==========
ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1002012000, "手机号不存在");
ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1002012001, "模板参数({})缺失");
}

View File

@@ -3,6 +3,11 @@ package cn.iocoder.dashboard.modules.system.enums.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 性别的枚举值
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum SysSexEnum {
@@ -10,6 +15,9 @@ public enum SysSexEnum {
MALE(1), // 男
FEMALE(2); // 女
private final Integer SEX;
/**
* 性别
*/
private final Integer sex;
}

View File

@@ -18,6 +18,10 @@ public enum SysDictTypeEnum {
SYS_LOGIN_RESULT("sys_login_result"), // 登陆结果
SYS_CONFIG_TYPE("sys_config_type"), // 参数配置类型
SYS_BOOLEAN_STRING("sys_boolean_string"), // Boolean 是否类型
SYS_SMS_CHANNEL_CODE("sys_sms_channel_code"), // 短信渠道编码
SYS_SMS_TEMPLATE_TYPE("sys_sms_template_type"), // 短信模板类型
SYS_SMS_SEND_STATUS("sys_sms_send_status"), // 短信发送状态
SYS_SMS_RECEIVE_STATUS("sys_sms_receive_status"), // 短信接收状态
INF_REDIS_TIMEOUT_TYPE("inf_redis_timeout_type"), // Redis 超时类型
INF_JOB_STATUS("inf_job_status"), // 定时任务状态的枚举

View File

@@ -0,0 +1,23 @@
package cn.iocoder.dashboard.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.dashboard.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.dashboard.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,29 @@
package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SysSmsChannelRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SysSmsChannelRefreshConsumer extends AbstractChannelMessageListener<SysSmsChannelRefreshMessage> {
@Resource
private SysSmsChannelService smsChannelService;
@Override
public void onMessage(SysSmsChannelRefreshMessage message) {
log.info("[onMessage][收到 SmsChannel 刷新消息]");
smsChannelService.initSmsClients();
}
}

View File

@@ -2,16 +2,29 @@ package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SysSmsSendMessage} 的消费者
*
* @author zzf
* @date 2021/3/9 16:35
*/
@Component
@Slf4j
public class SysSmsSendConsumer extends AbstractStreamMessageListener<SysSmsSendMessage> {
@Resource
private SysSmsService smsService;
@Override
public void onMessage(SysSmsSendMessage message) {
log.info("[onMessage][消息内容({})]", message);
smsService.doSendSms(message);
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SysSmsTemplateRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SysSmsTemplateRefreshConsumer extends AbstractChannelMessageListener<SysSmsTemplateRefreshMessage> {
@Resource
private SysSmsTemplateService smsTemplateService;
@Override
public void onMessage(SysSmsTemplateRefreshMessage message) {
log.info("[onMessage][收到 SmsTemplate 刷新消息]");
smsTemplateService.initLocalCache();
}
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.dashboard.modules.system.mq.message.sms;
import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
import lombok.Data;
/**
* 短信渠道的数据刷新 Message
*/
@Data
public class SysSmsChannelRefreshMessage implements ChannelMessage {
@Override
public String getChannel() {
return "system.sms-channel.refresh";
}
}

View File

@@ -1,10 +1,11 @@
package cn.iocoder.dashboard.modules.system.mq.message.sms;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.Map;
import java.util.List;
/**
* 短信发送消息
@@ -14,29 +15,30 @@ import java.util.Map;
@Data
public class SysSmsSendMessage implements StreamMessage {
/**
* 短信日志编号
*/
@NotNull(message = "短信日志编号不能为空")
private Long logId;
/**
* 手机号
*/
@NotNull(message = "手机号不能为空")
private String mobile;
/**
* 短信模板编号
* 短信渠道编号
*/
@NotNull(message = "短信模板编号不能为空")
private String templateCode;
@NotNull(message = "短信渠道编号不能为空")
private Long channelId;
/**
* 短信 API 的模板编号
*/
@NotNull(message = "短信 API 的模板编号不能为空")
private String apiTemplateId;
/**
* 短信模板参数
*/
private Map<String, Object> templateParams;
/**
* 用户编号,允许空
*/
private Integer userId;
/**
* 用户类型,允许空
*/
private Integer userType;
private List<KeyValue<String, Object>> templateParams;
@Override
public String getStreamKey() {

View File

@@ -0,0 +1,17 @@
package cn.iocoder.dashboard.modules.system.mq.message.sms;
import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
import lombok.Data;
/**
* 短信模板的数据刷新 Message
*/
@Data
public class SysSmsTemplateRefreshMessage implements ChannelMessage {
@Override
public String getChannel() {
return "system.sms-template.refresh";
}
}

View File

@@ -9,6 +9,8 @@ import javax.annotation.Resource;
/**
* Role 角色相关消息的 Producer
*
* @author 芋道源码
*/
@Component
public class SysRoleProducer {

View File

@@ -0,0 +1,60 @@
package cn.iocoder.dashboard.modules.system.mq.producer.sms;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
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 短信相关消息的 Producer
*
* @author zzf
* @date 2021/3/9 16:35
*/
@Slf4j
@Component
public class SysSmsProducer {
@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);
}
/**
* 发送 {@link SysSmsChannelRefreshMessage} 消息
*/
public void sendSmsChannelRefreshMessage() {
SysSmsChannelRefreshMessage message = new SysSmsChannelRefreshMessage();
RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
}
/**
* 发送 {@link SysSmsTemplateRefreshMessage} 消息
*/
public void sendSmsTemplateRefreshMessage() {
SysSmsTemplateRefreshMessage message = new SysSmsTemplateRefreshMessage();
RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
}
}

View File

@@ -18,7 +18,7 @@ import java.util.Set;
public interface SysPermissionService extends SecurityPermissionFrameworkService {
/**
* 初始化
* 初始化权限的本地缓存
*/
void initLocalCache();

View File

@@ -3,7 +3,6 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMenuMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysUserRoleMapper;

View File

@@ -59,7 +59,7 @@ public class SysRoleServiceImpl implements SysRoleService {
*/
private volatile Map<Long, SysRoleDO> roleCache;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
* 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@@ -78,7 +78,7 @@ public class SysRoleServiceImpl implements SysRoleService {
@Override
@PostConstruct
public void initLocalCache() {
// 获取菜单列表,如果有更新
// 获取角色列表,如果有更新
List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(roleList)) {
return;
@@ -99,23 +99,23 @@ public class SysRoleServiceImpl implements SysRoleService {
}
/**
* 如果菜单发生变化,从数据库中获取最新的全量菜单
* 如果角色发生变化,从数据库中获取最新的全量角色
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前菜单的最大更新时间
* @return 菜单列表
* @param maxUpdateTime 当前角色的最大更新时间
* @return 角色列表
*/
private List<SysRoleDO> loadRoleIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadRoleIfUpdate][首次加载全量菜单]");
} else { // 判断数据库中是否有更新的菜单
log.info("[loadRoleIfUpdate][首次加载全量角色]");
} else { // 判断数据库中是否有更新的角色
if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
return null;
}
log.info("[loadRoleIfUpdate][增量加载全量菜单]");
log.info("[loadRoleIfUpdate][增量加载全量角色]");
}
// 第二步,如果有更新,则从数据库加载所有菜单
// 第二步,如果有更新,则从数据库加载所有角色
return roleMapper.selectList();
}

View File

@@ -0,0 +1,79 @@
package cn.iocoder.dashboard.modules.system.service.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 短信渠道Service接口
*
* @author zzf
* @date 2021/1/25 9:24
*/
public interface SysSmsChannelService {
/**
* 初始化短信客户端
*/
void initSmsClients();
/**
* 创建短信渠道
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createSmsChannel(@Valid SysSmsChannelCreateReqVO createReqVO);
/**
* 更新短信渠道
*
* @param updateReqVO 更新信息
*/
void updateSmsChannel(@Valid SysSmsChannelUpdateReqVO updateReqVO);
/**
* 删除短信渠道
*
* @param id 编号
*/
void deleteSmsChannel(Long id);
/**
* 获得短信渠道
*
* @param id 编号
* @return 短信渠道
*/
SysSmsChannelDO getSmsChannel(Long id);
/**
* 获得短信渠道列表
*
* @param ids 编号
* @return 短信渠道列表
*/
List<SysSmsChannelDO> getSmsChannelList(Collection<Long> ids);
/**
* 获得所有短信渠道列表
*
* @return 短信渠道列表
*/
List<SysSmsChannelDO> getSmsChannelList();
/**
* 获得短信渠道分页
*
* @param pageReqVO 分页查询
* @return 短信渠道分页
*/
PageResult<SysSmsChannelDO> getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO);
}

View File

@@ -0,0 +1,77 @@
package cn.iocoder.dashboard.modules.system.service.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 短信日志 Service 实现类
*
* @author zzf
* @date 13:48 2021/3/2
*/
public interface SysSmsLogService {
/**
* 创建短信日志
*
* @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);
/**
* 获得短信日志分页
*
* @param pageReqVO 分页查询
* @return 短信日志分页
*/
PageResult<SysSmsLogDO> getSmsLogPage(SysSmsLogPageReqVO pageReqVO);
/**
* 获得短信日志列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 短信日志列表
*/
List<SysSmsLogDO> getSmsLogList(SysSmsLogExportReqVO exportReqVO);
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.dashboard.modules.system.service.sms;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
import java.util.List;
import java.util.Map;
/**
* 短信Service接口
* 只支持异步,因此没有返回值
*
* @author zzf
* @date 2021/1/25 9:24
*/
public interface SysSmsService {
/**
* 发送单条短信给用户(管理员)
*
* 在 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,115 @@
package cn.iocoder.dashboard.modules.system.service.sms;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 短信模板 Service 接口
*
* @author zzf
* @date 2021/1/25 9:24
*/
public interface SysSmsTemplateService {
/**
* 初始化短信模板的本地缓存
*/
void initLocalCache();
/**
* 获得短信模板
*
* @param code 模板编码
* @return 短信模板
*/
SysSmsTemplateDO getSmsTemplateByCode(String code);
/**
* 获得短信模板,从缓存中
*
* @param code 模板编码
* @return 短信模板
*/
SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code);
/**
* 格式化短信内容
*
* @param content 短信模板的内容
* @param params 内容的参数
* @return 格式化后的内容
*/
String formatSmsTemplateContent(String content, Map<String, Object> params);
/**
* 创建短信模板
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createSmsTemplate(@Valid SysSmsTemplateCreateReqVO createReqVO);
/**
* 更新短信模板
*
* @param updateReqVO 更新信息
*/
void updateSmsTemplate(@Valid SysSmsTemplateUpdateReqVO updateReqVO);
/**
* 删除短信模板
*
* @param id 编号
*/
void deleteSmsTemplate(Long id);
/**
* 获得短信模板
*
* @param id 编号
* @return 短信模板
*/
SysSmsTemplateDO getSmsTemplate(Long id);
/**
* 获得短信模板列表
*
* @param ids 编号
* @return 短信模板列表
*/
List<SysSmsTemplateDO> getSmsTemplateList(Collection<Long> ids);
/**
* 获得短信模板分页
*
* @param pageReqVO 分页查询
* @return 短信模板分页
*/
PageResult<SysSmsTemplateDO> getSmsTemplatePage(SysSmsTemplatePageReqVO pageReqVO);
/**
* 获得短信模板列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 短信模板分页
*/
List<SysSmsTemplateDO> getSmsTemplateList(SysSmsTemplateExportReqVO exportReqVO);
/**
* 获得指定短信渠道下的短信模板数量
*
* @param channelId 短信渠道编号
* @return 数量
*/
Integer countByChannelId(Long channelId);
}

View File

@@ -0,0 +1,172 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper;
import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
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.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
/**
* 短信渠道Service实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Service
@Slf4j
public class SysSmsChannelServiceImpl implements SysSmsChannelService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SysSmsChannelMapper smsChannelMapper;
@Resource
private SysSmsTemplateService smsTemplateService;
@Resource
private SysSmsProducer smsProducer;
@Override
@PostConstruct
public void initSmsClients() {
// 获取短信渠道,如果有更新
List<SysSmsChannelDO> smsChannels = this.loadSmsChannelIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(smsChannels)) {
return;
}
// 创建或更新短信 Client
List<SmsChannelProperties> propertiesList = SysSmsChannelConvert.INSTANCE.convertList02(smsChannels);
propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties));
// 写入缓存
assert smsChannels.size() > 0; // 断言,避免告警
maxUpdateTime = smsChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initSmsClients();
}
/**
* 如果短信渠道发生变化,从数据库中获取最新的全量短信渠道。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前短信渠道的最大更新时间
* @return 短信渠道列表
*/
private List<SysSmsChannelDO> loadSmsChannelIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadSmsChannelIfUpdate][首次加载全量短信渠道]");
} else { // 判断数据库中是否有更新的短信渠道
if (smsChannelMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadSmsChannelIfUpdate][增量加载全量短信渠道]");
}
// 第二步,如果有更新,则从数据库加载所有短信渠道
return smsChannelMapper.selectList();
}
@Override
public Long createSmsChannel(SysSmsChannelCreateReqVO createReqVO) {
// 插入
SysSmsChannelDO smsChannel = SysSmsChannelConvert.INSTANCE.convert(createReqVO);
smsChannelMapper.insert(smsChannel);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
// 返回
return smsChannel.getId();
}
@Override
public void updateSmsChannel(SysSmsChannelUpdateReqVO updateReqVO) {
// 校验存在
this.validateSmsChannelExists(updateReqVO.getId());
// 更新
SysSmsChannelDO updateObj = SysSmsChannelConvert.INSTANCE.convert(updateReqVO);
smsChannelMapper.updateById(updateObj);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
}
@Override
public void deleteSmsChannel(Long id) {
// 校验存在
this.validateSmsChannelExists(id);
// 校验是否有字典数据
if (smsTemplateService.countByChannelId(id) > 0) {
throw exception(SMS_CHANNEL_HAS_CHILDREN);
}
// 删除
smsChannelMapper.deleteById(id);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
}
private void validateSmsChannelExists(Long id) {
if (smsChannelMapper.selectById(id) == null) {
throw exception(SMS_CHANNEL_NOT_EXISTS);
}
}
@Override
public SysSmsChannelDO getSmsChannel(Long id) {
return smsChannelMapper.selectById(id);
}
@Override
public List<SysSmsChannelDO> getSmsChannelList(Collection<Long> ids) {
return smsChannelMapper.selectBatchIds(ids);
}
@Override
public List<SysSmsChannelDO> getSmsChannelList() {
return smsChannelMapper.selectList();
}
@Override
public PageResult<SysSmsChannelDO> getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO) {
return smsChannelMapper.selectPage(pageReqVO);
}
}

View File

@@ -0,0 +1,86 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsLogMapper;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 短信日志 Service 实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Slf4j
@Service
public class SysSmsLogServiceImpl implements SysSmsLogService {
@Resource
private SysSmsLogMapper smsLogMapper;
@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();
smsLogMapper.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;
smsLogMapper.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;
smsLogMapper.updateById(SysSmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()).receiveTime(receiveTime)
.apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}
@Override
public PageResult<SysSmsLogDO> getSmsLogPage(SysSmsLogPageReqVO pageReqVO) {
return smsLogMapper.selectPage(pageReqVO);
}
@Override
public List<SysSmsLogDO> getSmsLogList(SysSmsLogExportReqVO exportReqVO) {
return smsLogMapper.selectList(exportReqVO);
}
}

View File

@@ -0,0 +1,171 @@
package cn.iocoder.dashboard.modules.system.service.sms.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.common.enums.UserTypeEnum;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
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.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
/**
* 短信日志Service实现类
*
* @author zzf
* @date 2021/1/25 9:25
*/
@Service
@Slf4j
public class SysSmsServiceImpl implements SysSmsService {
@Resource
private SysSmsTemplateService smsTemplateService;
@Resource
private SysSmsLogService smsLogService;
@Resource
private SysSmsProducer smsProducer;
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SysUserService userService;
@Override
public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StrUtil.isEmpty(mobile)) {
SysUserDO user = userService.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 = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsProducer.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 = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
// 短信模板不存在
if (template == null) {
throw exception(SMS_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());
smsLogService.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 -> {
smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(),
result.getErrorCode(), result.getErrorCode());
});
}
}

Some files were not shown because too many files have changed in this diff Show More