Skip to content

管道与验证

  • 管道:后端是服务用户的,对于用户提交的数据,对于用户传递的五花八门的数据,我们需要借助管道来进行限定,限定用户传递某种内容。还有一种情况,当我们通过网络请求的时候,传递过来的数字1是一个字符串类型,但是ts是严格类型要求的,我们要求这个数字必须是数值类型,我们这时可以使用管道,将这个传递过来的内容的类型进行转化,总之,管道是对进入的数据进行处理的
  • 验证:对于用户提交的数据,我们需要进行验证,如内容长度太长,或者内容为空,这些情况是不会通过验证的,也就存储不到数据库中

我们可以使用Apifox在线工具来测试我们的接口,我们先创建一个项目,在项目中配置测试环境,本地开发测试环境可以设置为http://localhost:3000,新建一个接口,选择测试环境,输入路径进行接口的请求,其中/表示根路径

image-20250226194124681

请求得到的内容和在网页中展示的内容是一致的

对于项目中的根控制器app.controller.ts

ts
import { Controller, Get, Param } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get(':id')
  getHello(@Param('id') id: number) {
    return id;
  }
}

对于@Get()装饰器,是可以添加参数的,如@Get(':id'),与vue的写法类似

对于getHello()具体函数,可以使用@Param()装饰器来接收这个id,同时将这个值赋值给为数值变量类型的id

最后将id进行返回

在接口测试器中发送http://localhost:3000/1,就可以接收到这个参数1,同时在网页中显示

但是如果我们对这个返回的参数进行其类型的打印:console.log(typeof id),我们会发现这个id的类型是字符串类型,如果我们使用这个id去匹配数据库表中的对应为数值类型的字段,就会显示类型报错:

ts
import { Controller, Get, Param } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { AppService } from './app.service';

@Controller()
export class AppController {
  prisma: PrismaClient;
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient();
  }

  @Get(':id')
  getHello(@Param('id') id: number) {
    return this.prisma.article.findUnique({
      where: {
        id: id,
      }
    });
  }
}

我们尝试进行查询,在测试环境中发送/1,后端就会报错:Invalid value for argument id: invalid digit found in string. Expected big integer String.,说明:传入的是一个字符串类型,但是匹配需要的是数值类型,因此,我们需要使用管道来解决这个问题

转换数值管道

创建管道:nest g pi new --no-spec,执行后就会在src/new文件中创建管道文件new.pipe.ts

在根控制器app.controller.ts中使用管道:

ts
import { Controller, Get, Param } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { AppService } from './app.service';
import { NewPipe } from './new/new.pipe';

@Controller()
export class AppController {
  prisma: PrismaClient;
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient();
  }

  @Get(':id')
  getHello(@Param('id', NewPipe) id: number) {
    return this.prisma.article.findUnique({
      where: {
        id: +id,
      }
    });
  }
}

@Param('id', NewPipe):当数据进来的时候,就会使用跟在后面的NewPipe管道,使用管道先进行数据的处理,在管道中我们需要进行拦截操作

编辑new.pipe.ts文件,使管道对传入的内容进行类型的转换:如果要求的类型是数值类型,就将其转换为数值类型,如果要求的类型不是数值类型,就不做任何处理,直接将值进行返回:

ts
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class NewPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return metadata.metatype == Number ? +value : value;
  }
}

管道中有两个参数:

  • value:用户传入的参数值

  • metadata:原数据,包含了数据的额外的信息

    { metatype: [Function: Number], type: 'param', data: 'id' }

    • metatype:数据类型,构造函数是一个Number的数据类型
    • type:请求的类型,param表示地址栏参数
    • data:接收的参数,接收id的值

这样就可以解决传入类型与数据库匹配字段类型不同的问题了,在接口测试工具中输入/1,就可以返回文章表中id为1的这一条数据的全部内容了

对于一些高频使用的管道,系统都有提供,如:将数据类型转化为数值类型的管道ParseIntPipe

ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { AppService } from './app.service';

@Controller()
export class AppController {
  prisma: PrismaClient;
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient();
  }

  @Get(':id')
  getHello(@Param('id', ParseIntPipe) id: number) {
    return this.prisma.article.findUnique({
      where: {
        id: id,
      }
    });
  }
}

效果和我们自己写的NewPipe管道相同


管道的定义方式

管道定义方式一共有以下几种:

  • 控制器

    将管道注册在控制器层面,这样就会影响控制器内部所有的方法:

    ts
    import { Controller, Get, Param } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';
    import { AppService } from './app.service';
    import { NewPipe } from './new/new.pipe';
    
    @Controller()
    @UsePipes(NewPipe)
    export class AppController {
      prisma: PrismaClient;
      constructor(private readonly appService: AppService) {
        this.prisma = new PrismaClient();
      }
    
      @Get(':id')
      getHello(@Param('id') id: number) {
        return this.prisma.article.findUnique({
          where: {
            id: +id,
          }
        });
      }
    }

    结果还是正常的,可以根据输入的参数,获取对数据库中对应匹配的值

  • 控制器方法

    控制器方法层面,我们可以进行以下方式的注册:

    ts
    import { Controller, Get, Param } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';
    import { AppService } from './app.service';
    import { NewPipe } from './new/new.pipe';
    
    @Controller()
    export class AppController {
      prisma: PrismaClient;
      constructor(private readonly appService: AppService) {
        this.prisma = new PrismaClient();
      }
    
      @Get(':id')
      @UsePipes(NewPipe)
      getHello(@Param('id') id: number) {
        return this.prisma.article.findUnique({
          where: {
            id: +id,
          }
        });
      }
    }

    结果还是正常的,可以根据输入的参数,获取对数据库中对应匹配的值

  • 方法参数

    方法参数层面进行管道的注册,将管道注册在对应的参数后面

    ts
    import { Controller, Get, Param } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';
    import { AppService } from './app.service';
    import { NewPipe } from './new/new.pipe';
    
    @Controller()
    export class AppController {
      prisma: PrismaClient;
      constructor(private readonly appService: AppService) {
        this.prisma = new PrismaClient();
      }
    
      @Get(':id')
      getHello(@Param('id', NewPipe) id: number) {
        return this.prisma.article.findUnique({
          where: {
            id: +id,
          }
        });
      }
    }

    结果还是正常的,可以根据输入的参数,获取对数据库中对应匹配的值

    一般情况对于参数,我们管道注册在参数后面即可:@Param('id', NewPipe)

  • 模块

    我们也可以将管道放到模块中进行注册,我们在根模块app.module.ts中进行管道的注册:

    ts
    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { APP_PIPE } from '@nestjs/core';
    import { NewPipe } from './new/new.pipe';
    
    @Module({
      imports: [],
      controllers: [AppController],
      providers: [AppService, 
                 {  // 定义管道
                     provide: APP_PIPE,// 起一个NestJs要使用的管道名称,内部要APP_PIPE名称的提供者
                     useClass: NewPipe
                 }],
    })
    export class AppModule {}

    可以在管道模块中进行@Inject依赖的注入

  • 全局管道

    main.ts文件中进行全局管道的注册:

    ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { NewPipe } from './new/new.pipe';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.useGlobalPipes(new NewPipe());    // 全局进行管道的注册
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();

    全局注册后,我们就可以在任何地方去使用这个管道(不需要任何导入),但是在全局文件中进行注册是不能进行后续的依赖注入的


系统提供的常用管道

有些管道的逻辑在开发中是非常常用的,因此,系统将这些管道分装好,直接提供给我们进行使用

系统提供的常用管道有:

  • ParseIntPipe:将参数的数据类型转化为数值类型

    内置管道ParseIntPipe是用于转换 number 类型中的“整数”的。 如果说想转的值是浮点型或者NaN,像这些值虽然也属于 number 类型,但由于它们不属于“整数”,所以 ParseIntPipe会抛出异常

  • ParseFloatPipe:将参数的数据类型转化为浮点数类型

  • ParseBoolPipe:将参数的数据类型转化为布尔类型

  • ParseArrayPipe:将参数的数据类型转化为数组类型

  • ParseUUIDPipe:将参数的数据类型转化为UUID类型

  • ParseEnumPipe:将参数的数据类型转化为枚举类型

  • ParseFilePipe:将参数的数据类型转化为文件类型

  • ParseDatePipe:将参数的数据类型转化为日期类型

  • DefaultValuePipe:默认值管道,如果没有传递参数,可以使用默认值管道,使用默认值

    ts
    @Get()
    getHello(@Param('id', new DefaultValuePipe(1)) id: number) {
        return this.prisma.article.findUnique({
          where: {
            id: +id,
          }
        });
    }

    id没有进行传参的时候,就使用默认值管道中的默认值1

  • ValidationPipe:校验时必须要在Query,Body等装饰器上加上ValidationPipe管道才能对请求数据进行验证


