AsyncLocalStorage 使用快速参考

核心保证

✅ 1. 异步链起点包裹 - 已保证

RequestContextMiddleware 中,所有请求都被 RequestContext.run() 包裹:

// middleware 自动执行(AppModule 全局注册)
RequestContext.run(contextData, () => {
  next();  // 后续所有操作都在此上下文中
});

你可以放心:在任何 Service 中访问 getCurrentUserId() 都是安全的。


✅ 2. Promise.all 并发 - 安全可用

// ✅ 这样是安全的
async findAll() {
  const userId = this.userContextUtil.getCurrentUserId();

  // Promise.all 在同一个请求的 RequestContext 中
  const rows = await Promise.all(
    items.map(async (item) => {
      const customer = await this.prisma.customer.findUnique(...);
      return { ...item, customer, createdBy: userId };
    })
  );

  return rows;
}

你可以放心:所有 Promise.all 中的子任务都能正确访问上下文。


⚠️ 3. 数据存储 - 已优化

中间件现在只存储必要的简单字段:

// 存储的数据(轻量化)
{
  userId: BigInt(25),        // ✅ 用户 ID
  username: "zhangsan",      // ✅ 用户名
  departmentId: BigInt(1),   // ✅ 部门 ID
  // ❌ 不再存储完整的用户对象或敏感数据
}

优点

  • 内存占用最小化
  • 无敏感数据泄露风险
  • 性能最优

如果需要更多用户信息,在 Service 中直接查询数据库:

async create(dto: CreateWorkOrderDto) {
  const userId = this.userContextUtil.getCurrentUserId();

  // 如果需要完整用户信息,直接查询
  const user = await this.prisma.user.findUnique({
    where: { id: userId }
  });

  // 使用 user 的各个字段
  return { ...result, createdBy: user.name };
}

禁止场景

❌ 不要在 setTimeout 中使用

// ❌ 错误
setTimeout(() => {
  const userId = this.userContextUtil.getCurrentUserId();  // undefined
}, 100);

// ✅ 正确:在 async 中使用
await this.delay(100);  // 实现:return new Promise(resolve => setTimeout(resolve, ms))
const userId = this.userContextUtil.getCurrentUserId();

❌ 不要在 Worker Thread 中使用

// ❌ 错误:Worker 有独立的事件循环
const worker = new Worker('./worker.js');

// ✅ 正确:传递 userId 给 Worker
const userId = this.userContextUtil.getCurrentUserId();
worker.postMessage({ userId, data });

❌ 不要在 Queue 系统中直接依赖

// ❌ 错误:Queue worker 中 AsyncLocalStorage 为空
async handleJobCreated(dto) {
  const userId = this.userContextUtil.getCurrentUserId();  // undefined
  await this.queue.add('processOrder', { userId, dto });
}

// ✅ 正确:显式传递 userId
async handleJobCreated(dto) {
  const userId = this.userContextUtil.getCurrentUserId();
  await this.queue.add('processOrder', { userId, dto });  // 传入 userId
}

// Queue worker 中
async processOrder(job) {
  const { userId, dto } = job.data;  // 从 job.data 获取 userId
  // 现在可以使用 userId
}

常见用法示例

Service 中最常见的用法

@Injectable()
export class WorkOrdersService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly userContextUtil: UserContextUtil
  ) {}

  async create(dto: CreateWorkOrderDto) {
    // 1. 获取当前用户 ID(从 AsyncLocalStorage)
    const userId = this.userContextUtil.getCurrentUserId();

    // 2. 验证(可选)
    if (!userId) {
      throw new UnauthorizedException('User not authenticated');
    }

    // 3. 业务逻辑 - 使用 userId
    const workOrder = await this.prisma.workOrder.create({
      data: {
        orderNo: this.generateOrderNumber(),
        customerId: BigInt(dto.customerId),
        createBy: userId,       // ✅ 使用 userId
        updateBy: userId,       // ✅ 使用 userId
        currentHandlerId: userId, // ✅ 使用 userId
      }
    });

    // 4. 返回结果
    return workOrder;
  }

  async findAll(queryDto: QueryWorkOrderDto) {
    const userId = this.userContextUtil.getCurrentUserId();

    // Promise.all 在并发查询中
    const rows = await Promise.all(
      workOrders.map(async (order) => {
        const customer = await this.prisma.customer.findUnique(...);
        return { ...order, customer };  // userId 仍在上下文中可用
      })
    );

    return rows;
  }
}

