SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(1)权限管理

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(1)权限管理

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(2)角色管理

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(3)用户管理

SpringBoot3+Vue3 前后端分离项目基于RBAC权限访问控制-实现动态路由、动态菜单

1、RBAC-基于角色访问控制

权限授权给角色角色分配给用户。即,用户拥有了该角色下的权限

2、数据库设计 

-- 用户表
CREATE TABLE `user` (
  `id` varchar(40) NOT NULL COMMENT '主键',
  `username` varchar(40) DEFAULT NULL COMMENT '用户名',
  `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
  `name` varchar(40) DEFAULT NULL COMMENT '姓名',
  `email` varchar(40) DEFAULT NULL COMMENT '邮箱',
  `avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '头像地址',
  `creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

-- 用户-角色关联表
CREATE TABLE `user_role` (
  `id` varchar(40) NOT NULL COMMENT '主键',
  `user_id` varchar(40) NOT NULL COMMENT '用户ID',
  `role_id` varchar(40) NOT NULL COMMENT '角色ID',
  `creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色表';

-- 角色表
CREATE TABLE `role` (
  `id` varchar(40) NOT NULL COMMENT '主键',
  `role_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色名称',
  `role` varchar(40) DEFAULT NULL COMMENT '角色',
  `creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';

-- 角色-权限关联表
CREATE TABLE `role_menu` (
  `id` varchar(40) NOT NULL COMMENT '主键',
  `role_id` varchar(40) NOT NULL COMMENT '角色ID',
  `menu_id` varchar(40) NOT NULL COMMENT '菜单ID',
  `creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色菜单表';

-- 权限表
CREATE TABLE `menu` (
  `id` varchar(40) NOT NULL COMMENT '主键',
  `parent_id` varchar(40) DEFAULT NULL COMMENT '父ID',
  `menu_name` varchar(40) DEFAULT NULL COMMENT '菜单名',
  `type` int DEFAULT NULL COMMENT '类型(1:目录,2:菜单,3:按钮)',
  `icon` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图标',
  `path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '前端路由',
  `auth` varchar(128) DEFAULT NULL COMMENT '权限',
  `sort` int DEFAULT NULL COMMENT '排序',
  `status` int DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  `creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单表';

3、SpringBoot3 后端实现菜单管理

3.1 Maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.dragon</groupId>
    <artifactId>springboot3-vue3</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot3-vue3</name>
    <description>springboot3-vue3</description>
    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>

        <!-- mybatis-plus-boot-starter 中 mybatis-spring 版本不够,排除之后引入新版本 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>

        <!-- mybatis-plus 多表查询 -->
        <dependency>
            <groupId>com.github.yulichang</groupId>
            <artifactId>mybatis-plus-join-boot-starter</artifactId>
            <version>1.4.12</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- jaxb-api 处理 xml 数据 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.4.0-b180830.0359</version>
        </dependency>

        <!-- fastjson2 处理 json 数据 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.48</version>
        </dependency>

        <!-- 使用 springdoc 生成 swagger 文档  -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
        </dependency>


        <!-- 代码生成器  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.6</version>
        </dependency>

        <!-- 模板引擎  -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>

        <!-- Sa-Token -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>1.38.0</version>
        </dependency>

        <!-- Sa-Token 集成 jwt -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-jwt</artifactId>
            <version>1.38.0</version>
        </dependency>

        <!-- Sa-Token 集成 redis, 使用 jackson 序列化 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.38.0</version>
        </dependency>

        <!-- 提供Redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>


        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>


        <!-- Java 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.28</version>
        </dependency>

        <!-- 导入导出 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.5</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.2 Entity-实体类

package com.dragon.springboot3vue3.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 菜单表
 * </p>
 */
@Data
@Schema(name = "Menu", description = "菜单表")
public class Menu implements Serializable {
    private static final long serialVersionUID = 1L;

    @Schema(description = "主键")
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;

    @Schema(description = "父ID")
    private String parentId;

    @Schema(description = "菜单名")
    private String menuName;

    @Schema(description = "类型")
    private Integer type;

    @Schema(description = "图标")
    private String icon;

    @Schema(description = "路径")
    private String path;

    @Schema(description = "权限")
    private String auth;

    @Schema(description = "排序")
    private int sort=99;

    @Schema(description = "状态(0:禁用,1:启用)")
    private Integer status;

    @Schema(description = "创建人ID")
    @TableField(fill = FieldFill.INSERT)
    private String creatorId;

    @Schema(description = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime ts;

    @Schema(description = "逻辑删除字段(0:正常,1:删除)")
    @TableLogic
    private Integer deleteFlag;
}

3.3 Dto

3.3.1 MenuDto

package com.dragon.springboot3vue3.controller.dto.entityDto;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
public class MenuDto {
    @Schema(description = "主键")
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;

    @Schema(description = "父ID")
    private String parentId;

    @Schema(description = "菜单名")
    private String menuName;

    @Schema(description = "类型")
    private Integer type;

    @Schema(description = "图标")
    private String icon;

    @Schema(description = "路径")
    private String path;

    @Schema(description = "权限")
    private String auth;

    @Schema(description = "排序")
    private int sort=99;

    @Schema(description = "状态(0:禁用,1:启用)")
    private Integer status;

    @Schema(description = "创建人ID")
    @TableField(fill = FieldFill.INSERT)
    private String creatorId;

    @Schema(description = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime ts;

    @Schema(description = "逻辑删除字段 0.正常 1.删除")
    private Integer deleteFlag;

    @Schema(description = "菜单列表")
    private List<MenuDto> children=new ArrayList<>();
}

3.3.2 MenuPageDto

package com.dragon.springboot3vue3.controller.dto.pageDto;

import com.dragon.springboot3vue3.utils.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class MenuPageDto extends PageDTO {
    @Schema(description = "菜单名称")
    private String menuName;

    @Schema(description = "权限名称")
    private String auth;

}

3.3.3 PageDTO

package com.dragon.springboot3vue3.utils;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@Schema(description = "基础分页")
public class PageDTO {
    @NotNull
    @Schema(description = "当前页码")
    public Long currentPage;

    @NotNull
    @Schema(description = "每页记录数")
    public Long pageSize;
}

3.4 MenuController

package com.dragon.springboot3vue3.controller;

import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import com.dragon.springboot3vue3.service.IMenuService;
import com.dragon.springboot3vue3.utils.StringIdsDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 菜单表 前端控制器
 * </p>
 */
@Tag(name = "菜单接口")
@RestController
@RequestMapping("/menu")
public class MenuController {
    @Autowired
    private IMenuService menuService;

    @Operation(summary = "登录用户的菜单列表")
    @GetMapping("getMyMenu")
    public SaResult getMyMenu(){
        return SaResult.ok().setData(menuService.getMyMenu());
    }

    @Operation(summary = "登录用户的路由列表")
    @GetMapping("getMyRoute")
    public SaResult getMyRoute(){
        return SaResult.ok().setData(menuService.getMyRoute());
    }

    @Operation(summary = "所有菜单层级列表")
    @GetMapping("getAllMenu")
    public SaResult getAllMenu(){
        return SaResult.ok().setData(menuService.getAllMenu());
    }

    @Operation(summary = "分页列表")
    @PostMapping("/list")
    public SaResult list(@RequestBody MenuPageDto pageDto){
        return SaResult.ok().setData(menuService.list(pageDto));
    }

    @Operation(summary = "新增或更新")
    @PostMapping("/saveOrUpdate")
    public SaResult saveOrUpdate(@RequestBody @Validated Menu menuDto){
        Menu menu=new Menu();
        BeanUtils.copyProperties(menuDto,menu);
        menuService.saveOrUpdate(menu);
        return SaResult.ok();
    }

    @Operation(summary = "删除")
    @DeleteMapping("/remove")
    public SaResult remove(@RequestBody @Validated StringIdsDTO stringIdsDTO){
        menuService.removeByIds(stringIdsDTO.getIds());
        return SaResult.ok();
    }
}

3.5 IMenuService

package com.dragon.springboot3vue3.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.dragon.springboot3vue3.controller.dto.entityDto.MenuDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import java.util.List;

/**
 * <p>
 * 菜单表 服务类
 * </p>
 */
public interface IMenuService extends IService<Menu> {
    List<MenuDto> getMyMenu();
    List<Menu> getMyRoute();
    List<MenuDto> getAllMenu();
    Page<MenuDto> list(MenuPageDto pageDto);
}

3.6 MenuServiceImpl

package com.dragon.springboot3vue3.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dragon.springboot3vue3.controller.dto.entityDto.MenuDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import com.dragon.springboot3vue3.entity.RoleMenu;
import com.dragon.springboot3vue3.entity.UserRole;
import com.dragon.springboot3vue3.mapper.MenuMapper;
import com.dragon.springboot3vue3.service.IMenuService;
import com.dragon.springboot3vue3.service.IRoleMenuService;
import com.dragon.springboot3vue3.service.IUserRoleService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * <p>
 * 菜单表 服务实现类
 * </p>
 *
 * 获取菜单分级逻辑:先获取一级菜单,再往一级菜单存其所有子菜单
 *
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
    @Autowired
    private IUserRoleService userRoleService;
    @Autowired
    private IRoleMenuService roleMenuService;


    /**
     * 登录用户的菜单列表
     * @return
     */
    public List<MenuDto> getMyMenu(){
        List<UserRole> list = userRoleService.lambdaQuery().eq(UserRole::getUserId, StpUtil.getLoginIdAsString()).list();
        List<String> roleIds=list.stream().map(UserRole::getRoleId).toList();
        List<RoleMenu> list1 = roleMenuService.lambdaQuery().in(RoleMenu::getRoleId, roleIds).list();
        List<String> menuIds = list1.stream().distinct().map(RoleMenu::getMenuId).toList();

        List<Menu> menuList=this.lambdaQuery().in(Menu::getId,menuIds).orderByAsc(Menu::getSort).list();

        return getMenuList(menuList);
    }

    /**
     * 登录用户的路由列表
     * @return
     */
    public List<Menu> getMyRoute(){
        List<UserRole> list = userRoleService.lambdaQuery().eq(UserRole::getUserId, StpUtil.getLoginIdAsString()).list();
        List<String> roleIds=list.stream().map(UserRole::getRoleId).toList();
        List<RoleMenu> list1 = roleMenuService.lambdaQuery().in(RoleMenu::getRoleId, roleIds).list();
        List<String> menuIds = list1.stream().distinct().map(RoleMenu::getMenuId).toList();

        List<Menu> menuList=this.lambdaQuery().in(Menu::getId,menuIds).list();

        return getRouteList(menuList);
    }

    /**
     * 获取所有菜单层级列表
     * @return
     */
    @Override
    public List<MenuDto> getAllMenu() {
        // 所有菜单
        List<Menu> allList = this.list();
        return getMenuList(allList);
    }

    /**
     * 分页列表
     */
    @Override
    public Page<MenuDto> list(MenuPageDto pageDto) {
        // 创建分页对象
        Page<MenuDto> page=new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());
        Page<MenuDto> response = baseMapper.selectJoinPage(page,MenuDto.class,new MPJLambdaWrapper<>());

        // 重新封装分页数据
        List<MenuDto> menuList = getMenuList(this.list());
        response.setRecords(menuList);
        response.setTotal(menuList.size());
        return response;
    }


    /**
     * 获取 menuList 菜单层级列表
     */
    private List<MenuDto> getMenuList(List<Menu> menuList){
        List<MenuDto> menuDtoList=new ArrayList<>();

        // menuList 中一级菜单列表(要在所有一级菜单存其所有子菜单)
        List<Menu> list = menuList.stream().sorted((Comparator.comparing(Menu::getSort))).filter(item -> item.getParentId().equals("0")).toList();

        // 一级菜单 id 列表
        List<String> parentIds = menuList.stream().map(Menu::getId).toList();

        // menuList 中所有一级菜单 Menu 转换 MenuDto
        list.forEach(item->{
            MenuDto menuDto=new MenuDto();
            BeanUtils.copyProperties(item,menuDto);
            menuDtoList.add(menuDto);
        });

        // 遍历 menuList 中一级菜单列表,存储父id在 parentIds 的菜单即为其子菜单
        menuDtoList.forEach(item->{
            List<MenuDto> childrenList = recursive(item.getId(),parentIds);
            item.setChildren(childrenList);
        });

        return menuDtoList;
    }


    /**
     * 根据 parentId 递归,获取 父id 在 parentIds 内的菜单,即为其子菜单
     * @return
     */
    private List<MenuDto> recursive(String parentId,List<String> parentIds){
        List<MenuDto> childrenList=new ArrayList<>();
        // 所有菜单
        List<Menu> menuList=this.lambdaQuery().orderByAsc(Menu::getSort).list();

        menuList.forEach(item->{
            // 遍历所有菜单,筛选 parentId 的子菜单,并且子菜单类型不是按钮(item.getType()!=3),并且子菜单id包含在menuIds内
            if(item.getParentId().equals(parentId) && item.getType()!=3 && parentIds.contains(item.getId())){
                // 将 menu 转为 menuDto
                MenuDto menuDto=new MenuDto();
                BeanUtils.copyProperties(item,menuDto);
                childrenList.add(menuDto);
            }
        });

        // 递归查询 childrenList 子节点(遍历每个子节点,以每个子节点为 parentId 查询其子节点,直到没有子节点)
        childrenList.forEach(item->{
            item.setChildren(recursive(item.getId(),parentIds));
        });

        return childrenList;
    }

    /**
     * 获取路由列表
     */
    private List<Menu> getRouteList(List<Menu> menuList){
        List<Menu> routeList=new ArrayList<>();

        // menuList 中所有一级菜单(要在所有一级菜单存其所有子菜单)
        List<Menu> list = menuList.stream().filter(item -> item.getParentId().equals("0")).toList();

        // 遍历一级菜单
        list.forEach(item->{
            if(item.getType()==2){
                routeList.add(item);
            }
        });

        // 遍历 menuList 中所有一级菜单,获取所有子菜单
        list.forEach(item->{
            routeList.addAll(routeRecursive(item.getId()));
        });
        return routeList;
    }

    /**
     * 根据 parentId 递归 获取所有子节点
     * @return
     */
    private List<Menu> routeRecursive(String parentId){
        List<Menu> routeList=new ArrayList<>();
        // 所有菜单
        List<Menu> menuList=this.list();

        // 遍历 item.getParentId().equals(parentId) 的数据
        menuList.forEach(item->{
            // item.getType()!=3 过滤按钮
            if(item.getParentId().equals(parentId) && item.getType()!=3){
                routeList.add(item);
            }
        });

        // 递归查询 childrenList 子节点(遍历每个子节点,以每个子节点为 parentId 查询其子节点,直到没有子节点)
        routeList.forEach(item->{
            routeRecursive(item.getId());
        });

        return routeList;
    }
}

3.7 Swagger 请求后端数据

{
  "code": 200,
  "msg": "ok",
  "data": {
    "records": [
      {
        "id": "1",
        "parentId": "0",
        "menuName": "首页",
        "type": 2,
        "icon": "HomeFilled",
        "path": "/home/index",
        "auth": "",
        "sort": 99,
        "status": 1,
        "creatorId": null,
        "createTime": "2024-05-13 09:43:42",
        "ts": "2024-06-07 10:15:49",
        "deleteFlag": 0,
        "children": []
      },
      {
        "id": "3",
        "parentId": "0",
        "menuName": "文章管理",
        "type": 1,
        "icon": "Management",
        "path": "",
        "auth": "",
        "sort": 100,
        "status": 1,
        "creatorId": null,
        "createTime": "2024-05-13 09:46:16",
        "ts": "2024-06-13 16:02:31",
        "deleteFlag": 0,
        "children": [
          {
            "id": "4",
            "parentId": "3",
            "menuName": "文章",
            "type": 2,
            "icon": "Notebook",
            "path": "/article/index",
            "auth": "article.list",
            "sort": 99,
            "status": 1,
            "creatorId": null,
            "createTime": "2024-05-14 17:51:30",
            "ts": "2024-06-07 10:16:05",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "7",
            "parentId": "3",
            "menuName": "文章分类",
            "type": 2,
            "icon": "Collection",
            "path": "/article/category/index",
            "auth": "article.category.list",
            "sort": 99,
            "status": 1,
            "creatorId": null,
            "createTime": "2024-05-14 18:59:39",
            "ts": "2024-06-07 10:16:08",
            "deleteFlag": 0,
            "children": []
          }
        ]
      },
      {
        "id": "2",
        "parentId": "0",
        "menuName": "系统管理",
        "type": 1,
        "icon": "Setting",
        "path": null,
        "auth": "",
        "sort": 101,
        "status": 1,
        "creatorId": null,
        "createTime": "2024-05-13 09:44:38",
        "ts": "2024-06-07 10:18:24",
        "deleteFlag": 0,
        "children": [
          {
            "id": "16",
            "parentId": "2",
            "menuName": "菜单管理",
            "type": 2,
            "icon": "Menu",
            "path": "/sys/menu/index",
            "auth": "sys.menu.list",
            "sort": 99,
            "status": 1,
            "creatorId": null,
            "createTime": "2024-05-21 09:22:05",
            "ts": "2024-06-07 10:15:59",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "10",
            "parentId": "2",
            "menuName": "用户管理",
            "type": 2,
            "icon": "UserFilled",
            "path": "/sys/user/index",
            "auth": "sys.user.list",
            "sort": 100,
            "status": 1,
            "creatorId": null,
            "createTime": "2024-05-21 09:14:19",
            "ts": "2024-06-07 10:34:17",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "13",
            "parentId": "2",
            "menuName": "角色管理",
            "type": 2,
            "icon": "User",
            "path": "/sys/role/index",
            "auth": "sys.role.list",
            "sort": 101,
            "status": 1,
            "creatorId": null,
            "createTime": "2024-05-21 09:19:01",
            "ts": "2024-06-07 11:12:04",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "4d4d9ea9fb38520109b904008d42c158",
            "parentId": "2",
            "menuName": "文件管理",
            "type": 2,
            "icon": "Folder",
            "path": "/sys/files/index",
            "auth": "sys.files.list",
            "sort": 102,
            "status": 1,
            "creatorId": "1",
            "createTime": "2024-06-13 16:03:24",
            "ts": "2024-06-14 16:07:10",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "125ae0e59d0d00d8ef4fc5f735557e0c",
            "parentId": "2",
            "menuName": "图标管理",
            "type": 2,
            "icon": "Files",
            "path": "/sys/icon/index",
            "auth": "sys.icon.list",
            "sort": 103,
            "status": 1,
            "creatorId": "1",
            "createTime": "2024-05-28 17:02:18",
            "ts": "2024-06-14 16:07:33",
            "deleteFlag": 0,
            "children": []
          }
        ]
      },
      {
        "id": "19",
        "parentId": "0",
        "menuName": "个人中心",
        "type": 1,
        "icon": "UserFilled",
        "path": null,
        "auth": null,
        "sort": 102,
        "status": 1,
        "creatorId": null,
        "createTime": "2024-05-25 17:00:07",
        "ts": "2024-06-07 10:18:27",
        "deleteFlag": 0,
        "children": [
          {
            "id": "155aa550a6d10c94932d83f5e1f42d9c",
            "parentId": "19",
            "menuName": "个人信息",
            "type": 2,
            "icon": "User",
            "path": "/user/Info",
            "auth": "user.info",
            "sort": 99,
            "status": 1,
            "creatorId": "1",
            "createTime": "2024-05-25 17:02:39",
            "ts": "2024-06-07 10:15:58",
            "deleteFlag": 0,
            "children": []
          },
          {
            "id": "9626a79c1e689e01e100ad5cf6a265c6",
            "parentId": "19",
            "menuName": "重置密码",
            "type": 2,
            "icon": "Switch",
            "path": "/user/ResetPassword",
            "auth": "user.ResetPassword",
            "sort": 99,
            "status": 1,
            "creatorId": "1",
            "createTime": "2024-05-25 17:03:34",
            "ts": "2024-06-07 10:16:12",
            "deleteFlag": 0,
            "children": []
          }
        ]
      }
    ],
    "total": 4,
    "size": 10,
    "current": 1,
    "pages": 1
  }
}

4、Vue3 前端实现菜单管理

4.1 前端效果

4.2 Vue3 代码

4.2.1 api -> menu.ts(相关接口)

import request from "@/utils/request";

// 菜单管理 API
export default{
  menuList(value:any){
    return request.post('/menu/list',value);
  },
  saveOrUpdate(value:any){
    return request.post('/menu/saveOrUpdate',value);
  },
  remove(ids:any){
    return request.delete('/menu/remove',{ data:{ ids } });
  },
  getMyMenu(){
    return request.get('/menu/getMyMenu');
  },
  getMyRoute(){
    return request.get('/menu/getMyRoute');
  }
}

4.2.2 sys -> menu - > index.vue 

<template>
  <el-card class="container">
    <template #header>
      <div class="header">
        <el-breadcrumb :separator-icon="ArrowRight">
          <el-breadcrumb-item :to="{ path: '/home/index' }" class="title">首页</el-breadcrumb-item>
          <el-breadcrumb-item class="title">菜单管理</el-breadcrumb-item>
        </el-breadcrumb>
        <div>
          <el-button type="primary" @click="addButton">新增菜单</el-button>
          <el-button type="danger" @click="batchRemove">批量删除</el-button>
        </div>
      </div>
    </template>

    <!-- 搜索表单 -->
    <el-form inline>
      <el-form-item label="菜单名称">
        <el-input v-model="searchModel.menuName" placeholder="请输入菜单名称" style="width: 150px" clearable></el-input>
      </el-form-item>
      <el-form-item label="权限名称">
        <el-input v-model="searchModel.auth" placeholder="请输入权限名称" style="width: 150px" clearable></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getMenuList">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 列表 -->
    <!-- row-key="id" table 显示树形结构数据 -->
    <el-table :data="menuList" border stripe style="width: 100%" height="550" row-key="id" default-expand-all @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="50" />
      <el-table-column label="菜单名称" width="230" prop="menuName"></el-table-column>
      <el-table-column label="图标" width="60">
        <template #default="{ row }">
          <component :is="getIconComponent(row.icon)" style="height: 15px;" />
        </template>
      </el-table-column>
      <el-table-column label="类型" prop="type" width="60">
        <template #default="{ row }">
          <span v-if="row.type==1">目录</span>
          <span v-if="row.type==2">菜单</span>
          <span v-if="row.type==3">按钮</span>
        </template>
      </el-table-column>
      <el-table-column label="前端路由" prop="path"></el-table-column>
      <el-table-column label="权限标识" prop="auth"></el-table-column>
      <el-table-column label="排序" prop="sort"></el-table-column>
      <el-table-column label="是否启用" prop="status">
        <template #default="{ row }">
          <el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="change(row)" />
        </template>
      </el-table-column>
      <el-table-column label="创建时间" prop="createTime"></el-table-column>
      <el-table-column label="更新时间" prop="ts"> </el-table-column>
      <el-table-column label="操作" width="120">
        <template #default="{ row }">
          <el-button :icon="Edit" circle plain type="primary" @click="edit(row)"></el-button>
          <el-button :icon="Delete" circle plain type="danger" @click="remove(row)"></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>

    <!-- 分页 -->
    <el-pagination
      v-model:current-page="searchModel.currentPage"
      v-model:page-size="searchModel.pageSize"
      :page-sizes="[10, 30, 50, 100]"
      layout="jumper, total, sizes, prev, pager, next"
      :total="searchModel.total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      background
      style="margin: 10px 0; justify-content: flex-end"
    />

    <!-- 新增和修改的弹窗 -->
    <el-dialog v-model="dialogVisible" width="50%" center @close="close">
      <template #header>
        <h1>{{ title }}</h1>
      </template>
      <el-form :model="menuModel" ref="menuModelRef" label-width="120px" :rules="rules">
        <el-row>
          <el-col :span="12">
            <el-form-item label="上级菜单" prop="parentId">
              <el-tree-select v-model="menuModel.parentId" :data="menuSelectData" check-strictly :render-after-expand="false"  style="max-height: 200px;" :props="defaultProps" default-expand-all placeholder="请选择上级菜单"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="菜单类型" prop="type">
              <el-select v-model="menuModel.type" placeholder="请选择菜单类型">
                <el-option v-for="item in menuTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
          
        <el-row>
          <el-col :span="12">
            <el-form-item label="菜单名称" prop="menuName">
              <el-input v-model="menuModel.menuName" placeholder="请输入菜单名称"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="菜单图标" prop="icon">
              <el-select v-model="menuModel.icon" clearable placeholder="请选择菜单图标">
                <template #prefix>
                  <component :is="getIconComponent(menuModel.icon)" style="height: 15px;" />
                </template>
                <el-option class="icon" v-for="item in iconList" :key="item.id" :label="item.icon" :value="item.icon" >
                  <component :is="getIconComponent(item.icon)" style="height: 15px;" />
                  <span class="text">{{ item.icon }}</span>
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row>
          <el-col :span="12">
            <el-form-item label="前端路由" prop="path">
              <el-input v-model="menuModel.path" placeholder="请输入前端路由,如:/sys/user/index"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="权限标识" prop="auth">
              <el-input v-model="menuModel.auth" placeholder="请输入权限标识,如:sys.user.list"/>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row>
          <el-col :span="12">
            <el-form-item label="状态" prop="status">
              <el-switch v-model="menuModel.status" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="排序" prop="sort">
              <el-input v-model="menuModel.sort" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>

      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="save">确认</el-button>
        </span>
      </template>
    </el-dialog>

  </el-card>
</template>

<script setup lang="ts">
  import { ref,reactive,onMounted } from 'vue'
  import { Edit,Delete,ArrowRight  } from '@element-plus/icons-vue'
  import * as Icons from '@element-plus/icons-vue';
  import menuApi from '@/api/menu';
  import iconApi from '@/api/icon';
  import { ElMessage, ElMessageBox } from 'element-plus'

  // 菜单分页列表
  const menuList=ref()
  // 新增、编辑时,选拉框选择的菜单列表
  const menuSelectData=ref()
  // 新增、编辑数据模型
  const menuModel=reactive({
    id:'',
    parentId:'',
    menuName:'',
    type:undefined,
    path:'',
    auth:'',
    status:1,
    icon:'',
    sort:99
  })
  // 图标列表
  const iconList=ref()
  const initMenuModel={ ...menuModel }
  // 批量删除的 id
  const ids=ref<string[]>([])

  const dialogVisible = ref(false)

  const defaultProps = {
    children: 'children',
    label: 'label'
  }
  // 选中的图标
  const selectedIcon = ref('');

  // 新增或编辑的标题
  const title=ref();

  const menuModelRef=ref()

  // 分页&搜索模型
  const searchModel=reactive({
    currentPage:1,
    pageSize:10,
    total:0,
    menuName:'',
    auth:''
  })
  const initSearchModel={ ...searchModel }

  const rules = reactive({
    parentId:[
      { required: true, message:'请选择上级菜单', trigger: 'blur'},
    ],
    type:[
      { required: true, message:'请选择菜单类型', trigger: 'blur'},
    ],
    menuName:[
      { required: true, message:'请输入菜单名称', trigger: 'blur'},
    ],
    // path:[
    //   { required: true, message:'请输入前端路由', trigger: 'blur'},
    // ],
    // auth:[
    //   { required: true, message:'请输入权限标识', trigger: 'blur'},
    // ],
    icon:[
      { required: true, message:'请选择菜单图标', trigger: 'blur'},
    ],
    sort:[
      { required: true, message:'请输入排序', trigger: 'blur'},
    ]
  })

  // 菜单类型选择器
  let menuTypeOptions = ref([
    {
      value: 1,
      label: "目录"
    },
    {
      value: 2,
      label: "菜单"
    },
    {
      value: 3,
      label: "按钮"
    }
  ])

  // pageSize 变化时触发
  const handleSizeChange = (val: number) => {
    searchModel.pageSize=val;
    getMenuList();
  }

  // currentPage 变化时触发
  const handleCurrentChange = (val: number) => {
    searchModel.currentPage=val;
    getMenuList();
  }

  // 菜单列表
  const getMenuList= async()=>{
    const response= await menuApi.menuList(searchModel);
    menuList.value=response.data.records;
    searchModel.currentPage=response.data.current;
    searchModel.pageSize=response.data.size;
    searchModel.total=response.data.total;

    getMenuSelectData(menuList.value);
  }
  
  // 新增、编辑时的下拉菜单
  const getMenuSelectData = (val: any) => {
    // 前端添加一个根目录,将后端返回的菜单都放在根目录下
    const menuSelect = []; 
    menuSelect.push({label: '根目录',value: '0',children: []})
    menuSelect[0].children=recursive(val);
    menuSelectData.value = menuSelect;
  }

  // 递归处理子菜单
  const recursive = (data: any) => {
    return data.map((item: { id: any, menuName: any, children: any }) => ({
      value: item.id,
      label: item.menuName,
      children: Array.isArray(item.children) ? recursive(item.children) : []
    }));
  };

  // 新增或编辑保存
  const save= async()=>{
    menuModelRef.value.validate(async(valid:any) => {
      if (valid) {
        const response=await menuApi.saveOrUpdate(menuModel) as any;
        ElMessage.success(response.msg);
        dialogVisible.value=false;
        getMenuList();
      } else {
        return false;
      }
    })
  }

  // 编辑
  const edit= (row:any)=>{
    dialogVisible.value=true;
    title.value="编辑菜单";
    selectedIcon.value=row.icon;
    // 源对象赋值给目标对象,Object.assign(目标对象, 源对象)
    Object.assign(menuModel, row);
  }

  // 新增菜单按钮
  const addButton= ()=>{
    dialogVisible.value=true;
    title.value="添加菜单";
  }

  // 关闭新增、编辑对话框时,清空数据和校验信息
  const close= ()=>{
    // 清空数据
    Object.assign(menuModel, initMenuModel);
    // 清除校验信息
    menuModelRef.value.clearValidate();
  }

  // 重置搜索表单
  const reset= ()=>{
    Object.assign(searchModel, initSearchModel);
    getMenuList();
  }

  // 批量删除选择
  const handleSelectionChange = (rows: any) => {
    ids.value = rows.map((item:any) => item.id);
  }

  // 批量删除
  const batchRemove= ()=>{
    if(ids.value.length > 0){
      ElMessageBox.confirm(
        `是否批量删除?`,
        '温馨提示',
        {
          confirmButtonText: '确认',
          cancelButtonText: '取消',
          type: 'warning',
        }
      )
      .then(async() => {
        await menuApi.remove(ids.value);
        ElMessage({ type: 'success',message: '删除成功' });
        getMenuList();
      })
    }else{
      ElMessage.warning('请选择批量删除项');
    }
  }

  // 单条删除
  const remove= async(row:any)=>{
    ElMessageBox.confirm(
      `是否删除 [ ${row.menuName} ] 菜单?`,
      '温馨提示',
      {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning',
      }
    )
    .then(async() => {
      ids.value.push(row.id);
      await menuApi.remove(ids.value);
      ElMessage({ type: 'success', message: '删除成功' });
      getMenuList();
    })
  }

  // 菜单中是否启用按钮
  const change= async(row:any)=>{
    await menuApi.saveOrUpdate(row);
  }

  // 获取图标列表
  const getIconList= async()=>{
    const response=await iconApi.getAll();
    iconList.value=response.data;
  }

  // 获取图标组件
  const getIconComponent = (icon: string) => {
    return (Icons as any)[icon] || null;
  };
  

  onMounted(()=>{
    getMenuList();
    getIconList();
  })

</script>

<style scoped lang="less">
  .container{
    height: 100%;
    box-sizing: border-box; 
  }
  .header{
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .title{
    font-size: large;
    font-weight: 600;
  }
  .icon{
    display: flex;
    align-items: center;
  }
  .text{
    margin-left: 10px;
  }

</style>

  • 31
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
权限管理系统是一种用于管理用户权限和角色的系统,可以根据用户的身份和角色来控制其访问系统中的各种资源。基于SpringBootVue和Redis的前后端分离模式,可以更好地实现权限管理系统的功能。 在这个系统中,SpringBoot作为后端框架,提供了强大的功能和稳定的性能,可以处理用户的请求并进行权限验证。Vue作为前端框架,提供了友好的界面和良好的用户体验,可以让用户方便地进行权限管理操作。而Redis作为缓存数据库,可以用来存储权限信息和用户的登录状态,加快系统的响应速度和提高系统的性能。 在权限管理系统中,我们可以使用RBAC(基于角色的权限控制)模型,将用户分配到不同的角色,再将角色分配到不同的权限,从而实现对用户访问资源的控制。通过这种方式,可以实现灵活的权限管理,并且可以根据实际需求动态地调整用户的权限和角色。 通过使用SpringBootVue,我们可以实现前后端分离,让前端和后端分别进行开发和维护,降低了系统的耦合度,同时也增加了系统的灵活性和可维护性。而通过使用Redis,我们可以充分利用其高速的读取和写入能力,有效地提升系统的性能和响应速度。 综上所述,基于SpringBootVue和Redis的权限管理系统,可以实现灵活、高效和安全的权限管理功能,满足用户对于权限管理的各种需求。同时,前后端分离模式也使得系统更加灵活和易于维护。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值