普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月5日首页

第6章 Postgres数据库安装

作者 XiaoYu2002
2026年1月5日 10:13

PostgreSQL是世界上最先进的开源对象关系型数据库,比MySQL会更好用一些。

下载地址:PostgreSQL: Downloads,从该页面选择适合自己电脑系统的下载包,有Linux、macOS、Windows,BSD和Solaris五个选项。

对于Windows系统而言,下载地址为:EDB: Open-Source, Enterprise Postgres Database Management。选择PostgreSQL Version里最前列的版本(最新版本),在我下载时,最新版本为18.1,实际最新版本以你下载时为准,选择Windows x86-64选项进行下载。如图6-1所示。

image-20251214194715316

图6-1 PostgreSQL安装包下载地址

下载安装包之后,不断点击Next(下一步)就行。安装过程会确定以下11点步骤:

(1)Welcome to the PostgreSQL Setup Wizard(欢迎来到PostgreSQL安装向导),点击Next。

(2)安装目录(默认/自定义),通常默认就行。

(3)选择需要安装的组件(Select Components),默认勾选PostgreSQL Server、pgAdmin 4、Stack Builder和Command Line Tools四个选项,直接点击Next。

(4)创建存放数据的目录(Data Directory),就默认安装目录,继续点击Next。

(5)Password,输入登录密码(自定义),通常推荐123456,免得自己也忘了,然后点击Next。

(6)Post,分配给PostgreSQL的端口,默认分配端口可能每个人都不一样,但大概率是5432,直接点击Next。

(7)Advanced Options(高级选项),默认选择,点击Next。

(8)Pre Installation Summary(安装前的总结),直接点击Next。

(9)Ready to Install(准备安装),点击Next。

(10)等待安装结束,弹出Completing the PostgreSQL Setup Wizard(完成PostgreSQL安装向导),勾选Stack Builder may be used to download... 用于补充其他工具。

(11)弹出Stack Builder安装界面继续不断的Next就行,中途要输密码输密码,要选择安装应用程序就选择EDB Language Pack(EDB语言安装包)就行。

安装结束之后,我们需要确认是否安装成功。打开vscode或者cursor等编辑器,下载插件Database Client(数据可视化工具),然后使用。

PostgreSQL可视化工具如图6-2所示。服务类型选择PostgreSQL,输入密码123456。

image-20251214200916239

图6-2 PostgreSQL可视化工具

PostgreSQL安装成功如图6-3所示。当连接PostgreSQL数据库成功,说明PostgreSQL安装成功。

image-20251214201124048

图6-3 PostgreSQL安装成功

昨天以前首页

第4章 Nest.js业务合并

作者 XiaoYu2002
2026年1月3日 13:40

第4章 Nest.js业务合并

在实际项目中,不同的业务操作需要明确的反馈信息。例如:

  • 登录操作返回消息为「登录成功」,状态码为 1。
  • 注册操作返回消息为「注册成功」,状态码为 2。

状态码 作为业务逻辑判断的操作标识,是一个数字或字符串标识符。不同数值代表不同的业务含义(如1表示登录成功、2表示注册成功),前端或调用方可根据具体数值执行相应的逻辑分支。业务描述 则面向用户或开发者,用自然语言清晰传达操作结果,提供直观的反馈信息,辅助理解状态码对应的具体业务场景。

在全局拦截器的使用中,message 和 code 这两个参数需要根据业务需求进行自定义。下面将介绍如何在 Nest.js 中对这两个参数,以及更多同类参数进行规范化管理。

// src/interceptor/interceptor.interceptor.ts
return {
  timestmap: new Date().toISOString(),
  data: transformBigInt(data),
  path: request.url,
  message: 'success',//业务逻辑自定义
  code: 200,//业务逻辑自定义
  success: true,
};

要对全局拦截器的统一返回数据格式中的 message 和 code 进行自定义,应从专门的业务定义文件中引入自定义的业务状态码和描述信息,然后传递给 message 和 code 参数。

