Skip to content

DTO 参数校验和查询结果过滤

以用户为例:插入修改时校验接口参数、查询时过滤需要隐藏字段

安装插件

shell
pnpm install class-validator class-transformer

DTO 参数校验

DTO:数据传输对象 :::tip 特点:全局管道校验所有 POST、PUT 接口的入参,所以每一个接口都需要定义 DTO,否则会被校验拦截 :::

1、注册全局管道

ts
// src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { ClassSerializerInterceptor } from '@nestjs/common/serializer';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 全局管道
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动删除非白名单属性
      forbidNonWhitelisted: false, // 如果有非白名单属性,true 抛出错误,false 不抛出错误
      transform: true, // 自动类型转换,如字符串数字转 number
      // transformOptions: {
      //   enableImplicitConversion: true, // 隐式类型转换(如 '18' 转 18),此属性慎用:空字符会转为 0 会意外跳过为空验证
      // },
      disableErrorMessages: process.env.NODE_ENV === 'production', // 生产环境可关闭错误详情
    })
  );

  app.setGlobalPrefix('api/v1');
  await app.listen(3000);
}
bootstrap();

2、定义 DTO

  1. 创建用户接口的 DTO
ts
// src/user/dto/create-user.dto.ts
// src/user/dto/create-user.dto.ts
import {
  IsString,
  IsEmail,
  IsNotEmpty,
  MinLength,
  IsOptional,
  IsEnum,
  Matches,
} from 'class-validator';
import { UserStatus, UserRole } from '../../common/enums/user.enum';

export class CreateUserDto {
  /**
   * 用户名
   * @example "zhangsan"
   */
  @IsString({ message: '用户名必须为字符串' })
  @IsNotEmpty({ message: '用户名不能为空' })
  username: string;

  /**
   * 密码(最小6位,包含字母+数字)
   * @example "Zhang123"
   */
  @IsString({ message: '密码必须为字符串' })
  @IsNotEmpty({ message: '密码不能为空' })
  @MinLength(6, { message: '密码长度不能少于6位' })
  @Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, {
    message: '密码必须包含字母和数字',
  })
  password: string;

  /**
   * 邮箱
   * @example "zhangsan@example.com"
   */
  @IsOptional()
  @IsEmail({}, { message: '邮箱格式不正确' })
  @IsNotEmpty({ message: '邮箱不能为空' })
  email: string;

  /**
   * 手机号(可选,正则校验中国大陆手机号)
   * @example "13800138000"
   */
  @IsOptional()
  @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
  phone?: string;

  /**
   * 角色(可选,默认普通用户)
   * @example "user"
   */
  @IsOptional()
  @IsEnum(UserRole, { message: `角色只能是${Object.values(UserRole).join('/')}` })
  role: UserRole = UserRole.USER; // 默认值

  /**
   * 状态(可选,默认未激活)
   * @example "inactive"
   */
  @IsOptional()
  @IsEnum(UserStatus, { message: `状态只能是${Object.values(UserStatus).join('/')}` })
  status: UserStatus = UserStatus.INACTIVE; // 默认值

  /**
   * 备注(可选)
   * @example "测试用户"
   */
  @IsOptional()
  @IsString({ message: '备注必须为字符串' })
  remark?: string;
}
  1. 修改用户接口的 DTO
ts
// src/user/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { IsOptional, IsString, MinLength, Matches } from 'class-validator';

// PartialType: 继承CreateUserDto并将所有字段设为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {
  // 覆盖密码规则:修改时密码可选,且保留格式校验
  @IsOptional()
  @IsString({ message: '密码必须为字符串' })
  @MinLength(6, { message: '密码长度不能少于6位' })
  @Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, {
    message: '密码必须包含字母和数字',
  })
  password?: string;
}
  1. 定义枚举数据