验证

对于用户提交上来的数据,我们需要使用管道进行对其内容的验证,保证其内容是符合规定的,对于不符合规定的数据,我们要对其进行拦截,使其不能往数据库中进行写入

根控制器app.controller.ts文件:

ts
import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('store')
  add(@Body() dto: Record<string, any>) {
    console.log(dto);
    return dto;
  }
}

使用装饰器@Body()来接收数据dto

@Post('store')@Post()装饰器内部的字符串是地址,名称可以任意的取

在接口测试工具中,可以添加一个POST接口,进行数据的发送,选择测试环境,在地址中输入/store

选择Body的数据方式:

image-20250227165655745

我们还可以在命令终端以命令行的形式进行数据的发送:

curl -H "Content-Type:application/json" -X POST -d '{"title":"NestJs的使用","content":"管道的验证操作"}' http://localhost:3000/store

  • -H表示指定头信息
  • "Content-Type:application/json":告诉后端,我们发送的是一个JSON数据
  • -X:表示要发送的请求方式,指定的是POST请求
  • -d:表示要传递过去的数据,后面接传递过去的数据,数据要使用JSON格式,键和值都要用引号包裹

发送成功后,在后端就可以看到数据:

image-20250227164241890

上述情况下,我们如果发送的数据没有内容,也是可以进行接收的,这个在实际的项目中就不合理,我们需要设计管道来进行验证,如果title或者content的内容为空时,我们会抛出异常

创建一个管道nest g pi request --no-spec,用来对用户提交的内容进行验证,管道验证的内容为:

ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class RequestPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value.title) {
      throw new BadRequestException('标题不能为空');
    }
    if (!value.content) {
      throw new BadRequestException('内容不能为空');
    }
    return value;
  }
}

value:接收的对象数据

metadata:原数据包含如下的内容

{ metatype: [Function: Object], type: 'body', data: undefined }

  • metatype:数据类型,构造函数是一个Object的数据类型,定义的是一个对象
  • type:接收的类型,POST数据,类型是body
  • data:接收的参数,对于对象,没有指定某一个KEY,所以返回的是空

BadRequestException:错误请求异常

修改后的根控制器app.controller.ts文件,引入了管道进行验证:

ts
import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
import { RequestPipe } from './request/request.pipe';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('store')
  add(@Body(RequestPipe) dto: Record<string, any>) {
    console.log(dto);
    return dto;
  }
}

当发送内容为空时,会抛出异常:

image-20250227171109266

但是这种验证方式的复用性比较差,一个网站中提交的数据不单单只是文章的数据,还会有其他需要验证的数据,这时就要重新定义另外的验证管道进行验证,这样是不方便的

使用DTO进行验证

DTO是数据传输对象,对前端传递过来的数据进行处理,使用DTO进行验证有很好的复用性

我们在src目录中创建dto文件夹,在文件夹中创建create.article.dto.ts文件,用来实现对传递过来的文章数据进行验证处理:

ts
import { IsNotEmpty, Length } from 'class-validator';

export default class CreateArticleDto {
    @IsNotEmpty({ message: '标题不能为空' })
    @Length(1, 10, { message: '标题长度在1到10之间' })
    title: string;
    @IsNotEmpty({ message: '内容不能为空' })
    content: string;
}

这里使用的是类class,而没有使用接口的方式进行定义。因为在ts文件最终被编译成js的时候,ts这些声明都是会被忽略掉的,我们这里使用类进行定义,保证被编译成js时,这个验证过程还是有效的

使用@IsNotEmpty()装饰器对数据进行验证,这个装饰器需要使用class-validator包进行导入,@IsNotEmpty()装饰器的验证规则是不能为空,字段内容的属性都会经过这个装饰器进行验证处理,如果数据,没有值,就会抛出异常,我们可以在@IsNotEmpty()中定义错误消息:@IsNotEmpty({message:'内容不能为空'})

