缓存改造:Dept 使用 Redis 作为缓存

This commit is contained in:
YunaiV 2023-02-25 22:42:50 +08:00
parent 56c44acd11
commit 2db6a4510c
16 changed files with 161 additions and 213 deletions

View File

@ -133,9 +133,9 @@ public class YudaoTenantAutoConfiguration {
@Bean @Bean
@Primary // 引入租户时tenantRedisCacheManager 为主 Bean @Primary // 引入租户时tenantRedisCacheManager 为主 Bean
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate, public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration, RedisCacheConfiguration redisCacheConfiguration,
TenantProperties tenantProperties) { TenantProperties tenantProperties) {
// 创建 RedisCacheWriter 对象 // 创建 RedisCacheWriter 对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.tenant.core.redis; package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.util.StrUtil; 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.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import jodd.io.StreamUtil; import jodd.io.StreamUtil;
@ -13,12 +14,19 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
/** /**
* 多租户的 {@link RedisCacheManager} 实现类 * 多租户的 {@link RedisCacheManager} 实现类
* *
* 操作指定 name {@link Cache} 自动拼接租户后缀格式为 name + ":" + tenantId + 后缀 * 操作指定 name {@link Cache} 自动拼接租户后缀格式为 name + "::t" + tenantId + 后缀
* *
* @author airhead * @author airhead
*/ */
@Slf4j @Slf4j
public class TenantRedisCacheManager extends RedisCacheManager { public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
/**
* 多租户 Redis Key 的前缀补充在原有 name : 后面
*
* 原因如果只补充租户编号可读性较差
*/
private static final String PREFIX = "t";
private final TenantProperties tenantProperties; private final TenantProperties tenantProperties;
@ -33,7 +41,7 @@ public class TenantRedisCacheManager extends RedisCacheManager {
public Cache getCache(String name) { public Cache getCache(String name) {
// 如果不忽略多租户的 Cache则自动拼接租户后缀 // 如果不忽略多租户的 Cache则自动拼接租户后缀
if (!tenantProperties.getIgnoreCaches().contains(name)) { if (!tenantProperties.getIgnoreCaches().contains(name)) {
name = name + StrUtil.COLON + TenantContextHolder.getRequiredTenantId(); name = name + StrUtil.COLON + PREFIX + TenantContextHolder.getRequiredTenantId();
} }
// 继续基于父方法 // 继续基于父方法

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.redis.config; 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.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration; 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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer;
import java.util.Objects;
import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer; import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer;
/** /**
@ -50,4 +57,14 @@ public class YudaoCacheAutoConfiguration {
return config; return config;
} }
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration) {
// 创建 RedisCacheWriter 对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
// 创建 TenantRedisCacheManager 对象
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}
} }

View File

@ -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);
}
}

View File

@ -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.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 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.controller.admin.dept.vo.dept.DeptListReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List; import java.util.List;
@Mapper @Mapper
// TODO 芋艿@TenantDS @TenantDS
public interface DeptMapper extends BaseMapperX<DeptDO> { public interface DeptMapper extends BaseMapperX<DeptDO> {
default List<DeptDO> selectList(DeptListReqVO reqVO) { default List<DeptDO> selectList(DeptListReqVO reqVO) {
@ -26,4 +28,8 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
return selectCount(DeptDO::getParentId, parentId); return selectCount(DeptDO::getParentId, parentId);
} }
default List<DeptDO> selectListByParentId(Collection<Long> parentIds) {
return selectList(DeptDO::getParentId, parentIds);
}
} }

View File

@ -40,7 +40,7 @@ public interface RedisKeyConstants {
* KEY 格式user_role_ids::{userId} * KEY 格式user_role_ids::{userId}
* 数据类型String 角色编号集合 * 数据类型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} * KEY 格式user_role_ids::{menuId}
* 数据类型String 角色编号集合 * 数据类型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";
} }

View File

@ -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<DeptRefreshMessage> {
@Resource
private DeptService deptService;
@Override
public void onMessage(DeptRefreshMessage message) {
log.info("[onMessage][收到 Dept 刷新消息]");
deptService.initLocalCache();
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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.controller.admin.dept.vo.dept.DeptUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import java.util.Collection; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** /**
* 部门 Service 接口 * 部门 Service 接口
@ -19,11 +16,6 @@ import java.util.Map;
*/ */
public interface DeptService { public interface DeptService {
/**
* 初始化部门的本地缓存
*/
void initLocalCache();
/** /**
* 创建部门 * 创建部门
* *
@ -55,13 +47,23 @@ public interface DeptService {
List<DeptDO> getDeptList(DeptListReqVO reqVO); List<DeptDO> getDeptList(DeptListReqVO reqVO);
/** /**
* 获得所有子部门从缓存中 * 获得指定部门的所有子部门
* *
* @param parentId 部门编号 * @param id 部门编号
* @param recursive 是否递归获取所有
* @return 子部门列表 * @return 子部门列表
*/ */
List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive); List<DeptDO> getChildDeptList(Long id);
/**
* 获得所有子部门从缓存中
*
* 注意该缓存不是实时更新最多会有 1 分钟延迟
* 一般来说不会影响使用因为部门的变更不会频繁发生
*
* @param id 父部门编号
* @return 子部门列表
*/
Set<Long> getChildDeptIdListFromCache(Long id);
/** /**
* 获得部门信息数组 * 获得部门信息数组

View File

@ -2,29 +2,25 @@ package cn.iocoder.yudao.module.system.service.dept;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; 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.DeptListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; 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.convert.dept.DeptConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; 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.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.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 lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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.*; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
/** /**
@ -37,54 +33,9 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j @Slf4j
public class DeptServiceImpl implements DeptService { public class DeptServiceImpl implements DeptService {
/**
* 部门缓存
* key部门编号 {@link DeptDO#getId()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<Long, DeptDO> deptCache;
/**
* 父部门缓存
* key部门编号 {@link DeptDO#getParentId()}
* value: 直接子部门列表
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Multimap<Long, DeptDO> parentDeptCache;
@Resource @Resource
private DeptMapper deptMapper; private DeptMapper deptMapper;
@Resource
private DeptProducer deptProducer;
/**
* 初始化 {@link #parentDeptCache} {@link #deptCache} 缓存
*/
@Override
@PostConstruct
public synchronized void initLocalCache() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步查询数据
List<DeptDO> depts = deptMapper.selectList();
log.info("[initLocalCache][缓存部门,数量为:{}]", depts.size());
// 第二步构建缓存
ImmutableMap.Builder<Long, DeptDO> builder = ImmutableMap.builder();
ImmutableMultimap.Builder<Long, DeptDO> parentBuilder = ImmutableMultimap.builder();
depts.forEach(sysRoleDO -> {
builder.put(sysRoleDO.getId(), sysRoleDO);
parentBuilder.put(sysRoleDO.getParentId(), sysRoleDO);
});
deptCache = builder.build();
parentDeptCache = parentBuilder.build();
});
}
@Override @Override
public Long createDept(DeptCreateReqVO reqVO) { public Long createDept(DeptCreateReqVO reqVO) {
// 校验正确性 // 校验正确性
@ -95,8 +46,6 @@ public class DeptServiceImpl implements DeptService {
// 插入部门 // 插入部门
DeptDO dept = DeptConvert.INSTANCE.convert(reqVO); DeptDO dept = DeptConvert.INSTANCE.convert(reqVO);
deptMapper.insert(dept); deptMapper.insert(dept);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
return dept.getId(); return dept.getId();
} }
@ -110,8 +59,6 @@ public class DeptServiceImpl implements DeptService {
// 更新部门 // 更新部门
DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO); DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO);
deptMapper.updateById(updateObj); deptMapper.updateById(updateObj);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
} }
@Override @Override
@ -124,8 +71,6 @@ public class DeptServiceImpl implements DeptService {
} }
// 删除部门 // 删除部门
deptMapper.deleteById(id); deptMapper.deleteById(id);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
} }
@Override @Override
@ -134,48 +79,35 @@ public class DeptServiceImpl implements DeptService {
} }
@Override @Override
public List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive) { public List<DeptDO> getChildDeptList(Long id) {
if (parentId == null) { List<DeptDO> children = new LinkedList<>();
return Collections.emptyList(); // 遍历每一层
Collection<Long> parentIds = Collections.singleton(id);
for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下存在死循环
// 查询当前层所有的子部门
List<DeptDO> depts = deptMapper.selectListByParentId(parentIds);
// 1. 如果没有子部门则结束遍历
if (CollUtil.isEmpty(depts)) {
break;
}
// 2. 如果有子部门继续遍历
children.addAll(depts);
parentIds = convertSet(depts, DeptDO::getId);
} }
List<DeptDO> result = new ArrayList<>(); return children;
// 递归简单粗暴
getDeptsByParentIdFromCache(result, parentId,
recursive ? Integer.MAX_VALUE : 1, // 如果递归获取则无限否则只递归 1
parentDeptCache);
return result;
} }
/** @Override
* 递归获取所有的子部门添加到 result 结果 @DataPermission(enable = false) // 禁用数据权限避免简历不正确的缓存
* @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST
* @param result 结果 + "#" + RedisKeyConstants.DEPT_CHILDREN_ID_LIST_EXPIRE, key = "#id")
* @param parentId 父编号 public Set<Long> getChildDeptIdListFromCache(Long id) {
* @param recursiveCount 递归次数 // 补充说明为什么该缓存会有 1 分钟的延迟主要有两点
* @param parentDeptMap 父部门 Map使用缓存避免变化 // 1. Spring Cache 无法方便的批量清理所以使用 Redis 自动过期的方式
*/ // 2. 变更父节点的时候影响父子节点的数量很多包括原父节点及其父节点以及新父节点及其父节点
private void getDeptsByParentIdFromCache(List<DeptDO> result, Long parentId, int recursiveCount, // 如果你真的对延迟比较敏感可以考虑采用使用 allEntries = true 的方式清理所有缓存
Multimap<Long, DeptDO> parentDeptMap) { List<DeptDO> children = getChildDeptList(id);
// 递归次数为 0结束 return convertSet(children, DeptDO::getId);
if (recursiveCount == 0) {
return;
}
// 获得子部门
Collection<DeptDO> 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));
} }
private void validateForCreateOrUpdate(Long id, Long parentId, String name) { private void validateForCreateOrUpdate(Long id, Long parentId, String name) {
@ -205,7 +137,7 @@ public class DeptServiceImpl implements DeptService {
throw exception(DEPT_NOT_ENABLE); throw exception(DEPT_NOT_ENABLE);
} }
// 父部门不能是原来的子部门 // 父部门不能是原来的子部门
List<DeptDO> children = getDeptListByParentIdFromCache(id, true); List<DeptDO> children = getChildDeptList(id);
if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) { if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) {
throw exception(DEPT_PARENT_IS_CHILD); throw exception(DEPT_PARENT_IS_CHILD);
} }

