431 lines
15 KiB
Java
431 lines
15 KiB
Java
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]);
|
||
this.save(button);
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|