@Length()装饰器是对内容长度的验证,可以将多条验证规矩进行一起使用,但都是管道最近的一个字段

具体的详细规则可以去class-validator包的文档中进行查看

如果有其他的输入数据进行验证,我们可以创建不同的验证文件

之后,在控制器文件app.controller.ts中进行如下的修改:

ts
import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
import { RequestPipe } from './request/request.pipe';
import CreateArticleDto from './dto/create.article.dto';
import { PrismaClient } from '@prisma/client';

@Controller()
export class AppController {
  prisma: PrismaClient;
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient();
  }

  @Post('store')
  async add(@Body(RequestPipe) dto: CreateArticleDto) {
   	// 往数据库中添加数据
    await this.prisma.article.create({
        data: {
            title: dto.title,
            content: dto.content,
            thumb: "https://www.baidu.com",   // 由于数据表设计,加上额外的两个字段内容
            categoryId: 1,
        },
    });
  }
}

我们要在控制器中要告知dto这个数据是CreateArticleDto类型的

在管道文件request.pipe.ts中:

ts
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

@Injectable()
export class RequestPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const object = metadata.metatype ? plainToInstance(metadata.metatype, value) : value;
    const errors = await validate(object);
    // 深入读取错误信息,自定义错误消息内容
    if (errors.length) {
      const message = errors.map((error) => ({
        name: error.property,
        message: Object.values(error.constraints ? error.constraints : {}).map((v) => v),
      }));
      throw new HttpException(message, HttpStatus.BAD_REQUEST);
    }
    return value;
  }
}

metadata:原数据包含内容的数据类型就变成了class CreateArticleDt

{ metatype: [class CreateArticleDto], type: 'body', data: undefined }

但是value是没有类型的,只会显示Object,因此,对于这个数据要按照CreateArticleDt类去实例化出对象,我们需要使用工具plainToInstance(),将数据value通过metadata.metatype构造函数,去实例化出一个对象,这时object的类型就是类CreateArticleDt,其内容就是value的内容,数据会依次分配给新生成的对象

我们需要使用validate()方法让装饰器@IsNotEmpty()执行,validate()返回的是Promise,因此需要使用异步去加载

image-20250227214512835

如果输入符合添加的内容,就会往数据库中进行数据的添加:

image-20250227222601454

使用系统验证管道

上节是我们自己编写的管道进行验证,当用户没有按照规范提交消息的时候,会抛出异常提示,验证和抛出异常的逻辑,我们可以不用自己进行编写,系统提供了系统管道验证供我们进行直接使用,在定义完管道后,我们可以进行直接的调用

在入口文件main.ts中进行定义:

ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());     // 定义全局验证管道
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

ValidationPipe就是一个js的普通类,我们可以对其进行继承,继承之后加入一些自定义的内容,我们使用命令来创建类:nest g cl validate --no-spec,创建完后,就在src/validate目录中出现validate.ts文件

validate.ts文件中,对系统提供的ValidationPipe验证管道类进行继承:

ts
import { ValidationError, ValidationPipe } from "@nestjs/common";

export class Validate extends ValidationPipe {
    protected mapChildrenToValidationErrors(
        error: ValidationError, 
        parentPath?: string
    ): ValidationError[]{
        // 对错误的验证消息进行处理
        const errors = super.mapChildrenToValidationErrors(error, parentPath);
        errors.map(error => {
            for (const key in error.constraints) {
                error.constraints[key] = error.property + '-' + error.constraints[key];
            }
        })
        return errors;
    }
}

我们可以点入ValidationPipe类中查看该类的所有方法:

