Nest.js 实战 (九):使用拦截器记录用户 CURD 操作日志

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 这篇文章介绍了在Nest.js中如何实现记录用户CURD操作的需求。首先解释了什么是拦截器以及拦截器的作用,然后通过创建Prisma模型,添加Log模型,并通过编写LoggerInterceptor拦截器,实现了记录用户操作的功能。最后通过效果演示和总结,强调了使用拦截器实现此功能的有效性。

前言

有一天,公司的产品经理提了一个需求:系统需要记录每个用户的 CURD 操作,也就是说用户新增、编辑或者删除了什么数据,都需要记录下来,这个在 Nest.js 中如何实现呢?

这时候我们可以考虑使用 拦截器 来实现。

什么是拦截器?

拦截器 是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。

拦截器 具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

创建 Prisma 模型

schema.prisma 文件中添加 Log 模型:

// Log - method
enum Method {
  GET
  POST
  PATCH
  DELETE
}

// 系统管理 - 操作日志
model Log {
  id            String          @id @default(uuid()) // 主键
  userId        String          // 关联的用户 id
  user          User            @relation(fields: [userId], references: [id])
  ip            String          // ip
  action        String          // 请求地址
  method        Method          // 请求方法
  params        Json            // 请求参数(JSON 对象)
  os            String          // 系统
  browser       String          // 浏览器
  createdAt     DateTime        @default(now()) // 创建时间
}

这里可以根据自己实际需求调整。

创建 Module 模块

这里我们需要用到 Session 保存的用户数据,但 Service 中是不能直接获取 Session 的,我们需要注入作用域,以此来获取请求中的上下文。

新建 operation-log.service.ts 文件:

import {
   
    Inject, Injectable, Scope } from '@nestjs/common';
import {
   
    REQUEST } from '@nestjs/core';
import {
   
    Request } from 'express';
import UAParser from 'ua-parser-js';

import {
   
    PrismaService } from '@/modules/prisma/prisma.service';


@Injectable({
   
    scope: Scope.REQUEST })
export class OperationLogService {
   
   
  constructor(
    @Inject(REQUEST)
    private readonly request: Request & {
   
    session: Api.Common.SessionInfo },
    private prisma: PrismaService,
  ) {
   
    }

  **
   * @description: 录入日志
   */
  async logAction() {
   
   
    const {
   
    originalUrl, method, headers, ip, body, query } = this.request;
    const userAgent = headers['user-agent'];
    const parser = new UAParser(userAgent);
    let {
   
    userInfo } = this.request.session;
    // 登录接口需要单独处理
    const isLogin = originalUrl === '/auth/login';
    if ((userInfo && method.toUpperCase() !== 'GET') || isLogin) {
   
   
      if (isLogin) {
   
   
        // 查询数据库中对应的用户
        userInfo = await this.prisma.user.findUnique({
   
   
          where: {
   
    userName: body.userName },
        });
      }
      const data: any = {
   
   
        userId: userInfo.id,
        action: originalUrl,
        method: method.toUpperCase(),
        ip,
        params: {
   
    ...body, ...query },
        os: Object.values(parser.getOS()).join(' '),
        browser: parser.getBrowser().name,
      };
      // 插入数据到表
      await this.prisma.log.create({
   
   
        data,
      });
    }
  }
}

因为登录接口此时 Session 还没有保存用户数据,我们需要单独处理一下,这里我们只记录非 GET 请求的路由。

创建 LoggerInterceptor 拦截器

新建 interceptor/logger.interceptor.ts 文件,写入:

import {
   
    CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import {
   
    Observable } from 'rxjs';
import {
   
    map } from 'rxjs/operators';

import {
   
    OperationLogService } from '@/modules/system-manage/operation-log/operation-log.service';

@Injectable()
export class LoggerInterceptor implements NestInterceptor {
   
   
  constructor(private readonly operationLogService: OperationLogService) {
   
   }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
   
   
    this.operationLogService.logAction();
    return next.handle().pipe(map((data) => data));
  }
}

绑定拦截器

在需要绑定的 Controller 中使用 @UseInterceptors() 装饰器,与守卫一样, 拦截器可以是控制器范围内的, 方法范围内的或者全局范围内的。

import {
   
    UseInterceptors } from '@nestjs/common';
import {
   
    LoggerInterceptor } from '@/interceptor/logger.interceptor';

@UseInterceptors(LoggingInterceptor)
export class UserManageController {
   
   }

在绑定拦截器后,用户每次调用 Controller 中的路由处理程序都将使用 LoggingInterceptor,也就是说会把用户的操作等信息记录到表中。

效果演示

ip2e6ddl5ma51r6tyd8yt2jm4dl0gmqr.png

总结

这个功能本来一开始我是想使用 中间件 来开发的,后来不管怎么折腾,中间件Request 上下文始终获取不到 Session,但 拦截器 也不失是一种好方法。

Github 仓库OperationLogService

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
21天前
Next.js 实战 (二):搭建 Layouts 基础排版布局
本文介绍了作者在Next.js v15.x版本发布后,对一个旧项目的重构过程。文章详细说明了项目开发规范配置、UI组件库选择(最终选择了Ant-Design)、以及使用Ant Design的Layout组件实现中后台布局的方法。文末展示了布局的初步效果,并提供了GitHub仓库链接供读者参考学习。
Next.js 实战 (二):搭建 Layouts 基础排版布局
|
15天前
|
存储 网络架构
Next.js 实战 (四):i18n 国际化的最优方案实践
这篇文章介绍了Next.js国际化方案,作者对比了网上常见的方案并提出了自己的需求:不破坏应用程序的目录结构和路由。文章推荐使用next-intl库来实现国际化,并提供了详细的安装步骤和代码示例。作者实现了国际化切换时不改变路由,并把当前语言的key存储到浏览器cookie中,使得刷新浏览器后语言不会失效。最后,文章总结了这种国际化方案的优势,并提供Github仓库链接供读者参考。
|
16天前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
|
1月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
29 2
|
1月前
|
Java Maven Spring
超实用的SpringAOP实战之日志记录
【11月更文挑战第11天】本文介绍了如何使用 Spring AOP 实现日志记录功能。首先概述了日志记录的重要性及 Spring AOP 的优势,然后详细讲解了搭建 Spring AOP 环境、定义日志切面、优化日志内容和格式的方法,最后通过测试验证日志记录功能的准确性和完整性。通过这些步骤,可以有效提升系统的可维护性和可追踪性。
|
1月前
|
JSON 监控 JavaScript
Node.js-API 限流与日志优化
Node.js-API 限流与日志优化
|
2月前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
39 1
|
2月前
|
存储 JavaScript 前端开发
前端开发:Vue.js入门与实战
【10月更文挑战第9天】前端开发:Vue.js入门与实战
|
1月前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
49 0