Guard 中的使用(可选)

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(private userContextUtil: UserContextUtil) {}

  canActivate(context: ExecutionContext): boolean {
    // 在 Guard 中也可以访问
    const userId = this.userContextUtil.getCurrentUserId();

    // 执行权限检查逻辑
    return userId ? checkPermission(userId) : false;
  }
}

Filter 中的使用(可选)

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private userContextUtil: UserContextUtil) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const userId = this.userContextUtil.getCurrentUserId();

    // 记录错误时关联用户信息
    this.logger.error('Error occurred', {
      userId,
      error: exception,
      timestamp: new Date().toISOString()
    });
  }
}

逐步迁移指南

如果有旧代码仍在使用 @Inject(REQUEST) 方式,可以逐步迁移:

之前(旧方式)

@Injectable()
export class OldService {
  constructor(@Inject(REQUEST) private request: Request) {}

  async process() {
    const userId = this.request.user?.id;
  }
}

之后(新方式)

@Injectable()
export class NewService {
  constructor(private userContextUtil: UserContextUtil) {}

  async process() {
    const userId = this.userContextUtil.getCurrentUserId();
  }
}

迁移步骤

  1. 在 Service 中注入 UserContextUtil
  2. this.request.user?.id 替换为 this.userContextUtil.getCurrentUserId()
  3. 移除 @Inject(REQUEST) private request: Request
  4. 测试确认功能正常

性能建议

关于内存使用

// 当前使用(轻量化)
{
  userId: BigInt(25),        // ~30 bytes
  username: "zhangsan",      // ~20 bytes
  departmentId: BigInt(1),   // ~30 bytes
}
// 总计:~80 bytes 每个请求

// 之前存储整个用户对象(浪费内存)
{
  id: 25,
  username: "zhangsan",
  email: "zhangsan@example.com",
  password: "hashed_...",
  departmentId: 1,
  managerId: 2,
  status: "1",
  createTime: "2024-11-01T12:00:00Z",
  updateTime: "2024-11-01T12:00:00Z",
  createBy: 1,
  updateBy: 2,
  // ... 更多字段
}
// 总计:~500+ bytes 每个请求

改进:内存占用减少 85%!


监控和调试

打印当前上下文(调试)

// 在任何 Service 中
const context = RequestContext.get();
console.log('Current context:', context);
// 输出:{ userId: 25n, username: "zhangsan", departmentId: 1n }

验证上下文是否为空

async process() {
  const userId = this.userContextUtil.getCurrentUserId();

  if (!userId) {
    this.logger.warn('User context not found - request may not have proper authentication');
    // 决定是否抛出异常或使用默认值
    return this.userContextUtil.getCurrentUserIdOrDefault();
  }

  // 继续业务逻辑
}

添加日志追踪

async create(dto: CreateWorkOrderDto) {
  const userId = this.userContextUtil.getCurrentUserId();

  this.logger.log(`User ${userId} creating work order`, {
    customerId: dto.customerId,
    timestamp: new Date().toISOString()
  });

  // ... 业务逻辑
}

总结检查清单

项目状态说明
异步链起点包裹在 RequestContextMiddleware 中已实现
Promise.all 支持可安全使用在同一请求中
数据存储优化仅存储必要字段,已优化
敏感数据保护不存储 password、tokens 等
内存占用从 500+ bytes 降至 80 bytes/请求
编译验证零编译错误
向后兼容支持渐进迁移
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值