diff --git a/pom.xml b/pom.xml index 39f97ab43..163aa1941 100644 --- a/pom.xml +++ b/pom.xml @@ -222,6 +222,18 @@ ${easyexcel.verion} + + com.aliyun + aliyun-java-sdk-core + 4.5.18 + + + + com.aliyun + aliyun-java-sdk-dysmsapi + 2.1.0 + + diff --git a/sql/ruoyi-vue-pro.sql b/sql/ruoyi-vue-pro.sql index af7f492f9..edba4204c 100644 --- a/sql/ruoyi-vue-pro.sql +++ b/sql/ruoyi-vue-pro.sql @@ -884,4 +884,77 @@ INSERT INTO `sys_user_role` VALUES (5, 100, 1, '', NULL, '', NULL, b'0'); INSERT INTO `sys_user_role` VALUES (6, 100, 2, '', NULL, '', NULL, b'0'); COMMIT; + +-- ---------------------------- +-- Table structure for sms_channel +-- ---------------------------- +DROP TABLE IF EXISTS `sms_channel`; +CREATE TABLE `sms_channel` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', + `code` varchar(50) not null COMMENT '编码(来自枚举类 阿里、华为、七牛等)', + `api_key` varchar(100) NOT NULL COMMENT '账号id', -- add + `api_secret` varchar(100) NOT NULL COMMENT '账号秘钥', -- add + `priority` tinyint(8) NOT NULL default 1 COMMENT '优先级(存在多个签名时,选择值最小的,渠道不可用时,按优先级从小到大切换)', -- add + `api_signature_id` varchar(100) NOT NULL COMMENT '实际渠道签名唯一标识', + `name` varchar(50) not null COMMENT '名称', + `signature` varchar(50) not null COMMENT '签名值', + `remark` varchar(200) NOT NULL COMMENT '备注', + + `status` tinyint(4) NOT NULL default 0 COMMENT '启用状态(0正常 1停用)', + `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信渠道'; +/* + 优先级值一样时,按照id顺序取值 +*/ + +-- ---------------------------- +-- Table structure for sms_template +-- ---------------------------- +DROP TABLE IF EXISTS `sms_template`; +CREATE TABLE `sms_template` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', + `channel_code` varchar(50) not null COMMENT '短信渠道编码(来自枚举类)', + `channel_id` bigint(20) not null COMMENT '短信渠道id (对于前端来说就是绑定一个签名)', -- add + `type` tinyint(4) NOT NULL default 1 COMMENT '消息类型 [0验证码 1短信通知 2推广短信 3国际/港澳台消息]', + `biz_code` varchar(50) not null COMMENT '业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板)', -- add + `priority` tinyint(8) NOT NULL default 1 COMMENT '优先级(默认直接继承渠道表的,逻辑也与渠道表的一致,可以针对每个biz_code进行修改)',-- add + `code` varchar(50) not null COMMENT '编码', + `name` varchar(50) not null COMMENT '名称', + `api_template_id` varchar(100) NOT NULL COMMENT '实际渠道模板唯一标识', + `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', + `params` varchar(200) NOT NULL DEFAULT '' COMMENT '参数数组(自动根据内容生成)', -- add + `remark` varchar(200) NOT NULL COMMENT '备注', + + `status` tinyint(4) NOT NULL default 0 COMMENT '启用状态(0正常 1停用)', + `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信模板'; + +-- ---------------------------- +-- Table structure for sms_log +-- ---------------------------- +DROP TABLE IF EXISTS `sms_log`; +CREATE TABLE `sms_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', + `channel_code` varchar(50) not null COMMENT '短信渠道编码(来自枚举类)', + `api_sms_id` varchar(50) not null COMMENT '实际渠道短信唯一标识', + `template_id` bigint(20) NOT NULL COMMENT '模板id', + `phone` char(11) not null COMMENT '手机号', + `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', + `remark` varchar(200) NOT NULL COMMENT '备注', + `send_status` tinyint(4) NOT NULL default 0 COMMENT '发送状态(0发送中 1成功 2失败)', + `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信日志'; + SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/cn/iocoder/dashboard/DashboardApplication.java b/src/main/java/cn/iocoder/dashboard/DashboardApplication.java index 32783aa8c..498bf63c7 100644 --- a/src/main/java/cn/iocoder/dashboard/DashboardApplication.java +++ b/src/main/java/cn/iocoder/dashboard/DashboardApplication.java @@ -1,15 +1,15 @@ -package cn.iocoder.dashboard; - -import de.codecentric.boot.admin.server.config.EnableAdminServer; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@EnableAdminServer -public class DashboardApplication { - - public static void main(String[] args) { - SpringApplication.run(DashboardApplication.class, args); - } - -} +//package cn.iocoder.dashboard; +// +//import de.codecentric.boot.admin.server.config.EnableAdminServer; +//import org.springframework.boot.SpringApplication; +//import org.springframework.boot.autoconfigure.SpringBootApplication; +// +//@SpringBootApplication +//@EnableAdminServer +//public class DashboardApplication { +// +// public static void main(String[] args) { +// SpringApplication.run(DashboardApplication.class, args); +// } +// +//} diff --git a/src/main/java/cn/iocoder/dashboard/common/enums/SmsChannelEnum.java b/src/main/java/cn/iocoder/dashboard/common/enums/SmsChannelEnum.java new file mode 100644 index 000000000..fdb80387d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/enums/SmsChannelEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @date 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + ALI("ALI", "阿里"), + HUA_WEI("HUA_WEI", "华为"), + QI_NIU("QI_NIU", "七牛"), + TEN_XUN("TEN_XUN", "腾讯"); + + private final String code; + + private final String name; + + public static SmsChannelEnum getByCode(String code) { + for (SmsChannelEnum value : SmsChannelEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return null; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsBody.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsBody.java new file mode 100644 index 000000000..693838d4a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsBody.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.msg.sms; + +import cn.iocoder.dashboard.util.json.JsonUtils; +import lombok.Data; + +import java.util.Map; + +/** + * 消息内容实体类 + */ +@Data +public class SmsBody { + + /** + * 模板编码 + */ + private String code; + + /** + * 参数列表 + */ + private Map params; + + public String getParamsStr() { + return JsonUtils.toJsonString(params); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsResult.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsResult.java new file mode 100644 index 000000000..8a8741839 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsResult.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.framework.msg.sms; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 消息内容实体类 + */ +@Data +public class SmsResult implements Serializable { + + /** + * 是否成功 + */ + private Boolean success; + + /** + * 提示 + */ + private String message; + + /** + * 返回值 + */ + private T result; +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsSender.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsSender.java new file mode 100644 index 000000000..30b10df61 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/SmsSender.java @@ -0,0 +1,67 @@ +package cn.iocoder.dashboard.framework.msg.sms; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * 短信父接口 + * + * @author zzf + * @date 2021/1/25 14:14 + */ +public interface SmsSender { + + /** + * 发送通知 + * + * @param msgBody 通知内容 + * @param targets 发送对象列表 + * @return 是否发送成功 + */ + SmsResult send(SmsBody msgBody, Collection targets); + + /** + * 发送通知 + * + * @param msgBody 通知内容 + * @param target 发送对象列表 + * @return 是否发送成功 + */ + default SmsResult send(SmsBody msgBody, String target) { + if (StringUtils.isBlank(target)) { + return failResult(); + } + + return send(msgBody, Collections.singletonList(target)); + } + + /** + * 发送通知 + * + * @param msgBody 通知内容 + * @param targets 发送对象列表 + * @return 是否发送成功 + */ + default SmsResult send(SmsBody msgBody, String... targets) { + if (targets == null) { + return failResult(); + } + + return send(msgBody, Arrays.asList(targets)); + } + + default SmsResult failResult() { + SmsResult resultBody = new SmsResult<>(); + resultBody.setSuccess(false); + return resultBody; + } + + default SmsResult failResult(String message) { + SmsResult resultBody = failResult(); + resultBody.setMessage(message); + return resultBody; + } +} \ No newline at end of file diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/config/SmsConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/config/SmsConfiguration.java new file mode 100644 index 000000000..7b5b26a5b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/config/SmsConfiguration.java @@ -0,0 +1,48 @@ +package cn.iocoder.dashboard.framework.msg.sms.config; + +import cn.iocoder.dashboard.framework.msg.sms.factory.DefaultSmsSenderFactory; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.AbstractSmsIntercepterChain; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.DefaultSmsIntercepterChain; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.SmsLogIntercepter; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import cn.iocoder.dashboard.modules.msg.service.sms.SmsChannelService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 短信服务配置 + * + * @author guer + */ +@Configuration +@ConditionalOnProperty("sms.enabled") +public class SmsConfiguration { + + @Resource + private SmsChannelService channelService; + + + @Bean + public AbstractSmsIntercepterChain smsIntercepterChain() { + DefaultSmsIntercepterChain intercepterChain = new DefaultSmsIntercepterChain(); + //添加拦截器 + intercepterChain.addSmsIntercepter(new SmsLogIntercepter()); + return intercepterChain; + } + + @Bean + public DefaultSmsSenderFactory smsSenderFactory(AbstractSmsIntercepterChain intercepterChain) { + DefaultSmsSenderFactory defaultSmsSenderFactory = new DefaultSmsSenderFactory(); + List smsChannelAllVOList = channelService.listChannelAllEnabledInfo(); + //初始化渠道、模板信息 + defaultSmsSenderFactory.init(smsChannelAllVOList); + //注入拦截器链 + defaultSmsSenderFactory.setIntercepterChain(intercepterChain); + return defaultSmsSenderFactory; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/factory/DefaultSmsSenderFactory.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/factory/DefaultSmsSenderFactory.java new file mode 100644 index 000000000..123943a3d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/factory/DefaultSmsSenderFactory.java @@ -0,0 +1,137 @@ +package cn.iocoder.dashboard.framework.msg.sms.factory; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.dashboard.common.enums.SmsChannelEnum; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.framework.msg.sms.SmsSender; +import cn.iocoder.dashboard.framework.msg.sms.impl.ali.AliSmsSender; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.AbstractSmsIntercepterChain; +import cn.iocoder.dashboard.framework.msg.sms.proxy.DefaultSmsSenderProxy; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsTemplateVO; +import lombok.Setter; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; + +/** + * 短信发送者工厂 + * + * @author zzf + * @date 2021/1/25 16:18 + */ +public class DefaultSmsSenderFactory { + + /** + * sender索引 + * key: {@link SmsTemplateVO#getBizCode()} + * value: {@link SmsSender} + */ + private final ConcurrentHashMap> bizCode2SenderMap = new ConcurrentHashMap<>(8); + + /** + * sender索引 + * key: {@link SmsTemplateVO#getCode()} + * value: {@link SmsSender} + */ + private final ConcurrentHashMap> templateCode2SenderMap = new ConcurrentHashMap<>(8); + + + @Setter + private AbstractSmsIntercepterChain intercepterChain; + + /** + * 读写锁 + */ + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private final Lock writeLock = lock.writeLock(); + + + public void init(List smsChannelAllVOList) { + if (ObjectUtil.isEmpty(smsChannelAllVOList)) { + throw new ServiceException(SMS_CHANNEL_NOT_FOUND); + } + try { + writeLock.lock(); + addSender(smsChannelAllVOList); + } finally { + writeLock.unlock(); + } + } + + public SmsSender getSenderByBizCode(String bizCode) { + return getSmsSender(bizCode, bizCode2SenderMap); + } + + public SmsSender getSenderByTemplateCode(String templateCode) { + return getSmsSender(templateCode, templateCode2SenderMap); + } + + private SmsSender getSmsSender(String templateCode, ConcurrentHashMap> cacheMap) { + try { + readLock.lock(); + SmsSender smsSender = cacheMap.get(templateCode); + if (smsSender == null) { + throw new ServiceException(SMS_SENDER_NOT_FOUND); + } + return smsSender; + } finally { + readLock.unlock(); + } + } + + public void flush(List smsChannelAllVOList) { + try { + writeLock.lock(); + bizCode2SenderMap.clear(); + templateCode2SenderMap.clear(); + addSender(smsChannelAllVOList); + } finally { + writeLock.unlock(); + } + } + + + private void addSender(List smsChannelAllVOList) { + smsChannelAllVOList.forEach(channelAllVO -> addSender(SmsChannelEnum.getByCode(channelAllVO.getCode()), channelAllVO)); + } + + private void addSender(SmsChannelEnum channelEnum, SmsChannelAllVO channelAllVO) { + if (channelEnum == null) { + throw new ServiceException(INVALID_CHANNEL_CODE); + } + List templateList = channelAllVO.getTemplateList(); + if (ObjectUtil.isEmpty(templateList)) { + throw new ServiceException(SMS_TEMPLATE_NOT_FOUND); + } + + SmsSender aliSmsSender = getSender(channelEnum, channelAllVO); + + + templateList.forEach(smsTemplateVO -> { + bizCode2SenderMap.put(smsTemplateVO.getBizCode(), aliSmsSender); + templateCode2SenderMap.put(smsTemplateVO.getCode(), aliSmsSender); + }); + } + + private SmsSender getSender(SmsChannelEnum channelEnum, SmsChannelAllVO channelAllVO) { + switch (channelEnum) { + case ALI: + return new DefaultSmsSenderProxy<>(new AliSmsSender(channelAllVO), intercepterChain); + // TODO fill more channel + default: + break; + } + throw new ServiceException(SMS_SENDER_NOT_FOUND); + } + + + + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/impl/ali/AliSmsSender.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/impl/ali/AliSmsSender.java new file mode 100644 index 000000000..fbf6aeceb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/impl/ali/AliSmsSender.java @@ -0,0 +1,86 @@ +package cn.iocoder.dashboard.framework.msg.sms.impl.ali; + +import cn.iocoder.dashboard.framework.msg.sms.SmsBody; +import cn.iocoder.dashboard.framework.msg.sms.SmsResult; +import cn.iocoder.dashboard.framework.msg.sms.SmsSender; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; + +/** + * 阿里短信实现类 + * + * @author zzf + * @date 2021/1/25 14:17 + */ +@Slf4j +public class AliSmsSender implements SmsSender { + + private static final String OK = "OK"; + + private static final String PRODUCT = "Dysmsapi"; + + private static final String DOMAIN = "dysmsapi.aliyuncs.com"; + + private static final String ENDPOINT = "cn-hangzhou"; + + private final SmsChannelAllVO channelVO; + + private final IAcsClient acsClient; + + /** + * 构造阿里云短信发送处理 + * + * @param channelVO 阿里云短信配置 + */ + public AliSmsSender(SmsChannelAllVO channelVO) { + + this.channelVO = channelVO; + + String accessKeyId = channelVO.getApiKey(); + String accessKeySecret = channelVO.getApiSecret(); + + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, accessKeyId, accessKeySecret); + DefaultProfile.addEndpoint(ENDPOINT, PRODUCT, DOMAIN); + + acsClient = new DefaultAcsClient(profile); + } + + + @Override + public SmsResult send(SmsBody msgBody, Collection targets) { + SendSmsRequest request = new SendSmsRequest(); + request.setSysMethod(MethodType.POST); + request.setPhoneNumbers(StringUtils.join(targets, ",")); + request.setSignName(channelVO.getApiSignatureId()); + request.setTemplateCode(channelVO.getTemplateByTemplateCode(msgBody.getCode()).getApiTemplateId()); + request.setTemplateParam(msgBody.getParamsStr()); + + try { + SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request); + + boolean result = OK.equals(sendSmsResponse.getCode()); + if (!result) { + log.debug("send fail[code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage()); + } + SmsResult resultBody = new SmsResult<>(); + resultBody.setSuccess(result); + resultBody.setResult(sendSmsResponse); + return resultBody; + } catch (Exception e) { + log.debug(e.getMessage(), e); + } + + return failResult(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/AbstractSmsIntercepterChain.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/AbstractSmsIntercepterChain.java new file mode 100644 index 000000000..f95de7128 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/AbstractSmsIntercepterChain.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.framework.msg.sms.intercepter; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 消息父接口 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +public abstract class AbstractSmsIntercepterChain { + + @Getter + protected final List intercepterList = new ArrayList<>(8); + + + /** + * 添加短信拦截器 + * + * @param smsIntercepter 短信拦截器 + */ + public void addSmsIntercepter(SmsIntercepter smsIntercepter) { + addSmsIntercepter(Collections.singletonList(smsIntercepter)); + } + + /** + * 添加短信拦截器 + * + * @param smsIntercepterList 短信拦截器数组 + */ + abstract void addSmsIntercepter(List smsIntercepterList); + + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/DefaultSmsIntercepterChain.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/DefaultSmsIntercepterChain.java new file mode 100644 index 000000000..76d31906d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/DefaultSmsIntercepterChain.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.framework.msg.sms.intercepter; + +import java.util.Comparator; +import java.util.List; + +/** + * 消息父接口 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +public class DefaultSmsIntercepterChain extends AbstractSmsIntercepterChain { + + @Override + public void addSmsIntercepter(List smsIntercepterList) { + intercepterList.addAll(smsIntercepterList); + //排序 + intercepterList.sort(Comparator.comparingInt(SmsIntercepter::getOrder)); + } +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsIntercepter.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsIntercepter.java new file mode 100644 index 000000000..df83c8d6a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsIntercepter.java @@ -0,0 +1,41 @@ +package cn.iocoder.dashboard.framework.msg.sms.intercepter; + +import cn.iocoder.dashboard.framework.msg.sms.SmsBody; +import cn.iocoder.dashboard.framework.msg.sms.SmsResult; + +import java.util.Collection; + +/** + * 消息父接口 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +public interface SmsIntercepter { + + /** + * 监听发送前 + * + * @param msgBody 消息体 + * @param targets 发送对象数组 + */ + void beforeSender(SmsBody msgBody, Collection targets); + + /** + * 监听发送后 + * + * @param msgBody 消息体 + * @param targets 发送对象数组 + * @param resultBody 返回对象 + */ + void afterSender(SmsBody msgBody, Collection targets, SmsResult resultBody); + + /** + * 排序值,拦截器根据order值顺序执行 + *

+ * 值越小,越早执行 + * + * @return 排序值 + */ + int getOrder(); +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsLogIntercepter.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsLogIntercepter.java new file mode 100644 index 000000000..9a7effd95 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/intercepter/SmsLogIntercepter.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.framework.msg.sms.intercepter; + +import cn.iocoder.dashboard.framework.msg.sms.SmsBody; +import cn.iocoder.dashboard.framework.msg.sms.SmsResult; +import cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms.SmsLogMapper; +import cn.iocoder.dashboard.util.json.JsonUtils; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; + +/** + * 短信日志拦截器 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +@Slf4j +public class SmsLogIntercepter implements SmsIntercepter { + + + @Override + public void beforeSender(SmsBody msgBody, Collection targets) { + log.debug("ready send sms, body: {}, target: {}", JsonUtils.toJsonString(msgBody), targets); + + } + + @Override + public void afterSender(SmsBody msgBody, Collection targets, SmsResult resultBody) { + if (resultBody.getSuccess()) { + // + } else { + log.warn("send sms fail, body: {}, target: {}, resultBody: {}", + JsonUtils.toJsonString(msgBody), + targets, + JsonUtils.toJsonString(resultBody) + ); + } + + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/DefaultSmsSenderProxy.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/DefaultSmsSenderProxy.java new file mode 100644 index 000000000..f9d7e566f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/DefaultSmsSenderProxy.java @@ -0,0 +1,41 @@ +package cn.iocoder.dashboard.framework.msg.sms.proxy; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.dashboard.framework.msg.sms.SmsBody; +import cn.iocoder.dashboard.framework.msg.sms.SmsResult; +import cn.iocoder.dashboard.framework.msg.sms.SmsSender; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.AbstractSmsIntercepterChain; + +import java.util.Collection; + +/** + * 消息父接口 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +public class DefaultSmsSenderProxy implements SmsSender { + + private final SmsSender smsSender; + private final AbstractSmsIntercepterChain chain; + + @Override + public SmsResult send(SmsBody msgBody, Collection targets) { + if (ObjectUtil.isNotNull(chain) && ObjectUtil.isNotEmpty(chain.getIntercepterList())) { + chain.getIntercepterList().forEach(s -> s.beforeSender(msgBody, targets)); + } + + SmsResult resultBody = smsSender.send(msgBody, targets); + + if (ObjectUtil.isNotNull(chain) && ObjectUtil.isNotEmpty(chain.getIntercepterList())) { + chain.getIntercepterList().forEach(s -> s.afterSender(msgBody, targets, resultBody)); + } + return resultBody; + } + + public DefaultSmsSenderProxy(SmsSender smsSender, + AbstractSmsIntercepterChain chain) { + this.smsSender = smsSender; + this.chain = chain; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/SmsSenderProxy.java b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/SmsSenderProxy.java new file mode 100644 index 000000000..069916e2a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/msg/sms/proxy/SmsSenderProxy.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.msg.sms.proxy; + +import cn.iocoder.dashboard.framework.msg.sms.SmsSender; +import cn.iocoder.dashboard.framework.msg.sms.intercepter.SmsIntercepter; + +import java.util.Collections; +import java.util.List; + +/** + * 消息父接口 + * + * @author zzf + * @date 2021/1/22 15:46 + */ +public interface SmsSenderProxy extends SmsSender { + + /** + * 添加短信拦截器 + * + * @param smsIntercepter 短信拦截器 + */ + default void addSmsIntercepter(SmsIntercepter smsIntercepter) { + addSmsIntercepter(Collections.singletonList(smsIntercepter)); + } + + /** + * 添加短信拦截器 + * + * @param smsIntercepterList 短信拦截器数组 + */ + void addSmsIntercepter(List smsIntercepterList); + + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java index b9adcc3f1..ee3a342a0 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java @@ -1,8 +1,6 @@ package cn.iocoder.dashboard.framework.redis.core; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.Getter; import java.time.Duration; @@ -26,20 +24,15 @@ public class RedisKeyDefine { } - @Getter - @AllArgsConstructor - public enum TimeoutTypeEnum { + /** + * 过期时间 - 永不过期 + */ + public static final Duration TIMEOUT_FOREVER = null; - FOREVER(1), // 永不超时 - DYNAMIC(2), // 动态超时 - FIXED(3); // 固定超时 - - /** - * 类型 - */ - private final Integer type; - - } + /** + * 过期时间 - 动态,通过参数传入 + */ + public static final Duration TIMEOUT_DYNAMIC = null; /** * Key 模板 @@ -55,12 +48,10 @@ public class RedisKeyDefine { * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 */ private final Class valueType; - /** - * 超时类型 - */ - private final TimeoutTypeEnum timeoutType; /** * 过期时间 + * + * 为空时,表示永不过期 {@link #TIMEOUT_FOREVER} */ private final Duration timeout; @@ -68,20 +59,7 @@ public class RedisKeyDefine { this.keyTemplate = keyTemplate; this.keyType = keyType; this.valueType = valueType; - this.timeoutType = TimeoutTypeEnum.FIXED; this.timeout = timeout; - // 添加注册表 - RedisKeyRegistry.add(this); - } - - public RedisKeyDefine(String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { - this.keyTemplate = keyTemplate; - this.keyType = keyType; - this.valueType = valueType; - this.timeoutType = timeoutType; - this.timeout = Duration.ZERO; - // 添加注册表 - RedisKeyRegistry.add(this); } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java deleted file mode 100644 index 4a220fbd0..000000000 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@link RedisKeyDefine} 注册表 - */ -public class RedisKeyRegistry { - - private static final List defines = new ArrayList<>(); - - public static void add(RedisKeyDefine define) { - defines.add(define); - } - - public static List list() { - return defines; - } - - public static int size() { - return defines.size(); - } - -} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsChannelController.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsChannelController.java new file mode 100644 index 000000000..c28d9b862 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsChannelController.java @@ -0,0 +1,54 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp.SmsChannelEnumRespVO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; +import cn.iocoder.dashboard.modules.msg.service.sms.SmsChannelService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; + +@Api("短信 渠道/签名 API") +@RestController +@RequestMapping("/sms/channel") +public class SmsChannelController { + + @Resource + private SmsChannelService service; + + @ApiOperation("获取渠道/签名分页") + @GetMapping("/page") + public CommonResult> getPermissionInfo(@Validated SmsChannelPageReqVO reqVO) { + return success(service.pageChannels(reqVO)); + } + + @ApiOperation("获取渠道枚举") + @GetMapping("/list/channel-enum") + public CommonResult> getChannelEnums() { + return success(service.getChannelEnums()); + } + + + @ApiOperation("添加消息渠道") + @PostMapping("/create") + public CommonResult add(@Validated @RequestBody SmsChannelCreateReqVO reqVO) { + return success(service.createChannel(reqVO)); + } + + @ApiOperation("刷新消息渠道信息") + @PutMapping("/flush") + public CommonResult flushChannel() { + return success(service.flushChannel()); + } + + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsTemplateController.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsTemplateController.java new file mode 100644 index 000000000..8bd73b12e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/SmsTemplateController.java @@ -0,0 +1,84 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms; + +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthLoginReqVO; +import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthLoginRespVO; +import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthMenuRespVO; +import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthPermissionInfoRespVO; +import cn.iocoder.dashboard.modules.system.convert.auth.SysAuthConvert; +import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO; +import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum; +import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService; +import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; +import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; +import cn.iocoder.dashboard.modules.system.service.user.SysUserService; +import cn.iocoder.dashboard.util.collection.SetUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; +import static cn.iocoder.dashboard.framework.security.core.util.SecurityUtils.getLoginUserId; +import static cn.iocoder.dashboard.framework.security.core.util.SecurityUtils.getLoginUserRoleIds; + +@Api("认证 API") +@RestController +@RequestMapping("/sms/template") +public class SmsTemplateController { + + @Resource + private SysAuthService authService; + @Resource + private SysUserService userService; + @Resource + private SysRoleService roleService; + @Resource + private SysPermissionService permissionService; + + @ApiOperation("使用账号密码登录") + @PostMapping("/login") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult login(@RequestBody @Valid SysAuthLoginReqVO reqVO) { + String token = authService.login(reqVO.getUsername(), reqVO.getPassword(), reqVO.getUuid(), reqVO.getCode()); + // 返回结果 + return success(SysAuthLoginRespVO.builder().token(token).build()); + } + + @ApiOperation("获取登陆用户的权限信息") + @GetMapping("/get-permission-info") + public CommonResult getPermissionInfo() { + // 获得用户信息 + SysUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return null; + } + // 获得角色列表 + List roleList = roleService.listRolesFromCache(getLoginUserRoleIds()); + // 获得菜单列表 + List menuList = permissionService.listRoleMenusFromCache(getLoginUserRoleIds(), + SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()), + SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); + // 拼接结果返回 + return success(SysAuthConvert.INSTANCE.convert(user, roleList, menuList)); + } + + @ApiOperation("获得登陆用户的菜单列表") + @GetMapping("list-menus") + public CommonResult> listMenus() { + // 获得用户拥有的菜单列表 + List menuList = permissionService.listRoleMenusFromCache(getLoginUserRoleIds(), + SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型 + SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 + // 转换成 Tree 结构返回 + return success(SysAuthConvert.INSTANCE.buildMenuTree(menuList)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsChannelAllVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsChannelAllVO.java new file mode 100644 index 000000000..bac72c959 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsChannelAllVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * 渠道(包含模板)信息VO类 + * + * @author zzf + * @date 2021/1/25 17:01 + */ +@Data +@EqualsAndHashCode +public class SmsChannelAllVO implements Serializable { + + /** + * id + */ + private Long id; + + /** + * 编码(来自枚举类 阿里、华为、七牛等) + */ + private String code; + + /** + * 渠道账号id + */ + private String apiKey; + + /** + * 渠道账号秘钥 + */ + private String apiSecret; + + /** + * 实际渠道签名唯一标识 + */ + private String apiSignatureId; + + /** + * 签名值 + */ + private String signature; + + /** + * 该渠道名下的短信模板集合 + */ + private List templateList; + + public SmsTemplateVO getTemplateByTemplateCode(String tempCode) { + return templateList.stream().filter(s -> s.getCode().equals(tempCode)).findFirst().get(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsTemplateVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsTemplateVO.java new file mode 100644 index 000000000..04bfe9a62 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/SmsTemplateVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 渠道模板VO类 + * + * @author zzf + * @date 2021/1/25 17:03 + */ +@Data +@EqualsAndHashCode +public class SmsTemplateVO { + + /** + * 业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板) + */ + private String bizCode; + /** + * 编码 + */ + private String code; + + /** + * 实际渠道模板唯一标识 + */ + private String apiTemplateId; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelCreateReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelCreateReqVO.java new file mode 100644 index 000000000..729088b65 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelCreateReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo.req; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + + +@ApiModel("消息渠道创建 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class SmsChannelCreateReqVO implements Serializable { + + @ApiModelProperty("编码(来自枚举类 阿里、华为、七牛等)") + private String code; + + @ApiModelProperty("渠道账号id") + private String apiKey; + + @ApiModelProperty("渠道账号秘钥") + private String apiSecret; + + @ApiModelProperty("优先级(存在多个签名时,选择值最小的,渠道不可用时,按优先级从小到大切换)") + private Integer priority; + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("签名值") + private String signature; + + @ApiModelProperty("备注") + private String remark; + + @ApiModelProperty("启用状态(0正常 1停用)") + private Integer status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelPageReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelPageReqVO.java new file mode 100644 index 000000000..dd2df146e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/req/SmsChannelPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo.req; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +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 +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SmsChannelPageReqVO extends PageParam { + + @ApiModelProperty(value = "渠道名", example = "阿里", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "签名值", example = "源码", notes = "模糊匹配") + private String signature; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelEnumRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelEnumRespVO.java new file mode 100644 index 000000000..a3e56e0ca --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelEnumRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + + +@ApiModel("用户分页 Request VO") +@Data +@NoArgsConstructor +@EqualsAndHashCode +public class SmsChannelEnumRespVO implements Serializable { + + private String code; + + private String name; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelPageRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelPageRespVO.java new file mode 100644 index 000000000..603f13e07 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/controller/sms/vo/resp/SmsChannelPageRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +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 +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SmsChannelPageRespVO extends PageParam { + + @ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配") + private String username; + + @ApiModelProperty(value = "手机号码", example = "yudao", notes = "模糊匹配") + private String mobile; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 SysCommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + + @ApiModelProperty(value = "部门编号", example = "1024", notes = "同时筛选子部门") + private Long deptId; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsChannelConvert.java b/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsChannelConvert.java new file mode 100644 index 000000000..4728586a3 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsChannelConvert.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.modules.msg.convert.sms; + +import cn.iocoder.dashboard.common.enums.SmsChannelEnum; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp.SmsChannelEnumRespVO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; +import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserUpdateReqVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface SmsChannelConvert { + + SmsChannelConvert INSTANCE = Mappers.getMapper(SmsChannelConvert.class); + + @Mapping(source = "records", target = "list") + PageResult convertPage(IPage page); + + SmsChannelDO convert(SmsChannelCreateReqVO bean); + + SmsChannelDO convert(SysUserUpdateReqVO bean); + + List convertEnum(List bean); + + List convert(List bean); + + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsTemplateConvert.java b/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsTemplateConvert.java new file mode 100644 index 000000000..313e06c92 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/convert/sms/SmsTemplateConvert.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.modules.msg.convert.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsTemplateVO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsTemplateDO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface SmsTemplateConvert { + + SmsTemplateConvert INSTANCE = Mappers.getMapper(SmsTemplateConvert.class); + + @Mapping(source = "records", target = "list") + PageResult convertPage(IPage page); + + List convert(List bean); + + SmsTemplateVO convert(SmsTemplateDO bean); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsChannelMapper.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsChannelMapper.java new file mode 100644 index 000000000..f7ede3502 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsChannelMapper.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SmsChannelMapper extends BaseMapper { + + default IPage selectChannelPage(SmsChannelPageReqVO reqVO) { + return selectPage(MyBatisUtils.buildPage(reqVO), new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(reqVO.getName()), SmsChannelDO::getName, reqVO.getName()) + .like(StrUtil.isNotBlank(reqVO.getSignature()), SmsChannelDO::getName, reqVO.getSignature()) + ); + } + + default List selectEnabledList() { + return selectList(new LambdaQueryWrapper() + .eq(SmsChannelDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByAsc(SmsChannelDO::getId) + ); + } +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsLogMapper.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsLogMapper.java new file mode 100644 index 000000000..263783b47 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsLogMapper.java @@ -0,0 +1,10 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms; + +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsLogMapper extends BaseMapper { + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsTemplateMapper.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsTemplateMapper.java new file mode 100644 index 000000000..f2d2fb21b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/dao/sms/SmsTemplateMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms; + +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsTemplateDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SmsTemplateMapper extends BaseMapper { + + /** + * 根据短信渠道id查询短信模板集合 + * + * @param channelId 渠道id + * @return 模板集合 + */ + default List selectListByChannelId(Long channelId) { + return selectList(new LambdaQueryWrapper() + .eq(SmsTemplateDO::getChannelId, channelId) + .eq(SmsTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByAsc(SmsTemplateDO::getId) + ); + } + + /** + * 查询有效短信模板集合 + * + * @return 有效短信模板集合 + */ + default List selectEnabledList() { + return selectList(new LambdaQueryWrapper() + .eq(SmsTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByAsc(SmsTemplateDO::getId) + ); + } +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsChannelDO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsChannelDO.java new file mode 100644 index 000000000..3c0b5f396 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsChannelDO.java @@ -0,0 +1,65 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms; + +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + + +/** + * 短信渠道 + * + * @author zzf + * @since 2021-01-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sms_channel", autoResultMap = true) +public class SmsChannelDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + /** + * 编码(来自枚举类 阿里、华为、七牛等) + */ + private String code; + + /** + * 渠道账号id + */ + private String apiKey; + + /** + * 渠道账号秘钥 + */ + private String apiSecret; + + /** + * 实际渠道签名唯一标识 + */ + private String apiSignatureId; + + /** + * 名称 + */ + private String name; + + /** + * 签名值 + */ + private String signature; + + /** + * 备注 + */ + private String remark; + + /** + * 启用状态(0正常 1停用) + */ + private Integer status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsLog.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsLog.java new file mode 100644 index 000000000..e5af0471e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsLog.java @@ -0,0 +1,71 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 短信日志 + * + * @author zzf + * @since 2021-01-25 + */ +@Data +@EqualsAndHashCode +@TableName(value = "sms_log", autoResultMap = true) +public class SmsLog implements Serializable { + + /** + * 自增编号 + */ + private Long id; + + /** + * 短信渠道编码(来自枚举类) + */ + private String channelCode; + + /** + * 实际渠道短信唯一标识 + */ + private String apiSmsId; + + /** + * 模板id + */ + private Long templateId; + + /** + * 手机号 + */ + private String phone; + + /** + * 内容 + */ + private String content; + + /** + * 备注 + */ + private String remark; + + /** + * 发送状态(0发送中 1成功 2失败) + */ + private Integer sendStatus; + + /** + * 创建者 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsTemplateDO.java b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsTemplateDO.java new file mode 100644 index 000000000..39257a71d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/dal/mysql/daoobject/sms/SmsTemplateDO.java @@ -0,0 +1,81 @@ +package cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms; + +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 短信模板 + * + * @author zzf + * @since 2021-01-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sms_template", autoResultMap = true) +public class SmsTemplateDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + /** + * 短信渠道编码(来自枚举类) + */ + private String channelCode; + + /** + * 短信渠道id (对于前端来说就是绑定一个签名) + */ + private Long channelId; + + /** + * 消息类型 [0验证码 1短信通知 2推广短信 3国际/港澳台消息] + */ + private Integer type; + + /** + * 业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板) + */ + private String bizCode; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 实际渠道模板唯一标识 + */ + private String apiTemplateId; + + /** + * 内容 + */ + private String content; + + /** + * 参数数组(自动根据内容生成) + */ + private String params; + + /** + * 备注 + */ + private String remark; + + /** + * 启用状态(0正常 1停用) + */ + private Integer status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/package-info.java b/src/main/java/cn/iocoder/dashboard/modules/msg/package-info.java new file mode 100644 index 000000000..3a0e7635c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/package-info.java @@ -0,0 +1,6 @@ +/** + * msg 包,专门专门用于发送消息的功能,支撑上层的通用与核心业务。 + * 例如说:短信、邮件、app通知等等 + * + */ +package cn.iocoder.dashboard.modules.msg; diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsChannelService.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsChannelService.java new file mode 100644 index 000000000..ed2c047e8 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsChannelService.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.modules.msg.service.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp.SmsChannelEnumRespVO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; + +import java.util.List; + +/** + * 短信渠道Service接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsChannelService { + + PageResult pageChannels(SmsChannelPageReqVO reqVO); + + Long createChannel(SmsChannelCreateReqVO reqVO); + + List getChannelEnums(); + + /** + * 查询渠道(包含名下模块)信息集合 + * + * @return 渠道(包含名下模块)信息集合 + */ + List listChannelAllEnabledInfo(); + + boolean flushChannel(); +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsLogService.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsLogService.java new file mode 100644 index 000000000..70f4d9eeb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsLogService.java @@ -0,0 +1,10 @@ +package cn.iocoder.dashboard.modules.msg.service.sms; + +/** + * 短信渠道Service接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsLogService { +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsTemplateService.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsTemplateService.java new file mode 100644 index 000000000..2c1364934 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/SmsTemplateService.java @@ -0,0 +1,10 @@ +package cn.iocoder.dashboard.modules.msg.service.sms; + +/** + * 短信渠道Service接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsTemplateService { +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsChannelServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsChannelServiceImpl.java new file mode 100644 index 000000000..e5202f491 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsChannelServiceImpl.java @@ -0,0 +1,88 @@ +package cn.iocoder.dashboard.modules.msg.service.sms.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.dashboard.common.enums.SmsChannelEnum; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.msg.sms.factory.AbstractSmsSenderFactory; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.SmsChannelAllVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.req.SmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.msg.controller.sms.vo.resp.SmsChannelEnumRespVO; +import cn.iocoder.dashboard.modules.msg.convert.sms.SmsChannelConvert; +import cn.iocoder.dashboard.modules.msg.convert.sms.SmsTemplateConvert; +import cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms.SmsChannelMapper; +import cn.iocoder.dashboard.modules.msg.dal.mysql.dao.sms.SmsTemplateMapper; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsChannelDO; +import cn.iocoder.dashboard.modules.msg.dal.mysql.daoobject.sms.SmsTemplateDO; +import cn.iocoder.dashboard.modules.msg.service.sms.SmsChannelService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 短信渠道Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +public class SmsChannelServiceImpl implements SmsChannelService { + + @Resource + private SmsChannelMapper mapper; + + @Resource + private SmsTemplateMapper templateMapper; + + @Override + public PageResult pageChannels(SmsChannelPageReqVO reqVO) { + return SmsChannelConvert.INSTANCE.convertPage(mapper.selectChannelPage(reqVO)); + } + + @Override + public Long createChannel(SmsChannelCreateReqVO reqVO) { + SmsChannelDO channelDO = SmsChannelConvert.INSTANCE.convert(reqVO); + mapper.insert(channelDO); + return channelDO.getId(); + } + + @Override + public List getChannelEnums() { + return SmsChannelConvert.INSTANCE.convertEnum(Arrays.asList(SmsChannelEnum.values())); + } + + @Override + public List listChannelAllEnabledInfo() { + List channelDOList = mapper.selectEnabledList(); + if (ObjectUtil.isNull(channelDOList)) { + return null; + } + List channelAllVOList = SmsChannelConvert.INSTANCE.convert(channelDOList); + + channelAllVOList.forEach(smsChannelDO -> { + + List templateDOList = templateMapper.selectListByChannelId(smsChannelDO.getId()); + if (ObjectUtil.isNull(templateDOList)) { + templateDOList = new ArrayList<>(); + } + smsChannelDO.setTemplateList(SmsTemplateConvert.INSTANCE.convert(templateDOList)); + }); + return channelAllVOList; + } + + @Override + public boolean flushChannel() { + AbstractSmsSenderFactory smsSenderFactory = SpringUtil.getBean(AbstractSmsSenderFactory.class); + if (smsSenderFactory == null) { + return false; + } + + smsSenderFactory.flush(listChannelAllEnabledInfo()); + + return true; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsLogServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsLogServiceImpl.java new file mode 100644 index 000000000..f7e348a4d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsLogServiceImpl.java @@ -0,0 +1,15 @@ +package cn.iocoder.dashboard.modules.msg.service.sms.impl; + +import cn.iocoder.dashboard.modules.msg.service.sms.SmsLogService; +import org.springframework.stereotype.Service; + +/** + * 短信日志Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +public class SmsLogServiceImpl implements SmsLogService { + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsTemplateServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsTemplateServiceImpl.java new file mode 100644 index 000000000..caf6d8e1c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/msg/service/sms/impl/SmsTemplateServiceImpl.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.modules.msg.service.sms.impl; + +import cn.iocoder.dashboard.modules.msg.service.sms.SmsTemplateService; +import org.springframework.stereotype.Service; + +/** + * 短信模板Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +public class SmsTemplateServiceImpl implements SmsTemplateService { +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java index 1bbde5784..5aa257a97 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java @@ -75,4 +75,11 @@ public interface SysErrorCodeConstants { // ========== 文件 1002009000 ========== ErrorCode FILE_PATH_EXISTS = new ErrorCode(1002009001, "文件路径已经存在"); + + // ========== 消息 1003001000 ========== + ErrorCode SMS_CHANNEL_NOT_FOUND = new ErrorCode(1003001001, "没有短信渠道信息, 请初始化sms_channel表数据。"); + ErrorCode SMS_TEMPLATE_NOT_FOUND = new ErrorCode(1003001002, "没有短信模板信息, 请初始化sms_template表数据。"); + ErrorCode SMS_SENDER_NOT_FOUND = new ErrorCode(1003001003, "没有找到对应的短信发送对象,请检查sms_channel表和sms_template表数据"); + ErrorCode INVALID_CHANNEL_CODE = new ErrorCode(1003001004, "非法的短信渠道code,请检查sms_channel表的code值是否与SmsChannelEnum中的code值一致。"); + }