diff --git a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java index e2f2559d8..14f1d0ba0 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java +++ b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java @@ -4,10 +4,13 @@ import cn.iocoder.dashboard.common.pojo.PageParam; import cn.iocoder.dashboard.common.pojo.PageResult; import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Param; +import java.util.List; + /** * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 */ @@ -21,4 +24,8 @@ public interface BaseMapperX extends BaseMapper { return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); } + default List selectList() { + return selectList(new QueryWrapper<>()); + } + } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java index 59dbe287a..7dc56bbfd 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java @@ -1,17 +1,17 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission; +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuListReqVO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import java.util.Date; import java.util.List; @Mapper -public interface SysMenuMapper extends BaseMapper { +public interface SysMenuMapper extends BaseMapperX { default SysMenuDO selectByParentIdAndName(Long parentId, String name) { return selectOne(new QueryWrapper().eq("parent_id", parentId) @@ -27,10 +27,6 @@ public interface SysMenuMapper extends BaseMapper { .eqIfPresent("status", reqVO.getStatus())); } - default List selectList() { - return selectList(new QueryWrapper<>()); - } - default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { return selectOne(new QueryWrapper().select("id") .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMapper.java index eb6345a8b..e1f74bc01 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMapper.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMapper.java @@ -1,20 +1,22 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission; +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleExportReqVO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Mapper; import org.springframework.lang.Nullable; import java.util.Collection; +import java.util.Date; import java.util.List; @Mapper -public interface SysRoleMapper extends BaseMapper { +public interface SysRoleMapper extends BaseMapperX { default IPage selectPage(SysRolePageReqVO reqVO) { return selectPage(MyBatisUtils.buildPage(reqVO), @@ -43,4 +45,9 @@ public interface SysRoleMapper extends BaseMapper { return selectList(new QueryWrapperX().in("status", statuses)); } + default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { + return selectOne(new QueryWrapper().select("id") + .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; + } + } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleRefreshConsumer.java new file mode 100644 index 000000000..a5e77f7e5 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.mq.consumer.permission; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleRefreshMessage; +import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SysRoleRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SysRoleRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SysRoleService roleService; + + @Override + public void onMessage(SysRoleRefreshMessage message) { + log.info("[onMessage][收到 Role 刷新消息]"); + roleService.initLocalCache(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleRefreshMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleRefreshMessage.java new file mode 100644 index 000000000..b99401021 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.modules.system.mq.message.permission; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import lombok.Data; + +/** + * 角色数据刷新 Message + */ +@Data +public class SysRoleRefreshMessage implements ChannelMessage { + + @Override + public String getChannel() { + return "system.role.refresh"; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java new file mode 100644 index 000000000..e11945dfe --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.modules.system.mq.producer.permission; + +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; +import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleRefreshMessage; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Role 角色相关消息的 Producer + */ +@Component +public class SysRoleProducer { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 发送 {@link SysRoleRefreshMessage} 消息 + */ + public void sendRoleRefreshMessage() { + SysRoleRefreshMessage message = new SysRoleRefreshMessage(); + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java index db41e7111..d3ff381ef 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java @@ -88,7 +88,7 @@ public class SysDeptServiceImpl implements SysDeptService { parentDeptCache = parentBuilder.build(); assert deptList.size() > 0; // 断言,避免告警 maxUpdateTime = deptList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); - log.info("[init][初始化 Dept 数量为 {}]", deptList.size()); + log.info("[initLocalCache][初始化 Dept 数量为 {}]", deptList.size()); } @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) @@ -201,7 +201,6 @@ public class SysDeptServiceImpl implements SysDeptService { // 删除部门 deptMapper.deleteById(id); // TODO 需要处理下与角色的数据权限关联,等做数据权限一起处理下 - // 发送刷新消息 deptProducer.sendDeptRefreshMessage(); } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java index e33360fe7..338a6ef35 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java @@ -100,7 +100,7 @@ public class SysDictDataServiceImpl implements SysDictDataService { valueDictDataCache = valueDictDataBuilder.build(); assert dataList.size() > 0; // 断言,避免告警 maxUpdateTime = dataList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); - log.info("[init][缓存字典数据,数量为:{}]", dataList.size()); + log.info("[initLocalCache][缓存字典数据,数量为:{}]", dataList.size()); } @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java index 161a0d810..1c9b2a817 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java @@ -20,9 +20,9 @@ import java.util.Set; public interface SysRoleService { /** - * 初始化 + * 初始化角色的本地缓存 */ - void init(); + void initLocalCache(); /** * 获得角色,从缓存中 diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java index cb8dbadf0..97c889d11 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java @@ -98,7 +98,7 @@ public class SysMenuServiceImpl implements SysMenuService { permMenuCache = permMenuCacheBuilder.build(); assert menuList.size() > 0; // 断言,避免告警 maxUpdateTime = menuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); - log.info("[init][缓存菜单,数量为:{}]", menuList.size()); + log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size()); } @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java index 3cef2ac12..5a32be038 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java @@ -78,7 +78,7 @@ public class SysPermissionServiceImpl implements SysPermissionService { }); roleMenuCache = roleMenuCacheBuilder.build(); menuRoleCache = menuRoleCacheBuilder.build(); - log.info("[init][初始化角色与菜单的关联数量为 {}]", roleMenuList.size()); + log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size()); } @Override diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java index d98611cbf..cf21a8e6b 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java @@ -1,9 +1,11 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.iocoder.dashboard.common.enums.CommonStatusEnum; import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleCreateReqVO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleExportReqVO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; @@ -13,14 +15,18 @@ import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysRoleMappe import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO; import cn.iocoder.dashboard.modules.system.enums.permission.RoleCodeEnum; import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; import com.baomidou.mybatisplus.core.metadata.IPage; import com.google.common.collect.ImmutableMap; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.StringUtils; import javax.annotation.PostConstruct; @@ -39,6 +45,12 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; @Slf4j public class SysRoleServiceImpl implements SysRoleService { + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + /** * 角色缓存 * key:角色编号 {@link SysRoleDO#getId()} @@ -46,6 +58,10 @@ public class SysRoleServiceImpl implements SysRoleService { * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 */ private volatile Map roleCache; + /** + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; @Resource private SysPermissionService permissionService; @@ -53,19 +69,54 @@ public class SysRoleServiceImpl implements SysRoleService { @Resource private SysRoleMapper roleMapper; + @Resource + private SysRoleProducer roleProducer; + /** * 初始化 {@link #roleCache} 缓存 */ @Override @PostConstruct - public void init() { - // 从数据库中读取 - List roleDOList = roleMapper.selectList(null); + public void initLocalCache() { + // 获取菜单列表,如果有更新 + List roleList = this.loadRoleIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(roleList)) { + return; + } + // 写入缓存 ImmutableMap.Builder builder = ImmutableMap.builder(); - roleDOList.forEach(sysRoleDO -> builder.put(sysRoleDO.getId(), sysRoleDO)); + roleList.forEach(sysRoleDO -> builder.put(sysRoleDO.getId(), sysRoleDO)); roleCache = builder.build(); - log.info("[init][初始化 Role 数量为 {}]", roleDOList.size()); + assert roleList.size() > 0; // 断言,避免告警 + maxUpdateTime = roleList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); + log.info("[initLocalCache][初始化 Role 数量为 {}]", roleList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果菜单发生变化,从数据库中获取最新的全量菜单。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前菜单的最大更新时间 + * @return 菜单列表 + */ + private List loadRoleIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadRoleIfUpdate][首次加载全量菜单]"); + } else { // 判断数据库中是否有更新的菜单 + if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { + return null; + } + log.info("[loadRoleIfUpdate][增量加载全量菜单]"); + } + // 第二步,如果有更新,则从数据库加载所有菜单 + return roleMapper.selectList(); } @Override @@ -104,6 +155,8 @@ public class SysRoleServiceImpl implements SysRoleService { role.setType(SysRoleTypeEnum.CUSTOM.getType()); role.setStatus(CommonStatusEnum.ENABLE.getStatus()); roleMapper.insert(role); + // 发送刷新消息 + roleProducer.sendRoleRefreshMessage(); // 返回 return role.getId(); } @@ -117,6 +170,8 @@ public class SysRoleServiceImpl implements SysRoleService { // 更新到数据库 SysRoleDO updateObject = SysRoleConvert.INSTANCE.convert(reqVO); roleMapper.updateById(updateObject); + // 发送刷新消息 + roleProducer.sendRoleRefreshMessage(); } @Override @@ -128,6 +183,15 @@ public class SysRoleServiceImpl implements SysRoleService { roleMapper.deleteById(id); // 删除相关数据 permissionService.processRoleDeleted(id); + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + roleProducer.sendRoleRefreshMessage(); + } + + }); } @Override