{T}

NestJS数据脱敏与敏感数据处理实战

NestJS数据脱敏与敏感数据处理实战

学习目标:掌握数据安全的重要性、敏感数据处理方案、class-transformer 使用、自定义拦截器、自定义装饰器。


一、数据安全需求背景

1.1 为什么需要数据脱敏

code
数据安全的重要性:
│
├── 数据类型安全
│   ├── 类型校验
│   ├── 格式校验
│   └── 范围校验
│
├── 业务层面安全
│   ├── 敏感数据不能响应给前端
│   ├── 密码不能明文存储
│   └── 个人隐私数据保护
│
└── 常见敏感数据
    ├── password:用户密码
    ├── phone:手机号码
    ├── idCard:身份证号码
    ├── bankCard:银行卡号
    └── address:详细地址

1.2 敏感数据处理的两层考虑

code
敏感数据处理两层考虑:
│
├── 第一层:入库加密
│   ├── 密码不能明文存储
│   ├── 使用 bcrypt 加密
│   ├── 使用 MD5 或 SHA256 哈希
│   └── 对应:NestJS 鉴权相关章节
│
└── 第二层:响应脱敏
    ├── 敏感数据不能响应给前端
    ├── 删除或隐藏敏感字段
    ├── 数据序列化时过滤
    └── 对应:本节内容

1.3 数据脱敏常见方案

方案优点缺点推荐度
手动删除字段实现简单代码重复、维护性差
Helper 函数复用性好需要手动调用
拦截器处理自动应用、集中管理需要额外配置
装饰器标记优雅、可维护需要定义 DTO

二、class-transformer 简介

2.1 class-transformer 是什么

code
class-transformer 简介:
│
├── 作用
│   ├── 对象与类实例之间的转换
│   ├── 属性映射和转换
│   ├── 敏感数据过滤
│   └── 数据序列化和反序列化
│
├── 常用装饰器
│   ├── @Expose():暴露属性
│   ├── @Exclude():排除属性
│   ├── @Type():类型转换
│   └── @Transform():自定义转换
│
├── 常用方法
│   ├── plainToInstance():对象转类实例
│   ├── instanceToPlain():类实例转对象
│   └── serialize():序列化为字符串
│
└── 安装
    └── npm install class-transformer class-validator

2.2 @Exclude 装饰器使用

typescript
// src/modules/user/dto/user.dto.ts
import { Exclude, Expose } from 'class-transformer';

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  username: string;

  @Expose()
  email: string;

  @Exclude() //  排除敏感字段
  password: string;

  @Expose()
  createdAt: Date;
}

2.3 @Exclude 装饰器原理

code
@Exclude() 装饰器原理:
│
├── 标记属性
│   └── 在属性上添加 @Exclude() 装饰器
│
├── 序列化时处理
│   ├── class-transformer 检查装饰器
│   ├── 发现有 @Exclude() 标记
│   └── 自动过滤该属性
│
└── 结果
    └── 响应数据中不包含 password 字段

三、使用 ClassSerializerInterceptor

3.1 ClassSerializerInterceptor 简介

typescript
// NestJS 内置的序列化拦截器
import { ClassSerializerInterceptor } from '@nestjs/common';

@Controller('users')
@UseInterceptors(ClassSerializerInterceptor) //  应用拦截器
export class UserController {
  @Get()
  async getUsers(): Promise<UserDto[]> {
    // 返回的数据会自动应用 @Exclude() 装饰器
    return this.userService.getUsers();
  }
}

3.2 ClassSerializerInterceptor 的局限性

code
ClassSerializerInterceptor 局限性:
│
├── 问题:只对类实例生效
│   ├──  生效:返回 UserDto 实例
│   └──  不生效:返回普通 JavaScript 对象
│
├── 原因
│   ├── 拦截器检查是否为类实例
│   ├── 如果是普通对象,直接返回
│   └── 不会应用装饰器
│
└── 解决方案
    ├── 确保返回类实例(手动 new)
    └── 创建自定义拦截器(推荐)

3.3 问题示例

