diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index 321599f00..078deb7f4 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -144,6 +144,7 @@ public interface ErrorCodeConstants { // ========== 站内信模版 1002023000 ========== ErrorCode NOTIFY_TEMPLATE_NOT_EXISTS = new ErrorCode(1002023000, "站内信模版不存在"); + ErrorCode NOTIFY_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002023001, "已经存在编码为【{}】的站内信模板"); // ========== 站内信 1002024000 ========== ErrorCode NOTIFY_MESSAGE_NOT_EXISTS = new ErrorCode(1002024000, "站内信不存在"); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java index 52dd3656e..f77126f6c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java @@ -13,6 +13,9 @@ public class NotifyTemplateRespVO extends NotifyTemplateBaseVO { @ApiModelProperty(value = "ID", required = true) private Long id; + @ApiModelProperty(value = "参数数组", example = "name,code") + private List params; + @ApiModelProperty(value = "创建时间", required = true) private Date createTime; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notify/NotifyTemplateDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notify/NotifyTemplateDO.java index aa7cce11e..2066f0b23 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notify/NotifyTemplateDO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notify/NotifyTemplateDO.java @@ -1,16 +1,22 @@ package cn.iocoder.yudao.module.system.dal.dataobject.notify; -import lombok.*; -import java.util.*; -import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; /** * 站内信模版 DO * * @author xrcoder */ -@TableName("system_notify_template") +@TableName(value = "system_notify_template", autoResultMap = true) @KeySequence("system_notify_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @@ -25,28 +31,35 @@ public class NotifyTemplateDO extends BaseDO { */ @TableId private Long id; + /** * 模版编码 */ private String code; + /** * 模版标题 */ private String title; + /** * 模版内容 */ private String content; + /** * 参数数组 */ - private String params; + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** * 状态:1-启用 0-禁用 - * - * 枚举 {@link TODO common_status 对应的类} + *

+ * 枚举 {@link CommonStatusEnum} */ private String status; + /** * 备注 */ diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java index 115055b09..c71a054b1 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java @@ -8,7 +8,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateExportReqVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; /** * 站内信模版 Mapper @@ -18,6 +20,13 @@ import org.apache.ibatis.annotations.Mapper; @Mapper public interface NotifyTemplateMapper extends BaseMapperX { + @Select("SELECT COUNT(*) FROM system_notify_template WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + + default NotifyTemplateDO selectByCode(String code) { + return selectOne(NotifyTemplateDO::getCode, code); + } + default PageResult selectPage(NotifyTemplatePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(NotifyTemplateDO::getCode, reqVO.getCode()) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/notify/NotifyTemplateRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/notify/NotifyTemplateRefreshConsumer.java new file mode 100644 index 000000000..c2d8133b3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/notify/NotifyTemplateRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.notify; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.notify.NotifyTemplateRefreshMessage; +import cn.iocoder.yudao.module.system.service.notify.NotifyTemplateService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link NotifyTemplateRefreshMessage} 的消费者 + * + * @author xrcoder + */ +@Component +@Slf4j +public class NotifyTemplateRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private NotifyTemplateService notifyTemplateService; + + @Override + public void onMessage(NotifyTemplateRefreshMessage message) { + log.info("[onMessage][收到 NotifyTemplate 刷新消息]"); + notifyTemplateService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/notify/NotifyTemplateRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/notify/NotifyTemplateRefreshMessage.java new file mode 100644 index 000000000..3eddf0bc6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/notify/NotifyTemplateRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.notify; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 站内信模板的数据刷新 Message + * + * @author xrcoder + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NotifyTemplateRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.notify-template.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/notify/NotifyProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/notify/NotifyProducer.java new file mode 100644 index 000000000..3affe2c2f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/notify/NotifyProducer.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.mq.producer.notify; + +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.message.notify.NotifyTemplateRefreshMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Notify 站内信相关消息的 Producer + * + * @author xrcoder + * @since 2022-08-06 + */ +@Slf4j +@Component +public class NotifyProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + + /** + * 发送 {@link NotifyTemplateRefreshMessage} 消息 + */ + public void sendNotifyTemplateRefreshMessage() { + NotifyTemplateRefreshMessage message = new NotifyTemplateRefreshMessage(); + redisMQTemplate.send(message); + } + + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java index 744d5585e..f7a9318a1 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.Notify import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; /** * 站内信模版 Service 接口 @@ -17,6 +18,29 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; */ public interface NotifyTemplateService { + /** + * 初始化站内信模板的本地缓存 + */ + void initLocalCache(); + + /** + * 获得站内信模板,从缓存中 + * + * @param code 模板编码 + * @return 站内信模板 + */ + NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code); + + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + String formatNotifyTemplateContent(String content, Map params); + /** * 创建站内信模版 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java index eb43af802..65b71d92e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java @@ -1,23 +1,34 @@ package cn.iocoder.yudao.module.system.service.notify; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateExportReqVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.notify.NotifyTemplateConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; +import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyTemplateMapper; +import cn.iocoder.yudao.module.system.mq.producer.notify.NotifyProducer; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; -import java.util.*; - -import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; -import cn.iocoder.yudao.framework.common.pojo.PageResult; - -import cn.iocoder.yudao.module.system.convert.notify.NotifyTemplateConvert; -import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyTemplateMapper; +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; /** * 站内信模版 Service 实现类 @@ -26,16 +37,123 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; */ @Service @Validated +@Slf4j public class NotifyTemplateServiceImpl implements NotifyTemplateService { + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + @Resource private NotifyTemplateMapper notifyTemplateMapper; + @Resource + private NotifyProducer notifyProducer; + + /** + * 站内信模板缓存 + * key:站内信模板编码 {@link NotifyTemplateDO#getCode()} + *

+ * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Map notifyTemplateCache; + + /** + * 缓存站内信模板的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + /** + * 初始化站内信模板的本地缓存 + */ + @Override + public void initLocalCache() { + // 获取站内信模板列表,如果有更新 + List notifyTemplateList = this.loadNotifyTemplateIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(notifyTemplateList)) { + return; + } + + // 写入缓存 + notifyTemplateCache = CollectionUtils.convertMap(notifyTemplateList, NotifyTemplateDO::getCode); + maxUpdateTime = CollectionUtils.getMaxValue(notifyTemplateList, NotifyTemplateDO::getUpdateTime); + log.info("[initLocalCache][初始化 NotifyTemplate 数量为 {}]", notifyTemplateList.size()); + + } + + /** + * 如果站内信模板发生变化,从数据库中获取最新的全量站内信模板。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前站内信模板的最大更新时间 + * @return 站内信模板列表 + */ + private List loadNotifyTemplateIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadNotifyTemplateIfUpdate][首次加载全量站内信模板]"); + } else { // 判断数据库中是否有更新的站内信模板 + if (notifyTemplateMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadNotifyTemplateIfUpdate][增量加载全量站内信模板]"); + } + // 第二步,如果有更新,则从数据库加载所有站内信模板 + return notifyTemplateMapper.selectList(); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + + /** + * 获得站内信模板,从缓存中 + * + * @param code 模板编码 + * @return 站内信模板 + */ + @Override + public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) { + return notifyTemplateCache.get(code); + } + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + @Override + public String formatNotifyTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + @Override public Long createNotifyTemplate(NotifyTemplateCreateReqVO createReqVO) { + // 校验站内信编码是否重复 + checkNotifyTemplateCodeDuplicate(null, createReqVO.getCode()); + // 插入 NotifyTemplateDO notifyTemplate = NotifyTemplateConvert.INSTANCE.convert(createReqVO); + notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent())); notifyTemplateMapper.insert(notifyTemplate); + // 发送刷新消息 + notifyProducer.sendNotifyTemplateRefreshMessage(); // 返回 return notifyTemplate.getId(); } @@ -44,9 +162,16 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService { public void updateNotifyTemplate(NotifyTemplateUpdateReqVO updateReqVO) { // 校验存在 this.validateNotifyTemplateExists(updateReqVO.getId()); + // 校验站内信编码是否重复 + checkNotifyTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + // 更新 NotifyTemplateDO updateObj = NotifyTemplateConvert.INSTANCE.convert(updateReqVO); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + notifyTemplateMapper.updateById(updateObj); + // 发送刷新消息 + notifyProducer.sendNotifyTemplateRefreshMessage(); } @Override @@ -55,6 +180,8 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService { this.validateNotifyTemplateExists(id); // 删除 notifyTemplateMapper.deleteById(id); + // 发送刷新消息 + notifyProducer.sendNotifyTemplateRefreshMessage(); } private void validateNotifyTemplateExists(Long id) { @@ -83,4 +210,18 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService { return notifyTemplateMapper.selectList(exportReqVO); } + @VisibleForTesting + public void checkNotifyTemplateCodeDuplicate(Long id, String code) { + NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + } }