1. 新建租户、修改租户、修改租户套餐时,自动修改角色的权限

2. 租户的本地缓存,提升访问性能
3. 精简本地缓存的实现逻辑
This commit is contained in:
YunaiV 2022-02-23 00:38:49 +08:00
parent 4d53944771
commit e4be51b14a
33 changed files with 405 additions and 139 deletions

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.ImmutableMap;
import java.util.*;
import java.util.function.BinaryOperator;
@ -125,6 +126,15 @@ public class CollectionUtils {
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
}
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) {
return Collections.emptyMap();
}
ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
from.forEach(item -> builder.put(keyFunc.apply(item), item));
return builder.build();
}
public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
return org.springframework.util.CollectionUtils.containsAny(source, candidates);
}
@ -140,6 +150,15 @@ public class CollectionUtils {
return from.stream().filter(predicate).findFirst().orElse(null);
}
public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert from.size() > 0; // 断言避免告警
T t = from.stream().max(Comparator.comparing(valueFunc)).get();
return valueFunc.apply(t);
}
public static <T> void addIfNotNull(Collection<T> coll, T item) {
if (item == null) {
return;

View File

@ -33,6 +33,10 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>

View File

@ -6,15 +6,16 @@ import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayChannelMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
@ -27,12 +28,12 @@ import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_EXIST_SAME_CHANNEL_ERROR;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_EXISTS;
/**
* 支付渠道 Service 实现类
@ -66,9 +67,10 @@ public class PayChannelServiceImpl implements PayChannelService {
@Override
@PostConstruct
@TenantIgnore // 忽略自动化租户全局初始化本地缓存
public void initPayClients() {
// 获取支付渠道如果有更新
List<PayChannelDO> payChannels = this.loadPayChannelIfUpdate(maxUpdateTime);
List<PayChannelDO> payChannels = loadPayChannelIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(payChannels)) {
return;
}
@ -78,8 +80,7 @@ public class PayChannelServiceImpl implements PayChannelService {
payChannel.getCode(), payChannel.getConfig()));
// 写入缓存
assert payChannels.size() > 0; // 断言避免告警
maxUpdateTime = payChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
maxUpdateTime = CollectionUtils.getMaxValue(payChannels, PayChannelDO::getUpdateTime);
log.info("[initPayClients][初始化 PayChannel 数量为 {}]", payChannels.size());
}

View File

@ -72,7 +72,7 @@ public class AuthController {
// 获得角色列表
List<RoleDO> roleList = roleService.getRolesFromCache(getLoginUserRoleIds());
// 获得菜单列表
List<MenuDO> menuList = permissionService.getRoleMenusFromCache(
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(
getLoginUserRoleIds(), // 注意基于登录的角色因为后续的权限判断也是基于它
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus()));
@ -84,7 +84,7 @@ public class AuthController {
@ApiOperation("获得登录用户的菜单列表")
public CommonResult<List<AuthMenuRespVO>> getMenus() {
// 获得用户拥有的菜单列表
List<MenuDO> menuList = permissionService.getRoleMenusFromCache(
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(
getLoginUserRoleIds(), // 注意基于登录的角色因为后续的权限判断也是基于它
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的

View File

@ -55,7 +55,7 @@ public class MenuController {
}
@GetMapping("/list")
@ApiOperation("获取菜单列表")
@ApiOperation(value = "获取菜单列表", notes = "用于【菜单管理】界面")
@PreAuthorize("@ss.hasPermission('system:menu:query')")
public CommonResult<List<MenuRespVO>> getMenus(MenuListReqVO reqVO) {
List<MenuDO> list = menuService.getMenus(reqVO);
@ -64,13 +64,13 @@ public class MenuController {
}
@GetMapping("/list-all-simple")
@ApiOperation(value = "获取菜单精简信息列表", notes = "只包含被开启的菜单,主要用于前端的下拉选项")
@ApiOperation(value = "获取菜单精简信息列表", notes = "只包含被开启的菜单,用于【角色分配菜单】功能的选项")
public CommonResult<List<MenuSimpleRespVO>> getSimpleMenus() {
// 获得菜单列表只要开启状态的
MenuListReqVO reqVO = new MenuListReqVO();
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
List<MenuDO> list = menuService.getMenus(reqVO);
// 排序后返回个诶前端
// 排序后返回前端
list.sort(Comparator.comparing(MenuDO::getSort));
return success(MenuConvert.INSTANCE.convertList02(list));
}

View File

@ -37,7 +37,7 @@ public class PermissionController {
@GetMapping("/list-role-resources")
// @RequiresPermissions("system:permission:assign-role-menu")
public CommonResult<Set<Long>> listRoleMenus(Long roleId) {
return success(permissionService.listRoleMenuIds(roleId));
return success(permissionService.getRoleMenuIds(roleId));
}
@PostMapping("/assign-role-menu")

View File

@ -4,7 +4,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Set;
/**
* 租户套餐 Base VO提供给添加修改详细的子 VO 使用
@ -26,6 +26,6 @@ public class TenantPackageBaseVO {
@ApiModelProperty(value = "关联的菜单编号", required = true)
@NotNull(message = "关联的菜单编号不能为空")
private List<Long> menuIds;
private Set<Long> menuIds;
}

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.system.dal.dataobject.permission;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
import cn.iocoder.yudao.framework.security.core.enums.DataScopeEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@ -21,7 +21,7 @@ import java.util.Set;
@TableName(value = "system_role", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
public class RoleDO extends BaseDO {
public class RoleDO extends TenantBaseDO {
/**
* 角色ID

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.system.dal.dataobject.permission;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -14,7 +15,7 @@ import lombok.EqualsAndHashCode;
@TableName("system_role_menu")
@Data
@EqualsAndHashCode(callSuper = true)
public class RoleMenuDO extends BaseDO {
public class RoleMenuDO extends TenantBaseDO {
/**
* 自增主键

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.mysql.permission;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
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.QueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
@ -41,7 +42,7 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
}
default List<RoleDO> selectListByStatus(@Nullable Collection<Integer> statuses) {
return selectList(new QueryWrapperX<RoleDO>().in("status", statuses));
return selectList(new LambdaQueryWrapperX<RoleDO>().inIfPresent(RoleDO::getStatus, statuses));
}
@InterceptorIgnore(tenantLine = "true") // 该方法忽略多租户原因该方法被异步 task 调用此时获取不到租户编号

View File

@ -7,7 +7,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
import java.util.List;
/**
@ -43,7 +45,14 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
}
default Integer selectCountByPackageId(Long packageId) {
return selectCount("package_id", packageId);
return selectCount(TenantDO::getPackageId, packageId);
}
default List<TenantDO> selectListByPackageId(Long packageId) {
return selectList(TenantDO::getPackageId, packageId);
}
@Select("SELECT id FROM system_tenant WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@ -10,12 +10,17 @@ import lombok.Getter;
@AllArgsConstructor
public enum RoleCodeEnum {
ADMIN("admin"), // 超级管理员
SUPER_ADMIN("super_admin", "超级管理员"),
TENANT_ADMIN("tenant_admin", "租户管理员"),
;
/**
* 角色编码
*/
private final String key;
private final String code;
/**
* 名字
*/
private final String name;
}

View File

@ -12,7 +12,6 @@ import javax.annotation.Resource;
* 针对 {@link SmsSendMessage} 的消费者
*
* @author zzf
* @date 2021/3/9 16:35
*/
@Component
@Slf4j

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.mq.consumer.tenant;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class TenantRefreshConsumer extends AbstractChannelMessageListener<TenantRefreshMessage> {
@Resource
private TenantService tenantService;
@Override
public void onMessage(TenantRefreshMessage message) {
log.info("[onMessage][收到 Tenant 刷新消息]");
tenantService.initLocalCache();
}
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.system.mq.message.tenant;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 租户数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TenantRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.tenant.refresh";
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.mq.producer.tenant;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage;
import cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Tenant 租户相关消息的 Producer
*
* @author 芋道源码
*/
@Component
public class TenantProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link RoleRefreshMessage} 消息
*/
public void sendTenantRefreshMessage() {
TenantRefreshMessage message = new TenantRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
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;
@ -73,9 +73,10 @@ public class DeptServiceImpl implements DeptService {
@Override
@PostConstruct
@TenantIgnore // 初始化缓存无需租户过滤
public synchronized void initLocalCache() {
// 获取部门列表如果有更新
List<DeptDO> deptList = this.loadDeptIfUpdate(maxUpdateTime);
List<DeptDO> deptList = loadDeptIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(deptList)) {
return;
}
@ -90,8 +91,7 @@ public class DeptServiceImpl implements DeptService {
// 设置缓存
deptCache = builder.build();
parentDeptCache = parentBuilder.build();
assert deptList.size() > 0; // 断言避免告警
maxUpdateTime = deptList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
maxUpdateTime = CollectionUtils.getMaxValue(deptList, DeptDO::getUpdateTime);
log.info("[initLocalCache][初始化 Dept 数量为 {}]", deptList.size());
}
@ -107,7 +107,7 @@ public class DeptServiceImpl implements DeptService {
* @param maxUpdateTime 当前部门的最大更新时间
* @return 部门列表
*/
private List<DeptDO> loadDeptIfUpdate(Date maxUpdateTime) {
protected List<DeptDO> loadDeptIfUpdate(Date maxUpdateTime) {
// 第一步判断是否要更新
if (maxUpdateTime == null) { // 如果更新时间为空说明 DB 一定有新数据
log.info("[loadMenuIfUpdate][首次加载全量部门]");
@ -118,7 +118,7 @@ public class DeptServiceImpl implements DeptService {
log.info("[loadMenuIfUpdate][增量加载全量部门]");
}
// 第二步如果有更新则从数据库加载所有部门
return deptMapper.selectListIgnoreTenant();
return deptMapper.selectList();
}
@Override

View File

@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.service.dict;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO;
@ -99,8 +99,7 @@ public class DictDataServiceImpl implements DictDataService {
});
labelDictDataCache = labelDictDataBuilder.build();
valueDictDataCache = valueDictDataBuilder.build();
assert dataList.size() > 0; // 断言避免告警
maxUpdateTime = dataList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
maxUpdateTime = CollectionUtils.getMaxValue(dataList, DictDataDO::getUpdateTime);
log.info("[initLocalCache][缓存字典数据,数量为:{}]", dataList.size());
}

View File

@ -66,7 +66,7 @@ public interface MenuService {
* @param menusStatuses 菜单状态数组
* @return 菜单列表
*/
List<MenuDO> listMenusFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses);
List<MenuDO> getMenuListFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses);
/**
* 获得指定编号的菜单数组从缓存中
@ -78,7 +78,7 @@ public interface MenuService {
* @param menusStatuses 菜单状态数组
* @return 菜单数组
*/
List<MenuDO> listMenusFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
List<MenuDO> getMenuListFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
/**

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO;
@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
import cn.iocoder.yudao.module.system.enums.permission.MenuIdEnum;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import cn.iocoder.yudao.module.system.mq.producer.permission.MenuProducer;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
@ -95,8 +94,7 @@ public class MenuServiceImpl implements MenuService {
});
menuCache = menuCacheBuilder.build();
permissionMenuCache = permMenuCacheBuilder.build();
assert menuList.size() > 0; // 断言避免告警
maxUpdateTime = menuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
maxUpdateTime = CollectionUtils.getMaxValue(menuList, MenuDO::getUpdateTime);
log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size());
}
@ -201,7 +199,7 @@ public class MenuServiceImpl implements MenuService {
}
@Override
public List<MenuDO> listMenusFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses) {
public List<MenuDO> getMenuListFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses) {
// 任一一个参数为空则返回空
if (CollectionUtils.isAnyEmpty(menuTypes, menusStatuses)) {
return Collections.emptyList();
@ -213,7 +211,7 @@ public class MenuServiceImpl implements MenuService {
}
@Override
public List<MenuDO> listMenusFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
public List<MenuDO> getMenuListFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses) {
// 任一一个参数为空则返回空
if (CollectionUtils.isAnyEmpty(menuIds, menuTypes, menusStatuses)) {

View File

@ -33,7 +33,7 @@ public interface PermissionService extends SecurityPermissionFrameworkService, D
* @param menusStatuses 菜单状态数组
* @return 菜单列表
*/
List<MenuDO> getRoleMenusFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
List<MenuDO> getRoleMenuListFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
/**
@ -51,7 +51,7 @@ public interface PermissionService extends SecurityPermissionFrameworkService, D
* @param roleId 角色编号
* @return 菜单编号集合
*/
Set<Long> listRoleMenuIds(Long roleId);
Set<Long> getRoleMenuIds(Long roleId);
/**
* 设置角色菜单

View File

@ -3,6 +3,14 @@ package cn.iocoder.yudao.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.enums.DataScopeEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
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;
@ -12,13 +20,6 @@ import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper;
import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper;
import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.enums.DataScopeEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
@ -94,10 +95,10 @@ public class PermissionServiceImpl implements PermissionService {
*/
@Override
@PostConstruct
@TenantIgnore // 初始化缓存无需租户过滤
public void initLocalCache() {
Date now = new Date();
// 获取角色与菜单的关联列表如果有更新
List<RoleMenuDO> roleMenuList = this.loadRoleMenuIfUpdate(maxUpdateTime);
List<RoleMenuDO> roleMenuList = loadRoleMenuIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(roleMenuList)) {
return;
}
@ -111,8 +112,7 @@ public class PermissionServiceImpl implements PermissionService {
});
roleMenuCache = roleMenuCacheBuilder.build();
menuRoleCache = menuRoleCacheBuilder.build();
assert roleMenuList.size() > 0; // 断言避免告警
maxUpdateTime = now;
maxUpdateTime = CollectionUtils.getMaxValue(roleMenuList, RoleMenuDO::getUpdateTime);
log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size());
}
@ -128,7 +128,7 @@ public class PermissionServiceImpl implements PermissionService {
* @param maxUpdateTime 当前角色与菜单的关联的最大更新时间
* @return 角色与菜单的关联列表
*/
private List<RoleMenuDO> loadRoleMenuIfUpdate(Date maxUpdateTime) {
protected List<RoleMenuDO> loadRoleMenuIfUpdate(Date maxUpdateTime) {
// 第一步判断是否要更新
if (maxUpdateTime == null) { // 如果更新时间为空说明 DB 一定有新数据
log.info("[loadRoleMenuIfUpdate][首次加载全量角色与菜单的关联]");
@ -143,21 +143,22 @@ public class PermissionServiceImpl implements PermissionService {
}
@Override
public List<MenuDO> getRoleMenusFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
public List<MenuDO> getRoleMenuListFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses) {
// 任一一个参数为空时不返回任何菜单
if (CollectionUtils.isAnyEmpty(roleIds, menuTypes, menusStatuses)) {
return Collections.emptyList();
}
// 判断角色是否包含管理员
// 判断角色是否包含超级管理员如果是超级管理员获取到全部
List<RoleDO> roleList = roleService.getRolesFromCache(roleIds);
boolean hasAdmin = roleService.hasAnyAdmin(roleList);
// 获得角色拥有的菜单关联
if (hasAdmin) { // 管理员获取到全部
return menuService.listMenusFromCache(menuTypes, menusStatuses);
if (roleService.hasAnySuperAdmin(roleList)) {
return menuService.getMenuListFromCache(menuTypes, menusStatuses);
}
// 获得角色拥有的菜单关联
List<Long> menuIds = MapUtils.getList(roleMenuCache, roleIds);
return menuService.listMenusFromCache(menuIds, menuTypes, menusStatuses);
return menuService.getMenuListFromCache(menuIds, menuTypes, menusStatuses);
}
@Override
@ -174,10 +175,10 @@ public class PermissionServiceImpl implements PermissionService {
}
@Override
public Set<Long> listRoleMenuIds(Long roleId) {
public Set<Long> getRoleMenuIds(Long roleId) {
// 如果是管理员的情况下获取全部菜单编号
RoleDO role = roleService.getRole(roleId);
if (roleService.hasAnyAdmin(Collections.singletonList(role))) {
if (roleService.hasAnySuperAdmin(Collections.singletonList(role))) {
return CollectionUtils.convertSet(menuService.getMenus(), MenuDO::getId);
}
// 如果是非管理员的情况下获得拥有的菜单编号
@ -302,7 +303,7 @@ public class PermissionServiceImpl implements PermissionService {
return false;
}
// 判断是否是超管如果是当然符合条件
if (roleService.hasAnyAdmin(roleIds)) {
if (roleService.hasAnySuperAdmin(roleIds)) {
return true;
}
@ -337,7 +338,7 @@ public class PermissionServiceImpl implements PermissionService {
return false;
}
// 判断是否是超管如果是当然符合条件
if (roleService.hasAnyAdmin(roleIds)) {
if (roleService.hasAnySuperAdmin(roleIds)) {
return true;
}
Set<String> userRoles = CollectionUtils.convertSet(roleService.getRolesFromCache(roleIds),

View File

@ -90,12 +90,12 @@ public interface RoleService {
List<RoleDO> getRolesFromCache(Collection<Long> ids);
/**
* 判断角色数组中是否有管理员
* 判断角色数组中是否有超级管理员
*
* @param roleList 角色数组
* @return 是否有管理员
*/
boolean hasAnyAdmin(Collection<RoleDO> roleList);
boolean hasAnySuperAdmin(Collection<RoleDO> roleList);
/**
* 判断角色编号数组中是否有管理员
@ -103,8 +103,8 @@ public interface RoleService {
* @param ids 角色编号数组
* @return 是否有管理员
*/
default boolean hasAnyAdmin(Set<Long> ids) {
return hasAnyAdmin(getRolesFromCache(ids));
default boolean hasAnySuperAdmin(Set<Long> ids) {
return hasAnySuperAdmin(getRolesFromCache(ids));
}
/**

View File

@ -6,8 +6,8 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.security.core.enums.DataScopeEnum;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
@ -19,7 +19,6 @@ import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum;
import cn.iocoder.yudao.module.system.mq.producer.permission.RoleProducer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.Scheduled;
@ -78,19 +77,17 @@ public class RoleServiceImpl implements RoleService {
*/
@Override
@PostConstruct
@TenantIgnore // 忽略自动多租户全局初始化缓存
public void initLocalCache() {
// 获取角色列表如果有更新
List<RoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime);
List<RoleDO> roleList = loadRoleIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(roleList)) {
return;
}
// 写入缓存
ImmutableMap.Builder<Long, RoleDO> builder = ImmutableMap.builder();
roleList.forEach(sysRoleDO -> builder.put(sysRoleDO.getId(), sysRoleDO));
roleCache = builder.build();
assert roleList.size() > 0; // 断言避免告警
maxUpdateTime = roleList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
roleCache = CollectionUtils.convertMap(roleList, RoleDO::getId);
maxUpdateTime = CollectionUtils.getMaxValue(roleList, RoleDO::getUpdateTime);
log.info("[initLocalCache][初始化 Role 数量为 {}]", roleList.size());
}
@ -216,11 +213,11 @@ public class RoleServiceImpl implements RoleService {
}
@Override
public boolean hasAnyAdmin(Collection<RoleDO> roleList) {
public boolean hasAnySuperAdmin(Collection<RoleDO> roleList) {
if (CollectionUtil.isEmpty(roleList)) {
return false;
}
return roleList.stream().anyMatch(roleDO -> RoleCodeEnum.ADMIN.getKey().equals(roleDO.getCode()));
return roleList.stream().anyMatch(roleDO -> RoleCodeEnum.SUPER_ADMIN.getCode().equals(roleDO.getCode()));
}
@Override

View File

@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.system.service.sms;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO;
@ -8,10 +12,6 @@ import cn.iocoder.yudao.module.system.convert.sms.SmsChannelConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper;
import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@ -19,13 +19,12 @@ import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 短信渠道Service实现类
@ -74,8 +73,7 @@ public class SmsChannelServiceImpl implements SmsChannelService {
propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties));
// 写入缓存
assert smsChannels.size() > 0; // 断言避免告警
maxUpdateTime = smsChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
maxUpdateTime = CollectionUtils.getMaxValue(smsChannels, SmsChannelDO::getUpdateTime);
log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size());
}

View File

@ -3,24 +3,23 @@ package cn.iocoder.yudao.module.system.service.sms;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.sms.SmsTemplateConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsTemplateMapper;
import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@ -31,8 +30,8 @@ import javax.annotation.Resource;
import java.util.*;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
/**
* 短信模板 Service 实现类
@ -89,11 +88,8 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
}
// 写入缓存
ImmutableMap.Builder<String, SmsTemplateDO> builder = ImmutableMap.builder();
smsTemplateList.forEach(sysSmsTemplateDO -> builder.put(sysSmsTemplateDO.getCode(), sysSmsTemplateDO));
smsTemplateCache = builder.build();
assert smsTemplateList.size() > 0; // 断言避免告警
maxUpdateTime = smsTemplateList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
smsTemplateCache = CollectionUtils.convertMap(smsTemplateList, SmsTemplateDO::getCode);
maxUpdateTime = CollectionUtils.getMaxValue(smsTemplateList, SmsTemplateDO::getUpdateTime);
log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size());
}

View File

@ -1,15 +1,18 @@
package cn.iocoder.yudao.module.system.service.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.tenant.TenantPackageConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO;
import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantPackageMapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
@ -45,12 +48,18 @@ public class TenantPackageServiceImpl implements TenantPackageService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTenantPackage(TenantPackageUpdateReqVO updateReqVO) {
// 校验存在
this.validateTenantPackageExists(updateReqVO.getId());
TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId());
// 更新
TenantPackageDO updateObj = TenantPackageConvert.INSTANCE.convert(updateReqVO);
tenantPackageMapper.updateById(updateObj);
// 如果菜单发生变化则修改每个租户的菜单
if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) {
List<TenantDO> tenants = tenantService.getTenantListByPackageId(tenantPackage.getId());
tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds()));
}
}
@Override
@ -63,10 +72,12 @@ public class TenantPackageServiceImpl implements TenantPackageService {
tenantPackageMapper.deleteById(id);
}
private void validateTenantPackageExists(Long id) {
if (tenantPackageMapper.selectById(id) == null) {
private TenantPackageDO validateTenantPackageExists(Long id) {
TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id);
if (tenantPackage == null) {
throw exception(TENANT_PACKAGE_NOT_EXISTS);
}
return tenantPackage;
}
private void validateTenantUsed(Long id) {

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* 租户 Service 接口
@ -19,6 +20,11 @@ import java.util.List;
*/
public interface TenantService extends TenantFrameworkService {
/**
* 初始化租户的本地缓存
*/
void initLocalCache();
/**
* 创建租户
*
@ -34,6 +40,14 @@ public interface TenantService extends TenantFrameworkService {
*/
void updateTenant(@Valid TenantUpdateReqVO updateReqVO);
/**
* 更新租户的角色菜单
*
* @param tenantId 租户编号
* @param menuIds 菜单编号数组
*/
void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds);
/**
* 删除租户
*
@ -89,4 +103,12 @@ public interface TenantService extends TenantFrameworkService {
*/
Integer getTenantCountByPackageId(Long packageId);
/**
* 获得使用指定套餐的租户数组
*
* @param packageId 租户套餐编号
* @return 租户数组
*/
List<TenantDO> getTenantListByPackageId(Long packageId);
}

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.system.service.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@ -11,21 +14,27 @@ import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantEx
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO;
import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper;
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum;
import cn.iocoder.yudao.module.system.mq.producer.tenant.TenantProducer;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@ -37,8 +46,27 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
*/
@Service
@Validated
@Slf4j
public class TenantServiceImpl implements TenantService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 角色缓存
* key角色编号 {@link RoleDO#getId()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
private volatile Map<Long, TenantDO> tenantCache;
/**
* 缓存角色的最大更新时间用于后续的增量轮询判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private TenantMapper tenantMapper;
@ -51,15 +79,61 @@ public class TenantServiceImpl implements TenantService {
@Resource
private PermissionService permissionService;
@Resource
private TenantProducer tenantProducer;
/**
* 初始化 {@link #tenantCache} 缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 获取租户列表如果有更新
List<TenantDO> tenantList = loadTenantIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(tenantList)) {
return;
}
// 写入缓存
tenantCache = CollectionUtils.convertImmutableMap(tenantList, TenantDO::getId);
maxUpdateTime = CollectionUtils.getMaxValue(tenantList, TenantDO::getUpdateTime);
log.info("[initLocalCache][初始化 Tenant 数量为 {}]", tenantList.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initLocalCache();
}
/**
* 如果租户发生变化从数据库中获取最新的全量租户
* 如果未发生变化则返回空
*
* @param maxUpdateTime 当前租户的最大更新时间
* @return 租户列表
*/
private List<TenantDO> loadTenantIfUpdate(Date maxUpdateTime) {
// 第一步判断是否要更新
if (maxUpdateTime == null) { // 如果更新时间为空说明 DB 一定有新数据
log.info("[loadTenantIfUpdate][首次加载全量租户]");
} else { // 判断数据库中是否有更新的租户
if (tenantMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadTenantIfUpdate][增量加载全量租户]");
}
// 第二步如果有更新则从数据库加载所有租户
return tenantMapper.selectList();
}
@Override
public List<Long> getTenantIds() {
List<TenantDO> tenants = tenantMapper.selectList();
return CollectionUtils.convertList(tenants, TenantDO::getId);
return new ArrayList<>(tenantCache.keySet());
}
@Override
public void validTenant(Long id) {
TenantDO tenant = tenantMapper.selectById(id);
TenantDO tenant = tenantCache.get(id);
if (tenant == null) {
throw exception(TENANT_NOT_EXISTS);
}
@ -75,7 +149,7 @@ public class TenantServiceImpl implements TenantService {
@Transactional(rollbackFor = Exception.class)
public Long createTenant(TenantCreateReqVO createReqVO) {
// 校验套餐被禁用
tenantPackageService.validTenantPackage(createReqVO.getPackageId());
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
// 创建租户
TenantDO tenant = TenantConvert.INSTANCE.convert(createReqVO);
@ -83,13 +157,19 @@ public class TenantServiceImpl implements TenantService {
TenantUtils.execute(tenant.getId(), () -> {
// 创建角色
Long roleId = createRole();
Long roleId = createRole(tenantPackage);
// 创建用户并分配角色
Long userId = createUser(roleId, createReqVO);
// 修改租户的管理员
tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId));
});
// 返回
// 发送刷新消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
tenantProducer.sendTenantRefreshMessage();
}
});
return tenant.getId();
}
@ -101,36 +181,80 @@ public class TenantServiceImpl implements TenantService {
return userId;
}
private Long createRole() {
private Long createRole(TenantPackageDO tenantPackage) {
// 创建角色
RoleCreateReqVO reqVO = new RoleCreateReqVO();
reqVO.setName(RoleCodeEnum.ADMIN.name()).setCode(RoleCodeEnum.ADMIN.getKey()).setSort(0);
return roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType());
reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())
.setSort(0).setRemark("系统自动生成");
Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType());
// 分配权限
permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds());
return roleId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTenant(TenantUpdateReqVO updateReqVO) {
// 校验存在
this.validateTenantExists(updateReqVO.getId());
TenantDO tenant = validateTenantExists(updateReqVO.getId());
// 校验套餐被禁用
tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
// 更新
// 更新租户
TenantDO updateObj = TenantConvert.INSTANCE.convert(updateReqVO);
tenantMapper.updateById(updateObj);
// 如果套餐发生变化则修改其角色的权限
if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) {
updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds());
}
// 发送刷新消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
tenantProducer.sendTenantRefreshMessage();
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) {
TenantUtils.execute(tenantId, () -> {
// 获得所有角色
List<RoleDO> roles = roleService.getRoles(null);
roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配",
role.getId(), role.getTenantId(), tenantId)); // 兜底校验
// 重新分配每个角色的权限
roles.forEach(role -> {
// 如果是租户管理员重新分配其权限为租户套餐的权限
if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
permissionService.assignRoleMenu(role.getId(), menuIds);
log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds);
return;
}
// 如果是其他角色则去掉超过套餐的权限
Set<Long> roleMenuIds = permissionService.getRoleMenuIds(role.getId());
roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
permissionService.assignRoleMenu(role.getId(), roleMenuIds);
log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds);
});
});
}
@Override
public void deleteTenant(Long id) {
// 校验存在
this.validateTenantExists(id);
validateTenantExists(id);
// 删除
tenantMapper.deleteById(id);
}
private void validateTenantExists(Long id) {
if (tenantMapper.selectById(id) == null) {
private TenantDO validateTenantExists(Long id) {
TenantDO tenant = tenantMapper.selectById(id);
if (tenant == null) {
throw exception(TENANT_NOT_EXISTS);
}
return tenant;
}
@Override
@ -163,4 +287,9 @@ public class TenantServiceImpl implements TenantService {
return tenantMapper.selectCountByPackageId(packageId);
}
@Override
public List<TenantDO> getTenantListByPackageId(Long packageId) {
return tenantMapper.selectListByPackageId(packageId);
}
}

View File

@ -243,7 +243,7 @@ public class MenuServiceTest extends BaseDbUnitTest {
menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2);
mockCacheMap.put(menuDO.getId(), menuDO);
List<MenuDO> menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(MenuTypeEnum.MENU.getType()),
List<MenuDO> menuDOS = sysMenuService.getMenuListFromCache(Collections.singletonList(MenuTypeEnum.MENU.getType()),
Collections.singletonList(CommonStatusEnum.DISABLE.getStatus()));
assertEquals(menuDOS.size(), idMenuMap.size());
menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m));
@ -270,7 +270,7 @@ public class MenuServiceTest extends BaseDbUnitTest {
menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2);
mockCacheMap.put(menuDO.getId(), menuDO);
List<MenuDO> menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(1L),
List<MenuDO> menuDOS = sysMenuService.getMenuListFromCache(Collections.singletonList(1L),
Collections.singletonList(MenuTypeEnum.MENU.getType()), Collections.singletonList(1));
assertEquals(menuDOS.size(), idMenuMap.size());
menuDOS.forEach(menu -> assertPojoEquals(idMenuMap.get(menu.getId()), menu));