View File

@ -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.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 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.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.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO;
@ -71,7 +70,7 @@ public class PermissionServiceImpl implements PermissionService {
} }
@Override @Override
@Cacheable(value = RedisKeyConstants.MENU_ROLE_ID, key = "#menuId") @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId")
public Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId) { public Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId) {
return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId);
} }
@ -104,7 +103,7 @@ public class PermissionServiceImpl implements PermissionService {
} }
@Override @Override
@Cacheable(value = RedisKeyConstants.USER_ROLE_ID, key = "#userId") @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) { public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) {
return getUserRoleIdListByUserId(userId); return getUserRoleIdListByUserId(userId);
} }
@ -261,7 +260,7 @@ public class PermissionServiceImpl implements PermissionService {
} }
// 获得用户的部门编号的缓存通过 Guava Suppliers 惰性求值即有且仅有第一次发起 DB 的查询 // 获得用户的部门编号的缓存通过 Guava Suppliers 惰性求值即有且仅有第一次发起 DB 的查询
Supplier<Long> userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); Supplier<Long> userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());
// 遍历每个角色计算 // 遍历每个角色计算
for (RoleDO role : roles) { for (RoleDO role : roles) {
// 为空时跳过 // 为空时跳过
@ -278,20 +277,19 @@ public class PermissionServiceImpl implements PermissionService {
CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds());
// 自定义可见部门时保证可以看到自己所在的部门否则一些场景下可能会有问题 // 自定义可见部门时保证可以看到自己所在的部门否则一些场景下可能会有问题
// 例如说登录时基于 t_user username 查询会可能被 dept_id 过滤掉 // 例如说登录时基于 t_user username 查询会可能被 dept_id 过滤掉
CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); CollUtil.addAll(result.getDeptIds(), userDeptId.get());
continue; continue;
} }
// 情况三DEPT_ONLY // 情况三DEPT_ONLY
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) {
CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get()); CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get());
continue; continue;
} }
// 情况四DEPT_DEPT_AND_CHILD // 情况四DEPT_DEPT_AND_CHILD
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) {
List<DeptDO> depts = deptService.getDeptListByParentIdFromCache(userDeptIdCache.get(), true); CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get()));
CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId));
// 添加本身部门编号 // 添加本身部门编号
CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); CollUtil.addAll(result.getDeptIds(), userDeptId.get());
continue; continue;
} }
// 情况五SELF // 情况五SELF

