Files
youlai-boot/src/main/java/com/youlai/system/service/impl/SysMenuServiceImpl.java
2024-08-04 00:32:33 +08:00

434 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.youlai.system.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.youlai.system.common.constant.SystemConstants;
import com.youlai.system.enums.MenuTypeEnum;
import com.youlai.system.enums.StatusEnum;
import com.youlai.system.common.model.KeyValue;
import com.youlai.system.common.model.Option;
import com.youlai.system.converter.MenuConverter;
import com.youlai.system.mapper.SysMenuMapper;
import com.youlai.system.model.bo.RouteBO;
import com.youlai.system.model.entity.GenConfig;
import com.youlai.system.model.entity.SysMenu;
import com.youlai.system.model.form.MenuForm;
import com.youlai.system.model.query.MenuQuery;
import com.youlai.system.model.vo.MenuVO;
import com.youlai.system.model.vo.RouteVO;
import com.youlai.system.service.SysMenuService;
import com.youlai.system.service.SysRoleMenuService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 菜单业务实现类
*
* @author haoxr
* @since 2020/11/06
*/
@Service
@RequiredArgsConstructor
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
private final MenuConverter menuConverter;
private final SysRoleMenuService roleMenuService;
/**
* 菜单列表
*
* @param queryParams {@link MenuQuery}
*/
@Override
public List<MenuVO> listMenus(MenuQuery queryParams) {
List<SysMenu> menus = this.list(new LambdaQueryWrapper<SysMenu>()
.like(StrUtil.isNotBlank(queryParams.getKeywords()), SysMenu::getName, queryParams.getKeywords())
.orderByAsc(SysMenu::getSort)
);
// 获取所有菜单ID
Set<Long> menuIds = menus.stream()
.map(SysMenu::getId)
.collect(Collectors.toSet());
// 获取所有父级ID
Set<Long> parentIds = menus.stream()
.map(SysMenu::getParentId)
.collect(Collectors.toSet());
// 获取根节点ID递归的起点即父节点ID中不包含在部门ID中的节点注意这里不能拿顶级菜单 O 作为根节点,因为菜单筛选的时候 O 会被过滤掉
List<Long> rootIds = parentIds.stream()
.filter(id -> !menuIds.contains(id))
.toList();
// 使用递归函数来构建菜单树
return rootIds.stream()
.flatMap(rootId -> buildMenuTree(rootId, menus).stream())
.collect(Collectors.toList());
}
/**
* 递归生成菜单列表
*
* @param parentId 父级ID
* @param menuList 菜单列表
* @return 菜单列表
*/
private List<MenuVO> buildMenuTree(Long parentId, List<SysMenu> menuList) {
return CollectionUtil.emptyIfNull(menuList)
.stream()
.filter(menu -> menu.getParentId().equals(parentId))
.map(entity -> {
MenuVO menuVO = menuConverter.convertToVo(entity);
List<MenuVO> children = buildMenuTree(entity.getId(), menuList);
menuVO.setChildren(children);
return menuVO;
}).toList();
}
/**
* 菜单下拉数据
*
* @param onlyParent 是否只查询父级菜单 如果为true排除按钮
*/
@Override
public List<Option> listMenuOptions(boolean onlyParent) {
List<SysMenu> menuList = this.list(new LambdaQueryWrapper<SysMenu>()
.in(onlyParent, SysMenu::getType, MenuTypeEnum.CATALOG.getValue(), MenuTypeEnum.MENU.getValue())
.orderByAsc(SysMenu::getSort)
);
return buildMenuOptions(SystemConstants.ROOT_NODE_ID, menuList);
}
/**
* 递归生成菜单下拉层级列表
*
* @param parentId 父级ID
* @param menuList 菜单列表
* @return 菜单下拉列表
*/
private List<Option> buildMenuOptions(Long parentId, List<SysMenu> menuList) {
List<Option> menuOptions = new ArrayList<>();
for (SysMenu menu : menuList) {
if (menu.getParentId().equals(parentId)) {
Option option = new Option(menu.getId(), menu.getName());
List<Option> subMenuOptions = buildMenuOptions(menu.getId(), menuList);
if (!subMenuOptions.isEmpty()) {
option.setChildren(subMenuOptions);
}
menuOptions.add(option);
}
}
return menuOptions;
}
/**
* 获取菜单路由列表
*/
@Override
public List<RouteVO> listRoutes(Set<String> roles) {
if (CollectionUtil.isEmpty(roles)) {
return Collections.emptyList();
}
List<RouteBO> menuList = this.baseMapper.listRoutes(roles);
return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList);
}
/**
* 递归生成菜单路由层级列表
*
* @param parentId 父级ID
* @param menuList 菜单列表
* @return 路由层级列表
*/
private List<RouteVO> buildRoutes(Long parentId, List<RouteBO> menuList) {
List<RouteVO> routeList = new ArrayList<>();
for (RouteBO menu : menuList) {
if (menu.getParentId().equals(parentId)) {
RouteVO routeVO = toRouteVo(menu);
List<RouteVO> children = buildRoutes(menu.getId(), menuList);
if (!children.isEmpty()) {
routeVO.setChildren(children);
}
routeList.add(routeVO);
}
}
return routeList;
}
/**
* 根据RouteBO创建RouteVO
*/
private RouteVO toRouteVo(RouteBO routeBO) {
RouteVO routeVO = new RouteVO();
// 获取路由名称
String routeName = routeBO.getRouteName();
if (StrUtil.isBlank(routeName)) {
// 路由 name 需要驼峰,首字母大写
routeName = StringUtils.capitalize(StrUtil.toCamelCase(routeBO.getRoutePath(), '-'));
}
// 根据name路由跳转 this.$router.push({name:xxx})
routeVO.setName(routeName);
// 根据path路由跳转 this.$router.push({path:xxx})
routeVO.setPath(routeBO.getRoutePath());
routeVO.setRedirect(routeBO.getRedirect());
routeVO.setComponent(routeBO.getComponent());
RouteVO.Meta meta = new RouteVO.Meta();
meta.setTitle(routeBO.getName());
meta.setIcon(routeBO.getIcon());
meta.setHidden(StatusEnum.DISABLE.getValue().equals(routeBO.getVisible()));
// 【菜单】是否开启页面缓存
if (MenuTypeEnum.MENU.equals(routeBO.getType())
&& ObjectUtil.equals(routeBO.getKeepAlive(), 1)) {
meta.setKeepAlive(true);
}
meta.setAlwaysShow(ObjectUtil.equals(routeBO.getAlwaysShow(), 1));
String paramsJson = routeBO.getParams();
// 将 JSON 字符串转换为 Map<String, String>
if (StrUtil.isNotBlank(paramsJson)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
Map<String, String> paramMap = objectMapper.readValue(paramsJson, new TypeReference<>() {
});
meta.setParams(paramMap);
} catch (Exception e) {
throw new RuntimeException("解析参数失败", e);
}
}
routeVO.setMeta(meta);
return routeVO;
}
/**
* 新增/修改菜单
*/
@Override
@CacheEvict(cacheNames = "menu", key = "'routes'")
public boolean saveMenu(MenuForm menuForm) {
MenuTypeEnum menuType = menuForm.getType();
if (menuType == MenuTypeEnum.CATALOG) { // 如果是外链
String path = menuForm.getRoutePath();
if (menuForm.getParentId() == 0 && !path.startsWith("/")) {
menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头
}
menuForm.setComponent("Layout");
} else if (menuType == MenuTypeEnum.EXTLINK) { // 如果是目录
menuForm.setComponent(null);
}
SysMenu entity = menuConverter.toEntity(menuForm);
String treePath = generateMenuTreePath(menuForm.getParentId());
entity.setTreePath(treePath);
List<KeyValue> params = menuForm.getParams();
// 路由参数 [{key:"id",value:"1"}{key:"name",value:"张三"}] 转换为 [{"id":"1"},{"name":"张三"}]
if (CollectionUtil.isNotEmpty(params)) {
entity.setParams(JSONUtil.toJsonStr(params.stream()
.collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue))));
} else {
entity.setParams(null);
}
if (menuType != MenuTypeEnum.BUTTON) {
Assert.isFalse(this.exists(new LambdaQueryWrapper<SysMenu>()
.eq(SysMenu::getRouteName, entity.getRouteName())
.ne(menuForm.getId() != null, SysMenu::getId, menuForm.getId())
), "路由名称已存在");
}
boolean result = this.saveOrUpdate(entity);
if (result) {
// 编辑刷新角色权限缓存
if (menuForm.getId() != null) {
roleMenuService.refreshRolePermsCache();
}
}
return result;
}
/**
* 部门路径生成
*
* @param parentId 父ID
* @return 父节点路径以英文逗号(, )分割eg: 1,2,3
*/
private String generateMenuTreePath(Long parentId) {
if (SystemConstants.ROOT_NODE_ID.equals(parentId)) {
return String.valueOf(parentId);
} else {
SysMenu parent = this.getById(parentId);
return parent != null ? parent.getTreePath() + "," + parent.getId() : null;
}
}
/**
* 修改菜单显示状态
*
* @param menuId 菜单ID
* @param visible 是否显示(1->显示2->隐藏)
* @return 是否修改成功
*/
@Override
@CacheEvict(cacheNames = "menu", key = "'routes'")
public boolean updateMenuVisible(Long menuId, Integer visible) {
return this.update(new LambdaUpdateWrapper<SysMenu>()
.eq(SysMenu::getId, menuId)
.set(SysMenu::getVisible, visible)
);
}
/**
* 获取菜单表单数据
*
* @param id 菜单ID
* @return 菜单表单数据
*/
@Override
public MenuForm getMenuForm(Long id) {
SysMenu entity = this.getById(id);
Assert.isTrue(entity != null, "菜单不存在");
MenuForm formData = menuConverter.toForm(entity);
// 路由参数字符串 {"id":"1","name":"张三"} 转换为 [{key:"id", value:"1"}, {key:"name", value:"张三"}]
String params = entity.getParams();
if (StrUtil.isNotBlank(params)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 解析 JSON 字符串为 Map<String, String>
Map<String, String> paramMap = objectMapper.readValue(params, new TypeReference<>() {
});
// 转换为 List<KeyValue> 格式 [{key:"id", value:"1"}, {key:"name", value:"张三"}]
List<KeyValue> transformedList = paramMap.entrySet().stream()
.map(entry -> new KeyValue(entry.getKey(), entry.getValue()))
.toList();
// 将转换后的列表存入 MenuForm
formData.setParams(transformedList);
} catch (Exception e) {
throw new RuntimeException("解析参数失败", e);
}
}
return formData;
}
/**
* 删除菜单
*
* @param id 菜单ID
* @return 是否删除成功
*/
@Override
@CacheEvict(cacheNames = "menu", key = "'routes'")
public boolean deleteMenu(Long id) {
boolean result = this.remove(new LambdaQueryWrapper<SysMenu>()
.eq(SysMenu::getId, id)
.or()
.apply("CONCAT (',',tree_path,',') LIKE CONCAT('%,',{0},',%')", id));
// 刷新角色权限缓存
if (result) {
roleMenuService.refreshRolePermsCache();
}
return result;
}
/**
* 为代码生成添加菜单
*
* @param parentMenuId 父菜单ID
* @param genConfig 实体名称
*/
@Override
public void saveMenu(Long parentMenuId, GenConfig genConfig) {
SysMenu parentMenu = this.getById(parentMenuId);
Assert.notNull(parentMenu, "上级菜单不存在");
String entityName = genConfig.getEntityName();
long count = this.count(new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getRouteName, entityName));
if (count > 0) {
return;
}
// 获取父级菜单子菜单最带的排序
SysMenu maxSortMenu = this.getOne(new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getParentId, parentMenuId)
.orderByDesc(SysMenu::getSort)
.last("limit 1")
);
int sort = 1;
if (maxSortMenu != null) {
sort = maxSortMenu.getSort() + 1;
}
SysMenu menu = new SysMenu();
menu.setParentId(parentMenuId);
menu.setName(genConfig.getBusinessName());
menu.setRouteName(entityName);
menu.setRoutePath(StrUtil.toSymbolCase(entityName, '-'));
menu.setComponent(genConfig.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index");
menu.setType(MenuTypeEnum.MENU);
menu.setSort(sort);
menu.setVisible(1);
boolean result = this.save(menu);
if (result) {
// 生成treePath
String treePath = generateMenuTreePath(parentMenuId);
menu.setTreePath(treePath);
this.updateById(menu);
// 生成CURD按钮权限
String permPrefix = genConfig.getModuleName() + ":" + StrUtil.lowerFirst(entityName) + ":";
String[] actions = {"查询", "新增", "编辑", "删除"};
String[] perms = {"query", "add", "edit", "delete"};
for (int i = 0; i < actions.length; i++) {
SysMenu button = new SysMenu();
button.setParentId(menu.getId());
button.setType(MenuTypeEnum.BUTTON);
button.setName(actions[i]);
button.setPerm(permPrefix + perms[i]);
button.setSort(i + 1);
this.save(button);
// 生成 treepath
button.setTreePath(treePath + "," + button.getId());
this.updateById(button);
}
}
}
}