View File

@ -79,8 +79,8 @@ yudao:
- cn.iocoder.yudao.module.tool.enums.ErrorCodeConstants
tenant: # 多租户相关配置项
enable: true
ignore-urls: /admin-api/system/captcha/get-image, /admin-api/infra/file/get/*
ignore-tables: infra_config, infra_file, infra_job, infra_job_log, infra_job_log, system_tenant, system_tenant_package, system_dict_data, system_dict_type, system_error_code, system_menu, system_sms_channel, tool_codegen_column, tool_codegen_table, tool_test_demo, tables, columns
ignore-urls: /admin-api/system/tenant/get-id-by-name, /admin-api/system/captcha/get-image, /admin-api/infra/file/get/*
ignore-tables: infra_config, infra_file, infra_job, infra_job_log, infra_job_log, system_tenant, system_tenant_package, system_dict_data, system_dict_type, system_error_code, system_menu, system_sms_channel, system_sms_template, tool_codegen_column, tool_codegen_table, tool_test_demo, tables, columns
sms-code: # 短信验证码相关的配置项
expire-times: 10m
send-frequency: 1m

View File

@ -200,6 +200,8 @@ export default {
this.reset();
this.open = true;
this.title = "添加租户套餐";
// 使
this.menuCheckStrictly = false;
},
/** 修改按钮操作 */
handleUpdate(row) {
@ -222,12 +224,6 @@ export default {
/** 获得菜单 */
getMenus() {
listSimpleMenus().then(response => {
// BUTTON DIRMENU
for (let i = response.data.length -1; i >= 0 ; i--) {
if (response.data[i].type === SystemMenuTypeEnum.BUTTON) {
response.data.splice(i, 1);
}
}
// menuOptions
this.menuOptions = [];
//

View File

@ -29,8 +29,9 @@ TODO
* 【新增】后端 `yudao.tenant.enable` 配置项,前端 `VUE_APP_TENANT_ENABLE` 配置项,用于开关租户功能。 [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/79311ecc71f0c6beabe0e5f84e1423ce745a5f09)
* 【优化】调整默认所有表开启多租户的特性,可通过 `yudao.tenant.ignore-tables` 配置项进行忽略,替代原本默认不开启的策略 [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/79311ecc71f0c6beabe0e5f84e1423ce745a5f09)
* 【新增】通过 `yudao.tenant.ignore-urls` 配置忽略多租户的请求,例如说 ,例如说短信回调、支付回调等 Open API [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/79311ecc71f0c6beabe0e5f84e1423ce745a5f09)
* 【新增】新增 `@TenantIgnore` 注解,标记指定方法,忽略多租户的自动过滤,适合实现跨租户的逻辑 [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/4d53944771c66b563da1e3d68d3ba43405af8a06)
* 【新增】租户套餐的管理,可配置每个租户的可使用的功能权限 [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/6b6d676a6baa2dad16ae9bf03d5002209064c8cc)
* 【优化】新建租户时,自动创建对应的管理员账号、角色等基础信息 []()
* 【优化】新建租户时,自动创建对应的管理员账号、角色等基础信息 [commit](https://gitee.com/zhijiantianya/ruoyi-vue-pro/commit/2598c033a95d4b61d5f5ab3da5f1414f25c510d6)
### 🐞 Bug Fixes