View File

@ -290,8 +290,7 @@ public class AdminUserServiceImpl implements AdminUserService {
if (deptId == null) { if (deptId == null) {
return Collections.emptySet(); return Collections.emptySet();
} }
Set<Long> deptIds = convertSet(deptService.getDeptListByParentIdFromCache( Set<Long> deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId);
deptId, true), DeptDO::getId);
deptIds.add(deptId); // 包括自身 deptIds.add(deptId); // 包括自身
return deptIds; return deptIds;
} }

View File

@ -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.dept.DeptService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -37,7 +36,6 @@ import static java.util.Collections.singleton;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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 的目的看看会不会重复调用 when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的看看会不会重复调用
// mock 方法部门 // mock 方法部门
DeptDO deptDO = randomPojo(DeptDO.class); DeptDO deptDO = randomPojo(DeptDO.class);
when(deptService.getDeptListByParentIdFromCache(eq(3L), eq(true))) when(deptService.getChildDeptIdListFromCache(eq(3L), eq(true)))
.thenReturn(singletonList(deptDO)); .thenReturn(singletonList(deptDO));
// 调用 // 调用

View File

@ -345,7 +345,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
reqVO.setDeptId(1L); // 其中1L 2L 的父部门 reqVO.setDeptId(1L); // 其中1L 2L 的父部门
// mock 方法 // mock 方法
List<DeptDO> deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); List<DeptDO> 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<AdminUserDO> pageResult = userService.getUserPage(reqVO); PageResult<AdminUserDO> pageResult = userService.getUserPage(reqVO);
@ -368,7 +368,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
reqVO.setDeptId(1L); // 其中1L 2L 的父部门 reqVO.setDeptId(1L); // 其中1L 2L 的父部门
// mock 方法 // mock 方法
List<DeptDO> deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); List<DeptDO> 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<AdminUserDO> list = userService.getUserList(reqVO); List<AdminUserDO> list = userService.getUserList(reqVO);

View File

@ -7,6 +7,7 @@ spring:
main: main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许覆盖 bean 定义
# Servlet 配置 # Servlet 配置
servlet: servlet: