Skip to content

NEST 全局九件套 ​

九件套架构 ​

  • 新建一个 src/global 文件夹
bash

├── exception/                          # 自定义异常
│   ├── global_guard.exception.ts       # Guard 异常
│   └── global_pipe.exception.ts        # Pipe 异常
├── filter/                             # 异常过滤器
│   ├── global.filter.ts                # 全局异常过滤器
│   ├── global_guard_check.filter.ts    # Guard 异常过滤器
│   └── global_pipe_check.filter.ts     # Pipe 异常过滤器
├── guard/
│   ├── global.guard.ts                 # 全局守卫(JWT 校验)                    # 守卫
│   └── rule.guard.ts                   # RBAC守卫验证
├── interceptor/                        # 拦截器
│   └── global.interceptor.ts           # 全局拦截器(统一响应格式 + 日志)
├── passdecorator/                      # 放行装饰器
│   ├── rbac.decorator.ts               # 权限自定义装饰器
│   ├── guard_pass.decorator.ts         # Guard 放行装饰器
│   └── interceptor_pass.decorator.ts   # Interceptor 放行装饰器
└── pipe/                               # 管道
    └── global.pipe.ts                  # 全局管道(参数校验)

passdecorator ​

interceptor_pass.decorator.ts ​

  • 拦截器放行 自定义装饰器
ts
// 拦截器放行 自定义装饰器

import { SetMetadata } from "@nestjs/common";
// 加载环境变量
import dotenv from "dotenv";
dotenv.config();

export const BYPASS_KEY = process.env.BYPASS_KEY;

export function Bypass() {
  return SetMetadata(BYPASS_KEY, true);
}
  • 使用在控制器前面加上@Bypass 这样拦截器就放行了
bash
  @Get('exportexcel2')
  @Bypass()
  async exportexcel2(@Res() res: any) {
    // 这里应该上传成功后,返回一个url,然后通过url获取到excel文件
    const result = await this.testdemoService.exportexcel2();
    console.log(result);
    return res.send(result);
  }

guard_pass.decorator.ts ​

  • 守卫放行 自定义装饰器
ts
// 管道放行 自定义装饰器

import { SetMetadata } from "@nestjs/common";
// 加载环境变量
import dotenv from "dotenv";
dotenv.config();

export const GUARD_PASS_KEY = process.env.GUARD_PASS_KEY;

export function ByGuardpass() {
  return SetMetadata(GUARD_PASS_KEY, true);
}
  • 使用在控制器前面加上@ByGuardpass
bash
  @ByGuardpass()
  @Post('init')
  async init() {
    await this.userService.init();
  }

rbac.decorator.ts ​

ts
// 权限控制器
import { SetMetadata } from "@nestjs/common";

export function RBAC(arr: any[]) {
  return SetMetadata("permissions", arr);
}

exception ​

global_guard.exception.ts ​

  • 守卫异常(异常)
ts
// 守卫抛出的异常
export class GlobalGuardException {
  message: string;
  constructor(message: string) {
    this.message = message;
  }
}

global_pipe.exception.ts ​

  • 管道异常(异常)
ts
// 管道抛出的异常
export class GlobalPipeCheckException {
  message: string;
  constructor(message: string) {
    this.message = message;
  }
}

filter ​

global_guard_check.filter.ts ​

  • 守卫异常过滤器(过滤器)
ts
// 守卫验证异常过滤器

import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { GlobalGuardException } from "../exception/global_guard.exception.js";

// 增加日志模块
import { Logger } from "winston";

import { WINSTON_MODULE_PROVIDER } from "nest-winston";

// 引入工具服务
import { ToolsService } from "../../common/toolsModule/tools.service.js";

@Catch(GlobalGuardException)
export class GlobalGuardCheckFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
    private readonly toolsService: ToolsService,
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = 200;
    // 错误内容
    const message =
      exception instanceof GlobalGuardException
        ? exception.message
        : exception.stack;

    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 401,
      req: this.toolsService.getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 401,
      data: message,
      message: "操作失败",
    });
  }
}

global_pipe_check.filter.ts ​

  • 管道异常过滤器(过滤器)
ts
// 捕捉到管道抛出来的异常
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { GlobalPipeCheckException } from "../exception/global_pipe.exception.js";

// 增加日志模块
import { Logger } from "winston";

import { WINSTON_MODULE_PROVIDER } from "nest-winston";

// 引入工具服务
import { ToolsService } from "../../common/toolsModule/tools.service.js";

@Catch(GlobalPipeCheckException)
export class GlobalPipeCheckFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
    private readonly toolsService: ToolsService,
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = 200;
    // 错误内容
    const message =
      exception instanceof GlobalPipeCheckException
        ? exception.message
        : exception.stack;
    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 403,
      req: this.toolsService.getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 403,
      data: message,
      message: "操作失败",
    });
  }
}

global.filter.ts ​

  • 全局异常过滤器(过滤器)
ts
// 最后托底的异常过滤器

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Inject,
} from "@nestjs/common";

// 增加日志模块
import { Logger } from "winston";

import { WINSTON_MODULE_PROVIDER } from "nest-winston";

// 引入工具服务
import { ToolsService } from "../../common/toolsModule/tools.service.js";

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
    private readonly toolsService: ToolsService,
  ) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException ? exception.message : exception.stack;

    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: status,
      req: this.toolsService.getReqMainInfo(request),
      // stack: exception.stack,
    });

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message,
      name: "出问题了",
    });
  }
}

guard ​

global.guard.ts ​

  • 全局守卫验证(守卫)
ts
// 全局守卫验证

import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
} from "@nestjs/common";
import { GlobalGuardException } from "../exception/global_guard.exception.js";
import { Reflector } from "@nestjs/core";
import { GUARD_PASS_KEY } from "../passdecorator/guard_pass.decorator.js";
import { JwtAllService } from "../../modules/jwt/jwt.service.js";

@Injectable()
export class GlobalGuard implements CanActivate {
  @Inject()
  private reflector: Reflector;

  @Inject()
  private jwtService: JwtAllService;

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 获取装饰器标记,标记为 bypass 则直接放行
    const bypass = this.reflector.get<boolean>(
      GUARD_PASS_KEY,
      context.getHandler(),
    );
    if (bypass) {
      return true;
    }

    // 权限验证
    const request = context.switchToHttp().getRequest();
    const authorization: string = request.header("Authorization") || "";
    const [bearer, token] = authorization.split(" ");

    if (bearer !== "Bearer" || !token) {
      throw new GlobalGuardException("登录token错误");
    }

    try {
      const info = this.jwtService.verifyToken(token);
      // 把解析出的用户信息挂载到 request,控制器可直接获取
      (request as any).user = info;
      return true;
    } catch {
      throw new GlobalGuardException("token 错误或已过期");
    }
  }
}

rule.guard.ts(可有可无,最后加) ​

ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { Reflector } from "@nestjs/core";
import { PrismadbService } from "../../common/prismaModule/prisma.service.js";
import { RedisService } from "../../common/redisModule/redis.service.js";
import { GlobalGuardException } from "../exception/global_guard.exception.js";
@Injectable()
export class RuleGuard implements CanActivate {
  @Inject()
  private readonly redisService: RedisService;
  @Inject()
  private readonly prismadbService: PrismadbService;
  @Inject(Reflector)
  private readonly reflector: Reflector;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 获取到设定的权限
    const permissions = this.reflector.getAllAndOverride<string[]>(
      "permissions",
      [context.getHandler(), context.getClass()],
    );
    console.log(permissions);
    // 通过userId获取到用户角色
    // 权限验证 全局守卫会给你useId
    const request = context.switchToHttp().getRequest();
    // 1. 先去redis中查找 2. 找不到就走数据库 3. 数据库取出来存到redis中
    let userPermissionsName: any = await this.redisService.getlist(
      `userId_${request.userId}_roles`,
    );
    if (!userPermissionsName || userPermissionsName.length === 0) {
      console.log("进来了");
      // 1. 查出这个用户拥有的角色
      const RoleId: any = await this.prismadbService.user_roles.findMany({
        where: {
          user_id: request.userId,
        },
        select: {
          role_id: true,
        },
      });
      // 2. 查询出这个角色拥有的权限
      const userPermissionsId: any =
        await this.prismadbService.role_permissions.findMany({
          where: {
            role_id: {
              in: RoleId.map((item) => item.role_id),
            },
          },
          select: {
            permission_id: true,
          },
        });
      console.log(userPermissionsId);
      // 3. 查出这个权限Id对应的权限
      let userPermissions = await this.prismadbService.permissions.findMany({
        where: {
          id: {
            in: userPermissionsId.map((item) => item.permission_id),
          },
        },
        select: {
          code: true,
        },
      });
      userPermissionsName = userPermissions.map((item) => item.code);
      console.log("最后结果");
      console.log(userPermissionsName);
      // 3. 存到redis中
      this.redisService.setlist(
        `userId_${request.userId}_roles`,
        userPermissionsName,
        60 * 30,
      ); // 过期时间30分钟
    }
    // 判断返回
    const result = permissions.some((item) => {
      return userPermissionsName.includes(item);
    });

    if (result) {
      return true;
    } else {
      throw new GlobalGuardException("权限不足");
    }
  }
}
  • 使用
ts
  // 测试权限分级
  @UseGuards(RuleGuard)
  @RBAC(['guest'])
  @Post('rbac')
  async rbac(@Body() body: any) {
    console.log('----');
    console.log(body);
    console.log('-------');
    return '测试';
  }
}

interceptor ​

global.interceptor.ts ​

  • 全局统一响应格式(拦截器)
ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

import { Reflector } from "@nestjs/core";
// 增加日志模块
import { Logger } from "winston";

import { WINSTON_MODULE_PROVIDER } from "nest-winston";

// 引入工具服务
import { ToolsService } from "../../common/toolsModule/tools.service.js";

// 拦截器放行装饰器
import { BYPASS_KEY } from "../passdecorator/interceptor_pass.decorator.js";

@Injectable()
export class GlobalInterceptor implements NestInterceptor {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
    private readonly toolsService: ToolsService,
  ) {}

  @Inject()
  private reflector: Reflector;

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    // 装饰器 bypass 则放行(不包装统一响应格式)
    const bypass = this.reflector.get<boolean>(
      BYPASS_KEY,
      context.getHandler(),
    );

    return next.handle().pipe(
      map((data) => {
        // 记录请求日志
        this.logger.info("response", {
          responseData: data,
          req: this.toolsService.getReqMainInfo(request),
        });

        if (bypass) {
          return data;
        }

        // 全局统一返回格式
        // 控制器里面就可以返回 {code:200,data:xxx}
        const resultcode = data.code || 200;
        return {
          code: resultcode,
          message: resultcode === 200 ? "操作成功" : "操作失败",
          data,
        };
      }),
    );
  }
}
  • 使用在控制器前面加上@ByGuardpass
bash
  @ByGuardpass()
  @Post('init')
  async init() {
    await this.userService.init();
    return '初始化数据成功';
  }

app.module.ts ​

ts
import { PrismadbModule } from "./common/prismaModule/prisma.module.js";
import { Module } from "@nestjs/common";
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER, APP_PIPE } from "@nestjs/core";
import { RedisModule } from "./common/redisModule/redis.module.js";
import { GlobalWinstonModule } from "./common/winston/winston.module.js";
import { ToolsModule } from "./common/toolsModule/tools.module.js";
// 配置文件
import { ConfigModule } from "@nestjs/config";
import * as dotenv from "dotenv";

// 引入auth模块
import { AuthModule } from "./modules/auth/auth.module.js";

// 引入jwt模块
import { JwtAllModule } from "./modules/jwt/jwt.module.js";

// 引入全局组件
import { GlobalGuard } from "./global/guard/global.guard.js";
import { GlobalInterceptor } from "./global/interceptor/global.interceptor.js";
import { GlobalExceptionsFilter } from "./global/filter/global.filter.js";
import { GlobalGuardCheckFilter } from "./global/filter/global_guard_check.filter.js";
import { GlobalPipeCheckFilter } from "./global/filter/global_pipe_check.filter.js";
import { GlobalPipe } from "./global/pipe/global.pipe.js";

// 引入user模块
import { UserModule } from "./modules/user/user.module.js";

// 引入app模块
// import { AppController } from './app.controller.js';
// import { AppService } from './app.service.js';

const envPath = `.env.${process.env.NODE_ENV || "development"}`;

@Module({
  imports: [
    // 引入user模块
    UserModule,
    // 引入jwt模块
    JwtAllModule,
    // 引入auth模块
    AuthModule,
    // 引入工具模块
    ToolsModule,
    // win志
    GlobalWinstonModule.forRoot(),
    // redis
    RedisModule,
    // prisma
    PrismadbModule,
    // 修改配置
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envPath,
      // 这里新增.env的文件解析
      load: [() => dotenv.config({ path: ".env" })],
    }),
  ],
  controllers: [],
  providers: [
    // 全局守卫
    {
      provide: APP_GUARD,
      useClass: GlobalGuard,
    },
    // 全局拦截器
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalInterceptor,
    },
    // 全局过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsFilter,
    },
    // 全局管道检查过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalPipeCheckFilter,
    },
    // 全局守卫检查过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalGuardCheckFilter,
    },
    // 全局管道
    {
      provide: APP_PIPE,
      useClass: GlobalPipe,
    },
  ],
})
export class AppModule {}

补充 ​

  • 在common/toolsModule/tools.service.ts中添加工具服务

tools.service.ts ​

ts
import { Injectable } from "@nestjs/common";
import { randomUUID, createHash, randomBytes } from "node:crypto";
import type { Request } from "express";

/**
 * 请求主要信息的返回结构
 */
export interface ReqMainInfo {
  method: string;
  url: string;
  ip: string | undefined;
  userAgent: string | undefined;
  query: unknown;
  params: unknown;
  body: unknown;
  headers: unknown;
}

@Injectable()
export class ToolsService {
  /**
   * 提取请求的主要信息(用于日志记录)
   */
  getReqMainInfo(req: Request): ReqMainInfo {
    const { method, originalUrl, headers, query, params, body, ip } = req;
    return {
      method,
      url: originalUrl,
      // request-ip 中间件挂在 req.ip 上,兜底取连接地址
      ip: ip || req.socket?.remoteAddress,
      userAgent: headers["user-agent"],
      query,
      params,
      body,
      headers,
    };
  }

  /**
   * 生成 UUID
   */
  uuid(): string {
    return randomUUID();
  }

  /**
   * 生成指定长度的随机字符串
   */
  randomString(length = 16): string {
    return randomBytes(Math.ceil(length / 2))
      .toString("hex")
      .slice(0, length);
  }

  /**
   * MD5 加密
   */
  md5(str: string): string {
    return createHash("md5").update(str).digest("hex");
  }

  /**
   * SHA256 加密
   */
  sha256(str: string): string {
    return createHash("sha256").update(str).digest("hex");
  }

  /**
   * 休眠指定毫秒
   */
  sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  /**
   * 格式化日期为 YYYY-MM-DD HH:mm:ss
   */
  formatDate(date: Date = new Date()): string {
    const pad = (n: number) => n.toString().padStart(2, "0");
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
  }

  /**
   * 判断对象是否为空
   */
  isEmpty(obj: unknown): boolean {
    if (obj === null || obj === undefined) return true;
    if (typeof obj === "string") return obj.trim().length === 0;
    if (Array.isArray(obj)) return obj.length === 0;
    if (typeof obj === "object") return Object.keys(obj).length === 0;
    return false;
  }

  /**
   * 深度克隆对象
   */
  deepClone<T>(obj: T): T {
    return JSON.parse(JSON.stringify(obj));
  }

  /**
   * 驼峰转下划线
   */
  camelToSnake(str: string): string {
    return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
  }

  /**
   * 下划线转驼峰
   */
  snakeToCamel(str: string): string {
    return str.replace(/_([a-z])/g, (_, letter: string) =>
      letter.toUpperCase(),
    );
  }

  /**
   * 分页参数处理
   */
  pagination(page = 1, pageSize = 10) {
    const current = Math.max(1, Number(page));
    const size = Math.max(1, Number(pageSize));
    return {
      skip: (current - 1) * size,
      take: size,
      page: current,
      pageSize: size,
    };
  }

  /**
   * 排除对象中的指定字段
   */
  exclude<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
    const result = { ...obj };
    for (const key of keys) {
      delete result[key];
    }
    return result;
  }
}