This commit is contained in:
wol
2025-08-25 00:09:18 +08:00
parent f22189bc00
commit fb81670776
15 changed files with 673 additions and 2 deletions

View File

@@ -1,9 +1,17 @@
package com.agileboot.system.menu.controller;
import cn.hutool.core.lang.tree.Tree;
import com.agileboot.common.core.core.R;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.common.satoken.utils.LoginHelper;
import com.agileboot.system.menu.pojo.dto.*;
import com.agileboot.system.menu.service.ISysMenuService;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 菜单信息
@@ -14,4 +22,65 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
public class SysMenuController {
private final ISysMenuService sysMenuService;
/**
* 获取菜单列表
*/
@GetMapping
public R<List<MenuDTO>> menuList(MenuQuery menuQuery) {
List<MenuDTO> menuList = sysMenuService.getMenuList(menuQuery);
return R.ok(menuList);
}
/**
* 根据菜单编号获取详细信息
*/
@GetMapping(value = "/{menuId}")
public R<MenuDetailDTO> menuInfo(@PathVariable("menuId") @NotNull Long menuId) {
MenuDetailDTO menu = sysMenuService.getMenuInfo(menuId);
return R.ok(menu);
}
/**
* 获取菜单下拉树列表
*/
@GetMapping("/dropdown")
public R<List<Tree<Long>>> dropdownList() {
LoginUser loginUser = LoginHelper.getLoginUser();
List<Tree<Long>> dropdownList = sysMenuService.getDropdownList(loginUser);
return R.ok(dropdownList);
}
/**
* 新增菜单
* 需支持一级菜单以及 多级菜单 子菜单为一个 或者 多个的情况
* 隐藏菜单不显示 以及rank排序
* 内链 和 外链
*/
@PostMapping
public R<Void> add(@RequestBody AddMenuCommand addCommand) {
sysMenuService.addMenu(addCommand);
return R.ok();
}
/**
* 修改菜单
*/
@PostMapping("/{menuId}")
public R<Void> edit(@PathVariable("menuId") Long menuId, @RequestBody UpdateMenuCommand updateCommand) {
updateCommand.setMenuId(menuId);
sysMenuService.updateMenu(updateCommand);
return R.ok();
}
/**
* 删除菜单
*/
@PostMapping("/{menuId}")
public R<Void> remove(@PathVariable("menuId") Long menuId) {
sysMenuService.remove(menuId);
return R.ok();
}
}

View File