typescript
//  问题:返回普通对象,@Exclude() 不生效
@Controller('courses')
@UseInterceptors(ClassSerializerInterceptor)
export class CourseController {
  @Get()
  async getCourses() {
    const data = await this.courseService.getCourses();
    // data 是普通对象,不是类实例
    return data.map((item) => ({
      id: item.id,
      name: item.name,
      // ... 手动结构化,不是类实例
    }));
  }
}

//  解决:返回类实例
@Controller('courses')
@UseInterceptors(ClassSerializerInterceptor)
export class CourseController {
  @Get()
  async getCourses(): Promise<CourseDto[]> {
    const data = await this.courseService.getCourses();
    // 手动转换为类实例
    return data.map((item) => plainToInstance(CourseDto, item));
  }
}

四、创建响应数据 DTO

4.1 使用 AI 工具生成 DTO

code
使用 Copilot 生成 DTO:
│
├── 第一步:准备 JSON 数据
│   └── 从 Postman 复制响应数据
│
├── 第二步:编写 Prompt
│   └── 根据以下 JSON 数据生成 TypeScript 类
│       1. 使用 class-validator 和 class-transformer
│       2. 类名为 {ClassName}
│       3. 嵌套子类使用 @Type() 进行转换
│       4. 输出完整的类属性定义
│       5. 嵌套子类先定义,再定义父类
│
├── 第三步:粘贴 JSON 数据
│   └── 将 JSON 数据粘贴到 Prompt 后面
│
├── 第四步:执行生成
│   └── AI 自动生成完整的 DTO 类
│
└── 第五步:调整和优化
    ├── 调整类定义顺序
    ├── 添加 @Exclude() 装饰器
    └── 添加 @Expose() 装饰器

4.2 Prompt 模板

code
Prompt 模板:

根据以下 JSON 数据生成 TypeScript 类:

1. 使用 class-validator 和 class-transformer 装饰器
2. 类名为 {ClassName}Dto
3. 嵌套子类中使用 class-transformer 的 @Type() 方法进行 transform 转换
4. 输出完整的类的属性定义,不能有 null
5. 嵌套的子类先定义,后再定义父类

JSON 数据:
{粘贴 JSON 数据}

4.3 生成的 DTO 示例

typescript
// src/modules/course/dto/public-get-courses.dto.ts
import { Exclude, Expose, Type } from 'class-transformer';

/**
 * 用户 DTO(嵌套子类)
 */
export class UserDto {
  @Expose()
  id: number;

  @Expose()
  username: string;

  @Expose()
  email: string;

  @Exclude() //  排除敏感字段
  password: string;

  @Expose()
  createdAt: Date;
}

/**
 * 课程 DTO(嵌套子类)
 */
export class CourseDto {
  @Expose()
  id: number;

  @Expose()
  title: string;

  @Expose()
  content: string;

  @Expose()
  @Type(() => UserDto)
  author: UserDto;
}

/**
 * 公开课程响应 DTO(父类)
 */
export class PublicGetCoursesDto {
  @Expose()
  id: number;

  @Expose()
  name: string;

  @Expose()
  @Type(() => CourseDto)
  courses: CourseDto[];
}

五、创建自定义序列化拦截器

5.1 使用 NestJS CLI 创建拦截器

bash
# 创建拦截器(扁平结构,无测试文件)
nest g itc common/interceptors/serialize --no-spec --flat

5.2 自定义 SerializeInterceptor 实现

typescript
// src/common/interceptors/serialize.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToInstance } from 'class-transformer';

/**
 * 序列化拦截器
 * 自动将响应数据转换为 DTO 类实例
 */
@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  constructor(
    private readonly dto: any,
    private readonly excludeExtraneousValues: boolean = false,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) =>
        plainToInstance(this.dto, data, {
          excludeExtraneousValues: this.excludeExtraneousValues,
        }),
      ),
    );
  }
}

5.3 plainToInstance 方法详解

