数据序列化与脱敏
学习目标:掌握数据结构化处理、BigInt 序列化问题解决、NestJS 拦截器使用、三种序列化方案对比。
一、数据结构化处理需求
1.1 问题背景
数据结构化问题背景:
│
├── 数据库查询结果结构
│ ├── 分类数据嵌套层级深
│ ├── tags 数组包含 courses 数组
│ ├── courses 数组包含 course 对象
│ └── course 对象包含 author 对象
│
├── 前端期望的数据结构
│ ├── 简单清晰的层级
│ ├── 直接访问课程列表
│ ├── 不需要深层嵌套
│ └── 数据扁平化处理
│
└── 解决方案
├── 使用 map 方法重组数据
├── 使用 reduce 方法合并数组
└── 提取必要的字段信息
1.2 原始数据结构 vs 期望数据结构
原始数据结构(嵌套过深):
│
└── courseTypes
├── id: 11
├── name: '推荐内容'
└── tags: [
{
id: 23
name: 'Vue3 项目实战'
└── courses: [
{
courseId: 4
tagId: 23
└── course: {
id: 4
title: 'Vue3 项目实战'
└── author: { ... }
}
}
]
}
]
期望数据结构(扁平清晰):
│
└── [
{
id: 11
name: '推荐内容'
└── courses: [
{
id: 4
title: 'Vue3 项目实战'
└── author: { ... }
}
]
}
]
二、数据结构化实现
2.1 Service 层数据结构化
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
async getCoursesByType(dto: GetCoursesByTypeDto) {
// 1. 计算分页参数
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
// 2. 查询数据库
const res = await this.prisma.courseTypes.findMany({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true, // 包含作者信息
},
},
},
},
},
},
},
});
// 3. 数据结构化处理
if (res && res.length > 0) {
return res.map((item) => ({
id: item.id,
name: item.name,
// 使用 reduce 合并所有 tags 中的 courses
courses: item.tags.reduce((acc, tag) => {
// 提取 course 对象
const courses = tag.courses.map((c) => c.course);
return [...acc, ...courses];
}, []),
}));
}
return [];
}
}
2.2 数据结构化流程详解
数据结构化流程:
│
├── 第一步:遍历分类(map)
│ └── res.map((item) => { ... })
│
├── 第二步:提取分类基本信息
│ ├── id: item.id
│ └── name: item.name
│
├── 第三步:合并所有 tags 中的 courses(reduce)
│ ├── 初始值:[](空数组)
│ ├── 遍历:item.tags
│ ├── 提取:tag.courses.map((c) => c.course)
│ └── 合并:[...acc, ...courses]
│
└── 第四步:返回结构化数据
└── { id, name, courses }
2.3 reduce 方法详解
reduce 方法详解:
│
├── 语法
│ └── array.reduce((accumulator, currentValue) => { ... }, initialValue)
│
├── 参数说明
│ ├── accumulator:累计值
│ ├── currentValue:当前元素
│ └── initialValue:初始值
│
├── 示例:合并数组
│ ├── item.tags = [
│ │ { courses: [{ course: A }, { course: B }] },
│ │ { courses: [{ course: C }] }
│ │ ]
│ │
│ ├── 第一次循环:acc = [], tag = { courses: [A, B] }
│ │ └── courses = [A, B]
│ │ └── return [...[], ...[A, B]] = [A, B]
│ │
│ ├── 第二次循环:acc = [A, B], tag = { courses: [C] }
│ │ └── courses = [C]
│ │ └── return [...[A, B], ...[C]] = [A, B, C]
│ │
│ └── 最终结果:[A, B, C]
│
└── 使用场景
├── 数组求和
├── 数组去重
└── 数组合并
三、BigInt 序列化问题
3.1 问题描述
BigInt 序列化问题:
│
├── 错误信息
│ └── "Do not know how to serialize a BigInt"
│
├── 问题原因
│ ├── 数据库字段类型为 BIGINT
│ ├── Prisma 将 BIGINT 映射为 BigInt 类型
│ ├── JSON.stringify() 无法序列化 BigInt
│ └── NestJS 响应时自动调用 JSON.stringify()
│
├── 触发场景
│ ├── phone 字段(手机号,11 位)
│ ├── id 字段(可能超过 Number.MAX_SAFE_INTEGER)
│ └── 其他大整数字段
│
└── 影响范围
├── 数据库查询成功
├── Controller 接收成功
└── 响应时序列化失败
3.2 问题复现与定位
// 问题定位步骤
// 第一步:确认数据库查询成功
const res = await this.prisma.courseTypes.findMany({ ... });
console.log(res); // 打印成功,说明数据库查询没问题
// 第二步:确认 Controller 接收成功
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
const data = await this.courseService.getCoursesByType(dto);
console.log(data); // 打印成功,说明 Controller 接收成功
return data; // 响应时报错
}
// 结论:问题出在响应时的 JSON.stringify()
3.3 BigInt 序列化原理
JavaScript 数字范围:
│
├── Number.MAX_SAFE_INTEGER
│ └── 9007199254740991(约 9 千万亿)
│
├── BIGINT 范围
│ └── 任意大的整数(无上限)
│
├── JSON 序列化问题
│ ├── JSON.stringify() 只支持 Number 类型
│ ├── BigInt 类型无法序列化
│ └── 抛出错误:"Do not know how to serialize a BigInt"
│
└── 解决思路
├── 将 BigInt 转换为 String
├── 将 BigInt 转换为 Number(如果范围允许)
└── 自定义序列化逻辑
四、解决方案一:手动序列化函数
4.1 serializeBigInt 函数实现
// src/utils/serialize.ts
/**
* 序列化 BigInt 类型数据
* 将 BigInt 转换为 String
*
* @param data 需要序列化的数据
* @returns 序列化后的数据
*/
export function serializeBigInt(data: any): any {
// 1. 如果是 BigInt,直接转换为 String
if (typeof data === 'bigint') {
return data.toString();
}
// 2. 如果是对象,递归处理
if (typeof data === 'object' && data !== null) {
// 3. 遍历对象的所有键
for (const key in data) {
if (data.hasOwnProperty(key)) {
// 4. 递归调用 serializeBigInt
data[key] = serializeBigInt(data[key]);
}
}
}
return data;
}
4.2 在 Service 中使用
// src/modules/course/course.service.ts
import { serializeBigInt } from '@/utils/serialize';
@Injectable()
export class CourseService {
async getCoursesByType(dto: GetCoursesByTypeDto) {
const res = await this.prisma.courseTypes.findMany({ ... });
if (res && res.length > 0) {
const data = res.map((item) => ({
id: item.id,
name: item.name,
courses: item.tags.reduce((acc, tag) => {
const courses = tag.courses.map((c) => c.course);
return [...acc, ...courses];
}, []),
}));
// 使用 serializeBigInt 序列化
return serializeBigInt(data);
}
return [];
}
}
4.3 方案一优缺点
方案一:手动序列化函数
│
├── 优点
│ ├── 实现简单
│ ├── 逻辑清晰
│ └── 可控性强
│
└── 缺点
├── 每次都需要手动调用
├── 容易遗忘
├── 代码重复
└── 维护性差
五、解决方案二:修改原型链
5.1 修改 BigInt.prototype.toJSON
// src/main.ts
// 修改 BigInt 原型链(不推荐)
(BigInt.prototype as any).toJSON = function () {
// 1. 转换为字符串
const str = this.toString();
// 2. 尝试转换为 Number
const int = Number.parseInt(str);
// 3. 如果转换成功且在安全范围内,返回 Number
if (Number.isSafeInteger(int)) {
return int;
}
// 4. 否则返回 String
return str;
};
5.2 方案二原理
方案二原理:
│
├── 工作原理
│ ├── 修改 BigInt.prototype.toJSON 方法
│ ├── JSON.stringify() 调用 toJSON() 进行序列化
│ └── 自动处理所有 BigInt 类型数据
│
├── 代码逻辑
│ ├── BigInt.toString():转为字符串
│ ├── Number.parseInt():尝试转为数字
│ ├── Number.isSafeInteger():检查是否安全
│ └── 返回 Number 或 String
│
└── 生效范围
├── 全局生效
├── 所有 BigInt 类型自动处理
└── 无需手动调用
5.3 方案二优缺点
方案二:修改原型链
│
├── 优点
│ ├── 实现简单
│ ├── 全局生效
│ └── 无需手动调用
│
└── 缺点
├── 修改原型链(不安全)
├── 全局影响(不可控)
├── 可能影响其他代码
├── 不符合最佳实践
└── 不推荐使用
六、解决方案三:使用拦截器(推荐)
6.1 拦截器基础概念
NestJS 拦截器(Interceptor):
│
├── 作用
│ ├── 在请求到达 Controller 前做处理
│ ├── 在响应返回客户端前做处理
│ └── 实现面向切面编程(AOP)
│
├── 使用场景
│ ├── 日志记录
│ ├── 响应数据转换
│ ├── 异常处理
│ ├── 缓存
│ └── 性能监控
│
├── 工作流程
│ ├── 客户端请求 → 拦截器 → Controller
│ └── Controller → 拦截器 → 客户端响应
│
└── 装饰器
├── @UseInterceptors():应用拦截器
├── Controller 级别:整个控制器生效
└── 方法级别:单个方法生效
6.2 创建 BigInt 拦截器
// src/common/interceptors/bigint-transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
/**
* BigInt 序列化拦截器
* 自动将 BigInt 转换为 String
*/
@Injectable()
export class BigIntTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.serializeBigInt(data)),
);
}
/**
* 序列化 BigInt 类型数据
*/
private serializeBigInt(data: any): any {
// 1. 如果是 BigInt,直接转换为 String
if (typeof data === 'bigint') {
return data.toString();
}
// 2. 如果是数组,递归处理每个元素
if (Array.isArray(data)) {
return data.map((item) => this.serializeBigInt(item));
}
// 3. 如果是对象,递归处理每个属性
if (typeof data === 'object' && data !== null) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
data[key] = this.serializeBigInt(data[key]);
}
}
}
return data;
}
}
6.3 在 Controller 中使用拦截器
// src/modules/course/course.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common';
import { BigIntTransformInterceptor } from '@/common/interceptors/bigint-transform.interceptor';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
@UseInterceptors(BigIntTransformInterceptor) // 应用拦截器
export class CourseController {
constructor(private readonly courseService: CourseService) {}
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}
6.4 拦截器工作流程
拦截器工作流程:
│
├── 第一步:客户端发起请求
│ └── GET /courses/by-type
│
├── 第二步:拦截器拦截请求(可选)
│ └── intercept() 方法执行
│
├── 第三步:Controller 处理请求
│ └── getCoursesByType() 方法执行
│
├── 第四步:拦截器拦截响应
│ ├── next.handle() 获取响应数据
│ └── pipe(map()) 处理响应数据
│
├── 第五步:序列化 BigInt
│ ├── 遍历响应数据
│ └── 将 BigInt 转换为 String
│
└── 第六步:返回处理后的响应
└── 自动调用 JSON.stringify()
6.5 方案三优缺点
方案三:使用拦截器(推荐)
│
├── 优点
│ ├── NestJS 官方推荐方式
│ ├── 符合 AOP 编程思想
│ ├── 代码优雅、可维护
│ ├── 可控性强(可选择应用范围)
│ ├── 支持 Controller 级别或方法级别
│ └── 易于测试和扩展
│
└── 缺点
├── 学习成本稍高
└── 需要理解 RxJS
七、三种方案对比
7.1 实现复杂度对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|
| 实现难度 | 低 | 低 | 中 |
| 代码量 | 较少 | 最少 | 适中 |
| 学习成本 | 低 | 低 | 中 |
7.2 使用便捷性对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|
| 使用方式 | 手动调用 | 自动应用 | 装饰器应用 |
| 作用范围 | 单个方法 | 全局 | 可选择 |
| 可维护性 | 低 | 低 | 高 |
7.3 安全性对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|
| 代码安全 | 安全 | 不安全 | 安全 |
| 全局影响 | 无 | 有 | 无 |
| 推荐度 | 可用 | (不推荐) | 推荐 |
7.4 推荐选择
方案选择建议:
│
├── 推荐使用方案三:拦截器
│ ├── 优点:优雅、可控、符合最佳实践
│ ├── 缺点:学习成本稍高
│ └── 适用:所有项目
│
├── 方案一:手动函数
│ ├── 优点:简单、直接
│ ├── 缺点:维护性差
│ └── 适用:小型项目、快速原型
│
└── 方案二:原型链 (不推荐)
├── 优点:全局生效
├── 缺点:不安全、不推荐
└── 适用:无(不推荐使用)
八、拦截器扩展应用
8.1 日志拦截器
// src/common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const now = Date.now();
console.log(`Before... ${request.method} ${request.url}`);
return next.handle().pipe(
tap(() => {
console.log(`After... ${Date.now() - now}ms`);
}),
);
}
}
8.2 响应格式化拦截器
// src/common/interceptors/transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'success',
data,
timestamp: Date.now(),
})),
);
}
}
8.3 全局应用拦截器
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { BigIntTransformInterceptor } from './common/interceptors/bigint-transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局应用拦截器
app.useGlobalInterceptors(new BigIntTransformInterceptor());
await app.listen(3000);
}
bootstrap();
九、Postman 测试示例
9.1 测试一:数据结构化验证
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ ├── size: 10
│ └── types: 11,12,13
│
└── 预期响应:
[
{
"id": 11,
"name": "推荐内容",
"courses": [
{
"id": 4,
"title": "Vue3 项目实战",
"author": { ... }
}
]
}
]
9.2 测试二:BigInt 序列化验证
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
└── 预期响应:
├── 状态码:200
├── BigInt 字段已转换为 String
└── 无序列化错误
9.3 测试三:拦截器生效验证
验证步骤:
│
├── 第一步:应用拦截器
│ └── @UseInterceptors(BigIntTransformInterceptor)
│
├── 第二步:发起请求
│ └── GET /courses/by-type
│
├── 第三步:检查响应
│ ├── 状态码:200
│ └── BigInt 字段正常显示
│
└── 第四步:移除拦截器
├── 状态码:500
└── 错误:BigInt 序列化失败
十、完整实战示例
10.1 项目结构
project/
├── src/
│ ├── common/
│ │ ├── interceptors/
│ │ │ ├── bigint-transform.interceptor.ts
│ │ │ ├── logging.interceptor.ts
│ │ │ └── transform.interceptor.ts
│ │ └── utils/
│ │ └── serialize.ts
│ ├── modules/
│ │ └── course/
│ │ ├── dto/
│ │ │ └── get-courses-by-type.dto.ts
│ │ ├── course.controller.ts
│ │ ├── course.service.ts
│ │ └── course.module.ts
│ └── main.ts
└── package.json
10.2 完整 BigInt 拦截器实现
// src/common/interceptors/bigint-transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
/**
* BigInt 序列化拦截器
* 自动将 BigInt 转换为 String
*/
@Injectable()
export class BigIntTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.serializeBigInt(data)),
);
}
/**
* 序列化 BigInt 类型数据
*/
private serializeBigInt(data: any): any {
if (typeof data === 'bigint') {
return data.toString();
}
if (Array.isArray(data)) {
return data.map((item) => this.serializeBigInt(item));
}
if (typeof data === 'object' && data !== null) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
data[key] = this.serializeBigInt(data[key]);
}
}
}
return data;
}
}
10.3 完整 Service 实现
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
async getCoursesByType(dto: GetCoursesByTypeDto) {
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
const res = await this.prisma.courseTypes.findMany({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
// 数据结构化处理
if (res && res.length > 0) {
return res.map((item) => ({
id: item.id,
name: item.name,
courses: item.tags.reduce((acc, tag) => {
const courses = tag.courses.map((c) => c.course);
return [...acc, ...courses];
}, []),
}));
}
return [];
}
}
10.4 完整 Controller 实现
// src/modules/course/course.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common';
import { BigIntTransformInterceptor } from '@/common/interceptors/bigint-transform.interceptor';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
@UseInterceptors(BigIntTransformInterceptor) // 应用拦截器
export class CourseController {
constructor(private readonly courseService: CourseService) {}
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}
十一、常见问题与解决方案
11.1 数据结构化问题
| 问题 | 原因 | 解决方案 |
|---|
| 数据层级过深 | Prisma include 嵌套 | 使用 map 和 reduce 扁平化 |
| 数组未合并 | reduce 使用不当 | 检查 reduce 初始值和返回值 |
| 字段缺失 | include 未添加 | 添加 include: { author: true } |
11.2 BigInt 序列化问题
| 问题 | 原因 | 解决方案 |
|---|
| 序列化失败 | BigInt 无法 JSON.stringify | 使用拦截器转换 |
| 拦截器不生效 | 未应用拦截器 | 添加 @UseInterceptors |
| 部分字段未转换 | 递归逻辑错误 | 检查 serializeBigInt 逻辑 |
11.3 拦截器使用问题
// RxJS 7+ 推荐:直接从 rxjs 导入操作符
import { map } from 'rxjs'; // 推荐(RxJS 7+)
// 从 rxjs/operators 导入是旧写法,仍然兼容
import { map } from 'rxjs/operators'; // 旧写法
// 错误:拦截器未应用
@Controller('courses')
export class CourseController { ... }
// 正确:应用拦截器
@Controller('courses')
@UseInterceptors(BigIntTransformInterceptor)
export class CourseController { ... }
十二、最佳实践总结
12.1 数据结构化最佳实践
数据结构化最佳实践:
│
├── 使用 map 方法
│ ├── 遍历数组
│ ├── 提取需要的字段
│ └── 重组数据结构
│
├── 使用 reduce 方法
│ ├── 合并多个数组
│ ├── 累计计算
│ └── 数组去重
│
├── 数据扁平化
│ ├── 减少嵌套层级
│ ├── 提高可读性
│ └── 便于前端使用
│
└── 保持数据完整性
├── 包含必要字段
├── 添加关联数据
└── 避免数据丢失
12.2 BigInt 序列化最佳实践
BigInt 序列化最佳实践:
│
├── 推荐使用拦截器
│ ├── 符合 NestJS 最佳实践
│ ├── 代码优雅可维护
│ └── 可选择应用范围
│
├── 避免修改原型链
│ ├── 不安全
│ ├── 全局影响
│ └── 不符合最佳实践
│
├── 统一转换策略
│ ├── BigInt → String(推荐)
│ ├── BigInt → Number(不安全)
│ └── 保持一致性
│
└── 测试验证
├── 单元测试
├── 集成测试
└── 边界情况测试
12.3 拦截器使用最佳实践
拦截器使用最佳实践:
│
├── 命名规范
│ ├── 以 Interceptor 结尾
│ ├── 名称清晰表达功能
│ └── 示例:BigIntTransformInterceptor
│
├── 职责单一
│ ├── 一个拦截器做一件事
│ ├── 避免功能耦合
│ └── 便于测试和维护
│
├── 应用范围
│ ├── Controller 级别:整个控制器
│ ├── 方法级别:单个方法
│ └── 全局:所有路由
│
└── 性能优化
├── 避免复杂计算
├── 使用缓存
└── 异步处理
十三、命令速查表
13.1 数据结构化命令速查
| 操作 | 方法 | 说明 |
|---|
| 遍历数组 | array.map((item) => { ... }) | 提取和转换数据 |
| 合并数组 | array.reduce((acc, item) => [...acc, ...item], []) | 合并多个数组 |
| 提取字段 | { id, name } | 解构赋值 |
| 扁平化嵌套 | item.tags.reduce(...) | 减少嵌套层级 |
13.2 BigInt 序列化命令速查
| 操作 | 代码 | 说明 |
|---|
| 判断 BigInt | typeof data === 'bigint' | 判断是否为 BigInt |
| 转换为 String | data.toString() | BigInt 转 String |
| 转换为 Number | Number.parseInt(str) | String 转 Number |
| 检查安全范围 | Number.isSafeInteger(num) | 检查是否安全 |
13.3 拦截器命令速查
| 操作 | 代码 | 说明 |
|---|
| 定义拦截器 | implements NestInterceptor | 实现拦截器接口 |
| 拦截请求 | intercept(context, next) | 拦截方法 |
| 处理响应 | next.handle().pipe(map(...)) | 处理响应数据 |
| 应用拦截器 | @UseInterceptors(Interceptor) | 应用装饰器 |
| 全局应用 | app.useGlobalInterceptors() | 全局应用 |
十四、学习要点总结
14.1 核心知识点
本文核心要点:
│
├── 数据结构化处理
│ ├── 使用 map 重组数据
│ ├── 使用 reduce 合并数组
│ └── 扁平化嵌套结构
│
├── BigInt 序列化问题
│ ├── 问题:JSON.stringify 无法序列化 BigInt
│ ├── 原因:BigInt 类型不在 JSON 规范中
│ └── 影响:响应时序列化失败
│
├── 三种解决方案
│ ├── 方案一:手动序列化函数
│ ├── 方案二:修改原型链(不推荐)
│ └── 方案三:使用拦截器(推荐)
│
├── 拦截器使用
│ ├── 定义:implements NestInterceptor
│ ├── 应用:@UseInterceptors()
│ └── 范围:Controller 级别或方法级别
│
└── 最佳实践
├── 推荐使用拦截器
├── 避免修改原型链
└── 保持代码优雅可维护
14.2 重要程度标注
| 知识点 | 重要程度 | 说明 |
|---|
| 数据结构化处理 | 必须掌握 | 前端数据友好 |
| BigInt 序列化问题 | 必须掌握 | 常见问题 |
| 拦截器使用 | 必须掌握 | NestJS 核心功能 |
| reduce 方法 | 重要 | 数据处理技巧 |
| 原型链修改 | 了解 | 不推荐使用 |
14.3 学习路径规划
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数据结构化原理
│ ├── 理解 BigInt 序列化问题
│ └── 理解拦截器工作原理
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现数据结构化
│ ├── 实现三种序列化方案
│ └── 测试拦截器功能
│
└── 第三阶段:深入应用(持续)
├── 扩展拦截器功能
├── 性能优化
└── 其他序列化场景
十五、完整代码清单
// src/common/interceptors/bigint-transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class BigIntTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.serializeBigInt(data)),
);
}
private serializeBigInt(data: any): any {
if (typeof data === 'bigint') {
return data.toString();
}
if (Array.isArray(data)) {
return data.map((item) => this.serializeBigInt(item));
}
if (typeof data === 'object' && data !== null) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
data[key] = this.serializeBigInt(data[key]);
}
}
}
return data;
}
}
15.2 CourseService 完整代码
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
async getCoursesByType(dto: GetCoursesByTypeDto) {
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
const res = await this.prisma.courseTypes.findMany({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
if (res && res.length > 0) {
return res.map((item) => ({
id: item.id,
name: item.name,
courses: item.tags.reduce((acc, tag) => {
const courses = tag.courses.map((c) => c.course);
return [...acc, ...courses];
}, []),
}));
}
return [];
}
}
15.3 CourseController 完整代码
// src/modules/course/course.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common';
import { BigIntTransformInterceptor } from '@/common/interceptors/bigint-transform.interceptor';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
@UseInterceptors(BigIntTransformInterceptor)
export class CourseController {
constructor(private readonly courseService: CourseService) {}
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}
重要提示:数据结构化和 BigInt 序列化是后端开发的常见需求,掌握 map 和 reduce 的使用、理解 BigInt 序列化问题、学会使用拦截器优雅解决问题,对实际项目开发非常重要!推荐使用拦截器方案,代码优雅、可维护性强、符合 NestJS 最佳实践!
扩展建议:可以尝试实现日志拦截器、响应格式化拦截器、缓存拦截器等功能,进一步掌握拦截器的应用!
NestJS 数据脱敏与敏感数据处理实战
学习目标:掌握数据安全的重要性、敏感数据处理方案、class-transformer 使用、自定义拦截器、自定义装饰器。
一、数据安全需求背景
1.1 为什么需要数据脱敏
数据安全的重要性:
│
├── 数据类型安全
│ ├── 类型校验
│ ├── 格式校验
│ └── 范围校验
│
├── 业务层面安全
│ ├── 敏感数据不能响应给前端
│ ├── 密码不能明文存储
│ └── 个人隐私数据保护
│
└── 常见敏感数据
├── password:用户密码
├── phone:手机号码
├── idCard:身份证号码
├── bankCard:银行卡号
└── address:详细地址
1.2 敏感数据处理的两层考虑
敏感数据处理两层考虑:
│
├── 第一层:入库加密
│ ├── 密码不能明文存储
│ ├── 使用 bcrypt 加密
│ ├── 通用摘要可用 SHA256(密码场景别用 MD5,MD5 已不安全)
│ └── 对应:NestJS 鉴权相关章节
│
└── 第二层:响应脱敏
├── 敏感数据不能响应给前端
├── 删除或隐藏敏感字段
├── 数据序列化时过滤
└── 对应:本节内容
1.3 数据脱敏常见方案
| 方案 | 优点 | 缺点 | 推荐度 |
|---|
| 手动删除字段 | 实现简单 | 代码重复、维护性差 | 一般 |
| Helper 函数 | 复用性好 | 需要手动调用 | 一般 |
| 拦截器处理 | 自动应用、集中管理 | 需要额外配置 | 推荐 |
| 装饰器标记 | 优雅、可维护 | 需要定义 DTO | 推荐 |
class-transformer 简介:
│
├── 作用
│ ├── 对象与类实例之间的转换
│ ├── 属性映射和转换
│ ├── 敏感数据过滤
│ └── 数据序列化和反序列化
│
├── 常用装饰器
│ ├── @Expose():暴露属性
│ ├── @Exclude():排除属性
│ ├── @Type():类型转换
│ └── @Transform():自定义转换
│
├── 常用方法
│ ├── plainToInstance():对象转类实例
│ ├── instanceToPlain():类实例转对象
│ └── serialize():序列化为字符串
│
└── 安装
└── npm install class-transformer class-validator
2.2 @Exclude 装饰器使用
// 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 装饰器原理
@Exclude() 装饰器原理:
│
├── 标记属性
│ └── 在属性上添加 @Exclude() 装饰器
│
├── 序列化时处理
│ ├── class-transformer 检查装饰器
│ ├── 发现有 @Exclude() 标记
│ └── 自动过滤该属性
│
└── 结果
└── 响应数据中不包含 password 字段
三、使用 ClassSerializerInterceptor
3.1 ClassSerializerInterceptor 简介
// 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 的局限性
ClassSerializerInterceptor 局限性:
│
├── 问题:只对类实例生效
│ ├── 生效:返回 UserDto 实例
│ └── 不生效:返回普通 JavaScript 对象
│
├── 原因
│ ├── 拦截器检查是否为类实例
│ ├── 如果是普通对象,直接返回
│ └── 不会应用装饰器
│
└── 解决方案
├── 确保返回类实例(手动 new)
└── 创建自定义拦截器(推荐)
3.3 问题示例
// 问题:返回普通对象,@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
使用 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 模板
Prompt 模板:
根据以下 JSON 数据生成 TypeScript 类:
1. 使用 class-validator 和 class-transformer 装饰器
2. 类名为 {ClassName}Dto
3. 嵌套子类中使用 class-transformer 的 @Type() 方法进行 transform 转换
4. 输出完整的类的属性定义,不能有 null
5. 嵌套的子类先定义,后再定义父类
JSON 数据:
{粘贴 JSON 数据}
4.3 生成的 DTO 示例
// 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 创建拦截器
# 创建拦截器(扁平结构,无测试文件)
nest g itc common/interceptors/serialize --no-spec --flat
5.2 自定义 SerializeInterceptor 实现
// 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 方法详解
plainToInstance 方法详解:
│
├── 作用
│ ├── 将普通对象转换为类实例
│ ├── 应用 class-transformer 装饰器
│ └── 执行属性映射和转换
│
├── 参数
│ ├── classType:目标类
│ ├── plain:普通对象
│ └── options:配置选项
│
├── 常用选项
│ ├── excludeExtraneousValues:是否排除未标记的属性
│ ├── enableCircularCheck:启用循环引用检查
│ ├── enableImplicitConversion:启用隐式类型转换
│ └── version:版本控制
│
└── excludeExtraneousValues 详解
├── true:只处理有 @Expose() 的属性
│ ├── 必须显式标记 @Expose()
│ └── 未标记的属性不显示
│
└── false:处理所有属性,排除 @Exclude() 的属性
├── 默认暴露所有属性
└── 只排除 @Exclude() 标记的属性
| 选项 | true | false |
|---|
| 行为 | 只显示 @Expose() 的属性 | 显示所有属性,排除 @Exclude() |
| 安全性 | 高(显式控制) | 中(隐式控制) |
| 配置方式 | 每个属性加 @Expose() | 敏感属性加 @Exclude() |
| 推荐度 | 推荐 | 可用 |
六、创建自定义装饰器
6.1 自定义装饰器实现
// 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 装饰器使用示例
// 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 两种装饰器对比
@Serialize() vs @SerializeStrict():
│
├── @Serialize(dto, false)
│ ├── 行为:显示所有属性,排除 @Exclude()
│ ├── 配置:敏感属性加 @Exclude()
│ ├── 优点:配置简单
│ └── 缺点:可能遗漏敏感属性
│
└── @SerializeStrict(dto)(推荐)
├── 行为:只显示 @Expose() 的属性
├── 配置:需要暴露的属性加 @Expose()
├── 优点:显式控制、更安全
└── 缺点:配置稍多
七、完整实战示例
7.1 项目结构
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 实现
// 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 装饰器实现
// 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 实现
// 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 实现
// 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 装饰器验证
请求配置:
│
├── 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 装饰器验证
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
└── 预期响应:
[
{
"id": 11,
"name": "推荐内容",
"courses": [...]
// 只显示 @Expose() 标记的属性
}
]
注意:
├── 如果 DTO 中没有 @Expose() 标记
└── 响应为空对象:{}
8.3 测试三:验证敏感数据已排除
验证步骤:
│
├── 第一步:发起请求
│ └── GET /courses
│
├── 第二步:检查响应数据
│ ├── 包含:id, username, email
│ └── 不包含:password
│
└── 第三步:确认数据脱敏成功
└── password 字段已成功排除
九、常见问题与解决方案
9.1 @Exclude 不生效问题
| 问题 | 原因 | 解决方案 |
|---|
| @Exclude() 不生效 | 返回的是普通对象,不是类实例 | 使用 plainToInstance 转换 |
| 装饰器未应用 | 未应用拦截器 | 添加 @Serialize 装饰器 |
| 属性仍然显示 | excludeExtraneousValues 设置错误 | 检查装饰器配置 |
9.2 plainToInstance 使用问题
// 错误:直接返回普通对象
return data;
// 正确:转换为类实例
return plainToInstance(Dto, data, {
excludeExtraneousValues: false,
});
9.3 嵌套对象转换问题
// 错误:嵌套对象未添加 @Type() 装饰器
export class CourseDto {
@Expose()
author: UserDto; // 不会被转换
}
// 正确:添加 @Type() 装饰器
export class CourseDto {
@Expose()
@Type(() => UserDto) // 指定类型
author: UserDto;
}
十、最佳实践总结
10.1 数据脱敏最佳实践
数据脱敏最佳实践:
│
├── 使用 @SerializeStrict() 装饰器
│ ├── 显式控制暴露的属性
│ ├── 更安全的数据控制
│ └── 避免遗漏敏感属性
│
├── DTO 设计规范
│ ├── 敏感属性加 @Exclude()
│ ├── 需暴露属性加 @Expose()
│ └── 嵌套对象加 @Type()
│
├── 拦截器使用
│ ├── Controller 级别应用
│ ├── 方法级别应用
│ └── 全局应用(谨慎)
│
└── 测试验证
├── 单元测试
├── 集成测试
└── 安全审计
10.2 DTO 设计最佳实践
DTO 设计最佳实践:
│
├── 分离关注点
│ ├── 数据库 DTO:数据库操作
│ ├── 响应 DTO:接口响应
│ └── 请求 DTO:接口请求
│
├── 命名规范
│ ├── PublicXxxDto:公开响应 DTO
│ ├── CreateXxxDto:创建请求 DTO
│ └── UpdateXxxDto:更新请求 DTO
│
├── 类定义顺序
│ ├── 子类先定义
│ └── 父类后定义
│
└── 装饰器使用
├── @Expose():暴露属性
├── @Exclude():排除属性
└── @Type():嵌套类型
10.3 安全性最佳实践
安全性最佳实践:
│
├── 敏感数据处理
│ ├── 密码加密存储(bcrypt)
│ ├── 响应时排除敏感字段
│ └── 日志中过滤敏感信息
│
├── 数据验证
│ ├── 入参验证(class-validator)
│ ├── 类型校验
│ └── 格式校验
│
├── 权限控制
│ ├── 角色权限验证
│ ├── 接口访问控制
│ └── 数据访问控制
│
└── 审计日志
├── 操作日志
├── 访问日志
└── 异常日志
十一、命令速查表
| 装饰器 | 作用 | 示例 |
|---|
| @Expose() | 暴露属性 | @Expose() id: number; |
| @Exclude() | 排除属性 | @Exclude() password: string; |
| @Type() | 类型转换 | @Type(() => UserDto) author: UserDto; |
| @Transform() | 自定义转换 | @Transform(({ value }) => value.toUpperCase()) |
11.2 plainToInstance 选项速查
| 选项 | 类型 | 默认值 | 说明 |
|---|
| excludeExtraneousValues | boolean | false | 排除未标记的属性 |
| enableCircularCheck | boolean | false | 启用循环引用检查 |
| enableImplicitConversion | boolean | false | 启用隐式类型转换 |
| version | number | - | 版本控制 |
11.3 NestJS CLI 命令速查
| 命令 | 说明 |
|---|
nest g itc <path> | 创建拦截器 |
nest g itc <path> --no-spec | 创建拦截器(无测试) |
nest g itc <path> --flat | 创建拦截器(扁平结构) |
十二、扩展应用场景
12.1 多版本响应 DTO
// 支持版本控制的 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 自定义转换函数
// 使用 @Transform() 自定义转换
export class UserDto {
@Expose()
@Transform(({ value }) => value.toUpperCase())
username: string;
@Expose()
@Transform(({ value }) => `***${value.slice(-4)}`)
phone: string; // 手机号脱敏:只保留后 4 位,如 ***1234
}
12.3 条件暴露属性
// 根据条件暴露属性
export class UserDto {
@Expose()
id: number;
@Expose({ groups: ['admin'] })
email: string;
@Expose({ groups: ['user'] })
username: string;
}
// 使用时指定分组
plainToInstance(UserDto, data, { groups: ['admin'] });
十三、学习要点总结
13.1 核心知识点
本文核心要点:
│
├── 数据脱敏的重要性
│ ├── 业务层面安全
│ ├── 敏感数据保护
│ └── 常见敏感数据类型
│
├── 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 学习路径规划
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数据脱敏的重要性
│ ├── 理解 class-transformer 原理
│ └── 理解拦截器工作机制
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 创建响应 DTO
│ ├── 实现自定义拦截器
│ └── 实现自定义装饰器
│
└── 第三阶段:深入应用(持续)
├── 多版本响应 DTO
├── 自定义转换函数
└── 条件暴露属性
十四、完整代码清单
14.1 SerializeInterceptor 完整代码
// 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 装饰器完整代码
// 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 完整代码
// 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 完整代码
// 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 的高级应用!