package com.iailab.module.system.service.permission;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.collect.Lists;
|
import com.iailab.framework.common.enums.CommonStatusEnum;
|
import com.iailab.framework.common.util.object.BeanUtils;
|
import com.iailab.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
|
import com.iailab.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
|
import com.iailab.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO;
|
import com.iailab.module.system.dal.dataobject.app.AppDO;
|
import com.iailab.module.system.dal.dataobject.app.AppMenuDO;
|
import com.iailab.module.system.dal.dataobject.permission.MenuDO;
|
import com.iailab.module.system.dal.dataobject.permission.RoleDO;
|
import com.iailab.module.system.dal.dataobject.permission.RoleMenuDO;
|
import com.iailab.module.system.dal.dataobject.tenant.TenantDO;
|
import com.iailab.module.system.dal.dataobject.tenant.TenantPackageDO;
|
import com.iailab.module.system.dal.mysql.app.AppMapper;
|
import com.iailab.module.system.dal.mysql.app.AppMenuMapper;
|
import com.iailab.module.system.dal.mysql.permission.MenuMapper;
|
import com.iailab.module.system.dal.mysql.permission.RoleMenuMapper;
|
import com.iailab.module.system.dal.redis.RedisKeyConstants;
|
import com.iailab.module.system.enums.permission.MenuTypeEnum;
|
import com.iailab.module.system.service.app.AppService;
|
import com.iailab.module.system.service.tenant.TenantPackageService;
|
import com.iailab.module.system.service.tenant.TenantService;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import javax.annotation.Resource;
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static com.iailab.framework.common.pojo.CommonResult.success;
|
import static com.iailab.framework.common.util.collection.CollectionUtils.*;
|
import static com.iailab.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static com.iailab.framework.tenant.core.context.TenantContextHolder.getTenantId;
|
import static com.iailab.module.system.dal.dataobject.permission.MenuDO.ID_ROOT;
|
import static com.iailab.module.system.enums.ErrorCodeConstants.*;
|
|
|
/**
|
* 菜单 Service 实现
|
*
|
* @author iailab
|
*/
|
@Service
|
@Slf4j
|
public class MenuServiceImpl implements MenuService {
|
|
@Resource
|
private MenuMapper menuMapper;
|
@Resource
|
private PermissionService permissionService;
|
@Resource
|
@Lazy // 延迟,避免循环依赖报错
|
private TenantService tenantService;
|
|
@Resource
|
private TenantPackageService tenantPackageService;
|
|
@Resource
|
private AppService appService;
|
|
@Resource
|
private RoleService roleService;
|
|
@Resource
|
private RoleMenuMapper roleMenuMapper;
|
@Autowired
|
private AppMapper appMapper;
|
@Autowired
|
private AppMenuMapper appMenuMapper;
|
|
@Override
|
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission",
|
condition = "#createReqVO.permission != null")
|
@Transactional(rollbackFor = Exception.class)
|
public Long createMenu(MenuSaveVO createReqVO) {
|
// 校验父菜单存在
|
validateParentMenu(createReqVO.getParentId(), null);
|
// 校验菜单(自己)
|
validateMenu(createReqVO.getParentId(), createReqVO.getName(), null);
|
|
// 插入数据库
|
MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class);
|
initMenuProperty(menu);
|
|
//菜单归属租户和应用
|
Long tenantId = getTenantId();
|
menu.setTenantId(tenantId);
|
menu.setAppId(createReqVO.getAppId());
|
menuMapper.insert(menu);
|
if(tenantId != 1L) {
|
dealPermission(menu);
|
}
|
// 返回
|
return menu.getId();
|
}
|
|
@Override
|
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST,
|
allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效
|
@Transactional(rollbackFor = Exception.class)
|
public void updateMenu(MenuSaveVO updateReqVO) {
|
// 校验更新的菜单是否存在
|
if (menuMapper.selectById(updateReqVO.getId()) == null) {
|
throw exception(MENU_NOT_EXISTS);
|
}
|
// 校验父菜单存在
|
validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId());
|
// 校验菜单(自己)
|
validateMenu(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId());
|
|
// 更新到数据库
|
MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class);
|
initMenuProperty(updateObj);
|
//菜单归属租户和应用
|
Long tenantId = getTenantId();
|
AppDO appDO = appService.getAppByTenantId(tenantId);
|
if(appDO.getTenantId() != 1) {
|
updateObj.setTenantId(tenantId);
|
updateObj.setAppId(appDO.getId());
|
}
|
menuMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST,
|
allEntries = true) // allEntries 清空所有缓存,因为此时不知道 id 对应的 permission 是多少。直接清理,简单有效
|
public void deleteMenu(Long id) {
|
// 校验是否还有子菜单
|
if (menuMapper.selectCountByParentId(id) > 0) {
|
throw exception(MENU_EXISTS_CHILDREN);
|
}
|
// 校验删除的菜单是否存在
|
if (menuMapper.selectById(id) == null) {
|
throw exception(MENU_NOT_EXISTS);
|
}
|
// 标记删除
|
menuMapper.deleteById(id);
|
// 删除授予给角色的权限
|
permissionService.processMenuDeleted(id);
|
}
|
|
@Override
|
public List<MenuDO> getMenuList() {
|
return menuMapper.selectList();
|
}
|
|
@Override
|
public List<MenuDO> getMenuListByTenant(MenuListReqVO reqVO) {
|
// 查询所有菜单,并过滤掉关闭的节点
|
List<MenuDO> menus = getMenuList(reqVO);
|
// 开启多租户的情况下,需要过滤掉未开通的菜单
|
tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId())));
|
return menus;
|
}
|
|
@Override
|
public List<MenuDO> getAppMenuListByTenant(MenuListReqVO reqVO) {
|
// 获取 tenantId
|
Long tenantId = getTenantId();
|
// 查询所有菜单,并过滤掉关闭的节点
|
List<MenuDO> menus = getAppMenuList(tenantId, reqVO);
|
// 开启多租户的情况下,需要过滤掉未开通的菜单
|
tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId())));
|
return menus;
|
}
|
|
@Override
|
public List<MenuDO> filterDisableMenus(List<MenuDO> menuList) {
|
if (CollUtil.isEmpty(menuList)){
|
return Collections.emptyList();
|
}
|
Map<Long, MenuDO> menuMap = convertMap(menuList, MenuDO::getId);
|
|
// 遍历 menu 菜单,查找不是禁用的菜单,添加到 enabledMenus 结果
|
List<MenuDO> enabledMenus = new ArrayList<>();
|
Set<Long> disabledMenuCache = new HashSet<>(); // 存下递归搜索过被禁用的菜单,防止重复的搜索
|
for (MenuDO menu : menuList) {
|
if (isMenuDisabled(menu, menuMap, disabledMenuCache)) {
|
continue;
|
}
|
enabledMenus.add(menu);
|
}
|
return enabledMenus;
|
}
|
|
@Override
|
public List<MenuDO> filterMenus(List<MenuDO> menuList, String type) {
|
if (CollUtil.isEmpty(menuList)){
|
return Collections.emptyList();
|
}
|
Map<Long, MenuDO> menuMap = convertMap(menuList, MenuDO::getId);
|
LambdaQueryWrapper<AppDO> queryWrapper = new LambdaQueryWrapper<>();
|
|
//查询所有的系统应用菜单
|
if("system".equals(type)) {
|
queryWrapper.eq(AppDO::getType, 0);
|
} else if("app".equals(type)) {
|
queryWrapper.eq(AppDO::getType, 1);
|
}
|
List<AppDO> appDOS = appMapper.selectList(queryWrapper);
|
List<Long> appIds = appDOS.stream().map(AppDO::getId).collect(Collectors.toList());
|
List<MenuDO> menuDOS = menuMapper.selectList(new LambdaQueryWrapper<MenuDO>().in(MenuDO::getAppId, appIds));
|
List<Long> systemMenuIds = menuDOS.stream().map(MenuDO::getId).collect(Collectors.toList());
|
|
// 遍历 menu 菜单,查找不是禁用的菜单,添加到 系统菜单(应用菜单) 结果
|
List<MenuDO> systemMenus = new ArrayList<>();
|
Set<Long> appMenuCache = new HashSet<>(); // 存下递归搜索过被禁用的菜单,防止重复的搜索
|
for (MenuDO menu : menuList) {
|
if (isAppMenu(menu, menuMap, appMenuCache, systemMenuIds)) {
|
continue;
|
}
|
systemMenus.add(menu);
|
}
|
return systemMenus;
|
}
|
|
private boolean isMenuDisabled(MenuDO node, Map<Long, MenuDO> menuMap, Set<Long> disabledMenuCache) {
|
// 如果已经判定是禁用的节点,直接结束
|
if (disabledMenuCache.contains(node.getId())) {
|
return true;
|
}
|
|
// 1. 遍历到 parentId 为根节点,则无需判断
|
Long parentId = node.getParentId();
|
if (ObjUtil.equal(parentId, ID_ROOT)) {
|
if (CommonStatusEnum.isDisable(node.getStatus())) {
|
disabledMenuCache.add(node.getId());
|
return true;
|
}
|
return false;
|
}
|
|
// 2. 继续遍历 parent 节点
|
MenuDO parent = menuMap.get(parentId);
|
if (parent == null || isMenuDisabled(parent, menuMap, disabledMenuCache)) {
|
disabledMenuCache.add(node.getId());
|
return true;
|
}
|
return false;
|
}
|
|
private boolean isAppMenu(MenuDO node, Map<Long, MenuDO> menuMap, Set<Long> menuCache, List<Long> systemMenuIds) {
|
// 如果已经判定是禁用的节点,直接结束
|
if (menuCache.contains(node.getId())) {
|
return true;
|
}
|
|
// 2. 遍历到 parentId 为根节点,则无需判断
|
Long parentId = node.getParentId();
|
if (ObjUtil.equal(parentId, ID_ROOT)) {
|
if (!systemMenuIds.contains(node.getId())) {
|
menuCache.add(node.getId());
|
return true;
|
}
|
return false;
|
}
|
|
// 3. 继续遍历 parent 节点
|
MenuDO parent = menuMap.get(parentId);
|
if (parent == null || isAppMenu(parent, menuMap, menuCache, systemMenuIds)) {
|
menuCache.add(node.getId());
|
return true;
|
}
|
return false;
|
}
|
|
@Override
|
public List<MenuDO> getMenuList(MenuListReqVO reqVO) {
|
return menuMapper.selectList(reqVO);
|
}
|
|
@Override
|
public List<MenuDO> getAppMenuList(Long tenantId, MenuListReqVO reqVO) {
|
List<MenuDO> menuDOS = menuMapper.selectAppMenuList(reqVO);
|
menuDOS = filterMenus(menuDOS, "app");
|
Set<Long> menuDOIds = menuDOS.stream().map(MenuDO::getId).collect(Collectors.toSet());
|
TenantDO tenant = tenantService.getTenant(tenantId);
|
TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId());
|
Set<Long> tenantMenuIds = tenantPackage.getMenuIds();
|
menuDOS = menuDOS.stream().filter(menuDO -> tenantMenuIds.contains(menuDO.getId())).collect(Collectors.toList());
|
// 获得角色列表
|
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
|
List<RoleDO> roles = roleService.getRoleList(roleIds);
|
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
|
if (roles.stream().noneMatch(role -> role.getCode().equals("tenant_admin"))) {
|
// 获得菜单列表
|
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
|
//取交集
|
menuIds.retainAll(menuDOIds);
|
List<MenuDO> menuList = getMenuList(menuIds);
|
menuList = filterDisableMenus(menuList);
|
return menuList;
|
}
|
return menuDOS;
|
}
|
|
@Override
|
@Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission")
|
public List<Long> getMenuIdListByPermissionFromCache(String permission) {
|
List<MenuDO> menus = menuMapper.selectListByPermission(permission);
|
return convertList(menus, MenuDO::getId);
|
}
|
|
@Override
|
public MenuDO getMenu(Long id) {
|
return menuMapper.selectById(id);
|
}
|
|
@Override
|
public MenuDO getMenuByAppId(Long id) {
|
return menuMapper.selectOne(new LambdaQueryWrapper<MenuDO>().eq(MenuDO::getAppId, id).eq(MenuDO::getParentId, 0l));
|
}
|
|
@Override
|
public List<MenuDO> getMenuList(Collection<Long> ids) {
|
// 当 ids 为空时,返回一个空的实例对象
|
if (CollUtil.isEmpty(ids)) {
|
return Lists.newArrayList();
|
}
|
return menuMapper.selectBatchIds(ids);
|
}
|
|
@Override
|
public List<MenuDO> selectListByParentId(Collection<Long> ids) {
|
return menuMapper.selectListByParentId(ids);
|
}
|
|
/**
|
* 校验父菜单是否合法
|
* <p>
|
* 1. 不能设置自己为父菜单
|
* 2. 父菜单不存在
|
* 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型
|
*
|
* @param parentId 父菜单编号
|
* @param childId 当前菜单编号
|
*/
|
@VisibleForTesting
|
void validateParentMenu(Long parentId, Long childId) {
|
if (parentId == null || ID_ROOT.equals(parentId)) {
|
return;
|
}
|
// 不能设置自己为父菜单
|
if (parentId.equals(childId)) {
|
throw exception(MENU_PARENT_ERROR);
|
}
|
MenuDO menu = menuMapper.selectById(parentId);
|
// 父菜单不存在
|
if (menu == null) {
|
throw exception(MENU_PARENT_NOT_EXISTS);
|
}
|
// 父菜单必须是目录或者菜单类型
|
if (!MenuTypeEnum.DIR.getType().equals(menu.getType())
|
&& !MenuTypeEnum.MENU.getType().equals(menu.getType())) {
|
throw exception(MENU_PARENT_NOT_DIR_OR_MENU);
|
}
|
}
|
|
/**
|
* 校验菜单是否合法
|
* <p>
|
* 1. 校验相同父菜单编号下,是否存在相同的菜单名
|
*
|
* @param name 菜单名字
|
* @param parentId 父菜单编号
|
* @param id 菜单编号
|
*/
|
@VisibleForTesting
|
void validateMenu(Long parentId, String name, Long id) {
|
MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name);
|
if (menu == null) {
|
return;
|
}
|
// 如果 id 为空,说明不用比较是否为相同 id 的菜单
|
if (id == null) {
|
throw exception(MENU_NAME_DUPLICATE);
|
}
|
if (!menu.getId().equals(id)) {
|
throw exception(MENU_NAME_DUPLICATE);
|
}
|
}
|
|
/**
|
* 初始化菜单的通用属性。
|
* <p>
|
* 例如说,只有目录或者菜单类型的菜单,才设置 icon
|
*
|
* @param menu 菜单
|
*/
|
private void initMenuProperty(MenuDO menu) {
|
// 菜单为按钮类型时,无需 component、icon、path 属性,进行置空
|
if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) {
|
menu.setComponent("");
|
menu.setComponentName("");
|
menu.setIcon("");
|
menu.setPath("");
|
}
|
}
|
|
/**
|
* 新创建菜单赋权给租户管理员
|
*/
|
|
private void dealPermission(MenuDO menu) {
|
Long tenantId = menu.getTenantId();
|
RoleDO tenantRole = roleService.getTenantAdminRole(tenantId);
|
TenantDO tenant = tenantService.getTenant(tenantId);
|
TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId());
|
Set<Long> menuIds = tenantPackage.getMenuIds();
|
menuIds.add(menu.getId());
|
tenantPackage.setMenuIds(menuIds);
|
tenantPackageService.updateTenantPackage(BeanUtils.toBean(tenantPackage, TenantPackageSaveReqVO.class));
|
permissionService.assignRoleMenu(tenantRole.getId(), menuIds);
|
// 开发者自己创建的应用菜单默认赋权给创建者所拥有的角色
|
//查询当前用户所拥有的角色
|
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
|
List<RoleDO> roles = roleService.getRoleList(roleIds);
|
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
|
roles.removeIf(role -> tenantRole.getId().equals(role.getId())); // 移除租户管理员角色
|
if (!roles.isEmpty()) {
|
roles.stream().forEach(roleDO -> {
|
RoleMenuDO roleMenuDO = new RoleMenuDO();
|
roleMenuDO.setMenuId(menu.getId());
|
roleMenuDO.setRoleId(roleDO.getId());
|
roleMenuDO.setTenantId(tenant.getId());
|
roleMenuMapper.insert(roleMenuDO);
|
});
|
}
|
}
|
|
}
|