code
plainToInstance 方法详解:
│
├── 作用
│   ├── 将普通对象转换为类实例
│   ├── 应用 class-transformer 装饰器
│   └── 执行属性映射和转换
│
├── 参数
│   ├── classType:目标类
│   ├── plain:普通对象
│   └── options:配置选项
│
├── 常用选项
│   ├── excludeExtraneousValues:是否排除未标记的属性
│   ├── enableCircularCheck:启用循环引用检查
│   ├── enableImplicitConversion:启用隐式类型转换
│   └── version:版本控制
│
└── excludeExtraneousValues 详解
    ├── true:只处理有 @Expose() 的属性
    │   ├── 必须显式标记 @Expose()
    │   └── 未标记的属性不显示
    │
    └── false:处理所有属性,排除 @Exclude() 的属性
        ├── 默认暴露所有属性
        └── 只排除 @Exclude() 标记的属性

5.4 excludeExtraneousValues 对比

选项truefalse
行为只显示 @Expose() 的属性显示所有属性,排除 @Exclude()
安全性高(显式控制)中(隐式控制)
配置方式每个属性加 @Expose()敏感属性加 @Exclude()
推荐度

六、创建自定义装饰器

6.1 自定义装饰器实现

typescript
// src/common/decorators/serialize.decorator.ts
import { UseInterceptors } from '@nestjs/common';
import { SerializeInterceptor } from '../interceptors/serialize.interceptor';

/**
 * 类构造器接口
 */
interface ClassConstructor {
  new (...args: any[]): any;
}

/**
 * 序列化装饰器
 * @param dto DTO 类
 * @param excludeExtraneousValues 是否排除未标记的属性(默认 false)
 */
export function Serialize(
  dto: ClassConstructor,
  excludeExtraneousValues: boolean = false,
) {
  return UseInterceptors(new SerializeInterceptor(dto, excludeExtraneousValues));
}

/**
 * 严格序列化装饰器
 * 只显示有 @Expose() 标记的属性
 * @param dto DTO 类
 */
export function SerializeStrict(dto: ClassConstructor) {
  return UseInterceptors(new SerializeInterceptor(dto, true));
}

6.2 装饰器使用示例

typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { Serialize, SerializeStrict } from '@/common/decorators/serialize.decorator';
import { PublicGetCoursesDto } from './dto/public-get-courses.dto';
import { CourseService } from './course.service';

@Controller('courses')
export class CourseController {
  constructor(private readonly courseService: CourseService) {}

  /**
   * 方式一:使用 @Serialize() 装饰器
   * 排除 @Exclude() 标记的属性
   */
  @Get()
  @Serialize(PublicGetCoursesDto)
  async getCourses() {
    return this.courseService.getCourses();
  }

  /**
   * 方式二:使用 @SerializeStrict() 装饰器
   * 只显示 @Expose() 标记的属性
   */
  @Get('strict')
  @SerializeStrict(PublicGetCoursesDto)
  async getCoursesStrict() {
    return this.courseService.getCourses();
  }
}

6.3 两种装饰器对比

code
@Serialize() vs @SerializeStrict():
│
├── @Serialize(dto, false)
│   ├── 行为:显示所有属性,排除 @Exclude()
│   ├── 配置:敏感属性加 @Exclude()
│   ├── 优点:配置简单
│   └── 缺点:可能遗漏敏感属性
│
└── @SerializeStrict(dto)(推荐)
    ├── 行为:只显示 @Expose() 的属性
    ├── 配置:需要暴露的属性加 @Expose()
    ├── 优点:显式控制、更安全
    └── 缺点:配置稍多

七、完整实战示例

7.1 项目结构

code
project/
├── src/
│   ├── common/
│   │   ├── decorators/
│   │   │   └── serialize.decorator.ts
│   │   └── interceptors/
│   │       └── serialize.interceptor.ts
│   ├── modules/
│   │   └── course/
│   │       ├── dto/
│   │       │   ├── public-get-courses.dto.ts
│   │       │   └── get-courses-by-type.dto.ts
│   │       ├── course.controller.ts
│   │       ├── course.service.ts
│   │       └── course.module.ts
│   └── main.ts
└── package.json

7.2 完整 SerializeInterceptor 实现

typescript
// src/common/interceptors/serialize.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  constructor(
    private readonly dto: any,
    private readonly excludeExtraneousValues: boolean = false,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) =>
        plainToInstance(this.dto, data, {
          excludeExtraneousValues: this.excludeExtraneousValues,
        }),
      ),
    );
  }
}