在自定义参数时,有一个重要原则:message 和 code 的自定义数据需要与业务逻辑层分离。这些数据应作为纯粹的配置信息使用,类似于 JSON 配置文件。业务层则建立在统一的业务规范基础上进行进一步封装。

首先创建一个响应格式模块(response module)和服务(response service),用于统一处理业务响应。通过 Nest CLI 命令生成的 response.service.ts 文件会自动关联到 response.module.ts 文件中。

nest g mo response
nest g s response

在 src 目录下创建 business 文件夹,并在其中创建 index.ts 文件。该文件专门存放自定义的业务状态码和描述信息,我们只关心每个业务操作对应的状态码和消息内容。后续新增业务需求时,只需在此文件中添加相应的状态码和消息。

注意:业务字段命名通常采用大写字母和下划线组合的格式,即「功能_状态」的表达方式,如 LOGIN_SUCCESS。

// src/business/index.ts
export const business = {
  LOGIN_SUCCESS: {
    code: 1,
    message: '登录成功'
  },
  LOGIN_ERROR: {
    code: 2,
    message: '登录失败'
  },
  REGISTER_SUCCESS: {
    code: 2,
    message: '注册成功'
  },
}

在 response 目录的 response.service.ts 文件中编写具体的响应格式逻辑,定义操作成功和操作失败时需要返回的数据结构。

// src/response/response.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ResponseService {
  success(data: any = null, message: string = '操作成功', code: number = 200) {
    return {
      data,
      message,
      code
    }
  }
  error(message: string = '操作失败', code: number = 500) {
    return {
      message,
      code
    }
  }
}

如果我们想要使用自定义的业务状态码,要如何使用?假设要在user模块的业务层中使用。

// src/user/user.service.ts(业务层)
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    return `This action returns all user`;
  }
 // 省略...
}

假设需要在 user 模块的业务层中使用自定义业务状态码,操作步骤如下:

(1)引入依赖文件:同时引入业务定义文件和响应格式文件。

(2)依赖注入:在 UserService 类中注入 ResponseService 类。

(3)使用响应格式:在 findAll() 方法中使用 ResponseService 类,按照 data、message 和 code 的顺序传入数据。

(4)引用业务定义:message 和 code 从 business 业务文件中读取,本次「登录成功操作」使用业务字段 LOGIN_SUCCESS。

// src/user/user.service.ts(业务层)
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { ResponseService } from 'src/response/response.service';
import { business } from 'src/business';
@Injectable()
export class UserService {
  constructor(private readonly responseService: ResponseService) { }
  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    // 登录成功的消息内容和业务码
    const message = business.LOGIN_SUCCESS.message;
    const code = business.LOGIN_SUCCESS.code;
    return this.responseService.success('This action returns all user', message, code);
  }
}

此时启动项目,访问 localhost:3000/user 路由应返回登录成功的业务状态码。但可能出现以下错误:

RROR [ExceptionHandler] UnknownDependenciesException [Error]: Nest can't resolve dependencies of the UserService (?). Please make sure that the argument ResponseService at index [0] is available in the UserModule context.

错误信息表明 Nest 无法解析 UserService 的依赖,需要确保 ResponseService 在 UserModule 上下文中可用。这是因为 ResponseService 未被正确导出和导入。

解决上述问题需要如下3个步骤:

(1)导出服务:在 response.module.ts 文件中将 ResponseService 添加到 exports 数组中。

(2)导入模块:在 user.module.ts 文件中导入 ResponseModule 模块。

(3)完成注入:此时 UserModule 可以读取到 ResponseModule 导出的 ResponseService,UserService 才能正常使用 ResponseService。

// src/response/response.module.ts
import { Module } from '@nestjs/common';
import { ResponseService } from './response.service';

@Module({
  providers: [ResponseService],
  exports: [ResponseService]
})
export class ResponseModule { }

在user.module.ts 文件中导入 ResponseModule 模块,完成导入模块。

