NestJS数据结构化与BigInt序列化实战
学习目标:掌握数据结构化处理、BigInt 序列化问题解决、NestJS 拦截器使用、三种序列化方案对比。
一、数据结构化处理需求
1.1 问题背景
code
数据结构化问题背景:
│
├── 数据库查询结果结构
│ ├── 分类数据嵌套层级深
│ ├── tags 数组包含 courses 数组
│ ├── courses 数组包含 course 对象
│ └── course 对象包含 author 对象
│
├── 前端期望的数据结构
│ ├── 简单清晰的层级
│ ├── 直接访问课程列表
│ ├── 不需要深层嵌套
│ └── 数据扁平化处理
│
└── 解决方案
├── 使用 map 方法重组数据
├── 使用 reduce 方法合并数组
└── 提取必要的字段信息1.2 原始数据结构 vs 期望数据结构
code
原始数据结构(嵌套过深):
│
└── 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 层数据结构化
typescript
// 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 数据结构化流程详解
code
数据结构化流程:
│
├── 第一步:遍历分类(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 方法详解
code
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 问题描述
code
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 问题复现与定位
typescript
// 问题定位步骤
// 第一步:确认数据库查询成功
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 序列化原理
code
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 函数实现
typescript
// 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 中使用
typescript
// 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 方案一优缺点
code
方案一:手动序列化函数
│
├── 优点
│ ├── 实现简单
│ ├── 逻辑清晰
│ └── 可控性强
│
└── 缺点
├── 每次都需要手动调用
├── 容易遗忘
├── 代码重复
└── 维护性差五、解决方案二:修改原型链
5.1 修改 BigInt.prototype.toJSON
typescript
// 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 方案二原理
code
方案二原理:
│
├── 工作原理
│ ├── 修改 BigInt.prototype.toJSON 方法
│ ├── JSON.stringify() 调用 toJSON() 进行序列化
│ └── 自动处理所有 BigInt 类型数据
│
├── 代码逻辑
│ ├── BigInt.toString():转为字符串
│ ├── Number.parseInt():尝试转为数字
│ ├── Number.isSafeInteger():检查是否安全
│ └── 返回 Number 或 String
│
└── 生效范围
├── 全局生效
├── 所有 BigInt 类型自动处理
└── 无需手动调用5.3 方案二优缺点
code
方案二:修改原型链
│
├── 优点
│ ├── 实现简单
│ ├── 全局生效
│ └── 无需手动调用
│
└── 缺点
├── 修改原型链(不安全)
├── 全局影响(不可控)
├── 可能影响其他代码
├── 不符合最佳实践
└── 不推荐使用 六、解决方案三:使用拦截器(推荐)
6.1 拦截器基础概念
code
NestJS 拦截器(Interceptor):
│
├── 作用
│ ├── 在请求到达 Controller 前做处理
│ ├── 在响应返回客户端前做处理
│ └── 实现面向切面编程(AOP)
│
├── 使用场景
│ ├── 日志记录
│ ├── 响应数据转换
│ ├── 异常处理
│ ├── 缓存
│ └── 性能监控
│
├── 工作流程
│ ├── 客户端请求 → 拦截器 → Controller
│ └── Controller → 拦截器 → 客户端响应
│
└── 装饰器
├── @UseInterceptors():应用拦截器
├── Controller 级别:整个控制器生效
└── 方法级别:单个方法生效6.2 创建 BigInt 拦截器
typescript
// 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 中使用拦截器
typescript
// 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 拦截器工作流程
code
拦截器工作流程:
│
├── 第一步:客户端发起请求
│ └── GET /courses/by-type
│
├── 第二步:拦截器拦截请求(可选)
│ └── intercept() 方法执行
│
├── 第三步:Controller 处理请求
│ └── getCoursesByType() 方法执行
│
├── 第四步:拦截器拦截响应
│ ├── next.handle() 获取响应数据
│ └── pipe(map()) 处理响应数据
│
├── 第五步:序列化 BigInt
│ ├── 遍历响应数据
│ └── 将 BigInt 转换为 String
│
└── 第六步:返回处理后的响应
└── 自动调用 JSON.stringify()6.5 方案三优缺点
code
方案三:使用拦截器(推荐)
│
├── 优点
│ ├── NestJS 官方推荐方式
│ ├── 符合 AOP 编程思想
│ ├── 代码优雅、可维护
│ ├── 可控性强(可选择应用范围)
│ ├── 支持 Controller 级别或方法级别
│ └── 易于测试和扩展
│
└── 缺点
├── 学习成本稍高
└── 需要理解 RxJS七、三种方案对比
7.1 实现复杂度对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|---|---|---|
| 实现难度 | 低 | 低 | 中 |
| 代码量 | 较少 | 最少 | 适中 |
| 学习成本 | 低 | 低 | 中 |
7.2 使用便捷性对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|---|---|---|
| 使用方式 | 手动调用 | 自动应用 | 装饰器应用 |
| 作用范围 | 单个方法 | 全局 | 可选择 |
| 可维护性 | 低 | 低 | 高 |
7.3 安全性对比
| 维度 | 方案一:手动函数 | 方案二:原型链 | 方案三:拦截器 |
|---|---|---|---|
| 代码安全 | 安全 | 不安全 | 安全 |
| 全局影响 | 无 | 有 | 无 |
| 推荐度 | (不推荐) |
7.4 推荐选择
code
方案选择建议:
│
├── 推荐使用方案三:拦截器
│ ├── 优点:优雅、可控、符合最佳实践
│ ├── 缺点:学习成本稍高
│ └── 适用:所有项目
│
├── 方案一:手动函数
│ ├── 优点:简单、直接
│ ├── 缺点:维护性差
│ └── 适用:小型项目、快速原型
│
└── 方案二:原型链 (不推荐)
├── 优点:全局生效
├── 缺点:不安全、不推荐
└── 适用:无(不推荐使用)八、拦截器扩展应用
8.1 日志拦截器
typescript
// 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 响应格式化拦截器
typescript
// 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 全局应用拦截器
typescript
// 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 测试一:数据结构化验证
code
请求配置:
│
├── 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 序列化验证
code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
└── 预期响应:
├── 状态码:200
├── BigInt 字段已转换为 String
└── 无序列化错误9.3 测试三:拦截器生效验证
code
验证步骤:
│
├── 第一步:应用拦截器
│ └── @UseInterceptors(BigIntTransformInterceptor)
│
├── 第二步:发起请求
│ └── GET /courses/by-type
│
├── 第三步:检查响应
│ ├── 状态码:200
│ └── BigInt 字段正常显示
│
└── 第四步:移除拦截器
├── 状态码:500
└── 错误:BigInt 序列化失败十、完整实战示例
10.1 项目结构
code
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.json10.2 完整 BigInt 拦截器实现
typescript
// 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 实现
typescript
// 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 实现
typescript
// 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 拦截器使用问题
typescript
// 错误:忘记导入 RxJS
import { map } from 'rxjs'; // 错误
// 正确:从 rxjs/operators 导入
import { map } from 'rxjs/operators'; // 正确
// 错误:拦截器未应用
@Controller('courses')
export class CourseController { ... }
// 正确:应用拦截器
@Controller('courses')
@UseInterceptors(BigIntTransformInterceptor)
export class CourseController { ... }十二、最佳实践总结
12.1 数据结构化最佳实践
code
数据结构化最佳实践:
│
├── 使用 map 方法
│ ├── 遍历数组
│ ├── 提取需要的字段
│ └── 重组数据结构
│
├── 使用 reduce 方法
│ ├── 合并多个数组
│ ├── 累计计算
│ └── 数组去重
│
├── 数据扁平化
│ ├── 减少嵌套层级
│ ├── 提高可读性
│ └── 便于前端使用
│
└── 保持数据完整性
├── 包含必要字段
├── 添加关联数据
└── 避免数据丢失12.2 BigInt 序列化最佳实践
code
BigInt 序列化最佳实践:
│
├── 推荐使用拦截器
│ ├── 符合 NestJS 最佳实践
│ ├── 代码优雅可维护
│ └── 可选择应用范围
│
├── 避免修改原型链
│ ├── 不安全
│ ├── 全局影响
│ └── 不符合最佳实践
│
├── 统一转换策略
│ ├── BigInt → String(推荐)
│ ├── BigInt → Number(不安全)
│ └── 保持一致性
│
└── 测试验证
├── 单元测试
├── 集成测试
└── 边界情况测试12.3 拦截器使用最佳实践
code
拦截器使用最佳实践:
│
├── 命名规范
│ ├── 以 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 核心知识点
code
本文核心要点:
│
├── 数据结构化处理
│ ├── 使用 map 重组数据
│ ├── 使用 reduce 合并数组
│ └── 扁平化嵌套结构
│
├── BigInt 序列化问题
│ ├── 问题:JSON.stringify 无法序列化 BigInt
│ ├── 原因:BigInt 类型不在 JSON 规范中
│ └── 影响:响应时序列化失败
│
├── 三种解决方案
│ ├── 方案一:手动序列化函数
│ ├── 方案二:修改原型链(不推荐)
│ └── 方案三:使用拦截器(推荐)
│
├── 拦截器使用
│ ├── 定义:implements NestInterceptor
│ ├── 应用:@UseInterceptors()
│ └── 范围:Controller 级别或方法级别
│
└── 最佳实践
├── 推荐使用拦截器
├── 避免修改原型链
└── 保持代码优雅可维护14.2 重要程度标注
| 知识点 | 重要程度 | 说明 |
|---|---|---|
| 数据结构化处理 | 必须掌握 | 前端数据友好 |
| BigInt 序列化问题 | 必须掌握 | 常见问题 |
| 拦截器使用 | 必须掌握 | NestJS 核心功能 |
| reduce 方法 | 重要 | 数据处理技巧 |
| 原型链修改 | 了解 | 不推荐使用 |
14.3 学习路径规划
code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数据结构化原理
│ ├── 理解 BigInt 序列化问题
│ └── 理解拦截器工作原理
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现数据结构化
│ ├── 实现三种序列化方案
│ └── 测试拦截器功能
│
└── 第三阶段:深入应用(持续)
├── 扩展拦截器功能
├── 性能优化
└── 其他序列化场景十五、完整代码清单
15.1 BigIntTransformInterceptor 完整代码
typescript
// 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 完整代码
typescript
// 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 完整代码
typescript
// 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 最佳实践!
扩展建议:可以尝试实现日志拦截器、响应格式化拦截器、缓存拦截器等功能,进一步掌握拦截器的应用!