7.3 完整 Serialize 装饰器实现

typescript
// src/common/decorators/serialize.decorator.ts
import { UseInterceptors } from '@nestjs/common';
import { SerializeInterceptor } from '../interceptors/serialize.interceptor';

interface ClassConstructor {
  new (...args: any[]): any;
}

/**
 * 序列化装饰器
 */
export function Serialize(
  dto: ClassConstructor,
  excludeExtraneousValues: boolean = false,
) {
  return UseInterceptors(new SerializeInterceptor(dto, excludeExtraneousValues));
}

/**
 * 严格序列化装饰器
 */
export function SerializeStrict(dto: ClassConstructor) {
  return UseInterceptors(new SerializeInterceptor(dto, true));
}

7.4 完整 DTO 实现

typescript
// src/modules/course/dto/public-get-courses.dto.ts
import { Exclude, Expose, Type } from 'class-transformer';

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  username: string;

  @Expose()
  email: string;

  @Exclude() //  排除敏感字段
  password: string;
}

export class CourseDto {
  @Expose()
  id: number;

  @Expose()
  title: string;

  @Expose()
  @Type(() => UserDto)
  author: UserDto;
}

export class PublicGetCoursesDto {
  @Expose()
  id: number;

  @Expose()
  name: string;

  @Expose()
  @Type(() => CourseDto)
  courses: CourseDto[];
}

7.5 完整 Controller 实现

typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { Serialize, SerializeStrict } from '@/common/decorators/serialize.decorator';
import { PublicGetCoursesDto } from './dto/public-get-courses.dto';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';

@Controller('courses')
export class CourseController {
  constructor(private readonly courseService: CourseService) {}

  /**
   * 获取课程列表(排除敏感数据)
   * GET /courses
   */
  @Get()
  @Serialize(PublicGetCoursesDto)
  async getCourses() {
    return this.courseService.getCourses();
  }

  /**
   * 根据分类查询课程(严格模式)
   * GET /courses/by-type
   */
  @Get('by-type')
  @SerializeStrict(PublicGetCoursesDto)
  async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
    return this.courseService.getCoursesByType(dto);
  }
}

八、Postman 测试示例

8.1 测试一:@Serialize 装饰器验证

code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses
│
└── 预期响应:
    [
      {
        "id": 11,
        "name": "推荐内容",
        "courses": [
          {
            "id": 4,
            "title": "Vue3 项目实战",
            "author": {
              "id": 1,
              "username": "admin",
              "email": "admin@example.com"
              //  password 字段已被排除
            }
          }
        ]
      }
    ]

8.2 测试二:@SerializeStrict 装饰器验证

code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
└── 预期响应:
    [
      {
        "id": 11,
        "name": "推荐内容",
        "courses": [...]
        //  只显示 @Expose() 标记的属性
      }
    ]
    
注意:
├── 如果 DTO 中没有 @Expose() 标记
└── 响应为空对象:{}

8.3 测试三:验证敏感数据已排除

code
验证步骤:
│
├── 第一步:发起请求
│   └── GET /courses
│
├── 第二步:检查响应数据
│   ├──  包含:id, username, email
│   └──  不包含:password
│
└── 第三步:确认数据脱敏成功
    └── password 字段已成功排除

九、常见问题与解决方案

9.1 @Exclude 不生效问题

问题原因解决方案
@Exclude() 不生效返回的是普通对象,不是类实例使用 plainToInstance 转换
装饰器未应用未应用拦截器添加 @Serialize 装饰器
属性仍然显示excludeExtraneousValues 设置错误检查装饰器配置

9.2 plainToInstance 使用问题

typescript
//  错误:直接返回普通对象
return data;

//  正确:转换为类实例
return plainToInstance(Dto, data, {
  excludeExtraneousValues: false,
});

9.3 嵌套对象转换问题

typescript
//  错误:嵌套对象未添加 @Type() 装饰器
export class CourseDto {
  @Expose()
  author: UserDto; //  不会被转换
}

//  正确:添加 @Type() 装饰器
export class CourseDto {
  @Expose()
  @Type(() => UserDto) //  指定类型
  author: UserDto;
}