// src/user/user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { ResponseModule } from 'src/response/response.module';
@Module({
  imports: [ResponseModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

完成以上配置后,重新启动项目访问 http://localhost:3000/user,可见 data、message 和 code 三个业务字段的信息都成功输出。

image-20251213233534395

图4-1 业务字段信息插入

接下来只需从全局拦截器中重新统一数据格式即可。

// src/interceptor/interceptor.interceptor.ts
return {
  timestmap: new Date().toISOString(),
  data: transformBigInt(data.data) ?? null,
  path: request.url,
  message: data.message ?? 'success',//业务逻辑自定义
  code: data.code ?? 200,//业务逻辑自定义
  success: true,
};

统一业务字段格式如图4-2所示。

image-20251213234031967

图4-2 统一业务字段格式

通过以上步骤,Nest.js 的业务状态码和业务状态描述已成功应用到对应的 user 模块接口中。但在实现过程中,我们发现每个使用 ResponseService 的模块都需要:

(1)在提供模块中导出服务。

(2)在使用模块中导入模块。

如果项目有十几个模块都需要使用,这种重复导入导出的操作会变得繁琐。为此,可以将 response 模块注册为全局模块,这样在整个项目中都可以直接使用,无需在每个使用模块的 module.ts 文件中重复导入。

将 response 模块注册为全局模块的方法为以下2步:

(1)从 @nestjs/common 中导入 Global 装饰器

(2)将 @Global() 装饰器应用到 ResponseModule 上

注册为全局模块后,ResponseService 可以在任何模块中直接使用,无需在各使用模块中导入 ResponseModule。

// src/response/response.module.ts
import { Module, Global } from '@nestjs/common';
import { ResponseService } from './response.service';

@Global()
@Module({
  providers: [ResponseService],
  exports: [ResponseService]
})
export class ResponseModule { }

此时如果我们回到user.module.ts文件中,将刚才注册的ResponseModule删除,项目也不会报错。

对于全局模块的使用,若我想在xiaoyu模块中使用ResponseService,主要步骤如下,无需在XiaoyuModule中导入ResponseModule:

(1)在 xiaoyu.service.ts 文件中导入 ResponseService 服务类和 business 业务常量。

(2)在 XiaoyuService 的构造函数中注入 ResponseService,并使用其方法按照业务数据格式规范处理数据。

(3)在 xiaoyu.controller.ts 文件的控制器中注入 XiaoyuService,并在路由处理器中调用其业务方法返回处理结果。

注意:虽然 ResponseService 是全局模块无需导入,但 XiaoyuModule 仍需要在自身的 providers 中注册 XiaoyuService,在 controllers 中注册 XiaoyuController。

// src/xiaoyu/xiaoyu.service.ts
import { Injectable } from '@nestjs/common';
import { ResponseService } from 'src/response/response.service';
import { business } from 'src/business';
@Injectable()
export class XiaoyuService {
  constructor(private readonly responseService: ResponseService) {}
  getHello(): any {
    const message = business.LOGIN_SUCCESS.message;
    const code = business.LOGIN_SUCCESS.code;
    return this.responseService.success('This action returns all user', message, code);
  }
}
// src/xiaoyu/xiaoyu.controller.ts
import { Controller, Get } from '@nestjs/common';
import { XiaoyuService } from './xiaoyu.service';

@Controller('xiaoyu')
export class XiaoyuController {
  // 依赖注入
  constructor(private readonly xiaoyuService: XiaoyuService) {}
  @Get()
  getHello(): any {
    return this.xiaoyuService.getHello();
  }
}

xiaoyu模块业务合并如图4-3所示。

image-20251214004117303

图4-3 xiaoyu模块业务合并

以上就是Nest.js业务层的所有内容,我们回顾一下,Nest.js 的业务处理分为 business 和 response 两个部分:

(1)business 文件夹:集中管理业务状态码和描述信息。

(2)response 模块:专门用于构建统一的响应格式。

在实际项目中,这两者的变化频率不同:响应格式通常保持稳定,而业务状态码会随着业务发展不断新增或调整。因此需要将两者分离管理。response 模块的功能与全局拦截器中统一返回客户端响应格式的功能是一致的,区别在于我们将自定义部分拆分出来,提高了灵活性和可维护性,更符合实际项目的需求变化。

这种架构设计的优势在于:业务状态定义与响应格式构建职责明确(单一职责);业务状态变化不影响响应格式,响应格式调整不影响业务逻辑;统一的响应格式可跨模块、跨项目使用;新增业务状态只需在 business 文件中添加,不影响现有结构。通过这种规范化管理,可以构建出清晰、可维护、可扩展的业务层架构,适应各种复杂的业务场景需求。

除了message和code这两个字段,我们还可以有权限与安全控制(例如IP白名单、频率限制、黑白名单)、数据持久化(创建、读取、更新、删除业务数据)、业务流程(多级审批、会签、或签逻辑)、第三方集成(支付宝、微信支付回调处理)、监控与统计(接口响应时间、成功率)等多方面基于实际业务需求去增添。

第3章 Nest.js拦截器

作者 XiaoYu2002
2026年1月2日 12:30

3.1 拦截器介绍

Nest.js的拦截器和axios的拦截器是类似的,可以在网络请求处理的前后去执行额外的逻辑。拦截器从字面意思理解就是拦截,假设有流程A->B,拦截器要做的是A到B的过程中,将内容拦截下来处理后再丢给B,变成了A->拦截器->B。

在网络请求的逻辑中,拦截器的拦截位置如下:

  • 客户端请求->拦截器(前置逻辑)->路由处理器->拦截器(后置逻辑)->客户端响应。

Nest.js拦截器效果如图3-1所示。

image-20251212195404071

图3-1 Nest.js拦截器

Nest.js拦截器主要的用途有以下5点:

(1)统一响应格式:将返回数据包装成统一格式。

(2)日志记录:记录请求耗时、请求参数等信息。

(3)缓存处理:对响应数据进行缓存。

(4)异常映射:转换异常类型。

(5)数据转换:对响应数据进行序列化/转换。

在英语AI项目中,主要使用到第5点数据转换,因此我们主要学习这一点。

3.2 拦截器创建

如表1-2所示,可以通过nest g itc interceptor快速创建一个拦截器(interceptor可以替换为任何你想取的拦截器名称)。通过该命令会在src文件夹下创建interceptor文件夹,而interceptor文件夹下存放interceptor.interceptor.ts文件。

根据命令的生成规则,我们知道文件夹和文件的名称取决于我们命令对拦截器的名称,从而生成xxx文件夹和xxx.interceptor.ts文件。并且在这唯一的文件中,会提前生成好对应的Demo示例。

//src/interceptor/interceptor.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

拦截器有两种使用方式:

(1)局部使用。

(2)全局使用。

当想局部使用时,例如只想在src文件夹下的user模块使用,我们只需要注册到user模块中。那怎么注册?有3种注册方式,在user.module.ts、user.controller.ts以及user.controller.ts都可以注册,最主要的区别在于局部作用范围不同。Nest.js拦截器局部注册如表3-1所示。

表3-1 Nest.js拦截器局部注册

注册方式 作用范围 代码位置 优点 缺点
模块级别 整个模块所有控制器 user.module.ts 统一管理,自动应用到所有路由 无法灵活排除某些路由
控制器级别 单个控制器所有路由 user.controller.ts 控制器粒度控制 需在每个控制器添加装饰器
路由级别 单个路由方法 user.controller.ts 最精细的控制 代码重复,管理复杂

局部使用的具体代码不演示,可通过AI或者官方文档学习使用。

3.3 全局拦截器使用

在英语AI项目中会使用到全局使用,我们这里学习具体如何全局使用。步骤为以下2步:

(1)使用nest g itc <拦截器名称>快速创建一个拦截器。

(2)将拦截器注册到main.ts文件中,即在main.ts文件中导入刚创建的拦截器,并且使用Nest应用程序实例方法useGlobalInterceptors()。

// main.ts文件
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InterceptorInterceptor } from './interceptor/interceptor.interceptor';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new InterceptorInterceptor());
  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

当然由于InterceptorInterceptor拦截器是一个类,所以我们需要使用new运算符创建拦截器的实例以供使用。到这里,InterceptorInterceptor拦截器就是全局使用,即每一个接口都会经过该拦截器。Nest.js全局注册的官方文档如图3-2所示。

image-20251212212343423

图3-2 Nest.js拦截器全局注册

此时来编写InterceptorInterceptor拦截器内的逻辑,可见引入了来自rxjs的Observable类,rxjs是Nest.js内部自带的,主要用于处理流的,使用频率不高。通常获取数据需要区分同步与异步,同步直接获取,而异步通过Promise的then或者catch方法获取。如果此时有rxjs,就不需要我们去关注获取的数据是同步或者异步的问题,减少心智负担。rxjs会将这些数据统一转成一个数据流,然后通过管道(pipe)去接收,接收到之后可由我们处理该数据格式,无论是通过map遍历处理还是filter过滤等等,最终将处理好的数据格式返回就行。

以上是rxjs的核心理念,除此之外,它还可以同时处理多个异步,而then或者catch方法每次只能处理一个。

像InterceptorInterceptor拦截器中的所返回的next.handle()就是一个Observable(数据流),所以我们需要通过pipe(管道)去接收数据然后使用rxjs的map方法对数据处理之后再返回数据。

我们将原始数据包裹在一个标准响应结构中,添加了时间戳、请求路径、业务状态码、成功标志和自定义消息。这样确保了所有经过此拦截器的HTTP响应都遵循统一的JSON格式,包括 { timestamp, data, path, message, code, success } 等标准化字段,前端可以统一处理和错误追踪。

// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';

@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 将通用的执行上下文切换到HTTP特定的上下文
    const ctx = context.switchToHttp();
    // 获取当前HTTP请求的详细对象,包含了请求方法、URL、请求头、参数、主体等所有信息。
    const request = ctx.getRequest<Request>();
    return next.handle().pipe(map((data) => {
      return {
        timestmap: new Date().toISOString(),
        data: data,
        path: request.url,
        message: 'success',//业务逻辑自定义
        code: 200,//业务逻辑自定义
        success: true,
      };
    }));
  }
}

