You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于TypeORM与NestJS实现通用审计日志的方案问询

NestJS + TypeORM 通用审计日志实现:在订阅者中获取当前用户ID

针对你在NestJS API中实现审计日志时,TypeORM订阅者无法获取当前请求用户ID的问题,这里提供基于AsyncLocalStorage的通用解决方案,支持增删改所有操作,且无需在实体中添加临时字段。

核心思路

利用Node.js的AsyncLocalStorage(异步本地存储)保存请求上下文,在请求拦截器中将当前用户ID存入上下文,之后在TypeORM的订阅者中直接从上下文提取用户ID,确保所有数据库操作(包括删除)都能关联到操作用户。

步骤1:创建请求上下文存储服务

// src/common/request-context.service.ts
import { Injectable } from '@nestjs/common';
import { AsyncLocalStorage } from 'async_hooks';

@Injectable()
export class RequestContextService {
  private readonly als = new AsyncLocalStorage<{ userId: string | 'system' }>();

  // 启动上下文并存储用户ID
  runWithUserId(userId: string | 'system', callback: () => void): void {
    this.als.run({ userId }, callback);
  }

  // 获取当前上下文的用户ID
  getCurrentUserId(): string | 'system' {
    return this.als.getStore()?.userId || 'system';
  }
}

步骤2:实现请求拦截器注入用户ID

创建拦截器,从请求中提取用户ID(需根据你的认证方式调整,示例假设用户信息存在req.user.id),并注入到上下文:

// src/common/auth.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { RequestContextService } from './request-context.service';

@Injectable()
export class AuthInterceptor implements NestInterceptor {
  constructor(private readonly requestContextService: RequestContextService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const userId = request.user?.id || 'system';
    
    return new Observable(observer => {
      this.requestContextService.runWithUserId(userId, () => {
        next.handle().subscribe({
          next: val => observer.next(val),
          error: err => observer.error(err),
          complete: () => observer.complete(),
        });
      });
    });
  }
}

步骤3:修改TypeORM订阅者获取用户ID

更新你的LogEventService,注入上下文服务并在生成日志时获取用户ID:

// src/log/log-event.service.ts
import GenericEntity from "@generic/repository/generic.entity";
import { Injectable } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource, EntitySubscriberInterface, EventSubscriber, InsertEvent, RemoveEvent, UpdateEvent } from "typeorm";
import { LogService } from "./log.service";
import { eLogType } from "./repository/log.entity";
import { AppLogger } from "@utils/logger";
import { RequestContextService } from "../common/request-context.service";

interface Events {
    log: {
        type: eLogType;
        userId: string | "system";
        tabela: string;
        values: {
            id: string;
            old: Record<string, any>;
            new: Record<string, any>;
        };
    };
}

@EventSubscriber()
@Injectable()
export class LogEventService implements EntitySubscriberInterface {
    private tabelasSemLog: string[] = [];

    constructor(
        @InjectDataSource() readonly dataSource: DataSource,
        private readonly logService: LogService,
        private readonly requestContextService: RequestContextService,
    ) {
        dataSource.subscribers.push(this);
    }

    private async saveLog(data: Events["log"]) {
        if (await this.logService.saveLog(data.type, data.userId, data.tabela, data.values))
            AppLogger.log(`Log salvo com sucesso para a tabela ${data.tabela}`);
        else throw new Error("Erro ao salvar log -> " + JSON.stringify(data));
    }

    private async generateLogData(tabela: string, type: eLogType, entity: GenericEntity) {
        const copy = { ...entity };
        const oldValue = copy.previousState;
        delete copy.previousState;

        // 从上下文获取当前用户ID
        const userId = this.requestContextService.getCurrentUserId();

        await this.saveLog({
            tabela,
            type,
            userId,
            values: {
                id: entity.id,
                new: copy,
                old: oldValue,
            },
        });
    }

    async afterInsert(event: InsertEvent<GenericEntity>): Promise<any> {
        const tabela = event.metadata.tableName;
        if (this.tabelasSemLog.includes(tabela)) return;

        event.entity.id = event.entity.id ?? event.entityId?.toString();
        await this.generateLogData(tabela, eLogType.INCLUSAO, event.entity);
    }

    async afterUpdate(event: UpdateEvent<GenericEntity>): Promise<any> {
        const tabela = event.metadata.tableName;
        if (this.tabelasSemLog.includes(tabela)) return;

        event.entity.id = event.entity.id;
        await this.generateLogData(tabela, eLogType.ALTERACAO, event.entity);
    }

    async afterRemove(event: RemoveEvent<GenericEntity>): Promise<any> {
        const tabela = event.metadata.tableName;
        if (this.tabelasSemLog.includes(tabela)) return;

        event.entity.id = event.entity.id ?? event.entityId?.toString();
        await this.generateLogData(tabela, eLogType.EXCLUSAO, event.entity);
    }
}

步骤4:模块配置

在根模块中注册上下文服务和全局拦截器:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { RequestContextService } from './common/request-context.service';
import { AuthInterceptor } from './common/auth.interceptor';

@Module({
  providers: [
    RequestContextService,
    {
      provide: APP_INTERCEPTOR,
      useClass: AuthInterceptor,
    },
  ],
})
export class AppModule {}

关键说明

  • 全操作支持:新增、更新、删除操作均能获取用户ID,解决你之前删除操作无法关联用户的问题。
  • 无实体侵入:不需要修改实体类添加临时字段,保持实体结构纯净。
  • 上下文隔离:AsyncLocalStorage自动隔离不同请求的上下文,不会出现用户ID串号问题。
  • 系统操作兼容:无用户ID时(如定时任务、系统初始化),默认使用system作为操作标识。

内容的提问来源于stack exchange,提问作者Lucas Gardini Dias

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:07:35