@@ -0,0 +1,51 @@
package com.agileboot.system.menu.mapper;
import com.agileboot.system.menu.pojo.entity.SysMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* <p>
* 菜单权限表 Mapper 接口
* </p>
*
* @author valarchie
* @since 2022-06-16
*/
public interface SysMenuMapper extends BaseMapper<SysMenu> {
/**
* 根据用户查询出所有菜单
*
* @param userId 用户id
* @return 菜单列表
*/
@Select("SELECT DISTINCT m.* "
+ "FROM sys_menu m "
+ " LEFT JOIN sys_role_menu rm ON m.menu_id = rm.menu_id "
+ " LEFT JOIN sys_user u ON rm.role_id = u.role_id "
+ "WHERE u.user_id = #{userId} "
+ " AND m.status = 1 "
+ " AND m.deleted = 0 "
+ "ORDER BY m.parent_id")
List<SysMenu> selectMenuListByUserId(@Param("userId")Long userId);
/**
* 根据角色ID查询菜单树信息
*
* @param roleId 角色ID
* @return 选中菜单列表
*/
@Select("SELECT DISTINCT m.menu_id "
+ "FROM sys_menu m "
+ " LEFT JOIN sys_role_menu rm ON m.menu_id = rm.menu_id "
+ "WHERE rm.role_id = #{roleId} "
+ " AND m.deleted = 0 "
+ "GROUP BY m.menu_id ")
List<Long> selectMenuIdsByRoleId(@Param("roleId") Long roleId);
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.agileboot.system.menu.pojo.entity.SysMenu">
</mapper>

View File

@@ -0,0 +1,36 @@
package com.agileboot.system.menu.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class AddMenuCommand {
private Long parentId;
@NotBlank(message = "菜单名称不能为空")
@Size(max = 50, message = "菜单名称长度不能超过50个字符")
private String menuName;
/**
* 路由名称 必须唯一
*/
private String routerName;
@Size(max = 200, message = "路由地址不能超过200个字符")
private String path;
private Integer status;
private Integer menuType;
private Boolean isButton;
@Size(max = 100, message = "权限标识长度不能超过100个字符")
private String permission;
private MetaDTO meta;
}

View File

@@ -0,0 +1,16 @@
package com.agileboot.system.menu.pojo.dto;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class ExtraIconDTO {
// 是否是svg
private boolean svg;
// iconfont名称目前只支持iconfont后续拓展
private String name;
}

View File

@@ -0,0 +1,77 @@
package com.agileboot.system.menu.pojo.dto;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.enums.BasicEnumUtil;
import com.agileboot.common.core.enums.common.MenuTypeEnum;
import com.agileboot.common.core.enums.common.StatusEnum;
import com.agileboot.common.core.utils.jackson.JacksonUtil;
import com.agileboot.system.menu.pojo.entity.SysMenu;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author valarchie
*/
@Data
@NoArgsConstructor
public class MenuDTO {
public MenuDTO(SysMenu entity) {
if (entity != null) {
this.id = entity.getMenuId();
this.parentId = entity.getParentId();
this.menuName = entity.getMenuName();
this.routerName = entity.getRouterName();
this.path = entity.getPath();
this.status = entity.getStatus();
this.isButton = entity.getIsButton();
this.statusStr = BasicEnumUtil.getDescriptionByValue(StatusEnum.class, entity.getStatus());
if (!entity.getIsButton()) {
this.menuType = entity.getMenuType();
this.menuTypeStr = BasicEnumUtil.getDescriptionByValue(MenuTypeEnum.class, entity.getMenuType());
} else {
this.menuType = 0;
}
if (StrUtil.isNotEmpty(entity.getMetaInfo()) && JacksonUtil.isJson(entity.getMetaInfo())) {
MetaDTO meta = JacksonUtil.from(entity.getMetaInfo(), MetaDTO.class);
this.rank = meta.getRank();
this.icon = meta.getIcon();
}
this.createTime = entity.getCreateTime();
}
}
// 设置成id和parentId 便于前端处理树级结构
private Long id;
private Long parentId;
private String menuName;
private String routerName;
private String path;
private Integer rank;
private Integer menuType;
private String menuTypeStr;
private Boolean isButton;
private Integer status;
private String statusStr;
private Date createTime;
private String icon;
}

View File

@@ -0,0 +1,30 @@
package com.agileboot.system.menu.pojo.dto;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.utils.jackson.JacksonUtil;
import com.agileboot.system.menu.pojo.entity.SysMenu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class MenuDetailDTO extends MenuDTO {
public MenuDetailDTO(SysMenu entity) {
super(entity);
if (entity == null) {
return;
}
if (StrUtil.isNotEmpty(entity.getMetaInfo()) && JacksonUtil.isJson(entity.getMetaInfo())) {
this.meta = JacksonUtil.from(entity.getMetaInfo(), MetaDTO.class);
}
this.permission = entity.getPermission();
}
private String permission;
private MetaDTO meta;
}

View File

@@ -0,0 +1,15 @@
package com.agileboot.system.menu.pojo.dto;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class MenuQuery {
// 直接交给前端筛选
// private String menuName;
// private Boolean isVisible;
// private Integer status;
private Boolean isButton;
}

View File

@@ -0,0 +1,61 @@
package com.agileboot.system.menu.pojo.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 路由显示信息
* 必须加上@JsonInclude(Include.NON_NULL)的注解 否则传null值给Vue动态路由渲染时会出错
* @author valarchie
*/
@Data
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class MetaDTO {
// 菜单名称兼容国际化、非国际化如果用国际化的写法就必须在根目录的locales文件夹下对应添加
private String title;
// 菜单图标
private String icon;
// 是否显示该菜单
private Boolean showLink;
// 是否显示父级菜单
private Boolean showParent;
// 页面级别权限设置
private List<String> roles;
// 按钮级别权限设置
private List<String> auths;
// 需要内嵌的iframe链接地址
private String frameSrc;
/**
* 是否是内部页面 使用frameSrc来嵌入页面时当isFrameSrcInternal=true的时候, 前端需要做特殊处理
* 比如链接是 /druid/login.html
* 前端需要处理成 http://localhost:8080/druid/login.html
*/
private Boolean isFrameSrcInternal;
/**
* 菜单排序,值越高排的越后(只针对顶级路由)
*/
private Integer rank;
// ========= 目前系统仅支持以上这些参数的设置 后续有需要的话开发者可自行设置的这些参数 ===========
// 菜单名称右侧的额外图标
private ExtraIconDTO extraIcon;
// 是否缓存该路由页面(开启后,会保存该页面的整体状态,刷新后会清空状态)
private Boolean keepAlive;
// 内嵌的iframe页面是否开启首次加载动画
private Boolean frameLoading;
// 页面加载动画两种模式第一种直接采用vue内置的transitions动画第二种是使用animate.css编写进、离场动画平台更推荐使用第二种模式已经内置了animate.css直接写对应的动画名即可
private TransitionDTO transition;
// 当前菜单名称或自定义信息禁止添加到标签页
private Boolean hiddenTag;
// 显示在标签页的最大数量需满足后面的条件不显示在菜单中的路由并且是通过query或params传参模式打开的页面。在完整版全局搜dynamicLevel即可查看代码演示
private Integer dynamicLevel;
}

View File

@@ -0,0 +1,19 @@
package com.agileboot.system.menu.pojo.dto;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class TransitionDTO {
// 当前页面动画,这里是第一种模式,比如 name: "fade" 更具体看后面链接
// https://cn.vuejs.org/api/built-in-components.html#transition
private String name;
// 当前页面进场动画,这里是第二种模式,比如 enterTransition: "animate__fadeInLeft"
private String enterTransition;
// 当前页面离场动画,这里是第二种模式,比如 leaveTransition: "animate__fadeOutRight"
private String leaveTransition;
}

View File

@@ -0,0 +1,17 @@
package com.agileboot.system.menu.pojo.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class UpdateMenuCommand extends AddMenuCommand {
@NotNull
private Long menuId;
}

View File

@@ -0,0 +1,77 @@
package com.agileboot.system.menu.pojo.entity;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serial;
/**
* <p>
* 菜单权限表
* </p>
*
* @author valarchie
* @since 2023-07-21
*/
@Getter
@Setter
@TableName("sys_menu")
@ApiModel(value = "SysMenuEntity对象", description = "菜单权限表")
public class SysMenu extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@ApiModelProperty("菜单ID")
@TableId(value = "menu_id", type = IdType.AUTO)
private Long menuId;
@ApiModelProperty("菜单名称")
@TableField("menu_name")
private String menuName;
@ApiModelProperty("菜单的类型(1为普通菜单2为目录3为iFrame4为外部网站)")
@TableField("menu_type")
private Integer menuType;
@ApiModelProperty("路由名称需保持和前端对应的vue文件中的name保持一致defineOptions方法中设置的name")
@TableField("router_name")
private String routerName;
@ApiModelProperty("父菜单ID")
@TableField("parent_id")
private Long parentId;
@ApiModelProperty("组件路径对应前端项目view文件夹中的路径")
@TableField("path")
private String path;
@ApiModelProperty("是否按钮")
@TableField("is_button")
private Boolean isButton;
@ApiModelProperty("权限标识")
@TableField("permission")
private String permission;
@ApiModelProperty("路由元信息(前端根据这个信息进行逻辑处理)")
@TableField("meta_info")
private String metaInfo;
@ApiModelProperty("菜单状态1启用 0停用")
@TableField("`status`")
private Integer status;
@ApiModelProperty("备注")
@TableField("remark")
private String remark;
}