此时在浏览器的URL输入http://localhost:3000/user/123,访问带参数的get请求,get请求拦截效果如图3-3所示。在这里体现的是:路由处理器->拦截器(后置逻辑)->客户端响应。

image-20251212220853053

图3-3 Nest.js全局拦截器-get请求拦截效果

message和code字段属于业务逻辑的部分,后续完成英语AI项目时,会根据业务实际逻辑去自定义设置。

3.4 优化全局拦截器

但此时全局拦截器还有一个很大的Bug,假如接口返回一个很大的数据,我们通过BigInt数据类型去处理返回,那么在通过全局拦截器时就会出现报错情况,全局拦截器处理BigInt类型报错如图3-4所示。

image-20251212222211323

图3-4 全局拦截器处理BigInt类型报错

报错是error TS2322: Type 'bigint' is not assignable to type 'string'。即bigint类型无法赋值给string类型,这是很正常的。因为全局拦截器的这些参数都是通过JavaScript标准内置对象JSON.stringify()进行格式化的,而JSON.stringify()是没办法处理BigInt值的。在MDN文档中是这样表述这一异常情况:当尝试去转换 BigInt类型的值会抛出TypeError("BigInt value can't be serialized in JSON")(BigInt 值不能 JSON 序列化)。

// src/app.service.ts文件
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello() {
    return BigInt(123456789123456789123456789)
  }
}

