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

如何在Prisma Schema中实现@createdBy与@updatedBy审计注解?

在NestJS + Prisma中实现CreatedBy/UpdatedBy自动审计

一、先在Prisma Schema中定义审计字段

首先要给需要审计的模型添加createdBy和updatedBy字段,建议用片段复用避免重复代码:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  name  String
  // 关联审计字段的反向关系,注意区分关系名
  createdPosts Post[] @relation("CreatedBy")
  updatedPosts Post[] @relation("UpdatedBy")
}

// 审计字段复用片段
fragment AuditFields on Model {
  createdAt DateTime @default(now()) @db.Timestamp(6)
  updatedAt DateTime @updatedAt @db.Timestamp(6)
  createdBy Int
  updatedBy Int
  creator   User     @relation("CreatedBy", fields: [createdBy], references: [id])
  updater   User     @relation("UpdatedBy", fields: [updatedBy], references: [id])
}

// 示例模型:引用审计片段
model Post {
  id      Int     @id @default(autoincrement())
  title   String
  content String?
  ...AuditFields // 注入审计字段
}

执行npx prisma migrate dev同步数据库结构。


二、用AsyncLocalStorage存储请求上下文

Prisma中间件无法直接获取请求中的用户信息,需要用Node.js的AsyncLocalStorage在请求生命周期内保存当前用户ID:

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

@Injectable({ scope: Scope.REQUEST })
export class ContextService {
  private readonly als = new AsyncLocalStorage<{ userId: number }>();

  runWithUserId(userId: number, callback: () => void) {
    this.als.run({ userId }, callback);
  }

  getUserId(): number | undefined {
    return this.als.getStore()?.userId;
  }
}

然后创建拦截器,从请求中提取用户ID并注入上下文:

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

@Injectable()
export class ContextInterceptor implements NestInterceptor {
  constructor(private readonly contextService: ContextService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    // 这里假设用户信息已通过认证守卫存入request.user,根据你的认证逻辑调整
    const userId = request.user?.id;

    if (userId) {
      return new Observable(observer => {
        this.contextService.runWithUserId(userId, () => {
          next.handle().subscribe({
            next: val => observer.next(val),
            error: err => observer.error(err),
            complete: () => observer.complete(),
          });
        });
      });
    }

    return next.handle();
  }
}

在AppModule中全局注册拦截器:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ContextInterceptor } from './common/interceptors/context.interceptor';
import { ContextService } from './common/context/context.service';

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

三、编写Prisma中间件自动填充审计字段

通过Prisma中间件拦截CRUD操作,自动注入createdBy和updatedBy:

// src/prisma/middlewares/audit.middleware.ts
import { Prisma } from '@prisma/client';
import { ContextService } from '../../common/context/context.service';

export function auditMiddleware(contextService: ContextService): Prisma.Middleware {
  return async (params, next) => {
    const userId = contextService.getUserId();
    // 无用户ID时(如后台任务)直接执行原操作
    if (!userId) return next(params);

    const { model, action } = params;
    // 可自定义需要审计的模型白名单
    const auditableModels = ['Post', 'Comment', 'User'];
    if (!auditableModels.includes(model)) return next(params);

    // 创建操作:同时填充createdBy和updatedBy
    if (action === 'create') {
      params.args.data = { ...params.args.data, createdBy: userId, updatedBy: userId };
    }

    // 更新/批量更新:填充updatedBy
    if (action === 'update' || action === 'updateMany') {
      params.args.data = { ...params.args.data, updatedBy: userId };
    }

    //  Upsert操作:创建时填充createdBy,更新时填充updatedBy
    if (action === 'upsert') {
      params.args.create = { ...params.args.create, createdBy: userId, updatedBy: userId };
      params.args.update = { ...params.args.update, updatedBy: userId };
    }

    return next(params);
  };
}

在Prisma服务中注册中间件:

// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { auditMiddleware } from './middlewares/audit.middleware';
import { ContextService } from '../common/context/context.service';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  constructor(private readonly contextService: ContextService) {
    super();
  }

  async onModuleInit() {
    this.$use(auditMiddleware(this.contextService));
    await this.$connect();
  }
}

四、验证使用

现在在业务服务中调用Prisma的CRUD方法时,无需手动传递createdBy/updatedBy,中间件会自动填充:

// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePostDto } from './dto/create-post.dto';

@Injectable()
export class PostsService {
  constructor(private readonly prisma: PrismaService) {}

  async create(createPostDto: CreatePostDto) {
    // 无需手动添加createdBy/updatedBy
    return this.prisma.post.create({
      data: {
        title: createPostDto.title,
        content: createPostDto.content,
      },
    });
  }

  async update(id: number, updatePostDto: UpdatePostDto) {
    return this.prisma.post.update({
      where: { id },
      data: {
        title: updatePostDto.title,
        content: updatePostDto.content,
      },
    });
  }
}

内容的提问来源于stack exchange,提问作者Dimitri Kopriwa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 10:52:27