Commit 953d270dd90c5772d48e39c1622272db07071c0a

Authored by YunaiV
1 parent be8b892b

增加 sms 的缓存

Showing 18 changed files with 306 additions and 32 deletions
ruoyi-ui/src/views/system/sms/smsLog.vue
... ... @@ -84,7 +84,7 @@
84 84 <div>{{ parseTime(scope.row.receiveTime) }}</div>
85 85 </template>
86 86 </el-table-column>
87   - <el-table-column label="短信渠道" align="center" width="100">
  87 + <el-table-column label="短信渠道" align="center" width="120">
88 88 <template slot-scope="scope">
89 89 <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
90 90 <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
... ...
ruoyi-ui/src/views/system/sms/smsTemplate.vue
... ... @@ -68,7 +68,7 @@
68 68 </el-table-column>
69 69 <el-table-column label="备注" align="center" prop="remark" />
70 70 <el-table-column label="短信 API 的模板编号" align="center" prop="apiTemplateId" width="180" />
71   - <el-table-column label="短信渠道" align="center">
  71 + <el-table-column label="短信渠道" align="center" width="120">
72 72 <template slot-scope="scope">
73 73 <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
74 74 <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
... ...
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java
... ... @@ -50,7 +50,7 @@ public abstract class AbstractSmsClient implements SmsClient {
50 50  
51 51 public final void refresh(SmsChannelProperties properties) {
52 52 // 判断是否更新
53   - if (!properties.equals(this.properties)) {
  53 + if (properties.equals(this.properties)) {
54 54 return;
55 55 }
56 56 log.info("[refresh][配置({})发生变化,重新初始化]", properties);
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java
... ... @@ -7,7 +7,9 @@ import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChann
7 7 import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
8 8 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
9 9 import org.apache.ibatis.annotations.Mapper;
  10 +import org.apache.ibatis.annotations.Select;
10 11  
  12 +import java.util.Date;
11 13 import java.util.List;
12 14  
13 15 @Mapper
... ... @@ -21,9 +23,7 @@ public interface SysSmsChannelMapper extends BaseMapperX&lt;SysSmsChannelDO&gt; {
21 23 .orderByDesc("id"));
22 24 }
23 25  
24   - default List<SysSmsChannelDO> selectListByStatus(Integer status) {
25   - return selectList(new LambdaQueryWrapper<SysSmsChannelDO>().eq(SysSmsChannelDO::getStatus, status)
26   - .orderByAsc(SysSmsChannelDO::getId));
27   - }
  26 + @Select("SELECT id FROM sys_sms_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
  27 + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
28 28  
29 29 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java
... ... @@ -7,7 +7,9 @@ import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemp
7 7 import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
8 8 import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
9 9 import org.apache.ibatis.annotations.Mapper;
  10 +import org.apache.ibatis.annotations.Select;
10 11  
  12 +import java.util.Date;
11 13 import java.util.List;
12 14  
13 15 @Mapper
... ... @@ -45,4 +47,7 @@ public interface SysSmsTemplateMapper extends BaseMapperX&lt;SysSmsTemplateDO&gt; {
45 47 return selectCount("channel_id", channelId);
46 48 }
47 49  
  50 + @Select("SELECT id FROM sys_sms_template WHERE update_time > #{maxUpdateTime} LIMIT 1")
  51 + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
  52 +
48 53 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
  5 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link SysSmsChannelRefreshMessage} 的消费者
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class SysSmsChannelRefreshConsumer extends AbstractChannelMessageListener<SysSmsChannelRefreshMessage> {
  19 +
  20 + @Resource
  21 + private SysSmsChannelService smsChannelService;
  22 +
  23 + @Override
  24 + public void onMessage(SysSmsChannelRefreshMessage message) {
  25 + log.info("[onMessage][收到 SmsChannel 刷新消息]");
  26 + smsChannelService.initSmsClients();
  27 + }
  28 +
  29 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
  5 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link SysSmsTemplateRefreshMessage} 的消费者
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class SysSmsTemplateRefreshConsumer extends AbstractChannelMessageListener<SysSmsTemplateRefreshMessage> {
  19 +
  20 + @Resource
  21 + private SysSmsTemplateService smsTemplateService;
  22 +
  23 + @Override
  24 + public void onMessage(SysSmsTemplateRefreshMessage message) {
  25 + log.info("[onMessage][收到 SmsTemplate 刷新消息]");
  26 + smsTemplateService.initLocalCache();
  27 + }
  28 +
  29 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 短信渠道的数据刷新 Message
  8 + */
  9 +@Data
  10 +public class SysSmsChannelRefreshMessage implements ChannelMessage {
  11 +
  12 + @Override
  13 + public String getChannel() {
  14 + return "system.sms-channel.refresh";
  15 + }
  16 +
  17 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 短信模板的数据刷新 Message
  8 + */
  9 +@Data
  10 +public class SysSmsTemplateRefreshMessage implements ChannelMessage {
  11 +
  12 + @Override
  13 + public String getChannel() {
  14 + return "system.sms-template.refresh";
  15 + }
  16 +
  17 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java
... ... @@ -9,6 +9,8 @@ import javax.annotation.Resource;
9 9  
10 10 /**
11 11 * Role 角色相关消息的 Producer
  12 + *
  13 + * @author 芋道源码
12 14 */
13 15 @Component
14 16 public class SysRoleProducer {
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java
... ... @@ -2,7 +2,9 @@ package cn.iocoder.dashboard.modules.system.mq.producer.sms;
2 2  
3 3 import cn.iocoder.dashboard.common.core.KeyValue;
4 4 import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
  5 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
5 6 import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  7 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
6 8 import lombok.extern.slf4j.Slf4j;
7 9 import org.springframework.data.redis.core.StringRedisTemplate;
8 10 import org.springframework.stereotype.Component;
... ... @@ -11,7 +13,7 @@ import javax.annotation.Resource;
11 13 import java.util.List;
12 14  
13 15 /**
14   - * 短信发送流消息监听器
  16 + * Sms 短信相关消息的 Producer
15 17 *
16 18 * @author zzf
17 19 * @date 2021/3/9 16:35
... ... @@ -24,7 +26,7 @@ public class SysSmsProducer {
24 26 private StringRedisTemplate stringRedisTemplate;
25 27  
26 28 /**
27   - * 发送短信 Message
  29 + * 发送 {@link SysSmsSendMessage} 消息
28 30 *
29 31 * @param logId 短信日志编号
30 32 * @param mobile 手机号
... ... @@ -39,4 +41,20 @@ public class SysSmsProducer {
39 41 RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
40 42 }
41 43  
  44 + /**
  45 + * 发送 {@link SysSmsChannelRefreshMessage} 消息
  46 + */
  47 + public void sendSmsChannelRefreshMessage() {
  48 + SysSmsChannelRefreshMessage message = new SysSmsChannelRefreshMessage();
  49 + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
  50 + }
  51 +
  52 + /**
  53 + * 发送 {@link SysSmsTemplateRefreshMessage} 消息
  54 + */
  55 + public void sendSmsTemplateRefreshMessage() {
  56 + SysSmsTemplateRefreshMessage message = new SysSmsTemplateRefreshMessage();
  57 + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
  58 + }
  59 +
42 60 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/package-info.java deleted
1   -package cn.iocoder.dashboard.modules.system.service;
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java
... ... @@ -18,7 +18,7 @@ import java.util.Set;
18 18 public interface SysPermissionService extends SecurityPermissionFrameworkService {
19 19  
20 20 /**
21   - * 初始化
  21 + * 初始化权限的本地缓存
22 22 */
23 23 void initLocalCache();
24 24  
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
... ... @@ -58,7 +58,7 @@ public class SysRoleServiceImpl implements SysRoleService {
58 58 */
59 59 private volatile Map<Long, SysRoleDO> roleCache;
60 60 /**
61   - * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
  61 + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
62 62 */
63 63 private volatile Date maxUpdateTime;
64 64  
... ... @@ -77,7 +77,7 @@ public class SysRoleServiceImpl implements SysRoleService {
77 77 @Override
78 78 @PostConstruct
79 79 public void initLocalCache() {
80   - // 获取菜单列表,如果有更新
  80 + // 获取角色列表,如果有更新
81 81 List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime);
82 82 if (CollUtil.isEmpty(roleList)) {
83 83 return;
... ... @@ -98,23 +98,23 @@ public class SysRoleServiceImpl implements SysRoleService {
98 98 }
99 99  
100 100 /**
101   - * 如果菜单发生变化,从数据库中获取最新的全量菜单。
  101 + * 如果角色发生变化,从数据库中获取最新的全量角色。
102 102 * 如果未发生变化,则返回空
103 103 *
104   - * @param maxUpdateTime 当前菜单的最大更新时间
105   - * @return 菜单列表
  104 + * @param maxUpdateTime 当前角色的最大更新时间
  105 + * @return 角色列表
106 106 */
107 107 private List<SysRoleDO> loadRoleIfUpdate(Date maxUpdateTime) {
108 108 // 第一步,判断是否要更新。
109 109 if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
110   - log.info("[loadRoleIfUpdate][首次加载全量菜单]");
111   - } else { // 判断数据库中是否有更新的菜单
  110 + log.info("[loadRoleIfUpdate][首次加载全量角色]");
  111 + } else { // 判断数据库中是否有更新的角色
112 112 if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
113 113 return null;
114 114 }
115   - log.info("[loadRoleIfUpdate][增量加载全量菜单]");
  115 + log.info("[loadRoleIfUpdate][增量加载全量角色]");
116 116 }
117   - // 第二步,如果有更新,则从数据库加载所有菜单
  117 + // 第二步,如果有更新,则从数据库加载所有角色
118 118 return roleMapper.selectList();
119 119 }
120 120  
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateService.java
... ... @@ -21,6 +21,11 @@ import java.util.Map;
21 21 public interface SysSmsTemplateService {
22 22  
23 23 /**
  24 + * 初始化短信模板的本地缓存
  25 + */
  26 + void initLocalCache();
  27 +
  28 + /**
24 29 * 获得短信模板
25 30 *
26 31 * @param code 模板编码
... ... @@ -29,6 +34,14 @@ public interface SysSmsTemplateService {
29 34 SysSmsTemplateDO getSmsTemplateByCode(String code);
30 35  
31 36 /**
  37 + * 获得短信模板,从缓存中
  38 + *
  39 + * @param code 模板编码
  40 + * @return 短信模板
  41 + */
  42 + SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code);
  43 +
  44 + /**
32 45 * 格式化短信内容
33 46 *
34 47 * @param content 短信模板的内容
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsChannelServiceImpl.java
1 1 package cn.iocoder.dashboard.modules.system.service.sms.impl;
2 2  
3   -import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  3 +import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
5 6 import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
6 7 import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
7 8 import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
... ... @@ -10,13 +11,18 @@ import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChann
10 11 import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert;
11 12 import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
12 13 import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper;
  14 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
13 15 import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
14 16 import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.springframework.scheduling.annotation.Scheduled;
15 19 import org.springframework.stereotype.Service;
16 20  
17 21 import javax.annotation.PostConstruct;
18 22 import javax.annotation.Resource;
19 23 import java.util.Collection;
  24 +import java.util.Comparator;
  25 +import java.util.Date;
20 26 import java.util.List;
21 27  
22 28 import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -30,8 +36,20 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SM
30 36 * @date 2021/1/25 9:25
31 37 */
32 38 @Service
  39 +@Slf4j
33 40 public class SysSmsChannelServiceImpl implements SysSmsChannelService {
34 41  
  42 + /**
  43 + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
  44 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
  45 + */
  46 + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
  47 +
  48 + /**
  49 + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
  50 + */
  51 + private volatile Date maxUpdateTime;
  52 +
35 53 @Resource
36 54 private SmsClientFactory smsClientFactory;
37 55  
... ... @@ -41,23 +59,61 @@ public class SysSmsChannelServiceImpl implements SysSmsChannelService {
41 59 @Resource
42 60 private SysSmsTemplateService smsTemplateService;
43 61  
  62 + @Resource
  63 + private SysSmsProducer smsProducer;
  64 +
44 65 @Override
45 66 @PostConstruct
46 67 public void initSmsClients() {
47   - // 查询有效渠道信息
48   - List<SysSmsChannelDO> channelDOList = smsChannelMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
49   - // 创建渠道 Client
50   - List<SmsChannelProperties> propertiesList = SysSmsChannelConvert.INSTANCE.convertList02(channelDOList);
  68 + // 获取短信渠道,如果有更新
  69 + List<SysSmsChannelDO> smsChannels = this.loadSmsChannelIfUpdate(maxUpdateTime);
  70 + if (CollUtil.isEmpty(smsChannels)) {
  71 + return;
  72 + }
  73 +
  74 + // 创建或更新短信 Client
  75 + List<SmsChannelProperties> propertiesList = SysSmsChannelConvert.INSTANCE.convertList02(smsChannels);
51 76 propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties));
  77 +
  78 + // 写入缓存
  79 + assert smsChannels.size() > 0; // 断言,避免告警
  80 + maxUpdateTime = smsChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
  81 + log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size());
52 82 }
53 83  
54   - // TODO 芋艿:刷新缓存
  84 + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
  85 + public void schedulePeriodicRefresh() {
  86 + initSmsClients();
  87 + }
  88 +
  89 + /**
  90 + * 如果短信渠道发生变化,从数据库中获取最新的全量短信渠道。
  91 + * 如果未发生变化,则返回空
  92 + *
  93 + * @param maxUpdateTime 当前短信渠道的最大更新时间
  94 + * @return 短信渠道列表
  95 + */
  96 + private List<SysSmsChannelDO> loadSmsChannelIfUpdate(Date maxUpdateTime) {
  97 + // 第一步,判断是否要更新。
  98 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
  99 + log.info("[loadSmsChannelIfUpdate][首次加载全量短信渠道]");
  100 + } else { // 判断数据库中是否有更新的短信渠道
  101 + if (smsChannelMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
  102 + return null;
  103 + }
  104 + log.info("[loadSmsChannelIfUpdate][增量加载全量短信渠道]");
  105 + }
  106 + // 第二步,如果有更新,则从数据库加载所有短信渠道
  107 + return smsChannelMapper.selectList();
  108 + }
55 109  
56 110 @Override
57 111 public Long createSmsChannel(SysSmsChannelCreateReqVO createReqVO) {
58 112 // 插入
59 113 SysSmsChannelDO smsChannel = SysSmsChannelConvert.INSTANCE.convert(createReqVO);
60 114 smsChannelMapper.insert(smsChannel);
  115 + // 发送刷新消息
  116 + smsProducer.sendSmsChannelRefreshMessage();
61 117 // 返回
62 118 return smsChannel.getId();
63 119 }
... ... @@ -69,6 +125,8 @@ public class SysSmsChannelServiceImpl implements SysSmsChannelService {
69 125 // 更新
70 126 SysSmsChannelDO updateObj = SysSmsChannelConvert.INSTANCE.convert(updateReqVO);
71 127 smsChannelMapper.updateById(updateObj);
  128 + // 发送刷新消息
  129 + smsProducer.sendSmsChannelRefreshMessage();
72 130 }
73 131  
74 132 @Override
... ... @@ -79,8 +137,10 @@ public class SysSmsChannelServiceImpl implements SysSmsChannelService {
79 137 if (smsTemplateService.countByChannelId(id) > 0) {
80 138 throw exception(SMS_CHANNEL_HAS_CHILDREN);
81 139 }
82   - // 更新
  140 + // 删除
83 141 smsChannelMapper.deleteById(id);
  142 + // 发送刷新消息
  143 + smsProducer.sendSmsChannelRefreshMessage();
84 144 }
85 145  
86 146 private void validateSmsChannelExists(Long id) {
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsServiceImpl.java
... ... @@ -98,8 +98,9 @@ public class SysSmsServiceImpl implements SysSmsService {
98 98 }
99 99  
100 100 private SysSmsTemplateDO checkSmsTemplateValid(String templateCode) {
  101 + // 获得短信模板。考虑到效率,从缓存中获取
  102 + SysSmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
101 103 // 短信模板不存在
102   - SysSmsTemplateDO template = smsTemplateService.getSmsTemplateByCode(templateCode);
103 104 if (template == null) {
104 105 throw exception(SMS_TEMPLATE_NOT_EXISTS);
105 106 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java
1 1 package cn.iocoder.dashboard.modules.system.service.sms.impl;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.hutool.core.util.ReUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
6 7 import cn.iocoder.dashboard.common.pojo.PageResult;
  8 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
7 9 import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
8 10 import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
9 11 import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
... ... @@ -16,17 +18,19 @@ import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert;
16 18 import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
17 19 import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
18 20 import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsTemplateMapper;
  21 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
19 22 import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
20 23 import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
21 24 import com.google.common.annotations.VisibleForTesting;
  25 +import com.google.common.collect.ImmutableMap;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +import org.springframework.scheduling.annotation.Scheduled;
22 28 import org.springframework.stereotype.Service;
23 29 import org.springframework.util.Assert;
24 30  
  31 +import javax.annotation.PostConstruct;
25 32 import javax.annotation.Resource;
26   -import java.util.Collection;
27   -import java.util.List;
28   -import java.util.Map;
29   -import java.util.Objects;
  33 +import java.util.*;
30 34 import java.util.regex.Pattern;
31 35  
32 36 import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -39,6 +43,7 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
39 43 * @date 2021/1/25 9:25
40 44 */
41 45 @Service
  46 +@Slf4j
42 47 public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
43 48  
44 49 /**
... ... @@ -46,6 +51,24 @@ public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
46 51 */
47 52 private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}");
48 53  
  54 + /**
  55 + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
  56 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
  57 + */
  58 + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
  59 +
  60 + /**
  61 + * 短信模板缓存
  62 + * key:短信模板编码 {@link SysSmsTemplateDO#getCode()}
  63 + *
  64 + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
  65 + */
  66 + private volatile Map<String, SysSmsTemplateDO> smsTemplateCache;
  67 + /**
  68 + * 缓存短信模板的最大更新时间,用于后续的增量轮询,判断是否有更新
  69 + */
  70 + private volatile Date maxUpdateTime;
  71 +
49 72 @Resource
50 73 private SysSmsTemplateMapper smsTemplateMapper;
51 74  
... ... @@ -55,12 +78,67 @@ public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
55 78 @Resource
56 79 private SmsClientFactory smsClientFactory;
57 80  
  81 + @Resource
  82 + private SysSmsProducer smsProducer;
  83 +
  84 + /**
  85 + * 初始化 {@link #smsTemplateCache} 缓存
  86 + */
  87 + @Override
  88 + @PostConstruct
  89 + public void initLocalCache() {
  90 + // 获取短信模板列表,如果有更新
  91 + List<SysSmsTemplateDO> smsTemplateList = this.loadSmsTemplateIfUpdate(maxUpdateTime);
  92 + if (CollUtil.isEmpty(smsTemplateList)) {
  93 + return;
  94 + }
  95 +
  96 + // 写入缓存
  97 + ImmutableMap.Builder<String, SysSmsTemplateDO> builder = ImmutableMap.builder();
  98 + smsTemplateList.forEach(sysSmsTemplateDO -> builder.put(sysSmsTemplateDO.getCode(), sysSmsTemplateDO));
  99 + smsTemplateCache = builder.build();
  100 + assert smsTemplateList.size() > 0; // 断言,避免告警
  101 + maxUpdateTime = smsTemplateList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
  102 + log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size());
  103 + }
  104 +
  105 + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
  106 + public void schedulePeriodicRefresh() {
  107 + initLocalCache();
  108 + }
  109 +
  110 + /**
  111 + * 如果短信模板发生变化,从数据库中获取最新的全量短信模板。
  112 + * 如果未发生变化,则返回空
  113 + *
  114 + * @param maxUpdateTime 当前短信模板的最大更新时间
  115 + * @return 短信模板列表
  116 + */
  117 + private List<SysSmsTemplateDO> loadSmsTemplateIfUpdate(Date maxUpdateTime) {
  118 + // 第一步,判断是否要更新。
  119 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
  120 + log.info("[loadSmsTemplateIfUpdate][首次加载全量短信模板]");
  121 + } else { // 判断数据库中是否有更新的短信模板
  122 + if (smsTemplateMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
  123 + return null;
  124 + }
  125 + log.info("[loadSmsTemplateIfUpdate][增量加载全量短信模板]");
  126 + }
  127 + // 第二步,如果有更新,则从数据库加载所有短信模板
  128 + return smsTemplateMapper.selectList();
  129 + }
  130 +
58 131 @Override
59 132 public SysSmsTemplateDO getSmsTemplateByCode(String code) {
60 133 return smsTemplateMapper.selectByCode(code);
61 134 }
62 135  
63 136 @Override
  137 + public SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code) {
  138 + return smsTemplateCache.get(code);
  139 + }
  140 +
  141 + @Override
64 142 public String formatSmsTemplateContent(String content, Map<String, Object> params) {
65 143 return StrUtil.format(content, params);
66 144 }
... ... @@ -84,6 +162,8 @@ public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
84 162 template.setParams(parseTemplateContentParams(template.getContent()));
85 163 template.setChannelCode(channelDO.getCode());
86 164 smsTemplateMapper.insert(template);
  165 + // 发送刷新消息
  166 + smsProducer.sendSmsTemplateRefreshMessage();
87 167 // 返回
88 168 return template.getId();
89 169 }
... ... @@ -104,6 +184,8 @@ public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
104 184 updateObj.setParams(parseTemplateContentParams(updateObj.getContent()));
105 185 updateObj.setChannelCode(channelDO.getCode());
106 186 smsTemplateMapper.updateById(updateObj);
  187 + // 发送刷新消息
  188 + smsProducer.sendSmsTemplateRefreshMessage();
107 189 }
108 190  
109 191 @Override
... ... @@ -112,6 +194,8 @@ public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
112 194 this.validateSmsTemplateExists(id);
113 195 // 更新
114 196 smsTemplateMapper.deleteById(id);
  197 + // 发送刷新消息
  198 + smsProducer.sendSmsTemplateRefreshMessage();
115 199 }
116 200  
117 201 private void validateSmsTemplateExists(Long id) {
... ...