所以我们需要针对BigInt类型的值去处理,通过编写transformBigInt方法去单独处理这一情况,主要处理的事情是当遇到BigInt类型的值就将它转成一个字符串。

const transformBigInt = (data: any) => {
  if (typeof data === 'bigint') {
    return data.toString();
  }
  return data;
};

此时将接口(get请求)返回给用户的data数据放入transformBigInt方法中即可。

// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';

const transformBigInt = (data: any) => {
  if (typeof data === 'bigint') {
    return data.toString();
  }
  return data;
};

@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const request = ctx.getRequest<Request>();
    return next.handle().pipe(map((data) => {
      return {
        timestmap: new Date().toISOString(),
        data: transformBigInt(data),
        path: request.url,
        message: 'success',//业务逻辑自定义
        code: 200,//业务逻辑自定义
        success: true,
      };
    }));
  }
}

但此时还会报错同样的问题(Type 'bigint' is not assignable to type 'string'),这是很正常的。我们来梳理下流程:

(1)接口返回数据给前端。

(2)全局拦截器拦截接口返回的数据进行处理。

(3)全局处理后的数据返回给前端。

我们已经在全局拦截器中处理好类型转换问题(BigInt转String),如果还有问题,就只能在第一步的接口返回数据给前端的步骤中。前端访问的是接口,而接口是体现形式是路由,路由层从业务层获取数据返回给前端。因此在业务层的数据是BigInt类型,则路由层所拿到的数据也会是BigInt类型。由于Nest.js是强制使用TypeScript的,所以我们需要到app.controller.ts文件中将get默认请求所返回的类型限制从string改成any类型或者string和bigint的联合类型。此时就能正常运行代码。

// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Get()
  getHello(): string | bigint {
    return this.appService.getHello();
  }
}

出于严谨的考虑,我们需要处理相应的边界判断,假如BigInt类型在数组里,在对象里呢?原有的处理方式就又解析不了了。

return [BigInt(123456789123456789123456789)];
return { a: BigInt(123456789123456789123456789) };

所以需要进一步强化transformBigInt方法,对数组遍历处理内部可能存在的BigInt类型,而对象则通过Object.entries()静态方法将对象切换成保存键值对的二维数组后,遍历键值对并针对其中的value值处理可能存在的BigInt类型,最后通过Object.fromEntries()静态方法将键值对形式的二维数组重新转换回原始对象。

  • 对象打印效果:{ foo: "bar", baz: 42 }。

  • 将可迭代对象切成二维数组:[ ['foo', 'bar'], ['baz', 42] ]。

将对象切成二维数组更方便找到键值对的值并进行遍历操作。

const transformBigInt = (data: any) => {
  if (typeof data === 'bigint') {
    return data.toString();
  }
  if(Array.isArray(data)){
    return data.map(transformBigInt);
  }
  if(typeof data === 'object' && data !== null){
    return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
  }
  return data;
};

做完以上的优化后,我们会发现接口要返回Date日期没办法正常返回给前端了,因为我们把对象全部都处理了,而JavaScript标准内置对象Date的使用是通过new运算符调用的实例对象,实例对象也是对象,也会被transformBigInt方法一并处理,所以在判断对象的内部逻辑中还需要判断是否是Date类型,若为Date类型则直接原路返回,不处理。

if(typeof data === 'object' && data !== null){
  if(data instanceof Date){
    return data
  }
  return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
}

完整的全局拦截器如下代码所示,后续英语AI项目中,会将该全局拦截器直接拿过去使用。

// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';
//同步 异步 then catch ->数据流->pipe -> map filter -> 返回



const transformBigInt = (data: any) => {
  if (typeof data === 'bigint') {
    return data.toString();
  }
  if(Array.isArray(data)){
    return data.map(transformBigInt);
  }
  if(typeof data === 'object' && data !== null){
    if(data instanceof Date){
      return data
    }
    return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
  }
  return data;
};

@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const request = ctx.getRequest<Request>();
    return next.handle().pipe(map((data) => {
      return {
        timestmap: new Date().toISOString(),
        data: transformBigInt(data),
        path: request.url,
        message: 'success',//业务逻辑自定义
        code: 200,//业务逻辑自定义
        success: true,
      };
    }));
  }
}

接下来对异常也格式化统一处理一下,逻辑思路与全局拦截器类似。当前端发起不符合规范和要求的网络请求,后端就会返回异常信息,方便前端去统一处理。

