尚品甄選電商SpringBoot-Web開發3權限管理之選單管理

目標

  • 選單管理
    • 選單需求和表結構
    • 選單管理CRUD操作介面
    • 選單管理CRUD前端
  • 為角色分配選單
    • 需求分析
    • 介面
      • 搜尋所有選單和角色分配選單id列表
      • 儲存角色分配的選單數據
    • 前端
  • 動態選單
    • 需求分析
    • 介面
      • 搜尋當前登入用戶可以操作的選單
    • 前端

選單管理

選單需求和表結構

選單中的數據會有層級關係例如:

  • 權限管理
    • 用戶管理
    • 角色管理
    • 選單管理
  • 訂單管理
    • 訂單列表

那在資料庫中應該如何儲存這樣的層級關係呢?

可以使用idparentId來表示,例如

id與parentId

如此一來就可以連結

新增相關檔案,完成準備工作

新增menu相關MSCM

列出選單

一樣是controller -> service -> mapper,不過service的邏輯稍微複雜了一點

為了列出所有的選單,以及子選單,新增以下介面以供使用

// SysMenuController.java
// 選單列表
@GetMapping("/findNodes")
public Result findNodes() {
    List<SysMenu> list = sysMenuService.findNodes();
    return Result.build(list, ResultCodeEnum.SUCCESS);
}

service的部分則是分為兩個主要操作

  1. 調用mapper尋找所有存在於數據庫中的選單
  2. 依照層級關係封裝所有的選單,使前端的element-plus可以直接使用數據
// SysMenuServiceImpl.java
// 選單列表
@Override
public List<SysMenu> findNodes() {
    // 尋找所有選單,回傳list
    List<SysMenu> sysMenuList = sysMenuMapper.findAll();
    if (CollectionUtils.isEmpty(sysMenuList)) return null;
    // 使用工具,把回傳的list封裝成前端element-plus要求的格式
    return MenuHelper.buildTree(sysMenuList);
}

核心的部分是工具,利用遞迴一層一層封裝數據

// 封裝樹狀選單
public class MenuHelper {

    public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
        // 封裝數據
        List<SysMenu> trees = new ArrayList<>();

        // 疊代sysMenu列表
        for (SysMenu sysMenu : sysMenuList) {
          if (sysMenu.getParentId() == 0) {
            trees.add(findChildren(sysMenu, sysMenuList));
          }
        }

        return trees;
    }

    // 搜尋menu的子menu
    private static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> sysMenuList) {
        sysMenu.setChildren(new ArrayList<>());

        for (SysMenu ele : sysMenuList) {
            // 找到對應的子menu
            if (sysMenu.getId().longValue() == ele.getParentId().longValue()) {
                // 加入parent的小孩們,但可能有更下層的小孩,所以繼續呼叫findChildren
                sysMenu.getChildren().add(findChildren(ele, sysMenuList));
            }
        }

        return sysMenu;
    }
}

新增與修改選單

  • 這部分就是簡單的controller -> service -> mapper搞定,沒有額外的邏輯了

刪除選單

刪除選單時會有個問題,如果刪除高層選單,會需要考慮子選單是否一併刪除。這裡採取的方案是,如果刪除的選單包含子選單,那就不能刪除,所以步驟如下

  1. 判斷當前要刪除的選單是否包含子選單
  2. 包含:不能刪除;不包含:可以刪除

主要邏輯如下

SysMenuServiceImpl.java
// 選單刪除
@Override
public void removeById(Long id) {
    // 取得當前選單的子選單個數
    int count = sysMenuMapper.selectCountById(id);
    // 是否包含子選單,如包含,不可刪
    if (count > 0) throw new SimonException(ResultCodeEnum.NODE_ERROR);

    // 不包含子選單,刪除
    sysMenuMapper.delete(id);
}

前端整合

前端的code直接貼上,重點是後端的程式

為角色分配選單

需求分析

需要兩個介面:

  1. 查詢所有選單和角色分配過選單id的列表用以在前端顯示
  2. 儲存角色和選單之間的關係

查詢所有選單和角色分配過選單id的列表

也是簡單的controller -> service -> mapper