View File

@@ -0,0 +1,21 @@
package com.agileboot.system.menu.service;
import cn.hutool.core.lang.tree.Tree;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.system.menu.pojo.dto.*;
import java.util.List;
public interface ISysMenuService {
List<MenuDTO> getMenuList(MenuQuery menuQuery);
MenuDetailDTO getMenuInfo(Long menuId);
List<Tree<Long>> getDropdownList(LoginUser loginUser);
void addMenu(AddMenuCommand addCommand);
void updateMenu(UpdateMenuCommand updateCommand);
void remove(Long menuId);
}

View File

@@ -0,0 +1,130 @@
package com.agileboot.system.menu.service.impl;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import com.agileboot.common.core.enums.common.MenuTypeEnum;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.core.exception.error.ErrorCode;
import com.agileboot.common.core.utils.jackson.JacksonUtil;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.system.menu.mapper.SysMenuMapper;
import com.agileboot.system.menu.pojo.dto.*;
import com.agileboot.system.menu.pojo.entity.SysMenu;
import com.agileboot.system.menu.service.ISysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 菜单应用服务
*
* @author valarchie
*/
@Service
@RequiredArgsConstructor
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
@Override
public List<MenuDTO> getMenuList(MenuQuery query) {
Boolean isButton = query.getIsButton();
List<SysMenu> list = super.lambdaQuery()
.eq(isButton != null, SysMenu::getIsButton, isButton)
.orderByDesc(SysMenu::getParentId)
.list();
return list.stream().map(MenuDTO::new)
.sorted(Comparator.comparing(MenuDTO::getRank, Comparator.nullsLast(Integer::compareTo)))
.collect(Collectors.toList());
}
@Override
public MenuDetailDTO getMenuInfo(Long menuId) {
SysMenu sysMenu = super.getById(menuId);
return new MenuDetailDTO(sysMenu);
}
@Override
public List<Tree<Long>> getDropdownList(LoginUser loginUser) {
List<SysMenu> menuEntityList =
// loginUser.isAdmin() ?
super.list();
// :
// this.baseMapper.selectMenuListByUserId(loginUser.getUserId());
return buildMenuTreeSelect(menuEntityList);
}
@Override
public void addMenu(AddMenuCommand addCommand) {
SysMenu entity = new SysMenu();
BeanUtils.copyProperties(addCommand, entity, "menuId");
String metaInfo = JacksonUtil.to(addCommand.getMeta());
entity.setMetaInfo(metaInfo);
// 校验菜单名称是否唯一
boolean exists = super.lambdaQuery()
.eq(SysMenu::getMenuName, addCommand.getMenuName())
.eq(addCommand.getParentId() != null, SysMenu::getParentId, addCommand.getParentId())
.exists();
if (exists) throw new BizException(ErrorCode.Business.MENU_NAME_IS_NOT_UNIQUE);
SysMenu parentMenu = super.getById(addCommand.getParentId());
// Iframe和外链跳转类型 不允许添加按钮
if (parentMenu != null && parentMenu.getIsButton() && (
Objects.equals(parentMenu.getMenuType(), MenuTypeEnum.IFRAME.getValue())
|| Objects.equals(parentMenu.getMenuType(), MenuTypeEnum.OUTSIDE_LINK_REDIRECT.getValue())
)) {
throw new BizException(ErrorCode.Business.MENU_NOT_ALLOWED_TO_CREATE_BUTTON_ON_IFRAME_OR_OUT_LINK);
}
// 只允许在目录菜单类型底下 添加子菜单
if (parentMenu != null && !parentMenu.getIsButton() && (
!Objects.equals(parentMenu.getMenuType(), MenuTypeEnum.CATALOG.getValue())
)) {
throw new BizException(ErrorCode.Business.MENU_ONLY_ALLOWED_TO_CREATE_SUB_MENU_IN_CATALOG);
}
super.save(entity);
}
@Override
public void updateMenu(UpdateMenuCommand updateCommand) {
}
@Override
public void remove(Long menuId) {
// 是否存在菜单子节点
if (super.lambdaQuery().eq(SysMenu::getParentId, menuId).exists()) {
throw new BizException(ErrorCode.Business.MENU_EXIST_CHILD_MENU_NOT_ALLOW_DELETE);
}
// 查询菜单是否存在角色
if (super.lambdaQuery().eq(SysMenu::getMenuId, menuId).exists()) {
throw new BizException(ErrorCode.Business.MENU_ALREADY_ASSIGN_TO_ROLE_NOT_ALLOW_DELETE);
}
super.removeById(menuId);
}
/**
* 构建前端所需要树结构
*
* @param menus 菜单列表
* @return 树结构列表
*/
public List<Tree<Long>> buildMenuTreeSelect(List<SysMenu> menus) {
TreeNodeConfig config = new TreeNodeConfig();
//默认为id可以不设置
config.setIdKey("menuId");
return TreeUtil.build(menus, 0L, config, (menu, tree) -> {
// 也可以使用 tree.setId(dept.getId());等一些默认值
tree.setId(menu.getMenuId());
tree.setParentId(menu.getParentId());
tree.putExtra("label", menu.getMenuName());
});
}
}

View File

@@ -0,0 +1,46 @@
package com.agileboot.system.role.controller.pojo.entity;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serial;
import java.io.Serializable;
/**
* <p>
* 角色和菜单关联表
* </p>
*
* @author valarchie
* @since 2022-10-02
*/
@Getter
@Setter
@TableName("sys_role_menu")
@ApiModel(value = "SysRoleMenuXEntity对象", description = "角色和菜单关联表")
public class SysRoleMenu extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@ApiModelProperty("角色ID")
@TableId(value = "role_id", type = IdType.AUTO)
private Long roleId;
@ApiModelProperty("菜单ID")
@TableField("menu_id")
private Long menuId;
public Serializable pkVal() {
return this.menuId;
}
}