ts
// src/common/enums/user.enum.ts
export enum UserStatus {
  ACTIVE = 'active', // 激活
  INACTIVE = 'inactive', // 未激活
  LOCKED = 'locked', // 锁定
}

export enum UserRole {
  ADMIN = 'admin', // 管理员
  USER = 'user', // 普通用户
  OPERATOR = 'operator', // 运营
}

3、使用 DTO

ts
// src/user/user.controller.ts
import { Controller, Delete, Get, Param, Post, Body, Put } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('user')
export class UserController {
  // 新增用户
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    // 入参已通过DTO校验,可直接使用
    return { message: '用户创建成功', data: createUserDto };
  }

  // 修改用户(/:id 为用户ID,从URL获取)
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return { message: `用户${id}修改成功`, data: updateUserDto };
  }
}

4、测试

shell
# 接口
http://localhost:3000/api/v1/user

# 方法
POST

# 参数
{
   "username": "",
   "password": "1243"
}

# 结果
{
    "message": [
        "用户名不能为空",
        "password must be longer than or equal to 6 characters"
    ],
    "error": "Bad Request",
    "statusCode": 400
}

查询结果过滤

特点:全局拦截器过滤 GET 接口的返回值,主要是隐藏字段,只隐藏 @Exclude() 标记的字段,当前不对数据结构进行统一处理,只是过滤字段 :::

1、全局拦截器

ts
// 全局注册拦截器(需传入Reflector)(所有的接口参数都必须传入 DTO )
app.useGlobalInterceptors(
  new ClassSerializerInterceptor(app.get(Reflector), {
    excludeExtraneousValues: false, // 核心:只保留类中定义的字段,让@Exclude生效
  })
);

2、定义拦截规则

整改 entity 文件

ts
// src/user/user.entity.ts
import { Log } from 'src/log/log.entity';
import { Profile } from 'src/profile/profile.entity';
import { Roles } from 'src/roles/roles.entity';
import {
  Column,
  Entity,
  JoinTable,
  ManyToMany,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  DeleteDateColumn,
  VersionColumn,
} from 'typeorm';

import { Exclude } from 'class-transformer';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column({ select: false }) // 隐藏字段方案一
  password: string;

  @CreateDateColumn({ type: 'timestamp' })
  @Exclude() // 隐藏字段方案二
  createdAt: string;

  @UpdateDateColumn()
  @Exclude()
  updatedAt: Date;

  @DeleteDateColumn()
  @Exclude()
  deletedAt: Date;

  @VersionColumn()
  version?: number;

  // 表示 user 表与 profile 表建立一对一关系
  @OneToOne(() => Profile, (profile) => profile.user)
  profile: Profile;

  // 表示 user 表与 log 表建立一对多关系
  @OneToMany(() => Log, (logs) => logs.user)
  logs: Log[];

  // 表示 user 表与 roles 表建立多对多关系,并创建中间表 users_roles
  @ManyToMany(() => Roles, (roles) => roles.user)
  @JoinTable({
    name: 'users_roles', // 自定义连接表名
    joinColumn: {
      name: 'user_id',
      referencedColumnName: 'id',
    },
    inverseJoinColumn: {
      name: 'role_id',
      referencedColumnName: 'id',
    },
  })
  roles: Roles[];
}

4、测试

shell
# 接口
http://localhost:3000/api/v1/user

# 方法
GET

# 参数


# 结果
[
    {
        "id": 2,
        "username": "lisi",
        "version": 1
    },
    {
        "id": 3,
        "username": "alias",
        "version": 1
    }
]

注意

  1. 使用 @Request() 方法获取 req 参数,无法拦截 dto 校验,只能使用 @Body()/@Query()/@Param() 等参数,即:只能校验 req 内部的属性,Nest 不会处理整个 req 对象的校验
  2. 如果要给 req 参数校验,建议:login(@Body() LoginReqDto: LoginReqDto, @Request() req: any),使用其内部的@Body()等参数校验,然后在后面使用 req 进行挂载