SysRoleMenuController.java
// 搜尋所有選單以及搜尋角色分配過的選單id列表
@GetMapping("/findSysRoleMenuByRoleId/{roleId}")
public Result findSysRoleMenuByRoleId(@PathVariable("roleId") Long roleId) {
    Map<String, Object> map = sysRoleMenuService.findSysRoleMenuByRoleId(roleId);
    return Result.build(map, ResultCodeEnum.SUCCESS);
}
SysRoleMenuServiceImpl.java
// 搜尋所有選單以及搜尋角色分配過的選單id列表
@Override
public Map<String, Object> findSysRoleMenuByRoleId(Long roleId) {
    // 搜尋所有選單
    List<SysMenu> sysMenuList = sysMenuService.findNodes();

    // 根據id搜尋角色分配過的選單id列表
    List<Long> roleMenuIds = sysRoleMenuMapper.findSysRoleMenuByRoleId(roleId);

    Map<String, Object> map = new HashMap<>();
    map.put("sysMenuList", sysMenuList);
    map.put("roleMenuIds", roleMenuIds);

    return map;
}
SysRoleMenuMapper.xml
<select id="findSysRoleMenuByRoleId" resultType="long">
  select menu_id
  from sys_role_menu
  where role_id = #{roleId} and is_deleted = 0
</select>

其實可以在搜尋的時候去掉is_deleted = 0這個條件,因為後面實作重新分配角色選單的時候是直接把role<->menu關係刪除,只是tutorial中還是包含了這個條件所以我就保留了

儲存角色分配選單之數據

上一篇分配角色時的邏輯大致相同

SysRoleMenuServiceImpl.java
// 儲存角色分配選單數據
@Override
public void doAssign(AssignMenuDto assignMenuDto) {
    // 刪除原本角色分配的選單數據
    sysRoleMenuMapper.deleteByRoleId(assignMenuDto.getRoleId());

    // 為角色重新分配選單數據
    List<Map<String, Number>> menuInfo = assignMenuDto.getMenuIdList();
    if (menuInfo != null && !menuInfo.isEmpty()) {
        sysRoleMenuMapper.doAssign(assignMenuDto);
    }
}
SysRoleMenuMapper.xml
<!--刪除原本角色分配的選單數據-->
<delete id="deleteByRoleId">
    delete from sys_role_menu where role_id = #{roleId}
</delete>

<!--為角色分配選單數據-->
<insert id="doAssign">
    insert into sys_role_menu (role_id,
                                menu_id,
                                create_time,
                                update_time,
                                is_deleted,
                                is_half)
    values
        <foreach collection="menuIdList" item="menuInfo" separator=",">
            (#{roleId}, #{menuInfo.id}, now(), now(), 0, #{menuInfo.isHalf})
        </foreach>
</insert>

前端整合

這邊前端也是大同小異,直接照教程貼上理解即可

動態選單

後端的部分主要的邏輯在service

@Override
public List<SysMenuVo> findMenusByUserId() {
    // 取得當前登入用戶之用戶id
    Long userId = AuthContextUtil.get().getId();
    // 根據id搜尋其可使用之選單
    List<SysMenu> menuList = sysMenuMapper.findMenusByUserId(userId);
    // 封裝成選單樹
    List<SysMenu> sysMenuList = MenuHelper.buildTree(menuList);
    // 轉換選單為回傳格式並回傳
    return buildMenus(sysMenuList);
}

private List<SysMenuVo> buildMenus(List<SysMenu> menuList) {
    List<SysMenuVo> sysMenuVoList = new LinkedList<>();

    for (SysMenu sysMenu : menuList) {
        // Setup current menu
        SysMenuVo sysMenuVo = new SysMenuVo();
        sysMenuVo.setTitle(sysMenu.getTitle());
        sysMenuVo.setName(sysMenu.getComponent());
        // Get current menu's children
        List<SysMenu> children = sysMenu.getChildren();
        // Build its children if necessary
        if (!CollectionUtils.isEmpty(children)) {
            sysMenuVo.setChildren(buildMenus(children));
        }
        // Add to the list
        sysMenuVoList.add(sysMenuVo);
    }

    return sysMenuVoList;
}

以及最查詢資料庫,需要關聯三張表

<!--根據用戶id搜尋可以操作的選單們-->
<select id="findMenusByUserId" resultMap="sysMenuMap">
    select distinct sm.*
    from sys_menu sm
              inner join sys_role_menu srm on sm.id = srm.menu_id
              inner join sys_user_role sur on srm.role_id = sur.role_id
    where sur.user_id = #{userId}
      and sm.is_deleted = 0
</select>

前端的部分把相關的code改成動態選單就好了

bug問題

  1. 角色分配了某個選單以及其全部子選單,這時再添加一個新的子選單,該角色會自動被分配新的角色。
  • 例如:測試人員被分配系統管理選單下的所有子選單,這時再為系統管理新增一個子選單(例如地區管理)那測試人員就會被分配這個新的選單

如何解決?

  • 新增選單時,把其父選單isHalf重新賦值為1

尚品甄選電商SpringBoot-Web開發3權限管理之選單管理
https://f88083.github.io/2024/05/01/尚品甄選電商SpringBoot-Web開發3權限管理之選單管理/
作者
Simon Lai
發布於
2024年5月1日
許可協議