1. 修改 MenuDO 的字段,menuId 改成 id,menuName 改成 name

2. 添加 Menu 的创建、修改、删除接口
This commit is contained in:
YunaiV 2021-01-08 20:08:20 +08:00
parent dba723b8fc
commit ea4c9e4981
29 changed files with 503 additions and 237 deletions

View File

@ -10,9 +10,9 @@ export function listMenu(query) {
}
// 查询菜单详细
export function getMenu(menuId) {
export function getMenu(id) {
return request({
url: '/system/menu/' + menuId,
url: '/system/menu/get?id=' + id,
method: 'get'
})
}
@ -36,7 +36,7 @@ export function roleMenuTreeselect(roleId) {
// 新增菜单
export function addMenu(data) {
return request({
url: '/system/menu',
url: '/system/menu/create',
method: 'post',
data: data
})
@ -45,16 +45,16 @@ export function addMenu(data) {
// 修改菜单
export function updateMenu(data) {
return request({
url: '/system/menu',
method: 'put',
url: '/system/menu/update',
method: 'post',
data: data
})
}
// 删除菜单
export function delMenu(menuId) {
export function delMenu(id) {
return request({
url: '/system/menu/' + menuId,
method: 'delete'
url: '/system/menu/delete?id=' + id,
method: 'post'
})
}

View File

@ -28,8 +28,8 @@ const actions = {
}
// 处理 dictValue 层级
dictDataMap[dictData.dictType].push({
dictValue: dictData.dictValue,
dictLabel: dictData.dictLabel
value: dictData.value,
label: dictData.label
})
})
// 存储到 Store 中

View File

@ -44,7 +44,7 @@ function filterAsyncRouter(asyncRouterMap, isRewrite = false) {
// 将 ruoyi 后端原有耦合前端的逻辑,迁移到此处
// 处理 meta 属性
route.meta = {
title: route.menuName,
title: route.name,
icon: route.icon
}
// 处理 component 属性

View File

@ -0,0 +1,30 @@
/**
* Created by 芋道源码
*
* 枚举类
*/
/**
* 全局通用状态枚举
*/
export const SysCommonStatusEnum = {
ENABLE: 0, // 开启
DISABLE: 1 // 禁用
}
/**
* 菜单的类型枚举
*/
export const SysMenuTypeEnum = {
DIR : 1, // 目录
MENU: 2, // 菜单
BUTTON: 3 // 按钮
}
/**
* 角色的类型枚举
*/
export const RoleTypeEnum = {
SYSTEM: 1, // 内置角色
CUSTOM: 2 // 自定义角色
}

View File

@ -6,7 +6,8 @@
import store from '@/store'
export const DICT_TYPE = {
SYS_COMMON_STATUS: 'sys_common_status'
SYS_COMMON_STATUS: 'sys_common_status',
SYS_MENU_TYPE: 'menu_type'
}
/**
@ -28,8 +29,8 @@ export function getDictDataLabel(dictType, value) {
// 获取 value 对应的展示名
value = value + '' // 强制转换成字符串,因为 DictData 小类数值,是字符串
for (const dictData of dictDatas) {
if (dictData.dictValue === value) {
return dictData.dictLabel
if (dictData.value === value) {
return dictData.label
}
}
return ''

View File

@ -1,9 +1,9 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="菜单名称" prop="menuName">
<el-form-item label="菜单名称" prop="name">
<el-input
v-model="queryParams.menuName"
v-model="queryParams.name"
placeholder="请输入菜单名称"
clearable
size="small"
@ -42,10 +42,10 @@
<el-table
v-loading="loading"
:data="menuList"
row-key="menuId"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template slot-scope="scope">
<svg-icon :icon-class="scope.row.icon" />
@ -102,16 +102,18 @@
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio label="1">目录</el-radio>
<el-radio label="2">菜单</el-radio>
<el-radio label="3">按钮</el-radio>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio
v-for="dict in menuTypeDictDatas"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="form.menuType != '3'" label="菜单图标">
<el-form-item v-if="form.type != '3'" label="菜单图标">
<el-popover
placement="bottom-start"
width="460"
@ -133,65 +135,38 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
<el-form-item label="菜单名称" prop="name">
<el-input v-model="form.name" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
<el-form-item label="显示排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != '3'" label="是否外链">
<el-radio-group v-model="form.isFrame">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != '3'" label="路由地址" prop="path">
<el-form-item v-if="form.type != '3'" label="路由地址" prop="path">
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == '2'">
<el-col :span="12" v-if="form.type == '2'">
<el-form-item label="组件路径" prop="component">
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != '1'" label="权限标识">
<el-form-item v-if="form.type != '1'" label="权限标识">
<el-input v-model="form.perms" placeholder="请权限标识" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != '3'" label="显示状态">
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in visibleOptions"
:key="dict.dictValue"
:label="dict.dictValue"
>{{dict.dictLabel}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != '3'" label="菜单状态">
<el-form-item v-if="form.type != '3'" label="菜单状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.dictValue"
:label="dict.dictValue"
>{{dict.dictLabel}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == '2'" label="是否缓存">
<el-radio-group v-model="form.isCache">
<el-radio label="0">缓存</el-radio>
<el-radio label="1">不缓存</el-radio>
v-for="dict in statusDictDatas"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
@ -211,6 +186,7 @@ import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import IconSelect from "@/components/IconSelect";
import { SysMenuTypeEnum, SysCommonStatusEnum } from '@/utils/constants'
import { getDictDataLabel, getDictDatas, DICT_TYPE } from '@/utils/dict'
export default {
@ -230,29 +206,34 @@ export default {
title: "",
//
open: false,
//
visibleOptions: [],
//
statusOptions: [],
//
queryParams: {
menuName: undefined,
name: undefined,
visible: undefined
},
//
form: {},
//
rules: {
menuName: [
name: [
{ required: true, message: "菜单名称不能为空", trigger: "blur" }
],
orderNum: [
sort: [
{ required: true, message: "菜单顺序不能为空", trigger: "blur" }
],
path: [
{ required: true, message: "路由地址不能为空", trigger: "blur" }
]
}
},
//
MenuTypeEnum: SysMenuTypeEnum,
CommonStatusEnum: SysCommonStatusEnum,
//
menuTypeDictDatas: getDictDatas(DICT_TYPE.SYS_MENU_TYPE),
statusDictDatas: getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)
};
},
created() {
@ -267,7 +248,7 @@ export default {
getList() {
this.loading = true;
listMenu(this.queryParams).then(response => {
this.menuList = this.handleTree(response.data, "menuId");
this.menuList = this.handleTree(response.data, "id");
this.loading = false;
});
},
@ -277,8 +258,8 @@ export default {
delete node.children;
}
return {
id: node.menuId,
label: node.menuName,
id: node.id,
label: node.name,
children: node.children
};
},
@ -286,8 +267,8 @@ export default {
getTreeselect() {
listMenu().then(response => {
this.menuOptions = [];
const menu = { menuId: 0, menuName: '主类目', children: [] };
menu.children = this.handleTree(response.data, "menuId");
const menu = { id: 0, name: '主类目', children: [] };
menu.children = this.handleTree(response.data, "id");
this.menuOptions.push(menu);
});
},
@ -303,15 +284,12 @@ export default {
//
reset() {
this.form = {
menuId: undefined,
id: undefined,
parentId: 0,
menuName: undefined,
name: undefined,
icon: undefined,
menuType: "1",
orderNum: undefined,
isFrame: "1",
isCache: "0",
visible: "0",
type: "1",
sort: undefined,
status: "0"
};
this.resetForm("form");
@ -329,8 +307,8 @@ export default {
handleAdd(row) {
this.reset();
this.getTreeselect();
if (row != null && row.menuId) {
this.form.parentId = row.menuId;
if (row != null && row.id) {
this.form.parentId = row.id;
} else {
this.form.parentId = 0;
}
@ -341,7 +319,7 @@ export default {
handleUpdate(row) {
this.reset();
this.getTreeselect();
getMenu(row.menuId).then(response => {
getMenu(row.id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改菜单";
@ -351,7 +329,24 @@ export default {
submitForm: function() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.menuId != undefined) {
// route
if (this.form.type === ResourceTypeEnum.MENU) {
//
const route = this.resourceForm.route
if (route.indexOf('http://') === -1 || route.indexOf('https://') === -1) {
// route /
if (this.resourceForm.pid === 0 && route.charAt(0) !== '/') {
this.messageSuccess('前端必须以 / 开头')
return
} else if (this.resourceForm.pid !== 0 && route.charAt(0) === '/') {
this.messageSuccess('前端不能以 / 开头')
return
}
}
}
//
if (this.form.id !== undefined) {
updateMenu(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
@ -369,12 +364,12 @@ export default {
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除名称为"' + row.menuName + '"的数据项?', "警告", {
this.$confirm('是否确认删除名称为"' + row.name + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return delMenu(row.menuId);
return delMenu(row.id);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");

View File

@ -437,7 +437,7 @@ export default {
roleKey: undefined,
roleSort: 0,
status: "0",
menuIds: [],
ids: [],
deptIds: [],
menuCheckStrictly: true,
deptCheckStrictly: true,
@ -535,14 +535,14 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.roleId != undefined) {
this.form.menuIds = this.getMenuAllCheckedKeys();
this.form.ids = this.getMenuAllCheckedKeys();
updateRole(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
this.form.menuIds = this.getMenuAllCheckedKeys();
this.form.ids = this.getMenuAllCheckedKeys();
addRole(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;

View File

@ -167,7 +167,7 @@ export default {
});
/** 查询菜单下拉列表 */
getMenuTreeselect().then(response => {
this.menus = this.handleTree(response.data, "menuId");
this.menus = this.handleTree(response.data, "id");
});
}
},

View File

@ -219,8 +219,8 @@ export default {
delete node.children;
}
return {
id: node.menuId,
label: node.menuName,
id: node.id,
label: node.name,
children: node.children
};
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.dashboard.modules.system.controller.auth.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -15,32 +16,22 @@ import java.util.List;
@Builder
public class SysAuthMenuRespVO {
/**
* 菜单编号
*/
private Long menuId;
/**
* 父菜单编号
*/
@ApiModelProperty(value = "菜单名称", required = true, example = "芋道")
private Long id;
@ApiModelProperty(value = "父菜单 ID", required = true, example = "1024")
private Long parentId;
/**
* 菜单名称
*/
private String menuName;
@ApiModelProperty(value = "菜单名称", required = true, example = "芋道")
private String name;
/**
* 路由地址
*/
@ApiModelProperty(value = "路由地址", example = "post", notes = "仅菜单类型为菜单或者目录时,才需要传")
private String path;
/**
* 组件地址
*/
@ApiModelProperty(value = "组件路径", example = "system/post/index", notes = "仅菜单类型为菜单时,才需要传")
private String component;
/**
* 菜单图标
*/
@ApiModelProperty(value = "菜单图标", example = "/menu/list", notes = "仅菜单类型为菜单或者目录时,才需要传")
private String icon;
/**

View File

@ -12,9 +12,9 @@ public class SysDataDictSimpleVO {
private String dictType;
@ApiModelProperty(value = "字典键值", required = true, example = "1")
private String dictValue;
private String value;
@ApiModelProperty(value = "字典标签", required = true, example = "")
private String dictLabel;
private String label;
}

View File

@ -1,13 +1,17 @@
package cn.iocoder.dashboard.modules.system.controller.permission;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuListReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuRespVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuUpdateReqVO;
import cn.iocoder.dashboard.modules.system.convert.permission.SysMenuConvert;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@ -22,24 +26,21 @@ public class SysMenuController {
@Resource
private SysMenuService menuService;
/**
* 获取菜单列表
*/
@ApiOperation("获取菜单列表")
// @PreAuthorize("@ss.hasPermi('system:menu:list')")
@GetMapping("/list")
public CommonResult<List<SysMenuRespVO>> list(SysMenuListReqVO reqVO) {
return success(menuService.listMenus(reqVO));
}
//
// /**
// * 根据菜单编号获取详细信息
// */
@ApiOperation("获取菜单信息")
@GetMapping("/get")
// @PreAuthorize("@ss.hasPermi('system:menu:query')")
// @GetMapping(value = "/{menuId}")
// public AjaxResult getInfo(@PathVariable Long menuId) {
// return AjaxResult.success(menuService.selectMenuById(menuId));
// }
//
public CommonResult<SysMenuRespVO> getMenu(Long id) {
SysMenuDO menu = menuService.getMenu(id);
return success(SysMenuConvert.INSTANCE.convert(menu));
}
// /**
// * 获取菜单下拉树列表
// */
@ -63,57 +64,31 @@ public class SysMenuController {
// ajax.put("menus", menuService.buildMenuTreeSelect(menus));
// return ajax;
// }
//
// /**
// * 新增菜单
// */
@ApiOperation("新增菜单")
// @PreAuthorize("@ss.hasPermi('system:menu:add')")
// @Log(title = "菜单管理", businessType = BusinessType.INSERT)
// @PostMapping
// public AjaxResult add(@Validated @RequestBody SysMenu menu) {
// if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) {
// return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
// } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
// && !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) {
// return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
// }
// menu.setCreateBy(SecurityUtils.getUsername());
// return toAjax(menuService.insertMenu(menu));
// }
//
// /**
// * 修改菜单
// */
@PostMapping("/create")
public CommonResult<Long> createMenu(@Validated @RequestBody SysMenuCreateReqVO reqVO) {
Long menuId = menuService.createMenu(reqVO);
return success(menuId);
}
@ApiOperation("修改菜单")
// @PreAuthorize("@ss.hasPermi('system:menu:edit')")
// @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
// @PutMapping
// public AjaxResult edit(@Validated @RequestBody SysMenu menu) {
// if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) {
// return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
// } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
// && !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) {
// return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
// } else if (menu.getMenuId().equals(menu.getParentId())) {
// return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
// }
// menu.setUpdateBy(SecurityUtils.getUsername());
// return toAjax(menuService.updateMenu(menu));
// }
//
// /**
// * 删除菜单
// */
// @PreAuthorize("@ss.hasPermi('system:menu:remove')")
@PostMapping("/update")
public CommonResult<Boolean> updateMenu(@Validated @RequestBody SysMenuUpdateReqVO reqVO) {
menuService.updateMenu(reqVO);
return success(true);
}
@ApiOperation("删除菜单")
// @Log(title = "菜单管理", businessType = BusinessType.DELETE)
// @DeleteMapping("/{menuId}")
// public AjaxResult remove(@PathVariable("menuId") Long menuId) {
// if (menuService.hasChildByMenuId(menuId)) {
// return AjaxResult.error("存在子菜单,不允许删除");
// }
// if (menuService.checkMenuExistRole(menuId)) {
// return AjaxResult.error("菜单已分配,不允许删除");
// }
// return toAjax(menuService.deleteMenuById(menuId));
// }
public CommonResult<Boolean> deleteMenu(@RequestParam("id") Long id) {
menuService.deleteMenu(id);
return success(true);
}
}

View File

@ -17,15 +17,15 @@ public class SysMenuBaseVO {
@ApiModelProperty(value = "菜单名称", required = true, example = "芋道")
@NotBlank(message = "菜单名称不能为空")
@Size(max = 50, message = "菜单名称长度不能超过50个字符")
private String menuName;
private String name;
@ApiModelProperty(value = "权限标识", example = "sys:menu:add", notes = "仅菜单类型为按钮时,才需要传递")
@Size(max = 100)
private String permission;
@ApiModelProperty(value = "类型", required = true, example = "1", notes = "参见 MenuTypeEnum 枚举类")
@ApiModelProperty(value = "类型", required = true, example = "1", notes = "参见 SysMenuTypeEnum 枚举类")
@NotBlank(message = "菜单类型不能为空")
private Integer menuType;
private Integer type;
@ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
@NotBlank(message = "显示顺序不能为空")

View File

@ -14,7 +14,7 @@ public class SysMenuListReqVO {
@ApiModelProperty(value = "菜单名称", example = "芋道", notes = "模糊匹配")
private String menuName;
@ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类")
@ApiModelProperty(value = "展示状态", example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
private Integer status;
}

View File

@ -17,9 +17,9 @@ import java.util.Date;
public class SysMenuRespVO extends SysMenuBaseVO {
@ApiModelProperty(value = "菜单编号", required = true, example = "1024")
private Integer menuId;
private Integer id;
@ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类")
@ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
private Integer status;
@ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式")

View File

@ -13,6 +13,6 @@ public class SysMenuUpdateReqVO extends SysMenuBaseVO {
@ApiModelProperty(value = "菜单编号", required = true, example = "1024")
@NotNull(message = "菜单编号不能为空")
private Integer menuId;
private Long id;
}

View File

@ -1,17 +1,19 @@
package cn.iocoder.dashboard.modules.system.convert.auth;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthPermissionInfoRespVO;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthMenuRespVO;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.SysAuthPermissionInfoRespVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuIdEnum;
import cn.iocoder.dashboard.util.collection.CollectionUtils;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.*;
@Mapper
public interface SysAuthConvert {
@ -31,4 +33,36 @@ public interface SysAuthConvert {
SysAuthMenuRespVO convertTreeNode(SysMenuDO menu);
/**
* 将菜单列表构建成菜单树
*
* @param menuList 菜单列表
* @return 菜单树
*/
default List<SysAuthMenuRespVO> buildMenuTree(List<SysMenuDO> menuList) {
// 排序保证菜单的有序性
menuList.sort(Comparator.comparing(SysMenuDO::getSort));
// 构建菜单树
// 使用 LinkedHashMap 的原因是为了排序 实际也可以用 Stream API 就是太丑了
Map<Long, SysAuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
menuList.forEach(menu -> treeNodeMap.put(menu.getId(), SysAuthConvert.INSTANCE.convertTreeNode(menu)));
// 处理父子关系
treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(MenuIdEnum.ROOT.getId())).forEach((childNode) -> {
// 获得父节点
SysAuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
if (parentNode == null) {
LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
childNode.getId(), childNode.getParentId());
return;
}
// 将自己添加到父节点中
if (parentNode.getChildren() == null) {
parentNode.setChildren(new ArrayList<>());
}
parentNode.getChildren().add(childNode);
});
// 获得到所有的根节点
return CollectionUtils.filterList(treeNodeMap.values(), node -> MenuIdEnum.ROOT.getId().equals(node.getParentId()));
}
}

View File

@ -1,6 +1,8 @@
package cn.iocoder.dashboard.modules.system.convert.permission;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuRespVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@ -14,4 +16,10 @@ public interface SysMenuConvert {
List<SysMenuRespVO> convertList(List<SysMenuDO> list);
SysMenuDO convert(SysMenuCreateReqVO bean);
SysMenuDO convert(SysMenuUpdateReqVO bean);
SysMenuRespVO convert(SysMenuDO bean);
}

View File

@ -1,9 +1,20 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenuDO> {
default SysMenuDO selectByParentIdAndName(Long parentId, String name) {
return selectOne(new QueryWrapper<SysMenuDO>().eq("parent_id", parentId)
.eq("name", name));
}
default Integer selectCountByParentId(Long parentId) {
return selectCount(new QueryWrapper<SysMenuDO>().eq("parent_id", parentId));
}
}

View File

@ -26,30 +26,30 @@ public class SysDictDataDO extends BaseDO {
*/
@TableId
@Excel(name = "字典编码", cellType = Excel.ColumnType.NUMERIC)
private Long dictCode;
private Long id;
/**
* 字典排序
*/
@Excel(name = "字典排序", cellType = Excel.ColumnType.NUMERIC)
private Integer dictSort;
private Integer sort;
/**
* 字典标签
*/
@Excel(name = "字典标签")
@NotBlank(message = "字典标签不能为空")
@Size(max = 100, message = "字典标签长度不能超过100个字符")
private String dictLabel;
private String label;
/**
* 字典
* 字典
*/
@Excel(name = "字典键值")
@NotBlank(message = "字典键值不能为空")
@Size(max = 100, message = "字典键值长度不能超过100个字符")
private String dictValue;
private String value;
/**
* 字典类型
*
* 外键 {@link SysDictDataDO#getDictType()}
* 冗余 {@link SysDictDataDO#getDictType()}
*/
@Excel(name = "字典类型")
@NotBlank(message = "字典类型不能为空")

View File

@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.framework.excel.Excel;
import cn.iocoder.dashboard.framework.mybatis.core.BaseDO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -26,24 +27,22 @@ public class SysDictTypeDO extends BaseDO {
*/
@TableId
@Excel(name = "字典主键", cellType = Excel.ColumnType.NUMERIC)
private Long dictId;
private Long id;
/**
* 字典名称
*/
@Excel(name = "字典名称")
@NotBlank(message = "字典名称不能为空")
@Size(max = 100, message = "字典类型名称长度不能超过100个字符")
private String dictName;
private String name;
/**
* 字典类型
*/
@TableField("dict_type")
@Excel(name = "字典类型")
@NotBlank(message = "字典类型不能为空")
@Size(max = 100, message = "字典类型类型长度不能超过100个字符")
private String dictType;
private String type;
/**
* 状态
*

View File

@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.framework.mybatis.core.BaseDO;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -22,11 +23,11 @@ public class SysMenuDO extends BaseDO {
* 菜单ID
*/
@TableId
private Long menuId;
private Long id;
/**
* 菜单名称
*/
private String menuName;
private String name;
/**
* 权限标识
*
@ -43,7 +44,8 @@ public class SysMenuDO extends BaseDO {
*
* 枚举 {@link MenuTypeEnum}
*/
private Integer menuType;
@TableField("menu_type")
private Integer type;
/**
* 显示顺序
*/

View File

@ -18,4 +18,12 @@ public interface SysErrorCodeConstants {
ErrorCode TOKEN_EXPIRED = new ErrorCode(1002001000, "Token 已经过期");
ErrorCode TOKEN_PARSE_FAIL = new ErrorCode(1002001001, "Token 解析失败");
// ========== 菜单模块 1002002000 ==========
ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1002002000, "已经存在该名字的菜单");
ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1002002001, "父菜单不存在");
ErrorCode MENU_PARENT_ERROR = new ErrorCode(1002002002, "不能设置自己为父菜单");
ErrorCode MENU_NOT_EXISTS = new ErrorCode(1002002003, "菜单不存在");
ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1002002004, "存在子菜单,无法删除");
ErrorCode MENU_PARENT_NOT_MENU = new ErrorCode(1002002005, "父菜单的类型必须是菜单");
}

View File

@ -12,7 +12,6 @@ import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMe
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuIdEnum;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum;
import cn.iocoder.dashboard.modules.system.enums.user.UserStatus;
import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService;
@ -38,7 +37,10 @@ import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.*;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
@ -243,36 +245,12 @@ public class SysAuthServiceImpl implements SysAuthService {
@Override
public List<SysAuthMenuRespVO> listMenus(Long userId, Set<Long> roleIds) {
// 获得用户拥有的菜单列表
List<SysMenuDO> menuList = permissionService.listRoleMenusFromCache(roleIds,
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()),
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus()));
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
// 转换成 Tree 结构返回
return buildRouterTree(menuList);
}
private static List<SysAuthMenuRespVO> buildRouterTree(List<SysMenuDO> menuList) {
// 排序保证菜单的有序性
menuList.sort(Comparator.comparing(SysMenuDO::getSort));
// 构建菜单树
// 使用 LinkedHashMap 的原因是为了排序 实际也可以用 Stream API 就是太丑了
Map<Long, SysAuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
menuList.forEach(menu -> treeNodeMap.put(menu.getMenuId(), SysAuthConvert.INSTANCE.convertTreeNode(menu)));
// 处理父子关系
treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(MenuIdEnum.ROOT.getId())).forEach((childNode) -> {
// 获得父节点
SysAuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
if (parentNode == null) {
log.error("[buildRouterTree][resource({}) 找不到父资源({})]", childNode.getMenuId(), childNode.getParentId());
return;
}
// 将自己添加到父节点中
if (parentNode.getChildren() == null) {
parentNode.setChildren(new ArrayList<>());
}
parentNode.getChildren().add(childNode);
});
// 获得到所有的根节点
return CollectionUtils.filterList(treeNodeMap.values(), node -> MenuIdEnum.ROOT.getId().equals(node.getParentId()));
return SysAuthConvert.INSTANCE.buildMenuTree(menuList);
}
}

View File

@ -19,7 +19,7 @@ public class SysDictDataServiceImpl implements SysDictDataService {
private static final Comparator<SysDictDataDO> COMPARATOR_TYPE_AND_SORT = Comparator
.comparing(SysDictDataDO::getDictType)
.thenComparingInt(SysDictDataDO::getDictSort);
.thenComparingInt(SysDictDataDO::getSort);
@Resource
private SysDictDataMapper dictDataMapper;

View File

@ -1,7 +1,9 @@
package cn.iocoder.dashboard.modules.system.service.permission;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuListReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuRespVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuUpdateReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import java.util.Collection;
@ -9,6 +11,8 @@ import java.util.List;
/**
* 菜单 Service 接口
*
* @author 芋道源码
*/
public interface SysMenuService {
@ -49,4 +53,34 @@ public interface SysMenuService {
List<SysMenuDO> listMenusFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
/*
* 创建菜单
*
* @param reqVO 菜单信息
* @return 创建出来的菜单编号
*/
Long createMenu(SysMenuCreateReqVO reqVO);
/**
* 更新菜单
*
* @param reqVO 菜单信息
*/
void updateMenu(SysMenuUpdateReqVO reqVO);
/**
* 删除菜单
*
* @param id 菜单编号
*/
void deleteMenu(Long id);
/**
* 获得菜单
*
* @param id 菜单编号
* @return 菜单
*/
SysMenuDO getMenu(Long id);
}

View File

@ -48,4 +48,11 @@ public interface SysPermissionService {
*/
Long getDeptRoleId(Long deptId);
/**
* 删除授予给角色的菜单们
*
* @param menuId 菜单编号
*/
void deleteRolesMenuByMenuId(Long menuId);
}

View File

@ -1,11 +1,17 @@
package cn.iocoder.dashboard.modules.system.service.permission.impl;
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuCreateReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuListReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuRespVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.SysMenuUpdateReqVO;
import cn.iocoder.dashboard.modules.system.convert.permission.SysMenuConvert;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysMenuMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuIdEnum;
import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum;
import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import cn.iocoder.dashboard.util.collection.CollectionUtils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
@ -21,8 +27,12 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
/**
* 菜单 Service 实现
*
* @author 芋道源码
*/
@Service
@Slf4j
@ -46,6 +56,8 @@ public class SysMenuServiceImpl implements SysMenuService {
@Resource
private SysMenuMapper menuMapper;
@Resource
private SysPermissionService permissionService;
/**
* 初始化 {@link #menuCache} {@link #permMenuCache} 缓存
@ -57,7 +69,7 @@ public class SysMenuServiceImpl implements SysMenuService {
ImmutableMap.Builder<Long, SysMenuDO> menuCacheBuilder = ImmutableMap.builder();
ImmutableMultimap.Builder<String, SysMenuDO> permMenuCacheBuilder = ImmutableMultimap.builder();
menuList.forEach(menuDO -> {
menuCacheBuilder.put(menuDO.getMenuId(), menuDO);
menuCacheBuilder.put(menuDO.getId(), menuDO);
permMenuCacheBuilder.put(menuDO.getPermission(), menuDO);
});
menuCache = menuCacheBuilder.build();
@ -79,7 +91,7 @@ public class SysMenuServiceImpl implements SysMenuService {
return Collections.emptyList();
}
// 创建新数组避免缓存被修改
return menuCache.values().stream().filter(menu -> menuTypes.contains(menu.getMenuType())
return menuCache.values().stream().filter(menu -> menuTypes.contains(menu.getType())
&& menusStatuses.contains(menu.getStatus()))
.collect(Collectors.toList());
}
@ -91,10 +103,186 @@ public class SysMenuServiceImpl implements SysMenuService {
if (CollectionUtils.isAnyEmpty(menuIds, menuTypes, menusStatuses)) {
return Collections.emptyList();
}
return menuCache.values().stream().filter(menu -> menuIds.contains(menu.getMenuId())
&& menuTypes.contains(menu.getMenuType())
return menuCache.values().stream().filter(menu -> menuIds.contains(menu.getId())
&& menuTypes.contains(menu.getType())
&& menusStatuses.contains(menu.getStatus()))
.collect(Collectors.toList());
}
@Override
public Long createMenu(SysMenuCreateReqVO reqVO) {
// 校验父菜单存在
checkParentResource(reqVO.getParentId(), null);
// 校验菜单自己
checkResource(reqVO.getParentId(), reqVO.getName(), null);
// 插入数据库
SysMenuDO menu = SysMenuConvert.INSTANCE.convert(reqVO);
initMenuProperty(menu);
menuMapper.insert(menu);
// 返回
return menu.getId();
}
@Override
public void updateMenu(SysMenuUpdateReqVO reqVO) {
// 校验更新的菜单是否存在
if (menuMapper.selectById(reqVO.getId()) == null) {
throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS);
}
// 校验父菜单存在
checkParentResource(reqVO.getParentId(), reqVO.getId());
// 校验菜单自己
checkResource(reqVO.getParentId(), reqVO.getName(), reqVO.getId());
// 更新到数据库
SysMenuDO updateObject = SysMenuConvert.INSTANCE.convert(reqVO);
initMenuProperty(updateObject);
menuMapper.updateById(updateObject);
}
/**
* 删除菜单
*
* @param menuId 菜单编号
*/
public void deleteMenu(Long menuId) {
// 校验更新的菜单是否存在
if (menuMapper.selectById(menuId) == null) {
throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS);
}
// 校验是否还有子菜单
if (menuMapper.selectCountByParentId(menuId) > 0) {
throw ServiceExceptionUtil.exception(MENU_EXISTS_CHILDREN);
}
// 校验删除的菜单是否存在
if (menuMapper.selectById(menuId) == null) {
throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS);
}
// 标记删除
menuMapper.deleteById(menuId);
// 删除授予给角色的权限
permissionService.deleteRolesMenuByMenuId(menuId);
}
@Override
public SysMenuDO getMenu(Long id) {
return menuMapper.selectById(id);
}
// /**
// * 获得菜单列表
// *
// * @param menuIds 菜单编号列表
// * @return 菜单列表
// */
// public List<ResourceBO> listResources(List<Integer> menuIds) {
// List<ResourceDO> menuDOs = menuMapper.selectBatchIds(menuIds);
// return ResourceConvert.INSTANCE.convertList(menuDOs);
// }
//
// /**
// * 获得菜单全列表
// *
// * @return 菜单全列表
// */
// public List<ResourceBO> listResources() {
// List<ResourceDO> menuDOs = menuMapper.selectList(null);
// return ResourceConvert.INSTANCE.convertList(menuDOs);
// }
//
// /**
// * 获得指定类型的菜单列表
// *
// * @param type 菜单类型允许空
// * @return 菜单列表
// */
// public List<ResourceBO> listResourcesByType(Integer type) {
// List<ResourceDO> menuDOs = menuMapper.selectListByType(type);
// return ResourceConvert.INSTANCE.convertList(menuDOs);
// }
//
// /**
// * 获得角色拥有的菜单列表
// *
// * @param roleIds 角色编号
// * @param type 菜单类型允许空
// * @return 菜单列表
// */
// public List<ResourceBO> listRoleResourcesByType(Collection<Integer> roleIds, Integer type) {
// List<RoleResourceDO> roleResourceDOs = roleResourceMapper.selectListByRoleIds(roleIds);
// if (CollectionUtils.isEmpty(roleResourceDOs)) {
// return Collections.emptyList();
// }
// List<ResourceDO> menuDOs = menuMapper.selectListByIdsAndType(
// CollectionUtils.convertSet(roleResourceDOs, RoleResourceDO::getResourceId), type);
// return ResourceConvert.INSTANCE.convertList(menuDOs);
// }
//
/**
* 校验父菜单是否合法
*
* 1. 不能设置自己为父菜单
* 2. 父菜单不存在
* 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型
*
* @param parentId 父菜单编号
* @param childId 当前菜单编号
*/
private void checkParentResource(Long parentId, Long childId) {
if (parentId == null || MenuIdEnum.ROOT.getId().equals(parentId)) {
return;
}
// 不能设置自己为父菜单
if (parentId.equals(childId)) {
throw ServiceExceptionUtil.exception(MENU_PARENT_ERROR);
}
SysMenuDO menu = menuMapper.selectById(parentId);
// 父菜单不存在
if (menu == null) {
throw ServiceExceptionUtil.exception(MENU_PARENT_NOT_EXISTS);
}
// 父菜单必须是目录类型
if (!MenuTypeEnum.DIR.getType().equals(menu.getType())) {
throw ServiceExceptionUtil.exception(MENU_PARENT_NOT_MENU);
}
}
/**
* 校验菜单是否合法
*
* 1. 校验相同父菜单编号下是否存在相同的菜单名
*
* @param name 菜单名字
* @param parentId 父菜单编号
* @param id 菜单编号
*/
private void checkResource(Long parentId, String name, Long id) {
SysMenuDO menu = menuMapper.selectByParentIdAndName(parentId, name);
if (menu == null) {
return;
}
// 如果 menuId 为空说明不用比较是否为相同 menuId 的菜单
if (id == null) {
throw ServiceExceptionUtil.exception(MENU_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw ServiceExceptionUtil.exception(MENU_NAME_DUPLICATE);
}
}
/**
* 初始化菜单的通用属性
*
* 例如说只有目录或者菜单类型的菜单才设置 icon
*
* @param menu 菜单
*/
private void initMenuProperty(SysMenuDO menu) {
// 菜单为按钮类型时无需 componenticonpath 属性进行置空
if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) {
menu.setComponent("");
menu.setIcon("");
menu.setPath("");
}
}
}

View File

@ -107,4 +107,9 @@ public class SysPermissionServiceImpl implements SysPermissionService {
return roleDept != null ? roleDept.getRoleId() : null;
}
@Override
public void deleteRolesMenuByMenuId(Long menuId) {
// TODO 实现我
}
}