瀏覽代碼

1. 修改 MenuDO 的字段,menuId 改成 id,menuName 改成 name
2. 添加 Menu 的创建、修改、删除接口

YunaiV 4 年之前
父節點
當前提交
ea4c9e4981
共有 29 個文件被更改,包括 503 次插入237 次删除
  1. 9 9
      ruoyi-ui/src/api/system/menu.js
  2. 2 2
      ruoyi-ui/src/store/modules/dict.js
  3. 1 1
      ruoyi-ui/src/store/modules/permission.js
  4. 30 0
      ruoyi-ui/src/utils/constants.js
  5. 4 3
      ruoyi-ui/src/utils/dict.js
  6. 68 73
      ruoyi-ui/src/views/system/menu/index.vue
  7. 4 4
      ruoyi-ui/src/views/system/role/index.vue
  8. 1 1
      ruoyi-ui/src/views/tool/gen/editTable.vue
  9. 2 2
      ruoyi-ui/src/views/tool/gen/genInfoForm.vue
  10. 11 20
      src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/vo/SysAuthMenuRespVO.java
  11. 2 2
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/SysDataDictSimpleVO.java
  12. 36 61
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/SysMenuController.java
  13. 3 3
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuBaseVO.java
  14. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuListReqVO.java
  15. 2 2
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuRespVO.java
  16. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuUpdateReqVO.java
  17. 36 2
      src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java
  18. 8 0
      src/main/java/cn/iocoder/dashboard/modules/system/convert/permission/SysMenuConvert.java
  19. 11 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java
  20. 6 6
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/dict/SysDictDataDO.java
  21. 5 6
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/dict/SysDictTypeDO.java
  22. 5 3
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/permission/SysMenuDO.java
  23. 8 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java
  24. 8 30
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
  25. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
  26. 34 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java
  27. 7 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java
  28. 192 4
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
  29. 5 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java

+ 9 - 9
ruoyi-ui/src/api/system/menu.js

@@ -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'
   })
-}
+}

+ 2 - 2
ruoyi-ui/src/store/modules/dict.js

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

+ 1 - 1
ruoyi-ui/src/store/modules/permission.js

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

+ 30 - 0
ruoyi-ui/src/utils/constants.js

@@ -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 // 自定义角色
+}

+ 4 - 3
ruoyi-ui/src/utils/dict.js

@@ -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 ''

+ 68 - 73
ruoyi-ui/src/views/system/menu/index.vue

@@ -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>
-          </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 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="路由地址" 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("删除成功");

+ 4 - 4
ruoyi-ui/src/views/system/role/index.vue

@@ -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;
@@ -592,4 +592,4 @@ export default {
     }
   }
 };
-</script>
+</script>

+ 1 - 1
ruoyi-ui/src/views/tool/gen/editTable.vue

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

+ 2 - 2
ruoyi-ui/src/views/tool/gen/genInfoForm.vue

@@ -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
       };
     }

+ 11 - 20
src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/vo/SysAuthMenuRespVO.java

@@ -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;
 
     /**

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/SysDataDictSimpleVO.java

@@ -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;
 
 }

+ 36 - 61
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/SysMenuController.java

@@ -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);
+    }
 
 }

+ 3 - 3
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuBaseVO.java

@@ -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 = "显示顺序不能为空")

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuListReqVO.java

@@ -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;
 
 }

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuRespVO.java

@@ -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 = "时间戳格式")

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/SysMenuUpdateReqVO.java

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

+ 36 - 2
src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java

@@ -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()));
+    }
+
 }

+ 8 - 0
src/main/java/cn/iocoder/dashboard/modules/system/convert/permission/SysMenuConvert.java

@@ -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);
+
 }

+ 11 - 0
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java

@@ -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));
+    }
+
 }

+ 6 - 6
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/dict/SysDictDataDO.java

@@ -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 = "字典类型不能为空")

+ 5 - 6
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/dict/SysDictTypeDO.java

@@ -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;
     /**
      * 状态
      *

+ 5 - 3
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/permission/SysMenuDO.java

@@ -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;
     /**
      * 显示顺序
      */

+ 8 - 0
src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java

@@ -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, "父菜单的类型必须是菜单");
+
 }

+ 8 - 30
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -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);
     }
 
 }

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java

@@ -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;

+ 34 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java

@@ -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);
+
 }

+ 7 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java

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

+ 192 - 4
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java

@@ -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) {
+        // 菜单为按钮类型时,无需 component、icon、path 属性,进行置空
+        if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) {
+            menu.setComponent("");
+            menu.setIcon("");
+            menu.setPath("");
+        }
+    }
+
 }

+ 5 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java

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