十、最佳实践总结

10.1 数据脱敏最佳实践

code
数据脱敏最佳实践:
│
├── 使用 @SerializeStrict() 装饰器
│   ├── 显式控制暴露的属性
│   ├── 更安全的数据控制
│   └── 避免遗漏敏感属性
│
├── DTO 设计规范
│   ├── 敏感属性加 @Exclude()
│   ├── 需暴露属性加 @Expose()
│   └── 嵌套对象加 @Type()
│
├── 拦截器使用
│   ├── Controller 级别应用
│   ├── 方法级别应用
│   └── 全局应用(谨慎)
│
└── 测试验证
    ├── 单元测试
    ├── 集成测试
    └── 安全审计

10.2 DTO 设计最佳实践

code
DTO 设计最佳实践:
│
├── 分离关注点
│   ├── 数据库 DTO:数据库操作
│   ├── 响应 DTO:接口响应
│   └── 请求 DTO:接口请求
│
├── 命名规范
│   ├── PublicXxxDto:公开响应 DTO
│   ├── CreateXxxDto:创建请求 DTO
│   └── UpdateXxxDto:更新请求 DTO
│
├── 类定义顺序
│   ├── 子类先定义
│   └── 父类后定义
│
└── 装饰器使用
    ├── @Expose():暴露属性
    ├── @Exclude():排除属性
    └── @Type():嵌套类型

10.3 安全性最佳实践

code
安全性最佳实践:
│
├── 敏感数据处理
│   ├── 密码加密存储(bcrypt)
│   ├── 响应时排除敏感字段
│   └── 日志中过滤敏感信息
│
├── 数据验证
│   ├── 入参验证(class-validator)
│   ├── 类型校验
│   └── 格式校验
│
├── 权限控制
│   ├── 角色权限验证
│   ├── 接口访问控制
│   └── 数据访问控制
│
└── 审计日志
    ├── 操作日志
    ├── 访问日志
    └── 异常日志

十一、命令速查表

11.1 class-transformer 装饰器速查

装饰器作用示例
@Expose()暴露属性@Expose() id: number;
@Exclude()排除属性@Exclude() password: string;
@Type()类型转换@Type(() => UserDto) author: UserDto;
@Transform()自定义转换@Transform(({ value }) => value.toUpperCase())

11.2 plainToInstance 选项速查

选项类型默认值说明
excludeExtraneousValuesbooleanfalse排除未标记的属性
enableCircularCheckbooleanfalse启用循环引用检查
enableImplicitConversionbooleanfalse启用隐式类型转换
versionnumber-版本控制

11.3 NestJS CLI 命令速查

命令说明
nest g itc <path>创建拦截器
nest g itc <path> --no-spec创建拦截器(无测试)
nest g itc <path> --flat创建拦截器(扁平结构)

十二、扩展应用场景

12.1 多版本响应 DTO

typescript
// 支持版本控制的 DTO
export class UserDto {
  @Expose({ since: 1, until: 2 })
  name: string;

  @Expose({ since: 2 })
  fullName: string;

  @Exclude()
  password: string;
}

// 使用时指定版本
plainToInstance(UserDto, data, { version: 1 });

12.2 自定义转换函数

typescript
// 使用 @Transform() 自定义转换
export class UserDto {
  @Expose()
  @Transform(({ value }) => value.toUpperCase())
  username: string;

  @Expose()
  @Transform(({ value }) => `***${value.slice(-4)}`)
  phone: string; // 手机号脱敏:138****1234
}

12.3 条件暴露属性

typescript
// 根据条件暴露属性
export class UserDto {
  @Expose()
  id: number;

  @Expose({ groups: ['admin'] })
  email: string;

  @Expose({ groups: ['user'] })
  username: string;
}

// 使用时指定分组
plainToInstance(UserDto, data, { groups: ['admin'] });

十三、学习要点总结

13.1 核心知识点

