一个后台管理系统必不可少的就是对于菜单的权限控制,即前端的路由由后端进行返回,然后前端动态加载路由。
当用户登录完成之后,我们需要根据该用户的角色返回对应的菜单列表,并且要将菜单列表处理成树形结构给前端,因此在角色表中(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_relation
和menu
表被创建了,然后我们在menu
表中插入一些菜单如下

其中,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
中如下

菜单接口实现
接下来开始写一个新增菜单接口,先定义一下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中进行操作)

到这里菜单权限控制大致完成,当然后续肯定会进行完善,但是大致思路就是这样。如果你对本系列感兴趣的话可以关注公众号,将会定期更新本系列文章
留言获取源码
原文始发于微信公众号(web前端进阶):Vue3 + Nest 实现权限管理系统 后端篇(四):菜单权限管理
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/231954.html