From 2db6a4510cd4a43599bf85005298804291119a9a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 25 Feb 2023 22:42:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=94=B9=E9=80=A0=EF=BC=9ADe?= =?UTF-8?q?pt=20=E4=BD=BF=E7=94=A8=20Redis=20=E4=BD=9C=E4=B8=BA=E7=BC=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/YudaoTenantAutoConfiguration.java | 6 +- .../core/redis/TenantRedisCacheManager.java | 14 +- .../config/YudaoCacheAutoConfiguration.java | 17 +++ .../redis/core/TimeoutRedisCacheManager.java | 51 +++++++ .../system/dal/mysql/dept/DeptMapper.java | 8 +- .../system/dal/redis/RedisKeyConstants.java | 16 ++- .../mq/consumer/dept/DeptRefreshConsumer.java | 29 ---- .../mq/message/dept/DeptRefreshMessage.java | 21 --- .../system/mq/producer/dept/DeptProducer.java | 26 ---- .../system/service/dept/DeptService.java | 28 ++-- .../system/service/dept/DeptServiceImpl.java | 130 +++++------------- .../permission/PermissionServiceImpl.java | 16 +-- .../service/user/AdminUserServiceImpl.java | 3 +- .../permission/PermissionServiceTest.java | 4 +- .../user/AdminUserServiceImplTest.java | 4 +- .../src/main/resources/application.yaml | 1 + 16 files changed, 161 insertions(+), 213 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java delete mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java delete mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java delete mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java index ad6ae1a842..fe32004767 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java @@ -133,9 +133,9 @@ public class YudaoTenantAutoConfiguration { @Bean @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean - public RedisCacheManager tenantRedisCacheManager(RedisTemplate redisTemplate, - RedisCacheConfiguration redisCacheConfiguration, - TenantProperties tenantProperties) { + public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration, + TenantProperties tenantProperties) { // 创建 RedisCacheWriter 对象 RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java index 3e4a5cee3e..89ce0b526c 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.framework.tenant.core.redis; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager; import cn.iocoder.yudao.framework.tenant.config.TenantProperties; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import jodd.io.StreamUtil; @@ -13,12 +14,19 @@ import org.springframework.data.redis.cache.RedisCacheWriter; /** * 多租户的 {@link RedisCacheManager} 实现类 * - * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 + * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + "::t" + tenantId + 后缀 * * @author airhead */ @Slf4j -public class TenantRedisCacheManager extends RedisCacheManager { +public class TenantRedisCacheManager extends TimeoutRedisCacheManager { + + /** + * 多租户 Redis Key 的前缀,补充在原有 name 的 : 后面 + * + * 原因:如果只补充租户编号,可读性较差 + */ + private static final String PREFIX = "t"; private final TenantProperties tenantProperties; @@ -33,7 +41,7 @@ public class TenantRedisCacheManager extends RedisCacheManager { public Cache getCache(String name) { // 如果不忽略多租户的 Cache,则自动拼接租户后缀 if (!tenantProperties.getIgnoreCaches().contains(name)) { - name = name + StrUtil.COLON + TenantContextHolder.getRequiredTenantId(); + name = name + StrUtil.COLON + PREFIX + TenantContextHolder.getRequiredTenantId(); } // 继续基于父方法 diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java index d40ba4f777..79a75f8b1f 100644 --- a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.redis.config; +import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -7,9 +8,15 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; +import java.util.Objects; + import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer; /** @@ -50,4 +57,14 @@ public class YudaoCacheAutoConfiguration { return config; } + @Bean + public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + // 创建 TenantRedisCacheManager 对象 + return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java new file mode 100644 index 0000000000..cfdee653df --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.redis.core; + +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.convert.DurationStyle; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +/** + * 支持自定义过期时间的 {@link RedisCacheManager} 实现类 + * + * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间,单位为秒 + * + * @author 芋道源码 + */ +public class TimeoutRedisCacheManager extends RedisCacheManager { + + private static final String SPLIT = "#"; + + public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { + super(cacheWriter, defaultCacheConfiguration); + } + + @Override + protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { + if (StrUtil.isEmpty(name)) { + return super.createRedisCache(name, cacheConfig); + } + // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间 + String[] names = StrUtil.splitToArray(name, SPLIT); + if (names.length != 2) { + return super.createRedisCache(name, cacheConfig); + } + + // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间 + if (cacheConfig != null) { + // 移除 # 后面的 : 以及后面的内容,避免影响解析 + names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false); + // 解析时间 + Duration duration = DurationStyle.detectAndParse(names[1], ChronoUnit.SECONDS); + cacheConfig = cacheConfig.entryTtl(duration); + } + return super.createRedisCache(names[0], cacheConfig); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java index 28efbf9c8e..a85d297cd6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java @@ -2,14 +2,16 @@ package cn.iocoder.yudao.module.system.dal.mysql.dept; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.tenant.core.db.dynamic.TenantDS; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import org.apache.ibatis.annotations.Mapper; +import java.util.Collection; import java.util.List; @Mapper -// TODO 芋艿:@TenantDS +@TenantDS public interface DeptMapper extends BaseMapperX { default List selectList(DeptListReqVO reqVO) { @@ -26,4 +28,8 @@ public interface DeptMapper extends BaseMapperX { return selectCount(DeptDO::getParentId, parentId); } + default List selectListByParentId(Collection parentIds) { + return selectList(DeptDO::getParentId, parentIds); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java index 93ba3f9729..8f117a7cc7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java @@ -40,7 +40,7 @@ public interface RedisKeyConstants { * KEY 格式:user_role_ids::{userId} * 数据类型:String 角色编号集合 */ - String USER_ROLE_ID = "user_role_id"; + String USER_ROLE_ID_LIST = "user_role_ids"; /** * 拥有指定菜单的角色编号的缓存 @@ -48,6 +48,18 @@ public interface RedisKeyConstants { * KEY 格式:user_role_ids::{menuId} * 数据类型:String 角色编号集合 */ - String MENU_ROLE_ID = "menu_role_id"; + String MENU_ROLE_ID_LIST = "menu_role_ids"; + + /** + * 指定部门的所有子部门编号数组的缓存 + * + * KEY 格式:dept_children_ids::{id} + * 数据类型:String 子部门编号集合 + */ + String DEPT_CHILDREN_ID_LIST = "dept_children_ids"; + /** + * {@link #DEPT_CHILDREN_ID_LIST} 的过期时间 + */ + String DEPT_CHILDREN_ID_LIST_EXPIRE = "30s"; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java deleted file mode 100644 index 981244d909..0000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.yudao.module.system.mq.consumer.dept; - -import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; -import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage; -import cn.iocoder.yudao.module.system.service.dept.DeptService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; - -/** - * 针对 {@link DeptRefreshMessage} 的消费者 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class DeptRefreshConsumer extends AbstractChannelMessageListener { - - @Resource - private DeptService deptService; - - @Override - public void onMessage(DeptRefreshMessage message) { - log.info("[onMessage][收到 Dept 刷新消息]"); - deptService.initLocalCache(); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java deleted file mode 100644 index 80d3c8c39f..0000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.system.mq.message.dept; - -import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 部门数据刷新 Message - * - * @author 芋道源码 - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class DeptRefreshMessage extends AbstractChannelMessage { - - @Override - public String getChannel() { - return "system.dept.refresh"; - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java deleted file mode 100644 index 9a2ca1b9c5..0000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.system.mq.producer.dept; - -import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage; -import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; - -/** - * Dept 部门相关消息的 Producer - */ -@Component -public class DeptProducer { - - @Resource - private RedisMQTemplate redisMQTemplate; - - /** - * 发送 {@link DeptRefreshMessage} 消息 - */ - public void sendDeptRefreshMessage() { - DeptRefreshMessage message = new DeptRefreshMessage(); - redisMQTemplate.send(message); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java index 87033d4b3a..f21b3e861a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java @@ -7,10 +7,7 @@ import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqV import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; /** * 部门 Service 接口 @@ -19,11 +16,6 @@ import java.util.Map; */ public interface DeptService { - /** - * 初始化部门的本地缓存 - */ - void initLocalCache(); - /** * 创建部门 * @@ -55,13 +47,23 @@ public interface DeptService { List getDeptList(DeptListReqVO reqVO); /** - * 获得所有子部门,从缓存中 + * 获得指定部门的所有子部门 * - * @param parentId 部门编号 - * @param recursive 是否递归获取所有 + * @param id 部门编号 * @return 子部门列表 */ - List getDeptListByParentIdFromCache(Long parentId, boolean recursive); + List getChildDeptList(Long id); + + /** + * 获得所有子部门,从缓存中 + * + * 注意,该缓存不是实时更新,最多会有 1 分钟延迟。 + * 一般来说,不会影响使用,因为部门的变更,不会频繁发生。 + * + * @param id 父部门编号 + * @return 子部门列表 + */ + Set getChildDeptIdListFromCache(Long id); /** * 获得部门信息数组 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java index 1b2bbec044..5f0fbeb5f2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java @@ -2,29 +2,25 @@ package cn.iocoder.yudao.module.system.service.dept; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; import cn.iocoder.yudao.module.system.convert.dept.DeptConvert; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; +import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.system.enums.dept.DeptIdEnum; -import cn.iocoder.yudao.module.system.mq.producer.dept.DeptProducer; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; /** @@ -37,54 +33,9 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; @Slf4j public class DeptServiceImpl implements DeptService { - /** - * 部门缓存 - * key:部门编号 {@link DeptDO#getId()} - * - * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 - */ - @Getter - private volatile Map deptCache; - /** - * 父部门缓存 - * key:部门编号 {@link DeptDO#getParentId()} - * value: 直接子部门列表 - * - * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 - */ - @Getter - private volatile Multimap parentDeptCache; - @Resource private DeptMapper deptMapper; - @Resource - private DeptProducer deptProducer; - - /** - * 初始化 {@link #parentDeptCache} 和 {@link #deptCache} 缓存 - */ - @Override - @PostConstruct - public synchronized void initLocalCache() { - // 注意:忽略自动多租户,因为要全局初始化缓存 - TenantUtils.executeIgnore(() -> { - // 第一步:查询数据 - List depts = deptMapper.selectList(); - log.info("[initLocalCache][缓存部门,数量为:{}]", depts.size()); - - // 第二步:构建缓存 - ImmutableMap.Builder builder = ImmutableMap.builder(); - ImmutableMultimap.Builder parentBuilder = ImmutableMultimap.builder(); - depts.forEach(sysRoleDO -> { - builder.put(sysRoleDO.getId(), sysRoleDO); - parentBuilder.put(sysRoleDO.getParentId(), sysRoleDO); - }); - deptCache = builder.build(); - parentDeptCache = parentBuilder.build(); - }); - } - @Override public Long createDept(DeptCreateReqVO reqVO) { // 校验正确性 @@ -95,8 +46,6 @@ public class DeptServiceImpl implements DeptService { // 插入部门 DeptDO dept = DeptConvert.INSTANCE.convert(reqVO); deptMapper.insert(dept); - // 发送刷新消息 - deptProducer.sendDeptRefreshMessage(); return dept.getId(); } @@ -110,8 +59,6 @@ public class DeptServiceImpl implements DeptService { // 更新部门 DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO); deptMapper.updateById(updateObj); - // 发送刷新消息 - deptProducer.sendDeptRefreshMessage(); } @Override @@ -124,8 +71,6 @@ public class DeptServiceImpl implements DeptService { } // 删除部门 deptMapper.deleteById(id); - // 发送刷新消息 - deptProducer.sendDeptRefreshMessage(); } @Override @@ -134,48 +79,35 @@ public class DeptServiceImpl implements DeptService { } @Override - public List getDeptListByParentIdFromCache(Long parentId, boolean recursive) { - if (parentId == null) { - return Collections.emptyList(); + public List getChildDeptList(Long id) { + List children = new LinkedList<>(); + // 遍历每一层 + Collection parentIds = Collections.singleton(id); + for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环 + // 查询当前层,所有的子部门 + List depts = deptMapper.selectListByParentId(parentIds); + // 1. 如果没有子部门,则结束遍历 + if (CollUtil.isEmpty(depts)) { + break; + } + // 2. 如果有子部门,继续遍历 + children.addAll(depts); + parentIds = convertSet(depts, DeptDO::getId); } - List result = new ArrayList<>(); - // 递归,简单粗暴 - getDeptsByParentIdFromCache(result, parentId, - recursive ? Integer.MAX_VALUE : 1, // 如果递归获取,则无限;否则,只递归 1 次 - parentDeptCache); - return result; + return children; } - /** - * 递归获取所有的子部门,添加到 result 结果 - * - * @param result 结果 - * @param parentId 父编号 - * @param recursiveCount 递归次数 - * @param parentDeptMap 父部门 Map,使用缓存,避免变化 - */ - private void getDeptsByParentIdFromCache(List result, Long parentId, int recursiveCount, - Multimap parentDeptMap) { - // 递归次数为 0,结束! - if (recursiveCount == 0) { - return; - } - - // 获得子部门 - Collection depts = parentDeptMap.get(parentId); - if (CollUtil.isEmpty(depts)) { - return; - } - // 针对多租户,过滤掉非当前租户的部门 - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId != null) { - depts = CollUtil.filterNew(depts, dept -> tenantId.equals(dept.getTenantId())); - } - result.addAll(depts); - - // 继续递归 - depts.forEach(dept -> getDeptsByParentIdFromCache(result, dept.getId(), - recursiveCount - 1, parentDeptMap)); + @Override + @DataPermission(enable = false) // 禁用数据权限,避免简历不正确的缓存 + @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST + + "#" + RedisKeyConstants.DEPT_CHILDREN_ID_LIST_EXPIRE, key = "#id") + public Set getChildDeptIdListFromCache(Long id) { + // 补充说明:为什么该缓存会有 1 分钟的延迟?主要有两点: + // 1. Spring Cache 无法方便的批量清理,所以使用 Redis 自动过期的方式。 + // 2. 变更父节点的时候,影响父子节点的数量很多,包括原父节点及其父节点,以及新父节点及其父节点。 + // 如果你真的对延迟比较敏感,可以考虑采用使用 allEntries = true 的方式,清理所有缓存。 + List children = getChildDeptList(id); + return convertSet(children, DeptDO::getId); } private void validateForCreateOrUpdate(Long id, Long parentId, String name) { @@ -205,7 +137,7 @@ public class DeptServiceImpl implements DeptService { throw exception(DEPT_NOT_ENABLE); } // 父部门不能是原来的子部门 - List children = getDeptListByParentIdFromCache(id, true); + List children = getChildDeptList(id); if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) { throw exception(DEPT_PARENT_IS_CHILD); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index 422899713c..cab5e21732 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; @@ -71,7 +70,7 @@ public class PermissionServiceImpl implements PermissionService { } @Override - @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID, key = "#menuId") + @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") public Set getMenuRoleIdListByMenuIdFromCache(Long menuId) { return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); } @@ -104,7 +103,7 @@ public class PermissionServiceImpl implements PermissionService { } @Override - @Cacheable(value = RedisKeyConstants.USER_ROLE_ID, key = "#userId") + @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public Set getUserRoleIdListByUserIdFromCache(Long userId) { return getUserRoleIdListByUserId(userId); } @@ -261,7 +260,7 @@ public class PermissionServiceImpl implements PermissionService { } // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 - Supplier userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); + Supplier userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); // 遍历每个角色,计算 for (RoleDO role : roles) { // 为空时,跳过 @@ -278,20 +277,19 @@ public class PermissionServiceImpl implements PermissionService { CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 - CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况三,DEPT_ONLY if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { - CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get()); + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); continue; } // 情况四,DEPT_DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { - List depts = deptService.getDeptListByParentIdFromCache(userDeptIdCache.get(), true); - CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId)); + CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); // 添加本身部门编号 - CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况五,SELF diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java index bb43a89c2b..f9e575e6a2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java @@ -290,8 +290,7 @@ public class AdminUserServiceImpl implements AdminUserService { if (deptId == null) { return Collections.emptySet(); } - Set deptIds = convertSet(deptService.getDeptListByParentIdFromCache( - deptId, true), DeptDO::getId); + Set deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId); deptIds.add(deptId); // 包括自身 return deptIds; } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index 37132a8081..e8f8cd72b7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -18,7 +18,6 @@ import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer; import cn.iocoder.yudao.module.system.service.dept.DeptService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -37,7 +36,6 @@ import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -471,7 +469,7 @@ public class PermissionServiceTest extends BaseDbUnitTest { when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // mock 方法(部门) DeptDO deptDO = randomPojo(DeptDO.class); - when(deptService.getDeptListByParentIdFromCache(eq(3L), eq(true))) + when(deptService.getChildDeptIdListFromCache(eq(3L), eq(true))) .thenReturn(singletonList(deptDO)); // 调用 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java index a5e0183a44..133363270a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java @@ -345,7 +345,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 // mock 方法 List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); - when(deptService.getDeptListByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); + when(deptService.getChildDeptIdListFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); // 调用 PageResult pageResult = userService.getUserPage(reqVO); @@ -368,7 +368,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 // mock 方法 List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); - when(deptService.getDeptListByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); + when(deptService.getChildDeptIdListFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); // 调用 List list = userService.getUserList(reqVO); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 21eda5c800..2ac58b42e5 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -7,6 +7,7 @@ spring: main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许覆盖 bean 定义 # Servlet 配置 servlet: