{T}

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.json

10.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 序列化命令速查

操作代码说明
判断 BigInttypeof data === 'bigint'判断是否为 BigInt
转换为 Stringdata.toString()BigInt 转 String
转换为 NumberNumber.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 最佳实践!

扩展建议:可以尝试实现日志拦截器、响应格式化拦截器、缓存拦截器等功能,进一步掌握拦截器的应用!