image-20251212235546507

图3-5 异常情况的处理

此时我们需要总结nest命令的表1-2,找到filter命令来生成一个过滤器。命令是:nest g f <过滤器名称>,我们就通过nest g f exceptionFilter来生成一份过滤器吧。成功在src文件夹下创建exception-filter文件夹和exception-filter文件夹下的exception-filter.filter.ts文件,这些生成文件的命名规则都是一致的,不再赘述。

// src/exception-filter/exception-filter.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch()
export class ExceptionFilterFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {}
}

通过以上exception-filter.filter.ts文件的代码,我们发现异常处理@Catch()装饰器是空的,空的表示处理所有的异常操作,包括非HTTP请求都会处理,但我希望这个业务只处理和HTTP相关的异常就可以了。所以我们需要从@nestjs/common中引入一个HttpException类,然后让@Catch()装饰器去继承HttpException类就可以了。

// src/exception-filter/exception-filter.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class ExceptionFilterFilter<T extends HttpException> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {}
}

在这里我们可以看到这个很有意思的设计理念,通过Nest命令生成的内容,它希望我们都能用得上,这种思想和TypeScript所想表达的含义是一致的,只写用得上且必要的部分。因此在通过Nest CLI 在生成过滤器模板时,会会默认使用 @Catch()(不带任何参数),示例性地展示如何捕获所有异常。但它只是一个类模板,需要我们手动把它注册为全局过滤器,或者在控制器上使用。

只有当我们明确在@Catch()中指定具体的异常类型(如 @Catch(HttpException) 或 @Catch(WsException)),过滤器才会从“捕获所有异常”转变为“仅处理特定类型的异常”。如图3-6所示的官方文档也说明了不同协议层(HTTP 与 WebSocket)对应的异常类型不同,因此需要在 @Catch() 中明确指定对应的异常类型。

image-20251212235819873

图3-6 HTTP异常过滤层的说明

接下来我们来对异常处理情况进行统一的格式化处理。这里的code(异常状态码)就不采用我们自定义的,而是使用exception内部定义的状态码,因为Nest内置的HttpException已经为所有常见错误定义了标准化的状态码(如 400、401、403、404、500 等),这些状态码符合 HTTP 协议本身的语义。直接使用exception.getStatus()可以确保服务端返回的错误信息在网络层面是可预测和通用的。Nest.js内置异常处理层说明如图3-7所示。

image-20251213000123007

图3-7 Nest.js内置异常处理层说明

当token过期了,exception.getStatus()会自动识别并设置成401状态码,没有权限则403状态码。因此exception.getStatus()会自动化的根据实际情况去调整,非常方便。对应的详细讲解可阅读Nest.js的官方文档:Exception filters | NestJS - A progressive Node.js framework

如果再自定义一套error code,就等于需要维护两套错误体系:HTTP 状态码 + 我们自己额外设计的业务错误码,这会造成重复劳动、文档负担加重以及维护难度上升。而直接使用 HttpException 内部的状态码可以保持异常捕获逻辑与框架一致,不需要额外重复造轮子。

// src/exception-filter/exception-filter.filter.ts文件
import { ArgumentsHost, Catch, ExceptionFilter,HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class ExceptionFilterFilter<T extends HttpException> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const request = ctx.getRequest<Request>()
    const response = ctx.getResponse<Response>()
    return response.status(exception.getStatus()).json({
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
      code: exception.getStatus(),
      success: false,
    })
  }
}

最后,过滤器和拦截器一样,在main.ts文件中全局注册一下,则可以作用于整个项目的异常情况处理。

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InterceptorInterceptor } from './interceptor/interceptor.interceptor';
import { ExceptionFilterFilter } from './exception-filter/exception-filter.filter';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new InterceptorInterceptor());
  app.useGlobalFilters(new ExceptionFilterFilter());
  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

全局异常情况的过滤处理效果如图3-8所示。

image-20251212235417767

图3-8 全局异常情况的过滤处理效果

❌
❌