feat(menu): 优化removeMenuById方法实现递归删除子菜单

This commit is contained in:
PIGCLOUD committed 2025-12-28 11:25:26 +08:00
1 parent 65297a5574
commit ab97082cd8
1 file changed
+191 -112
@@ -37,9 +37,11 @@ import com.pig4cloud.pigx.admin.service.SysMenuService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.MenuTypeEnum;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.exception.ErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -66,134 +68,211 @@ import java.util.function.Predicate;
@Slf4j
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
private final SysRoleMenuMapper sysRoleMenuMapper;
private final SysRoleMenuMapper sysRoleMenuMapper;
private final SysI18nService sysI18nService;
private final SysI18nService sysI18nService;
@Override
@Cacheable(value = CacheConstants.MENU_DETAILS, key = "#roleId", unless = "#result.isEmpty()")
public List<SysMenu> findMenuByRoleId(Long roleId) {
return baseMapper.listMenusByRoleId(roleId);
}
@Override
@Cacheable(value = CacheConstants.MENU_DETAILS, key = "#roleId", unless = "#result.isEmpty()")
public List<SysMenu> findMenuByRoleId(Long roleId) {
return baseMapper.listMenusByRoleId(roleId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public R removeMenuById(Long id) {
// 查询父节点为当前节点的节点
List<SysMenu> menuList = this.list(Wrappers.<SysMenu>query().lambda().eq(SysMenu::getParentId, id));
if (CollUtil.isNotEmpty(menuList)) {
return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_MENU_DELETE_EXISTING));
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public R removeMenuById(Long id) {
// 1. 检查菜单是否存在
SysMenu menu = this.getById(id);
if (menu == null) {
return R.failed("菜单不存在");
}
sysRoleMenuMapper.delete(Wrappers.<SysRoleMenu>query().lambda().eq(SysRoleMenu::getMenuId, id));
// 删除当前菜单及其子菜单
return R.ok(this.removeById(id));
}
// 2. 递归收集当前菜单及所有子孙菜单的ID
List<Long> menuIdsToDelete = new ArrayList<>();
collectMenuIds(id, menuIdsToDelete);
@Override
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public Boolean updateMenuById(SysMenu sysMenu) {
return this.updateById(sysMenu);
}
// 3. 批量删除所有角色-菜单关联
if (CollUtil.isNotEmpty(menuIdsToDelete)) {
sysRoleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().in(SysRoleMenu::getMenuId, menuIdsToDelete));
}
/**
* 构建树查询 1. 不是懒加载情况,查询全部 2. 是懒加载,根据parentId 查询 2.1 父节点为空,则查询ID -1
* @param parentId 父节点ID
* @param menuName 菜单名称
* @return
*/
@Override
public List<Tree<Long>> treeMenu(Long parentId, String menuName, String type) {
Long parent = parentId == null ? CommonConstants.MENU_TREE_ROOT_ID : parentId;
// 4. 批量逻辑删除菜单
return R.ok(this.removeByIds(menuIdsToDelete));
}
List<TreeNode<Long>> collect = baseMapper
.selectList(Wrappers.<SysMenu>lambdaQuery()
.eq(Objects.nonNull(parentId), SysMenu::getParentId, parentId)
.like(StrUtil.isNotBlank(menuName), SysMenu::getName, menuName)
.eq(StrUtil.isNotBlank(type), SysMenu::getMenuType, type)
.orderByAsc(SysMenu::getSortOrder))
.stream()
.map(getNodeFunction())
.toList();
/**
* 递归收集菜单ID及其所有子孙菜单ID
* @param menuId 菜单ID
* @param menuIds 收集结果列表
*/
private void collectMenuIds(Long menuId, List<Long> menuIds) {
menuIds.add(menuId);
// 模糊查询 不组装树结构 直接返回 表格方便编辑
if (StrUtil.isNotBlank(menuName)) {
return collect.stream().map(node -> {
Tree<Long> tree = new Tree<>();
tree.putAll(node.getExtra());
BeanUtils.copyProperties(node, tree);
return tree;
List<SysMenu> children = this.list(Wrappers.<SysMenu>lambdaQuery().eq(SysMenu::getParentId, menuId));
if (CollUtil.isNotEmpty(children)) {
for (SysMenu child : children) {
collectMenuIds(child.getMenuId(), menuIds);
}
}
}
@Override
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public Boolean updateMenuById(SysMenu sysMenu) {
return this.updateById(sysMenu);
}
/**
* 构建树查询 1. 不是懒加载情况,查询全部 2. 是懒加载,根据parentId 查询 2.1 父节点为空,则查询ID -1
*
* @param parentId 父节点ID
* @param menuName 菜单名称
* @return
*/
@Override
public List<Tree<Long>> treeMenu(Long parentId, String menuName, String type) {
Long parent = parentId == null ? CommonConstants.MENU_TREE_ROOT_ID : parentId;
List<TreeNode<Long>> collect = baseMapper.selectList(Wrappers.<SysMenu>lambdaQuery().eq(Objects.nonNull(parentId), SysMenu::getParentId, parentId).like(StrUtil.isNotBlank(menuName), SysMenu::getName, menuName).eq(StrUtil.isNotBlank(type), SysMenu::getMenuType, type).orderByAsc(SysMenu::getSortOrder)).stream().map(getNodeFunction()).toList();
// 模糊查询 不组装树结构 直接返回 表格方便编辑
if (StrUtil.isNotBlank(menuName)) {
return collect.stream().map(node -> {
Tree<Long> tree = new Tree<>();
tree.putAll(node.getExtra());
BeanUtils.copyProperties(node, tree);
return tree;
}).toList();
}
}
return TreeUtil.build(collect, parent);
}
return TreeUtil.build(collect, parent);
}
/**
* 查询菜单
* @param all 全部菜单
* @param type 类型
* @param parentId 父节点ID
* @return
*/
@Override
public List<Tree<Long>> filterMenu(Set<SysMenu> all, String type, Long parentId) {
List<SysI18nEntity> list = sysI18nService.list();
List<TreeNode<Long>> collect = all.stream().filter(menuTypePredicate(type)).peek(item -> {
Optional<SysI18nEntity> first = list.stream().filter(it -> it.getZhCn().equals(item.getName())).findFirst();
first.ifPresent(sysI18nEntity -> item.setName(sysI18nEntity.getName()));
/**
* 查询菜单
*
* @param all 全部菜单
* @param type 类型
* @param parentId 父节点ID
* @return
*/
@Override
public List<Tree<Long>> filterMenu(Set<SysMenu> all, String type, Long parentId) {
List<SysI18nEntity> list = sysI18nService.list();
List<TreeNode<Long>> collect = all.stream().filter(menuTypePredicate(type)).peek(item -> {
Optional<SysI18nEntity> first = list.stream().filter(it -> it.getZhCn().equals(item.getName())).findFirst();
first.ifPresent(sysI18nEntity -> item.setName(sysI18nEntity.getName()));
}).map(getNodeFunction()).toList();
Long parent = parentId == null ? CommonConstants.MENU_TREE_ROOT_ID : parentId;
return TreeUtil.build(collect, parent);
}
Long parent = parentId == null ? CommonConstants.MENU_TREE_ROOT_ID : parentId;
return TreeUtil.build(collect, parent);
}
@NotNull
private Function<SysMenu, TreeNode<Long>> getNodeFunction() {
return menu -> {
TreeNode<Long> node = new TreeNode<>();
node.setId(menu.getMenuId());
node.setName(menu.getName());
node.setParentId(menu.getParentId());
node.setWeight(menu.getSortOrder());
// 扩展属性
Map<String, Object> extra = new HashMap<>();
extra.put("path", menu.getPath());
extra.put("componentPath", menu.getComponent());
extra.put("menuType", menu.getMenuType());
extra.put("permission", menu.getPermission());
extra.put("sortOrder", menu.getSortOrder());
@NotNull
private Function<SysMenu, TreeNode<Long>> getNodeFunction() {
return menu -> {
TreeNode<Long> node = new TreeNode<>();
node.setId(menu.getMenuId());
node.setName(menu.getName());
node.setParentId(menu.getParentId());
node.setWeight(menu.getSortOrder());
// 扩展属性
Map<String, Object> extra = new HashMap<>();
extra.put("path", menu.getPath());
extra.put("componentPath", menu.getComponent());
extra.put("menuType", menu.getMenuType());
extra.put("permission", menu.getPermission());
extra.put("sortOrder", menu.getSortOrder());
extra.put("isHomePage", menu.getIsHomePage());
// 适配 vue3
Map<String, Object> meta = new HashMap<>();
meta.put("title", menu.getName());
meta.put("isLink", menu.getPath() != null && menu.getPath().startsWith("http") ? menu.getPath() : "");
meta.put("isHide", !BooleanUtil.toBooleanObject(menu.getVisible()));
meta.put("isKeepAlive", BooleanUtil.toBooleanObject(menu.getKeepAlive()));
meta.put("isAffix", false);
meta.put("isIframe", BooleanUtil.toBooleanObject(menu.getEmbedded()));
meta.put("icon", menu.getIcon());
// 适配 vue3
Map<String, Object> meta = new HashMap<>();
meta.put("title", menu.getName());
meta.put("isLink", menu.getPath() != null && menu.getPath().startsWith("http") ? menu.getPath() : "");
meta.put("isHide", !BooleanUtil.toBooleanObject(menu.getVisible()));
meta.put("isKeepAlive", BooleanUtil.toBooleanObject(menu.getKeepAlive()));
meta.put("isAffix", false);
meta.put("isIframe", BooleanUtil.toBooleanObject(menu.getEmbedded()));
meta.put("icon", menu.getIcon());
extra.put("meta", meta);
node.setExtra(extra);
return node;
};
}
extra.put("meta", meta);
node.setExtra(extra);
return node;
};
}
/**
* menu 类型断言
* @param type 类型
* @return Predicate
*/
private Predicate<SysMenu> menuTypePredicate(String type) {
return vo -> {
if (MenuTypeEnum.TOP_MENU.getDescription().equals(type)) {
return MenuTypeEnum.TOP_MENU.getType().equals(vo.getMenuType());
}
// 其他查询 左侧 + 顶部
return !MenuTypeEnum.BUTTON.getType().equals(vo.getMenuType());
};
}
/**
* menu 类型断言
*
* @param type 类型
* @return Predicate
*/
private Predicate<SysMenu> menuTypePredicate(String type) {
return vo -> {
if (MenuTypeEnum.TOP_MENU.getDescription().equals(type)) {
return MenuTypeEnum.TOP_MENU.getType().equals(vo.getMenuType());
}
// 其他查询 左侧 + 顶部
return !MenuTypeEnum.BUTTON.getType().equals(vo.getMenuType());
};
}
/**
* 设置首页菜单
*
* @param menuId 菜单ID
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public R<Boolean> setHomePage(Long menuId) {
// 查询菜单是否存在且为菜单类型(非按钮)
SysMenu menu = this.getById(menuId);
if (menu == null) {
return R.failed("菜单不存在");
}
if (MenuTypeEnum.BUTTON.getType().equals(menu.getMenuType())) {
return R.failed("按钮类型不能设置为首页");
}
// Toggle 模式:如果已是首页则取消,否则设置为首页
if (YesNoEnum.YES.getCode().equals(menu.getIsHomePage())) {
// 已是首页,取消首页设置
menu.setIsHomePage(YesNoEnum.NO.getCode());
this.updateById(menu);
return R.ok(Boolean.TRUE, "取消首页成功");
} else {
// 不是首页,先将所有菜单的首页标记重置为0,再设置当前菜单为首页
this.update(Wrappers.<SysMenu>lambdaUpdate().set(SysMenu::getIsHomePage, YesNoEnum.NO.getCode()));
menu.setIsHomePage(YesNoEnum.YES.getCode());
this.updateById(menu);
return R.ok(Boolean.TRUE, "设置首页成功");
}
}
/**
* 获取首页菜单
* 根据当前用户的角色ID查询其有权限访问的首页菜单
*
* @return 首页菜单
*/
@Override
public SysMenu getHomePage() {
// 获取当前用户的角色ID列表
List<Long> roleIds = SecurityUtils.getRoleIds();
if (CollUtil.isEmpty(roleIds)) {
return null;
}
// 获取用户所有角色对应的菜单(直接调用mapper避免self-invocation导致缓存失效)
Set<SysMenu> userMenus = new HashSet<>();
roleIds.forEach(roleId -> userMenus.addAll(baseMapper.listMenusByRoleId(roleId)));
// 在用户有权限的菜单中查找首页菜单
return userMenus.stream().filter(menu -> YesNoEnum.YES.getCode().equals(menu.getIsHomePage())).findFirst().orElse(null);
}
}