Vue3 + Nest 实现权限管理系统 后端篇(四):菜单权限管理

一个后台管理系统必不可少的就是对于菜单的权限控制,即前端的路由由后端进行返回,然后前端动态加载路由。

当用户登录完成之后,我们需要根据该用户的角色返回对应的菜单列表,并且要将菜单列表处理成树形结构给前端,因此在角色表中(user)需要与菜单表(menu)进行关联,和角色权限一样,它们之间也是多对多的关系。当拿到用户登录信息之后,根据关联关系便可查寻出该用户角色所对应的菜单列表

创建菜单模块

执行nest g res menu创建一个菜单模块

我们需要创建的菜单表实体如下

//menu.entity.ts
import {


  Column,


  CreateDateColumn,


  Entity,


  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';



@Entity()
export class Menu {
  @PrimaryGeneratedColumn()
  id: string;
  //菜单名称
  @Column({
    length: 20,
  })
  menuName: string;
  //排序
  @Column()
  orderNum: number;
  //父id
  @Column({ nullable: true })
  parentId: number;
  @Column({
    length: 10,
  })
  menuType: string;
  //菜单图标
  @Column({
    length: 50,
    nullable: true,
  })
  icon: string;

  //组件路径
  @Column({
    length: 50,
  })
  component: string;

  //路由
  @Column({
    length: 50,
  })
  path: string;

  @Column({
    length: 50,
    nullable: true,
  })
  createBy: string;
  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}

其实这个菜单就是返回给前端当路由使用的,所以需要包含路由,组件路径等字段,当然最终字段肯定不止这些,后期会进行完善。

然后将在role.entity.ts新增一个字段(menus)并进行关联

//role.entity.ts

import {


  Column,


  CreateDateColumn,


  Entity,


  JoinTable,

  ManyToMany,

  PrimaryGeneratedColumn,

  UpdateDateColumn,

} from 'typeorm';

import { Permission } from '../../permission/entities/permission.entity';

import { Menu } from '../../menu/entities/menu.entity';

@Entity()

export class Role {

  @PrimaryGeneratedColumn()

  id: string;



  @Column({

    length: 20,

  })

  name: string;



  @CreateDateColumn()

  createTime: Date;



  @UpdateDateColumn()

  updateTime: Date;




  @ManyToMany(() => Permission)

  @JoinTable({

    name: 'role_permission_relation',

  })

  permissions: Permission[];

  @ManyToMany(() => Menu)

  @JoinTable({

    name: 'role_menu_relation',

  })

  menus: Menu[];

}

启动项目就会发现role_menu_relationmenu表被创建了,然后我们在menu表中插入一些菜单如下

image.png

其中,parentId 为 null 代表最高级菜单,子菜单1.xx父菜单1的子菜单们,以此类推最后我们要将他们组装成树状结构返回给前端

角色绑定菜单

接下来需要给每个角色都绑定不同的菜单,这样便可以根据用户角色来控制每个用户的菜单权限了,我们给 role 表添加一个 menus 字段如下

//role.entity.ts

import {


  Column,


  CreateDateColumn,


  Entity,


  JoinTable,

  ManyToMany,

  PrimaryGeneratedColumn,

  UpdateDateColumn,

} from 'typeorm';

import { Permission } from '../../permission/entities/permission.entity';

import { Menu } from '../../menu/entities/menu.entity';

@Entity()

export class Role {

  @PrimaryGeneratedColumn()

  id: string;



  @Column({

    length: 20,

  })

  name: string;



  @CreateDateColumn()

  createTime: Date;



  @UpdateDateColumn()

  updateTime: Date;




  @ManyToMany(() => Permission)

  @JoinTable({

    name: 'role_permission_relation',

  })

  permissions: Permission[];

  @ManyToMany(() => Menu)

  @JoinTable({

    name: 'role_menu_relation',

  })

  menus: Menu[];

}

然后给不同的角色新增不同的菜单权限,其中超级管理员拥有所有菜单权限,管理员子菜单1.1``子菜单1.2``子菜单1.3权限,用户子菜单1.1权限,它们的关系体现在role_menu_relation中如下

image.png

菜单接口实现

接下来开始写一个新增菜单接口,先定义一下create-menu.dto.ts规定一下前端传递的规则

import { IsNotEmpty } from 'class-validator';
export class CreateMenuDto {
  @IsNotEmpty({ message: '菜单名不可为空' })
  menuName: string;
  orderNum: number;
  parentId: number;
  menuType: string;
  icon: string;
  @IsNotEmpty({ message: '组件路径不可为空' })
  component: string;
  @IsNotEmpty({ message: '路由不可为空' })
  path: string;
  createBy: string;
}

在 controller 中写一个 post 接口

//menu.controller.ts
  @Post()
  create(@Body() createMenuDto: CreateMenuDto) {
    return this.menuService.create(createMenuDto);
  }

在 service 中写下操作数据库逻辑

import { Injectable } from '@nestjs/common';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Menu } from './entities/menu.entity';
import { User } from 'src/user/entities/user.entity';
import { Repository } from 'typeorm';
import { convertToTree } from 'src/utils/convertToTree';