ts
export declare class ValidationPipe implements PipeTransform<any> {
 protected isTransformEnabled: boolean;
 protected isDetailedOutputDisabled?: boolean;
 protected validatorOptions: ValidatorOptions;
 protected transformOptions: ClassTransformOptions | undefined;
 protected errorHttpStatusCode: ErrorHttpStatusCode;
 protected expectedType: Type<any> | undefined;
 protected exceptionFactory: (errors: ValidationError[]) => any;
 protected validateCustomDecorators: boolean;
 constructor(options?: ValidationPipeOptions);
 protected loadValidator(validatorPackage?: ValidatorPackage): ValidatorPackage;
 protected loadTransformer(transformerPackage?: TransformerPackage): TransformerPackage;
 transform(value: any, metadata: ArgumentMetadata): Promise<any>;
 createExceptionFactory(): (validationErrors?: ValidationError[]) => unknown;
 protected toValidate(metadata: ArgumentMetadata): boolean;
 protected transformPrimitive(value: any, metadata: ArgumentMetadata): any;
 protected toEmptyIfNil<T = any, R = T>(value: T, metatype: Type<unknown> | object): R | object | string;
 protected stripProtoKeys(value: any): void;
 protected isPrimitive(value: unknown): boolean;
 protected validate(object: object, validatorOptions?: ValidatorOptions): Promise<ValidationError[]> | ValidationError[];
 protected flattenValidationErrors(validationErrors: ValidationError[]): string[];
 protected mapChildrenToValidationErrors(error: ValidationError, parentPath?: string): ValidationError[];
 protected prependConstraintsWithParentProp(parentPath: string, error: ValidationError): ValidationError;
}

我们可以使用mapChildrenToValidationErrors方法来处理我们的错误消息

对于全局定义的管道,我们也不需要在根控制器文件中进行引入,会自动生效的,根控制器文件:

ts
import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
import CreateArticleDto from './dto/create.article.dto';
import { PrismaClient } from '@prisma/client';

@Controller()
export class AppController {
  prisma: PrismaClient;
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient();
  }

  @Post('store')
  async add(@Body() dto: CreateArticleDto) {
    await this.prisma.article.create({
        data: {
            title: dto.title,
            content: dto.content,
            thumb: "https://www.baidu.com",
            categoryId: 1,
        },
    });
  }
}

当我们没有按照要求发送内容是,会显示我们自定义的错误消息内容:

image-20250301122515353

使用过滤器处理验证异常

使用管道的时候,异常都会被系统捕获到,捕获到后会处理这个异常,这个异常可以通过过滤器来进行控制

创建过滤器:nest g f validate-exception --no-spec

过滤器的绑定方式和管道类似,下面展示在入口文件main.ts中进行全局绑定:

ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Validate } from './validate/validate';
import { ValidateExceptionFilter } from './validate-exception/validate-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new Validate());     // 定义全局验证管道
  app.useGlobalFilters(new ValidateExceptionFilter);   // 定义全局过滤器
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

在过滤器文件中validate-exception.filter.ts可以对我们的异常进行处理:

ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch()
export class ValidateExceptionFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
      console.log(123)
  }
}

当我们发生异常的时候,就会被过滤器捕获,如果用户输入不符合规范,就会在终端中显示123

一般,我们会在过滤器中进行异常的判断,对特定的异常进行处理:

ts
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';

@Catch()
export class ValidateExceptionFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
      // 接收一下异常,获取http中的上下文
      const ctx = host.switchToHttp();
      // 获取一下响应对象
      const response = ctx.getResponse();
      // 对表单验证异常进行判断,异常来源于请求错误,格式化响应错误
      if (exception instanceof BadRequestException) {
          const responseObject = exception.getResponse() as any;
          // 返回的状态码是422
          return response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
              // 设置返回的状态码和消息
              code: HttpStatus.UNPROCESSABLE_ENTITY,
              message: responseObject.message.map((error) => {
                  const info = error.split('-')
                  return {field:info[0],message:info[1]}
              })
          })
      }
      // 如果不是表单异常,就响应以下的内容,返回的状态码是400
      response.status(400).json({})
  }
}

我们要保证异常有响应的内容,否则就会被阻塞

输入不规范的内容,会显示:

image-20250301133518921

有了错误的消息,前端就会拿这个错误的消息进行验证

自定义验证规则

不管系统为我们提供多少的验证规则,在特殊的情况下都可能会出现不够用的情况,因此我们需要自定义验证规则来满足我们的需求

密码比对验证规则

用户在登录的时候,我们后端要对用户两次输入的密码进行比对验证,我们需要自定义密码比对验证规则