code
本文核心要点:
│
├── 数据脱敏的重要性
│   ├── 业务层面安全
│   ├── 敏感数据保护
│   └── 常见敏感数据类型
│
├── class-transformer 使用
│   ├── @Exclude() 排除属性
│   ├── @Expose() 暴露属性
│   ├── @Type() 类型转换
│   └── plainToInstance() 方法
│
├── 自定义拦截器
│   ├── SerializeInterceptor 实现
│   ├── excludeExtraneousValues 选项
│   └── plainToInstance 转换
│
├── 自定义装饰器
│   ├── @Serialize() 装饰器
│   ├── @SerializeStrict() 装饰器
│   └── 简化使用方式
│
├── 使用 AI 工具生成 DTO
│   ├── Copilot prompt 编写
│   ├── 类定义顺序
│   └── 调整和优化
│
└── 最佳实践
    ├── 使用 @SerializeStrict()
    ├── 显式控制暴露属性
    └── 测试验证

13.2 重要程度标注

知识点重要程度说明
@Exclude() 装饰器必须掌握敏感数据排除
plainToInstance 方法必须掌握对象转换核心
自定义拦截器必须掌握序列化处理
@Expose() 装饰器重要显式控制暴露
AI 工具生成 DTO了解提高效率

13.3 学习路径规划

code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│   ├── 理解数据脱敏的重要性
│   ├── 理解 class-transformer 原理
│   └── 理解拦截器工作机制
│
├── 第二阶段:实践操作(2-3 天)
│   ├── 创建响应 DTO
│   ├── 实现自定义拦截器
│   └── 实现自定义装饰器
│
└── 第三阶段:深入应用(持续)
    ├── 多版本响应 DTO
    ├── 自定义转换函数
    └── 条件暴露属性

十四、完整代码清单

14.1 SerializeInterceptor 完整代码

typescript
// src/common/interceptors/serialize.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  constructor(
    private readonly dto: any,
    private readonly excludeExtraneousValues: boolean = false,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) =>
        plainToInstance(this.dto, data, {
          excludeExtraneousValues: this.excludeExtraneousValues,
        }),
      ),
    );
  }
}

14.2 Serialize 装饰器完整代码

typescript
// src/common/decorators/serialize.decorator.ts
import { UseInterceptors } from '@nestjs/common';
import { SerializeInterceptor } from '../interceptors/serialize.interceptor';

interface ClassConstructor {
  new (...args: any[]): any;
}

export function Serialize(
  dto: ClassConstructor,
  excludeExtraneousValues: boolean = false,
) {
  return UseInterceptors(new SerializeInterceptor(dto, excludeExtraneousValues));
}

export function SerializeStrict(dto: ClassConstructor) {
  return UseInterceptors(new SerializeInterceptor(dto, true));
}

14.3 PublicGetCoursesDto 完整代码

typescript
// src/modules/course/dto/public-get-courses.dto.ts
import { Exclude, Expose, Type } from 'class-transformer';

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  username: string;

  @Expose()
  email: string;

  @Exclude()
  password: string;
}

export class CourseDto {
  @Expose()
  id: number;

  @Expose()
  title: string;

  @Expose()
  @Type(() => UserDto)
  author: UserDto;
}

export class PublicGetCoursesDto {
  @Expose()
  id: number;

  @Expose()
  name: string;

  @Expose()
  @Type(() => CourseDto)
  courses: CourseDto[];
}

14.4 CourseController 完整代码

typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { Serialize, SerializeStrict } from '@/common/decorators/serialize.decorator';
import { PublicGetCoursesDto } from './dto/public-get-courses.dto';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';

@Controller('courses')
export class CourseController {
  constructor(private readonly courseService: CourseService) {}

  @Get()
  @Serialize(PublicGetCoursesDto)
  async getCourses() {
    return this.courseService.getCourses();
  }

  @Get('by-type')
  @SerializeStrict(PublicGetCoursesDto)
  async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
    return this.courseService.getCoursesByType(dto);
  }
}

重要提示:数据脱敏是后端开发的重要安全措施,掌握 class-transformer 的使用、理解 plainToInstance 方法、学会创建自定义拦截器和装饰器,对实际项目开发非常重要!推荐使用 @SerializeStrict() 装饰器,显式控制暴露的属性,确保数据安全!

扩展建议:可以尝试实现多版本响应 DTO、自定义转换函数、条件暴露属性等功能,进一步掌握 class-transformer 的高级应用!