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 getMenuList() { return menuMapper.selectList(); } @Override public List getMenuListByTenant(MenuListReqVO reqVO) { // 查询所有菜单,并过滤掉关闭的节点 List menus = getMenuList(reqVO); // 开启多租户的情况下,需要过滤掉未开通的菜单 tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); return menus; } @Override public List getAppMenuListByTenant(MenuListReqVO reqVO) { // 获取 tenantId Long tenantId = getTenantId(); // 查询所有菜单,并过滤掉关闭的节点 List menus = getAppMenuList(tenantId, reqVO); // 开启多租户的情况下,需要过滤掉未开通的菜单 tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); return menus; } @Override public List filterDisableMenus(List menuList) { if (CollUtil.isEmpty(menuList)){ return Collections.emptyList(); } Map menuMap = convertMap(menuList, MenuDO::getId); // 遍历 menu 菜单,查找不是禁用的菜单,添加到 enabledMenus 结果 List enabledMenus = new ArrayList<>(); Set disabledMenuCache = new HashSet<>(); // 存下递归搜索过被禁用的菜单,防止重复的搜索 for (MenuDO menu : menuList) { if (isMenuDisabled(menu, menuMap, disabledMenuCache)) { continue; } enabledMenus.add(menu); } return enabledMenus; } @Override public List filterMenus(List menuList, String type) { if (CollUtil.isEmpty(menuList)){ return Collections.emptyList(); } Map menuMap = convertMap(menuList, MenuDO::getId); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //查询所有的系统应用菜单 if("system".equals(type)) { queryWrapper.eq(AppDO::getType, 0); } else if("app".equals(type)) { queryWrapper.eq(AppDO::getType, 1); } List appDOS = appMapper.selectList(queryWrapper); List appIds = appDOS.stream().map(AppDO::getId).collect(Collectors.toList()); List menuDOS = menuMapper.selectList(new LambdaQueryWrapper().in(MenuDO::getAppId, appIds)); List systemMenuIds = menuDOS.stream().map(MenuDO::getId).collect(Collectors.toList()); // 遍历 menu 菜单,查找不是禁用的菜单,添加到 系统菜单(应用菜单) 结果 List systemMenus = new ArrayList<>(); Set 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 menuMap, Set 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 menuMap, Set menuCache, List 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 getMenuList(MenuListReqVO reqVO) { return menuMapper.selectList(reqVO); } @Override public List getAppMenuList(Long tenantId, MenuListReqVO reqVO) { List menuDOS = menuMapper.selectAppMenuList(reqVO); menuDOS = filterMenus(menuDOS, "app"); Set menuDOIds = menuDOS.stream().map(MenuDO::getId).collect(Collectors.toSet()); TenantDO tenant = tenantService.getTenant(tenantId); TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId()); Set tenantMenuIds = tenantPackage.getMenuIds(); menuDOS = menuDOS.stream().filter(menuDO -> tenantMenuIds.contains(menuDO.getId())).collect(Collectors.toList()); // 获得角色列表 Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); List roles = roleService.getRoleList(roleIds); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 if (roles.stream().noneMatch(role -> role.getCode().equals("tenant_admin"))) { // 获得菜单列表 Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); //取交集 menuIds.retainAll(menuDOIds); List menuList = getMenuList(menuIds); menuList = filterDisableMenus(menuList); return menuList; } return menuDOS; } @Override @Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission") public List getMenuIdListByPermissionFromCache(String permission) { List 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().eq(MenuDO::getAppId, id).eq(MenuDO::getParentId, 0l)); } @Override public List getMenuList(Collection ids) { // 当 ids 为空时,返回一个空的实例对象 if (CollUtil.isEmpty(ids)) { return Lists.newArrayList(); } return menuMapper.selectBatchIds(ids); } @Override public List selectListByParentId(Collection ids) { return menuMapper.selectListByParentId(ids); } /** * 校验父菜单是否合法 *

* 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); } } /** * 校验菜单是否合法 *

* 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); } } /** * 初始化菜单的通用属性。 *

* 例如说,只有目录或者菜单类型的菜单,才设置 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 menuIds = tenantPackage.getMenuIds(); menuIds.add(menu.getId()); tenantPackage.setMenuIds(menuIds); tenantPackageService.updateTenantPackage(BeanUtils.toBean(tenantPackage, TenantPackageSaveReqVO.class)); permissionService.assignRoleMenu(tenantRole.getId(), menuIds); // 开发者自己创建的应用菜单默认赋权给创建者所拥有的角色 //查询当前用户所拥有的角色 Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); List 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); }); } } }