Vue3+NestJS实现权限管理系统(七):菜单管理和角色管理增删改查接口

971 阅读4分钟

前面的文章已经完成了登录、路由以及权限等相关的通用接口逻辑的开发。接下来,我们将开始正式开发一些常用的业务接口提供给前端使用。本篇文章将带领大家完成菜单管理和角色管理页面增删改查接口的开发。

菜单新增

菜单新增很简单,只需要将前端传来的字段插入数据库中即可

//新增菜单
  async createMenu(createMenuDto: CreateMenuDto) {
    try {
      await this.menuRepository.save(createMenuDto);
      return '菜单新增成功';
    } catch (error) {
      throw new ApiException('菜单新增失败', 20000);
    }
  }

其中CreateMenuDto中定义一下前端需要传的字段

//menu/dto/create-menu.dto.ts
import { IsNotEmpty, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateMenuDto {
  @IsNotEmpty({ message: '菜单名不可为空' })
  @ApiProperty({
    example: '菜单1',
  })
  title: string;

  @ApiProperty({
    example: 1,
  })
  order_num: number;

  @ApiProperty({
    example: 1,
    required: false,
  })
  @IsOptional()
  parent_id?: number;

  @ApiProperty({
    example: 1,
  })
  menu_type: number;
  @ApiProperty({
    example: 'menu',
  })
  icon: string;

  @IsOptional()
  @ApiProperty({
    example: 'AA/BB',
    required: false,
  })
  component?: string;

  @IsNotEmpty({ message: '路由不可为空' })
  @ApiProperty({
    example: 'BB',
  })
  path: string;
  @ApiProperty({
    example: 11,
  })
  create_by: number;

  @IsOptional()
  @ApiProperty({
    example: 'sys:post:list',
    required: false,
  })
  permission?: string;
}

然后在controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:menu:add

 //新增菜单
  @Post('/createMenu')
  @Permissions('system:menu:add')
  @ApiParam({ name: 'createMenuDto', type: CreateMenuDto })
  @ApiOperation({ summary: '菜单管理-新增' })
  async createMenu(
    @Body()
    createMenuDto: CreateMenuDto,
  ) {
    return await this.menuService.createMenu(createMenuDto);
  }

我们还需要在权限守卫中(guards/permissions.guard.ts)给管理员用户放行

image.png

这样管理员就拥有所有接口权限了,方便后续添加菜单及权限的操作。

到这里,菜单新增接口就开发完成了。

菜单查询

菜单查询一般会返回给前端一个树状结构给前端渲染,前面文章中我们已经完成了菜单树的查询,这里也是一样的逻辑,直接复用即可。这里需要注意的是我们返回给前端的菜单是当前登录用户所拥有的菜单权限。我们在menu.service.ts中定义一个方法findMenuTree

 //菜单列表查询
  async findMenuList(findMenuListDto: FindMenuListDto, req) {
    //user.guard中注入的解析后的JWTtoken的user
    const { user } = req;
    //根据关联关系通过user查询user下的菜单和角色,并根据findMenuListDto条件查询,条件字段为空默认匹配所有
    const userList: User = await this.getUser(user, findMenuListDto);
    const menuList = rolesToMenus(userList?.roles);
    const treeMenuList = convertToTree(menuList);
    //是否显示树形菜单 没有传title且菜单状态为开启时候才显示树形菜单
    const isShowTreeMenu = !findMenuListDto.title && (findMenuListDto.status == 1 || !findMenuListDto.status);
    return isShowTreeMenu ? treeMenuList : menuList;
  }

其中getUser方法是根据user查询user下的角色和菜单,并根据findMenuListDto条件查询,条件字段为空默认匹配所有。

async getUser(user, condition?) {
    try {
      const queryBuilder = this.userRepository
        .createQueryBuilder('fs_user')
        .leftJoinAndSelect('fs_user.roles', 'fs_role')
        .leftJoinAndSelect('fs_role.menus', 'fs_menu')
        .where({ id: user.sub })


      if (condition?.title) {
        //根据菜单title模糊查询
        queryBuilder.andWhere('fs_menu.title LIKE :title', { title: `%${condition.title}%` })
      }
      if (condition?.status) {
        //根据菜单状态查询
        queryBuilder.andWhere('fs_menu.status = :status', { status: condition.status })
      }

      queryBuilder.orderBy('fs_menu.order_num', 'ASC')
      queryBuilder.getOne();
      const User = await queryBuilder.getOne();
      return User

    } catch (error) {
      console.log(error);
      throw new ApiException('查询失败', ApiErrorCode.COMMON_CODE);
    }

  }

rolesToMenus方法是将当前用户所有角色下的菜单取出并进行去重

import { Menu } from "src/menu/entities/menu.entity";
import { Role } from "src/role/entities/role.entity";

export const rolesToMenus = (roles: Role[] = []) => {
  interface MenuMap {
    [key: string]: Menu;
  }
  // console.log(userList.roles[0].menus);

  //根据id去重
  const menus: MenuMap = roles.reduce((mergedMenus: MenuMap, role: Role) => {
    role.menus.forEach((menu: Menu) => {
      mergedMenus[menu.id] = menu;
    });
    return mergedMenus;
  }, {});
  return Object.values(menus);
};

最后在controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:menu:list,注意这里因为是查询,所以使用get请求方式(我们尽量遵循 Restful 风格)

@Get('/list')
@ApiParam({ name: 'findMenuListDto', type: FindMenuListDto })
@ApiOperation({ summary: '获取菜单列表' })
async list(@Query() findMenuListDto: FindMenuListDto, @Request() req) {
    return await this.menuService.findMenuList(findMenuListDto, req);
}

菜单修改

菜单修改和菜单新增的逻辑查不到,只需要将前端传来的字段根据菜单 id 更新到数据库中即可。

//修改菜单
  async updateMenu(updateMenuDto: UpdateMenuDto) {
    try {
      await this.menuRepository.update(updateMenuDto.id, updateMenuDto);
      return '菜单更新成功';
    } catch (error) {
      throw new ApiException('菜单更新失败', 20000);
    }
  }

其中UpdateMenuDto中除了 id 都是可选参数

//menu/dto/update-menu.dto.ts

import { IsNotEmpty, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateMenuDto {
    @IsOptional()
    @ApiProperty({
        example: '菜单1',
        required: false,
    })
    title: string;

    @IsNotEmpty({ message: 'id不可为空' })
    @ApiProperty({
        example: 1,
    })
    id: number;
    @IsOptional()
    @ApiProperty({
        example: 1,
        required: false,
    })
    order_num: number;

    @ApiProperty({
        example: 1,
        required: false,
    })
    @IsOptional()
    parent_id?: number;

    @IsOptional()
    @ApiProperty({
        example: 1,
        required: false,
    })
    menu_type: number;

    @ApiProperty({
        example: 'menu',
        required: false,
    })
    @IsOptional()
    icon: string;

    @IsOptional()
    @ApiProperty({
        example: 'AA/BB',
        required: false,
    })
    component?: string;

    @IsOptional()
    @ApiProperty({
        example: 'BB',
        required: false,
    })
    path: string;

    @ApiProperty({
        example: 11,
    })
    update_by: number;

    @IsOptional()
    @ApiProperty({
        example: 'sys:post:list',
        required: false,
    })
    permission?: string;
}

controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:menu:edit,这里注意用Put请求方式

//更新菜单
  @Put('/updateMenu')
  @Permissions('system:menu:edit')
  @ApiParam({ name: 'updateMenu', type: UpdateMenuDto })
  @ApiOperation({ summary: '菜单管理-更新' })
  async updateMenu(
    @Body()
    updateMenuDto: UpdateMenuDto,
  ) {
    return await this.menuService.updateMenu(updateMenuDto);
  }

菜单删除

菜单删除就很简单了,只需要将前端传来的 id或者一个id数组删除即可。

  //删除菜单
  async deleteMenu(ids:number[]) {
    try {
      await this.menuRepository.delete(id);
      return '菜单删除成功';
    } catch (error) {
      throw new ApiException('菜单删除失败', 20000);
    }
  }

controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:menu:delete,这里注意param方式传参只能是string类型,这里将以,隔开其转换成数组形式

 @ApiOperation({
    summary: '菜单管理-删除',
  })
  @Permissions('system:menu:delete')
  @Delete('deleteMenu/:menuId')
  deleteMenu(@Param('menuId') menuId: string) {
   return this.menuService.deleteMenu(menuId.split(',').map(Number));
  }

到这里我们就完成了菜单管理的增删改查接口的开发。我们可以让前端打开 swagger 的页面查看接口相关文档(http://localhost:3000/fs_admin/api#/)。

image.png

接下来就是角色管理的开发了。

新增角色

新增角色和新增菜单不同的是,新增角色前端需要传入它所拥有的菜单权限列表menu_ids,然后根据这些 id 查到到菜单列表,再将菜单列表和角色关联即可。

//新增角色
  async create(createRoleDto: CreateRoleDto): Promise<string> {
    const row = await this.roleRepository.findOne({
      where: { role_name: createRoleDto.role_name },
    });
    if (row) {
      throw new ApiException('角色已存在', ApiErrorCode.COMMON_CODE);
    }
    const newRole = new Role();
    if (createRoleDto.menu_ids?.length) {
      const menuList = await this.menuRepository.find({
        where: {
          id: In(createRoleDto.menu_ids),
        },
      });
      newRole.menus = menuList;
    }
    try {
      await this.roleRepository.save({ ...createRoleDto, ...newRole });
      return '新增成功';
    } catch (error) {
      throw new ApiException('系统异常', ApiErrorCode.FAIL);
    }
  }

controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:role:create,这里注意用Post请求方式

@Post('createRole')
  @Permissions('system:role:create')
  @ApiOperation({
    summary: '角色管理-新增',
  })
  @ApiParam({
    name: 'CreateRoleDto',
    type: CreateRoleDto,
  })
  createRole(@Body() createRoleDto: CreateRoleDto) {
    return this.roleService.create(createRoleDto);
  }

其中CreateRoleDto中定义了前端需要传的字段和字段类型

import { IsArray, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateRoleDto {
  @IsNotEmpty({ message: '角色名不可为空' })
  @ApiProperty({
    example: '技术人员',
  })
  role_name: string;

  @ApiProperty({
    example: '备注',
    required: false,
  })
  @IsOptional()
  remark?: string;

  @IsNotEmpty({ message: '角色状态不可为空' })
  @ApiProperty({
    example: 1,
    description: '角色状态,1表示启用,0表示禁用',
  })
  status: number;

  @ApiProperty({
    example: [1],
    required: false,
  })
  @IsOptional()
  @IsArray({
    message: 'role_ids必须是数组',
  })
  @IsNumber({}, { each: true, message: 'role_ids必须是数字数组' })
  menu_ids?: number[];

  @IsNotEmpty({ message: '排序不可为空' })
  @ApiProperty({
    example: 1,
  })
  role_sort: number;
  @IsNotEmpty({ message: '创建人id不可为空' })
  @ApiProperty({
    example: 1,
  })
  create_by: number;
  @IsNotEmpty({ message: '更新人id不可为空' })
  @ApiProperty({
    example: 1,
  })
  update_by: number;
}

角色查询

角色查询需要根据前端传来的一些字段如角色名,字段,时间段等条件查询,如果没有传默认就查询所有角色。同时加入分页功能。


//查询当前用户角色列表
  async findRoleList(findMenuListDto: FindRoleListDto) {
    let queryBuilder = this.roleRepository.createQueryBuilder()
    if (findMenuListDto.role_name) {
      queryBuilder.andWhere('role_name like :role_name', { role_name: `%${findMenuListDto?.role_name}%` });
    }
    if (findMenuListDto.status) {
      queryBuilder.andWhere('status = :status', { status: findMenuListDto.status });
    }
    if (findMenuListDto.begin_time && findMenuListDto.end_time) {
      queryBuilder.andWhere('create_time BETWEEN :start AND :end', { start: findMenuListDto.begin_time, end: findMenuListDto.end_time });
    }
    handlePage(queryBuilder, findMenuListDto.page_num, findMenuListDto.page_size)
    const [list, count] = await queryBuilder.getManyAndCount();
    return { list, count };
  }

其中的handlePage是一个分页的方法,这里使用typeormqueryBuilderskiptake方法来实现分页。skip表示跳过多少条数据,take表示取多少条数据。

import { SelectQueryBuilder } from "typeorm";
/**
 *
 * @param queryBuilder 查询器
 * @param pageNum 页数
 * @param pageSize 每页条数
 * @returns queryBuilder
 */
export const handlePage = (
  queryBuilder: SelectQueryBuilder<any>,
  pageNum = 1,
  pageSize = 10
) => {
  return queryBuilder.skip((pageNum - 1) * pageSize).take(pageSize);
};

controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:role:list,这里注意用Get请求方式

 //查询
  @Get('findRoleList')
  @Permissions('system:role:list')
  @ApiOperation({
    summary: '角色管理-查询',
  })
  @ApiParam({
    name: 'CreateRoleDto',
    type: FindRoleListDto,
  })

  findRoleList(@Query() findRoleListDto: FindRoleListDto) {
    return this.roleService.findRoleList(findRoleListDto);
  }

其中FindRoleListDto

import { ApiProperty } from "@nestjs/swagger";
import { IsOptional } from "class-validator";

export class FindRoleListDto {
    @ApiProperty({
        example: '角色名称',
        required: false,
    })
    @IsOptional()
    role_name?: string;

    @ApiProperty({
        example: '状态:0禁用,1启用',
        required: false,
    })
    status: number;
    @ApiProperty({
        example: '结束时间',
        required: false,
    })
    end_time: string;
    @ApiProperty({
        example: '开始时间',
        required: false,
    })
    begin_time: string;
    @ApiProperty({
        example: '当前页',

        required: false,
    })
    page_num: number;
    @ApiProperty({
        example: '每页条数',
        required: false,
    })
    page_size: number;
}

角色更新

角色更新和菜单更新基本逻辑一致

//修改角色
  async updateRole(updateRoleDto: UpdateRoleDto) {
    try {
      await this.menuRepository.update(updateRoleDto.id, updateRoleDto);
      return '角色更新成功';
    } catch (error) {
      throw new ApiException('角色更新失败', 20000);
    }
  }

其中的UpdateRoleDto

import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional } from "class-validator";

export class UpdateRoleDto {

    @IsNotEmpty({ message: 'id不可为空' })
    @ApiProperty({
        example: 1,
    })
    id: number;
    @ApiProperty({
        example: '角色名称',
        required: false,
    })
    @IsOptional()
    role_name?: string;

    @ApiProperty({
        example: '角色描述',
        required: false,
    })
    @IsOptional()
    description?: string;

    @ApiProperty({
        example: '角色排序',
        required: false,
    })
    @IsOptional()
    order_num?: number;
}

它的controller

//更新角色
  @Put('/updateRole')
  @Public()
  @Permissions('system:role:edit')
  @ApiParam({ name: 'updateRole', type: UpdateRoleDto })
  @ApiOperation({ summary: '角色管理-更新' })
  async updateMenu(
    @Body()
    updateRoleDto: UpdateRoleDto,
  ) {
    return await this.roleService.updateRole(updateRoleDto);
  }

角色删除

角色删除和菜单删除一样只需要 id 就行

 //删除角色
  async deleteRole(id): Promise<string> {
    try {
      await this.roleRepository.delete(id);
      return '删除成功';
    } catch (error) {
      throw new ApiException('删除失败', ApiErrorCode.COMMON_CODE);
    }
  }

controller中定义请求方式以及一些关于swagger相关的装饰器,同时加上这个接口的权限字段system:role:delete,这里注意用Delete请求方式


  @ApiOperation({
    summary: '角色管理-删除',
  })
  @Permissions('system:role:delete')
  @Delete('deleteRole/:roleId')
  deleteRole(@Param('roleId') roleId: string) {
    return this.roleService.deleteRole(+roleId);
  }

到这里我们便完成了菜单及角色的增删改查,后续很多功能的增删改查代码都基本一致,包括前端 Vue 的代码也一样,所以后面会加代码生成功能生成一些常用重复代码。

代码地址各位看官们可以去下载源码,喜欢的话可以给个 star 哦!