自定义类来进行验证
  • 对于用户User表解构:

    ts
    model User {
      id        Int      @id @default(autoincrement()) @db.UnsignedInt
      email     String   @db.Char(50)
      password  String
      name      String?
      avatar    String?
      github    String?
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
  • 创建一个模块和控制器:nest g mo auth --no-spec nest g co auth --no-spec

    在控制器auth.controller.ts中完成注册,同时引入dto验证:

    ts
    import { Body, Controller, Post } from '@nestjs/common';
    import RegisterDto from './dto/register.dto';
    
    @Controller('auth')
    export class AuthController {
        @Post('register')
        register(@Body() dto: RegisterDto) {
            console.log(dto);
            return dto;
        }
    }
  • 创建一个dto文件进行验证:在src/auth/dto/register.dto.tsregister.dto.ts文件的内容为:

    ts
    import { IsNotEmpty, Length, Validate } from 'class-validator';
    import { IsConfirmed } from 'src/rules/is-confirmed.rule';
    
    export default class RegisterDto {
        @IsNotEmpty({ message: '用户名不能为空' })
        name: string;
        @IsNotEmpty({ message: '密码不能为空' })
        @Validate(IsConfirmed, { message: '确认密码输入错误' })
        password: string;
    }

    @IsNotEmpty是直接使用系统提供的规则进行验证,使用装饰器进行验证

    @Validate是使用类来进行验证

  • 创建一个密码确认验证规则:创建验证比对文件src/rules/is-confirmed.rule.ts is-confirmed.rule.ts的内容为:

    ts
    import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
    
    @ValidatorConstraint()
    export class IsConfirmed implements ValidatorConstraintInterface {
        // 进行密码比对
        async validate(value: string, args: ValidationArguments) {
            // 返回假表示验证失败
            return value === args.object[args.property + '_confirmed'];
        }
        // 默认消息:比对失败的提示消息
        defaultMessage(args: ValidationArguments) {
            return '比对失败';
        }
    }
    • value:表示传入的内容值,在dto文件中@Validate(IsConfirmed)下面定义的数据内容

    • args:表示原数据

      image-20250301142329630

      • property:表示验证哪个字段
      • object:表示整个表单提交的数据
      • value:验证表单字段的具体值

如果发送的两个密码不一致,就会显示确认密码输入错误:

image-20250301143327596

如果两次密码一致,数据就会被成功发送

自定义装饰器进行验证

在之前的基础上加上一个规则:如果数据库中已经有了当前输入name字段内容一致的数据,那么不能进行注册,使用自定义装饰器进行验证:

  • src/rules文件夹中添加自定义装饰器is-not-exists.rule.ts,具体内容为:

    ts
    import { PrismaClient } from "@prisma/client";
    import { registerDecorator, ValidationArguments, ValidationOptions } from "class-validator";
    
    // 装饰器执行的函数,函数可以进行参数的设置,从外界中传入参数
    export function IsNotExistsRule(
    	// table表示定义具体操作数据中的哪张表
        table: string,
        validationOptions?: ValidationOptions,
    ) { 
        return function (object: Record<string, any>, propertyName: string) {
            registerDecorator({
                name: "IsNotExistsRule",
                target: object.constructor,
                propertyName: propertyName,
                constraints: [table],
                options: validationOptions,
                validator: {
                    async validate(value: string, args: ValidationArguments) {
                        const prisma = new PrismaClient();
                        const user = await prisma[table].findFirst({
                            where: {
                                // propertyName变量的属性名是name
                                [propertyName]: args.value,
                            },
                        });
                        // 返回假表示验证失败
                        return !Boolean(user);
                    },
                },
            })
        }
    }

    参数:valueargs一个表示具体值,一个表示原数据,和类中返回的参数差不多

  • dto文件中直接使用自定义的装饰器:

    ts
    import { IsNotEmpty, Length, Validate } from 'class-validator';
    import { IsConfirmed } from 'src/rules/is-confirmed.rule';
    import { IsNotExistsRule } from 'src/rules/is-not-exists.rule';
    
    export default class RegisterDto {
        @IsNotEmpty({ message: '用户名不能为空' })
        @IsNotExistsRule('user', { message: '用户已经存在' })   // 第一个参数表示验证哪张表
        name: string;
        @IsNotEmpty({ message: '密码不能为空' })
        @Validate(IsConfirmed, { message: '确认密码输入错误' })
        password: string;
    }

如果输入的name的内容在数据库中已经存在,就会提示用户已经存在:

image-20250301151243371

Released under the MIT License.