缓存改造:Dept 使用 Redis 作为缓存
This commit is contained in:
parent
56c44acd11
commit
2db6a4510c
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续基于父方法
|
// 继续基于父方法
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得部门信息数组
|
* 获得部门信息数组
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue