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;
}
}