本系列教程将教你使用 NestJS 构建一个生产级别的 REST API 风格的权限管理后台服务【代码仓库地址】。
【在线预览地址】账号:test,密码:d.12345
本章节内容: 1. 获取权限列表接口;2. 获取权限树接口;3. 创建权限接口;4. 获取权限详情接口;5. 编辑权限接口;6. 删除权限接口。
1. 获取权限列表接口
1.1 定义 DTO 对象
首先我们需要先定义可以根据哪些参数查询,一般来说会有分页、排序、时间等参数。
新建 /src/common/dto/base-query.dto.ts 文件并添加以下内容:
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional, Matches, Min } from 'class-validator';
import { Transform } from 'class-transformer';
export class BaseQueryDto {
@Min(1)
@Transform(({ value }) => parseInt(value))
@IsOptional()
@ApiProperty({ required: false, default: 1 })
page?: number;
@Min(10)
@Transform(({ value }) => parseInt(value))
@IsOptional()
@ApiProperty({ required: false, default: 10 })
pageSize?: number;
@Matches(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{3})?Z$/, {
message: '开始时间格式错误',
})
@IsOptional()
@ApiProperty({
required: false,
description: '开始时间',
})
beginTime?: string;
@Matches(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{3})?Z$/, {
message: '结束时间格式错误',
})
@IsOptional()
@ApiProperty({
required: false,
description: '结束时间',
})
endTime?: string;
@Matches(/^(asc|desc)$/, { message: '排序格式错误' })
@IsOptional()
@ApiProperty({
required: false,
default: 'desc',
enum: ['asc', 'desc'],
description: '排序方式,默认按创建时间倒序排列',
})
sort?: 'asc' | 'desc';
}
@Min() 装饰器用来设置该字段的最小值。
@Matches 装饰器用来对字段值进行正则校验。
@Transform() 装饰器用来将其他格式的数据转换为布尔值。
@IsOptional() 装饰器用来表示该字段是可选的,只有在存在时,才会执行上面的校验。
@ApiProperty() 装饰器用来定义 API 端点的请求参数的属性与描述性信息,以便自动生成 Swagger 文档。
该 DTO 将用作对后续查询接口的基础校验类。为什么有些参数使用了 @Transform() 方法转换类型?因为是 query 类型的参数(拼接在 url 上),格式都是字符串,所以有些其他类型的参数需要转换一下。
使用 DTO 文件的好处:
- 数据验证:通过使用类验证器(如
class-validator),可以确保传入的数据符合预期的格式和规则。 - 数据转换:可以使用类转换器(如
class-transformer)将普通的 JavaScript 对象转换为类实例,从而利用类的功能。 - 文档生成:结合
@nestjs/swagger等工具,可以自动生成 API 文档,方便前后端协作。 - 代码可读性和维护性:明确的数据结构定义使代码更易读、更易维护。
新建 /src/permission/dto/query-permission.dto.ts 文件并添加以下内容:
import { ApiProperty } from '@nestjs/swagger';
import { BaseQueryDto } from '../../common/dto/base-query.dto';
import { Permission } from '@prisma/client';
import { IsBoolean, IsOptional, Matches, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
export class QueryPermissionDto extends BaseQueryDto {
@IsBoolean()
@Transform(({ value }) => value === '1')
@IsOptional()
@ApiProperty({ required: false })
disabled?: boolean;
@Matches(/^(DIRECTORY|MENU|BUTTON)$/, { message: '类型格式错误' })
@IsOptional()
@ApiProperty({
required: false,
enum: ['DIRECTORY', 'MENU', 'BUTTON'],
})
type?: Permission['type'];
@MaxLength(50)
@Matches(/^[a-zA-Z0-9\u4e00-\u9fa5]+$/, { message: '名称格式错误' })
@IsOptional()
@ApiProperty({
required: false,
description: '名称关键字, 不能超过50个字符',
})
keyword?: string;
}
通过继承基础的查询对象类,扩展一些专属于权限列表的查询参数。该类将用作 Swagger API 文档中接口所需参数数据的类型说明。
1.2 定义返回实体类
接下来,我们还需要确定要返回哪些字段。
新建 /src/permission/entitites/permission-list.entity.ts 文件并添加以下内容:
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { PermissionEntity } from './permission.entity';
export class PermissionList extends OmitType(PermissionEntity, [
'redirect',
'hidden',
'cache',
'props',
'component',
'createdAt',
'updatedAt',
] as const) {
@ApiProperty({ description: '创建时间(UTC)' })
createdAt: string;
@ApiProperty({
description: '子菜单',
required: false,
default: [],
type: [PermissionList],
})
children?: PermissionList[];
}
export class PermissionListEntity {
@ApiProperty({ description: '总数' })
total: number;
@ApiProperty({ description: '权限列表', type: [PermissionList] })
list: PermissionList[];
}
OmitType 用来排除不需要的属性。type: [PermissionList], 为什么要这样指定类型,因为 Swagger 库无法直接识别复杂数据类型,需要手动指定。
该实体将用作 Swagger API 文档中接口响应数据的类型说明。
1.3 添加方法
打开 /src/permission/permission.service.ts 文件并添加以下方法:
import { QueryPermissionDto } from './dto/query-permission.dto';
import { generateMenus } from 'src/common/utils';
async findAll(query: QueryPermissionDto) {
const {
keyword,
disabled,
type,
page = 1,
pageSize = 10,
sort = 'desc',
beginTime,
endTime,
} = query;
const permissions = await this.prismaService.permission.findMany({
where: {
deleted: false,
name: {
contains: keyword,
mode: 'insensitive',
},
disabled,
type,
createdAt: {
gte: beginTime,
lte: endTime,
},
},
select: {
id: true,
pid: true,
name: true,
type: true,
permission: true,
icon: true,
path: true,
sort: true,
disabled: true,
createdAt: true,
},
orderBy: [
{
sort: 'desc',
},
{
createdAt: sort,
},
],
});
const offset = (page - 1) * pageSize;
if (permissions.length < offset) {
return { list: [], total: 0 };
}
const permissionTree = generateMenus(
permissions.map((item) => {
return {
...item,
createdAt: item.createdAt.toISOString(),
};
}),
);
if (permissionTree.length < offset) {
return { list: [], total: 0 };
}
const total = permissionTree.length;
const list = permissionTree.slice(offset, offset + pageSize);
return { list, total };
}
以下是用在 Prisma ORM 的查询中的一些特殊条件:
name: 根据关键字模糊查询权限名contains: keyword: 包含指定关键字mode: 'insensitive': 大小写不敏感
createdAt: 根据创建时间范围筛选gte: beginTime: 大于等于开始时间lte: endTime: 小于等于结束时间
orderBy: 指定数据排序条件createdAt: sort: 根据传入参数决定是按创建时间升序还是降序
因为权限列表是一个多级列表,所以我们可以先查询出所有符合条件的项,然后转换为树后再分页。
1.4 添加接口
打开 /src/permission/permission.controller.ts 文件并修改为以下内容:
import { Controller, Get, Query } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiBaseResponse } from 'src/common/decorator/api-base-response.decorator';
import { PermissionListEntity } from './entities/permission-list.entity';
import { QueryPermissionDto } from './dto/query-permission.dto';
@ApiTags('permission')
@ApiBearerAuth()
@Controller('permission')
export class PermissionController {
constructor(private readonly permissionService: PermissionService) {}
@ApiOperation({ summary: '获取权限列表' })
@ApiBaseResponse(PermissionListEntity)
@Get()
findAll(
@Query() queryDto: QueryPermissionDto,
): Promise<PermissionListEntity> {
return this.permissionService.findAll(queryDto);
}
}
@ApiTags('permission') 装饰器用来给整个控制器添加 permission 标签,方便对 Swagger API 文档中的接口进行分组管理。
@ApiBearerAuth() 装饰器用来在 Swagger API 文档中标识需要 Bearer Token 认证的接口,加在控制器上,则该控制器里的所有接口都需要 Bearer Token 认证。
@ApiOperation() 用来为 Swagger 文档中该接口添加说明信息。
@ApiBaseResponse() 是我们之前封装的一个装饰器,是用来定义 API 端点的成功响应并为 Swagger API 文档提供接口响应数据的类型说明。
@Get() 装饰器用来声明一个 GET 类型的接口。
@Query() queryDto: QueryPermissionDto 表示获取接口地址上的 query 参数并指定使用 QueryPermissionDto 类对参数数据进行校验与格式转换。
但现在还无法根据 dto 中的定义自动转换 query 的参数,还需在 main.ts 中开启自动转换。
打开 /src/main.ts 文件并在 new ValidationPipe() 的配置对象中添加一个 transform: true 属性,如下:
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
至此,这个接口就开发完成了,赶紧去试试吧。
2. 获取权限树接口
添加子权限时,需要关联父权限,所以需要先开发获取权限树接口。
2.1 定义 DTO 对象
因为本系统的设计是,按钮级权限不能作为父权限,所以这个接口返回的权限需要过滤掉按钮级别。
但是添加角色时,又是需要按钮级权限的,所以我们需要一个参数来控制该接口是否返回按钮级权限。
新建 /src/permission/query-permission-tree.dto.ts 文件并添加以下内容:
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
export class QueryPermissionTreeDto {
@Transform(({ value }) => value === 'true')
@IsOptional()
@ApiProperty({ required: false, description: '是否包含按钮级' })
containButton?: boolean;
}
将用作校验接口参数与 Swagger API 文档中的接口参数说明。
2.2 定义返回实体类
新建 /src/permission/entities/permission-tree.entity.ts 文件并添加以下内容:
import { ApiProperty, PickType } from '@nestjs/swagger';
import { PermissionEntity } from './permission.entity';
export class PermissionTreeEntity extends PickType(PermissionEntity, [
'id',
'pid',
'name',
'type',
'disabled',
]) {
@ApiProperty({
description: '子权限',
required: false,
default: [],
type: [PermissionTreeEntity],
})
children?: PermissionTreeEntity[];
}
该实体将用作 Swagger API 文档中接口响应数据的类型说明。
2.3 添加方法
只需查询出 permission 表中所有的未删除数据,然后再转换为树列表即可。
打开 /src/permission/permission.service.ts 文件并添加以下方法:
import { Prisma } from '@prisma/client';
async findTree(containButton = false) {
const whereCondition: Prisma.PermissionWhereInput = {
deleted: false,
};
if (!containButton) {
// 排除该类型的权限
whereCondition.type = {
not: 'BUTTON',
};
}
const permissions = await this.prismaService.permission.findMany({
where: whereCondition,
select: { id: true, pid: true, name: true, type: true, disabled: true },
orderBy: { sort: 'desc' },
});
const tree = generateMenus(permissions);
return tree;
}
该方法接收一个 containButton 参数,默认值为 false。当值为 false 时,则从表中过滤掉类型为 BUTTON 的权限,反之则不过滤。
2.4 添加接口
打开 /src/permission/permission.controller.ts 文件并添加以下接口:
@ApiOperation({ summary: '获取权限树' })
@ApiBaseResponse(PermissionTreeEntity, 'array')
@Get('tree')
findTree(
@Query() queryDto: QueryPermissionTreeDto,
): Promise<PermissionTreeEntity[]> {
return this.permissionService.findTree(queryDto.containButton);
}
@ApiBaseResponse() 是我们之前封装的一个装饰器,是用来定义 API 端点的成功响应并为 Swagger API 文档提供接口响应数据的类型说明,具体实现请查看前面的章节。
那么这个接口就开发完成了,打开 Postman 试试吧!
3. 创建权限接口
3.1 定义 DTO 对象
新建 /src/permission/dto/create-permission.dto.ts 文件并添加以下内容:
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Matches,
Max,
MaxLength,
Min,
MinLength,
} from 'class-validator';
export class CreatePermissionDto {
@IsNumber({}, { message: '父权限 ID 类型错误' })
@IsOptional()
@ApiProperty({
description: '父级权限ID',
default: null,
required: false,
})
pid?: number;
@Matches(/^[a-zA-Z\u4e00-\u9fa5]{1,50}$/, { message: '权限名称格式错误' })
@IsNotEmpty({ message: '权限名称不能为空' })
@ApiProperty({ description: '权限名称' })
name: string;
@IsEnum(['DIRECTORY', 'MENU', 'BUTTON'], { message: '权限类型错误' })
@ApiProperty({
description: '权限类型',
enum: ['DIRECTORY', 'MENU', 'BUTTON'],
})
type: 'DIRECTORY' | 'MENU' | 'BUTTON';
@Matches(/^[a-z:]{1,50}$/, { message: '权限标识格式错误' })
@IsOptional()
@ApiProperty({ description: '权限标识', required: false })
permission?: string;
@MaxLength(50, { message: '图标名称长度不能超过50' })
@IsString({ message: '图标名称必须为字符串类型' })
@IsOptional()
@ApiProperty({ description: '图标名称', required: false })
icon?: string;
@MinLength(2, { message: '路径长度不能小于2' })
@MaxLength(50, { message: '路径长度不能超过50' })
@Matches(/^\/?([a-zA-Z]+)(\/[a-zA-Z]+|\/:[a-zA-Z]+)*$/, {
message: '路径格式错误',
})
@IsOptional()
@ApiProperty({ description: '权限路径', required: false })
path?: string;
@MinLength(6, { message: '组件地址长度不能小于6' })
@MaxLength(100, { message: '组件地址长度不能超过100' })
@Matches(/^(\/[a-zA-Z]+[-_]?[a-zA-Z]+)+(.vue|.tsx|.jsx)$/, {
message: '组件地址格式错误',
})
@IsOptional()
@ApiProperty({ description: '组件地址', required: false })
component?: string;
@Max(255, { message: '最大255' })
@Min(0, { message: '最小0' })
@IsNumber({}, { message: '排序权重必须为数字类型' })
@IsOptional()
@ApiProperty({
description: '排序权重',
required: false,
default: 0,
})
sort?: number;
@MinLength(2, { message: '重定向地址长度不能小于2' })
@MaxLength(50, { message: '重定向地址长度不能超过50' })
@Matches(/^(\/?[a-zA-Z0-9]+)+$/, { message: '重定向地址格式错误' })
@IsOptional()
@ApiProperty({
description: '重定向地址',
required: false,
})
redirect?: string;
@IsBoolean({ message: '禁用状态必须为布尔值' })
@IsOptional()
@ApiProperty({
description: '禁用状态',
required: false,
default: false,
})
disabled?: boolean;
@IsBoolean({ message: '隐藏状态必须为布尔值' })
@IsOptional()
@ApiProperty({
description: '隐藏状态',
required: false,
default: false,
})
hidden?: boolean;
@IsBoolean({ message: '是否缓存必须为布尔值' })
@IsOptional()
@ApiProperty({
description: '是否缓存',
required: false,
default: false,
})
cache?: boolean;
@IsBoolean({ message: 'vue-router 的 props 属性必须为布尔值' })
@IsOptional()
@ApiProperty({
description: 'vue-router 的 props 属性',
required: false,
default: false,
})
props?: boolean;
}
该 DTO 类将用来校验创建权限接口接收参数的格式是否正确,并生成 Swagger API 文档。
3.2 添加方法
打开 /src/permission/permission.service.ts 文件并添加以下方法:
async create(createDto: CreatePermissionDto) {
if (createDto.type !== 'BUTTON' && !createDto.path) {
throw new BadRequestException('目录/菜单的路径不能为空');
}
if (createDto.type === 'MENU' && !createDto.component) {
throw new BadRequestException('菜单的组件地址不能为空');
}
if (createDto.type === 'BUTTON' && !createDto.permission) {
throw new BadRequestException('按钮的权限标识不能为空');
}
const permission = await this.prismaService.permission.findFirst({
where: {
OR: [
{
id: createDto.pid,
},
{
name: createDto.name,
},
{
permission: createDto.permission,
},
],
deleted: false,
},
select: {
type: true,
name: true,
permission: true,
},
});
if (!permission && createDto.pid) {
throw new BadRequestException('父权限不存在');
}
if (permission) {
if (permission.name === createDto.name) {
throw new BadRequestException('权限名称已存在');
}
if (
createDto.permission &&
permission.permission === createDto.permission
) {
throw new BadRequestException('权限标识已存在');
}
if (permission.type === 'BUTTON') {
throw new BadRequestException('按钮权限不能添加子权限');
}
}
await this.prismaService.permission.create({
data: createDto,
});
}
在这个创建方法中,我们对数据进行了一些必要的校验。findFirst 方法用来查询第一个符合条件的数据。
3.3 添加接口
打开 /src/permission/permission.controller.ts 文件并添加以下接口:
import { CreatePermissionDto } from './dto/create-permission.dto';
import { Authority } from 'src/common/decorator/authority.decorator';
@ApiOperation({ summary: '创建权限' })
@ApiBaseResponse()
@Authority('system:menu:add')
@Post()
create(@Body() createPermissionDto: CreatePermissionDto) {
return this.permissionService.create(createPermissionDto);
}
这里引入了一个特殊的装饰器 @Authority 用来做接口的权限控制,只有用户拥有 system:menu:add 权限时才能创建权限。该装饰器的实现见前面的章节。
@Body() createPermissionDto: CreatePermissionDto 表示获取接口请求体中的参数数据并使用
CreatePermissionDto 类对其进行校验。
4. 获取权限详情接口
4.1 添加方法
打开 /src/permission/permission.service.ts 文件并添加以下方法:
findOne(id: number) {
return this.prismaService.permission.findUnique({
where: {
id,
deleted: false,
},
select: {
pid: true,
name: true,
type: true,
path: true,
permission: true,
icon: true,
cache: true,
props: true,
hidden: true,
component: true,
disabled: true,
redirect: true,
sort: true,
},
});
}
该方法接收一个 ID 参数,并根据 ID 从数据库中查询符合条件的数据。
4.2 添加接口
打开 /src/permission/permission.controller.ts 文件并添加以下接口:
@ApiOperation({ summary: '获取单个权限详情' })
@ApiBaseResponse(PermissionEntity)
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.permissionService.findOne(id);
}
@Get(':id') 表示定义一个 Get 请求的动态路由,:id 是一个动态路由参数。
@Param(id) 用于获取 URL 中的 id 值,ParseIntPipe 用于将字符串转换为数字。
5. 编辑权限接口
5.1 定义 DTO 对象
新建 /src/permission/dto/update-permission.dto.ts 文件并添加以下内容:
import { PartialType } from '@nestjs/swagger';
import { CreatePermissionDto } from './create-permission.dto';
export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {}
只需将 CreatePermissionDto 的属性都改为可选即可。
5.2 添加方法
首先我们先要引入 redis 模块,后续要用到。
打开 /src/permission/permission.module.ts 文件添加以下代码:
import { Module } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { PermissionController } from './permission.controller';
import { RedisModule } from 'src/redis/redis.module'; // 新增
@Module({
imports: [RedisModule], // 新增
controllers: [PermissionController],
providers: [PermissionService],
exports: [PermissionService],
})
export class PermissionModule {}
然后在 /src/permission/permission.service.ts 文件中添加以下内容:
...
import { UpdatePermissionDto } from './dto/update-permission.dto';
import { RedisService } from 'src/redis/redis.service';
@Injectable()
export class PermissionService {
constructor(
private readonly prismaService: PrismaService,
private readonly redisService: RedisService,
) {}
...
async update(id: number, updateDto: UpdatePermissionDto) {
const permission = await this.prismaService.permission.findUnique({
where: { id, deleted: false },
include: {
permissionInRole: { // 关联权限角色表查询
where: {
roles: { // 只查询未删除的角色关联的权限
deleted: false,
},
},
include: {
roles: {
include: {
roleInUser: { // 关联角色用户表查询用户ID
select: {
userId: true,
},
},
},
},
},
},
},
});
if (!permission) {
throw new BadRequestException('权限不存在');
}
if (permission.permissionInRole.length > 0) {
const sensitiveFields = ['type', 'permission', 'pid'];
const hasChangeSensitiveField = sensitiveFields.some(
(field) => updateDto[field] && updateDto[field] !== permission[field],
);
if (hasChangeSensitiveField) {
throw new BadRequestException(
'该权限已被角色使用,不能修改类型、权限标识、父权限',
);
}
}
const safeFields = [
'name',
'icon',
'sort',
'path',
'component',
'hidden',
'disabled',
'cache',
'redirect',
'props',
];
const updateData: UpdatePermissionDto = Object.keys(updateDto)
.filter((key) => safeFields.includes(key))
.reduce(
(acc, key) => ({
...acc,
[key]: updateDto[key],
}),
{},
);
// 如果禁用了权限,删除用户的权限缓存
if (updateData.disabled === false) {
const userIds = permission.permissionInRole.flatMap((item) => {
return item.roles.roleInUser.map((item) => item.userId);
});
userIds.forEach((userId) => {
this.redisService.delUserPermission(userId);
});
}
await this.prismaService.permission.update({
where: { id },
data: updateData,
});
}
}
在该方法中,首先我们查询了该权限是否存在,如果存在,则将关联的用户与角色信息也查询出来。如果关联了未删除的角色,则不允许修改一些敏感信息。如果禁用了该权限,则清空相应用户的权限缓存。
你可能会说,为什么启用权限不用清空用户的权限缓存呢?
这是因为,我们在权限守卫中,如果缓存里没有这个权限,会去数据库里再查询一次用户的完整权限。
5.3 添加接口
打开 /src/permission/permission.controller.ts 文件并新增以下接口:
@ApiOperation({ summary: '更新权限' })
@ApiBaseResponse()
@Authority('system:menu:edit')
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updatePermissionDto: UpdatePermissionDto,
) {
return this.permissionService.update(id, updatePermissionDto);
}
Patch 从 @nestjs/common 包中导入。
@Patch(':id') 表示定义一个 Patch 请求的动态路由,:id 是一个动态路由参数,格式例如:/permission/10,id 为 10。
6. 删除权限接口
6.1 删除单个权限接口
首先,在 /src/permission/permission.service.ts 中添加以下方法:
async remove(id: number) {
const permission = await this.prismaService.permission.findUnique({
where: { id, deleted: false },
include: {
permissionInRole: { // 关联权限角色表查询
where: {
roles: { // 关联角色表查询
deleted: false,
},
},
},
children: { // 查询该权限的子权限
where: {
deleted: false,
},
},
},
});
if (!permission) {
throw new BadRequestException('权限不存在');
}
if (permission.permissionInRole.length > 0) {
throw new BadRequestException('该权限已被角色使用,不能删除');
}
if (permission.children.length > 0) {
throw new BadRequestException('该权限下存在子权限,不能删除');
}
await this.prismaService.permission.update({
where: { id },
data: {
deleted: true,
},
});
}
然后,在 /src/permission/permission.controller.ts 中添加以下代码:
@ApiOperation({ summary: '删除单个权限' })
@ApiBaseResponse()
@Authority('system:menu:del')
@Patch(':id/delete')
remove(@Param('id', ParseIntPipe) id: number) {
return this.permissionService.remove(id);
}
因为这里只是软删除,仅修改 deleted 字段的状态而已,所以使用 Patch 更合适。
6.2 批量删除权限
首先新建 /src/permission/dto/remove-permission.dto.ts 文件并添加以下代码:
import { ArrayNotEmpty, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RemovePermissionDto {
@IsNumber({}, { message: 'id 必须为数字', each: true })
@ArrayNotEmpty({ message: 'id 列表不能为空' })
@ApiProperty({ description: 'id 列表', type: [Number] })
ids: number[];
}
然后在 /src/permission/permission.service.ts 中添加以下代码:
async batchRemove(ids: number[]) {
const permissions = await this.prismaService.permission.findMany({
where: {
id: {
in: ids,
},
deleted: false,
},
select: {
id: true,
permissionInRole: {
where: {
roles: {
deleted: false,
},
},
},
children: {
where: {
deleted: false,
},
},
},
});
if (!permissions?.length) {
throw new BadRequestException('权限不存在');
}
const canRemovePermissions = permissions.filter((item) => {
return item.permissionInRole.length === 0 && item.children.length === 0;
});
if (canRemovePermissions.length < ids.length) {
throw new BadRequestException(
'部分权限已被角色使用或存在子权限,不能删除',
);
}
await this.prismaService.permission.updateMany({
where: {
id: {
in: ids,
},
},
data: {
deleted: true,
},
});
}
最后在 /src/permission/permission.controller.ts 中添加以下代码:
@ApiOperation({ summary: '批量删除权限' })
@ApiBaseResponse()
@Authority('system:menu:del')
@Patch('batch/delete')
batchRemove(@Body() data: RemovePermissionDto) {
return this.permissionService.batchRemove(data.ids);
}
注意:需要将该接口添加在“删除单个权限”接口之前,不然将无法匹配。
这里我没有专门创建一个批量删除的权限,而是使用与删除权限一样的标识,你可以根据实际情况考虑。
相关单元测试代码请查看代码仓库。
下一章节见~