@Injectable()
export class MenuService {
  constructor(
    @InjectRepository(Menu)
    private menuRepository: Repository<Menu>,
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
   async create(createMenuDto: CreateMenuDto) {
    try {
      await this.menuRepository.save(createMenuDto);
    } catch (error) {
      throw new ApiException(error, ApiErrorCode.DATABASE_ERROR);
    }
    return '操作成功';
  }


}



到这里创建菜单的接口就完成了,创建接口很简单,重点其实是接下来的查询接口

当用户登录的时候我们只能拿到用户的登录信息,即用户表的内容。所以我们需要根据关联关系查询到所关联的角色表进而获取到角色对应的菜单表,然后菜单表查询的结果需要根据 orderNum 进行升序(ASC)排序,最后去重再处理成树状结构

在 utils 下新增一个转树状结构函数

//utils/convertToTree.ts
export const convertToTree = (menuList, parentId: number | null = null) => {
  const tree = [];

  for (let i = 0; i < menuList.length; i++) {
    if (menuList[i].parentId === parentId) {
      const children = convertToTree(menuList, menuList[i].id);
      if (children.length) {
        menuList[i].children = children;
      }
      tree.push(menuList[i]);
    }

  }
  return tree;
};

这个函数从一个菜单列表开始,通过递归的方式遍历菜单项,并根据每个菜单项的 parentId 属性将其添加到相应的父菜单下。如果一个菜单项有子菜单,那么它的 children 属性将被设置为该子菜单的数组。

menu.controller.ts写一个查询路由,传入用户名

  @Get()
  findMenu(@Request() req) {
    return this.menuService.findMenu(req.user);
  }

menu.service.ts加上查询逻辑

  async findMenu(user) {
    const userList: User = await this.userRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.roles', 'role')
      .leftJoinAndSelect('role.menus', 'menu')
      .where({ username: user.username })
      .orderBy('menu.orderNum', 'ASC')
      .getOne();



    interface MenuMap {
      [key: string]: Menu;
    }


    const menus: MenuMap = userList?.roles.reduce(
      (mergedMenus: MenuMap, role: any) => {
        role.menus.forEach((menu: Menu) => {
          mergedMenus[menu.id] = menu;
        });
        return mergedMenus;
      },
      {},
    );

    // 去重后的菜单数组
    const uniqueMenus: Menu[] = Object.values(menus);


    return convertToTree(uniqueMenus);
  }

这里使用了QueryBuilder的方式查询数据库,其中leftJoinAndSelect用于执行关联查询,将多个表按照指定的关联关系连接在一起,并选择需要返回的字段。

最后调用查询接口就会查到我们需要的结构(这里先调用登录接口获得token,然后将token模拟放到headers中进行操作)

image.png

到这里菜单权限控制大致完成,当然后续肯定会进行完善,但是大致思路就是这样。如果你对本系列感兴趣的话可以关注专栏Vue3+NestJS全栈开发后台权限管理系统

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY8V5DFj' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片