NestJS 集成 RabbitMQ(CloudAMQP)实战指南

NestJS 集成 RabbitMQ(CloudAMQP)实战指南

目标:让你在 15~30 分钟 内,亲手完成一个"定时扫描 → 发布事件 → 消费并发送邮件通知"的完整 MQ 流程。


1. 准备工作

1.1 账号 & 环境

  • Node.js ≥ 18.x(建议 20.x)
  • Nest CLI:npm i -g @nestjs/cli
  • Git、VS Code 等基础工具

1.2 云服务账号

服务 用途 免费层 说明
CloudAMQP 托管 RabbitMQ 队列 选 "Little Lemur"
QQ / Gmail SMTP 发件人(可选) 用于邮件提醒,可使用自定义 SMTP

2. 创建 CloudAMQP 实例

  1. 访问 www.cloudamqp.com/ 注册并登录

  2. Create new instance → 选择 "Little Lemur" → 选择离你最近的 Region

  3. 创建成功后,在实例详情页找到 AMQP URL,格式类似:

    perl 复制代码
    amqps://用户名:密码@host/vhost

    这就是后端连接 RabbitMQ 所需的 Connection String。


3. 搭建 Nest 项目骨架

bash 复制代码
nest new nest-mq-demo
cd nest-mq-demo
npm install @nestjs/microservices amqplib amqp-connection-manager
npm install nodemailer @nestjs/schedule

目录结构(简化):

arduino 复制代码
src
├── app.module.ts
├── main.ts
└── modules
    └── notifications
        ├── notifications.module.ts
        ├── notification-producer.service.ts
        ├── notification-consumer.service.ts
        ├── notification-scanner.service.ts
        └── email.service.ts

4. 配置环境变量

config/local.env.example(参考):

dotenv 复制代码
RABBITMQ_URL=amqps://<user>:<password>@<host>/<vhost>
RABBITMQ_NOTIFICATION_QUEUE=notifications

NOTIFICATION_EMAIL_ENABLED=true
NOTIFICATION_LEAD_DAYS=3
NOTIFICATION_EMAIL_RECIPIENTS=you@example.com

MAIL_HOST=smtp.qq.com
MAIL_PORT=465
MAIL_SECURE=true
MAIL_USER=you@qq.com
MAIL_PASS=<QQ邮箱授权码>
MAIL_FROM=Housekeeper <you@qq.com>

QQ 邮箱需要在"设置 → 账户 → POP3/SMTP" 中开启服务并获取授权码。


5. 配置中心:config/configuration.ts

为了统一读取配置,建议在 config/configuration.ts 中扩展配置项:

ts 复制代码
import { registerAs } from '@nestjs/config';

export const appConfig = registerAs('app', () => ({
  rabbitmq: {
    url: process.env.RABBITMQ_URL!,
    notificationQueue:
      process.env.RABBITMQ_NOTIFICATION_QUEUE ?? 'notifications',
  },
  notification: {
    emailEnabled: process.env.NOTIFICATION_EMAIL_ENABLED !== 'false',
    leadDays: parseInt(process.env.NOTIFICATION_LEAD_DAYS ?? '3', 10),
    recipients: (process.env.NOTIFICATION_EMAIL_RECIPIENTS ?? '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean),
  },
  mail: {
    host: process.env.MAIL_HOST ?? '',
    port: parseInt(process.env.MAIL_PORT ?? '465', 10),
    secure: process.env.MAIL_SECURE !== 'false',
    user: process.env.MAIL_USER ?? '',
    pass: process.env.MAIL_PASS ?? '',
    from: process.env.MAIL_FROM ?? 'no-reply@example.com',
  },
}));

6. App 模块 & 主程序

6.1 app.module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import configuration from './config/configuration';
import { NotificationsModule } from './modules/notifications/notifications.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
    ScheduleModule.forRoot(),
    TypeOrmModule.forRoot({ /* ...数据库配置... */ }),
    NotificationsModule,
    // 其他业务模块...
  ],
})
export class AppModule {}

6.2 main.ts

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { Logger } from '@nestjs/common';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { ConfigService } from '@nestjs/config';
import { AppConfigType } from './config/configuration';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  const configService = app.get<ConfigService<{ app: AppConfigType }, true>>(
    ConfigService,
  );
  const appConfig = configService.getOrThrow<AppConfigType>('app');

  const microserviceOptions: MicroserviceOptions = {
    transport: Transport.RMQ,
    options: {
      urls: [appConfig.rabbitmq.url],
      queue: appConfig.rabbitmq.notificationQueue,
      queueOptions: { durable: true },
    },
  };

  app.connectMicroservice<MicroserviceOptions>(microserviceOptions);

  await app.startAllMicroservices();
  await app.listen(appConfig.port ?? 3000);

  Logger.log('Notification microservice connected to RabbitMQ');
}

bootstrap();

7. Notifications 模块实现

7.1 notifications.module.ts

ts 复制代码
import { Module, Controller } from '@nestjs/common';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Stock } from '../stock/entities/stock.entity';   // 示例实体
import { NotificationProducerService } from './notification-producer.service';
import { NotificationConsumerService } from './notification-consumer.service';
import { EmailService } from './email.service';
import { NotificationScannerService } from './notification-scanner.service';
import { AppConfigType } from '../../config/configuration';

@Module({
  imports: [
    ConfigModule,
    TypeOrmModule.forFeature([Stock /*, Item, Location ... */]),
  ],
  controllers: [NotificationConsumerService],
  providers: [
    {
      provide: 'NOTIFICATION_QUEUE',
      inject: [ConfigService],
      useFactory: (config: ConfigService<{ app: AppConfigType }, true>) => {
        const appConfig = config.getOrThrow<AppConfigType>('app');
        return ClientProxyFactory.create({
          transport: Transport.RMQ,
          options: {
            urls: [appConfig.rabbitmq.url],
            queue: appConfig.rabbitmq.notificationQueue,
            queueOptions: { durable: true },
          },
        });
      },
    },
    NotificationProducerService,
    EmailService,
    NotificationScannerService,
  ],
  exports: [NotificationProducerService, NotificationScannerService],
})
export class NotificationsModule {}

7.2 事件定义 & 生产者

notification-producer.service.ts

ts 复制代码
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

export interface StockNotificationEvent {
  stockId: string;
  itemName: string;
  quantity: number;
  minQuantity: number;
  expiryDate: string | null;
  type: 'low_stock' | 'near_expiry';
  detectedAt: string;
}

@Injectable()
export class NotificationProducerService {
  private readonly logger = new Logger(NotificationProducerService.name);

  constructor(
    @Inject('NOTIFICATION_QUEUE') private readonly client: ClientProxy,
  ) {}

  publish(event: StockNotificationEvent): void {
    this.client.emit<StockNotificationEvent>('notifications.stock', event);
    this.logger.debug(
      `Published notification event for stock ${event.stockId}: ${event.type}`,
    );
  }
}

7.3 消费者(事件处理器)

notification-consumer.service.ts

ts 复制代码
import { Controller, Logger } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
import { ConfigService } from '@nestjs/config';
import type { StockNotificationEvent } from './notification-producer.service';
import { EmailService } from './email.service';
import { AppConfigType } from '../../config/configuration';

@Controller()
export class NotificationConsumerService {
  private readonly logger = new Logger(NotificationConsumerService.name);
  private readonly emailEnabled: boolean;
  private readonly recipients: string[];

  constructor(
    configService: ConfigService<{ app: AppConfigType }, true>,
    private readonly emailService: EmailService,
  ) {
    const appConfig = configService.getOrThrow<AppConfigType>('app');
    this.emailEnabled = appConfig.notification.emailEnabled;
    this.recipients = appConfig.notification.recipients;
  }

  @EventPattern('notifications.stock')
  async handleStockNotification(
    @Payload() event: StockNotificationEvent,
  ): Promise<void> {
    this.logger.log(
      `Received notification for stock ${event.stockId}: ${event.type}`,
    );

    if (!this.emailEnabled || !this.recipients.length) return;

    await this.emailService.sendStockNotification(event, this.recipients);
  }
}

7.4 邮件服务

email.service.ts

ts 复制代码
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import type { Transporter } from 'nodemailer';
import type { StockNotificationEvent } from './notification-producer.service';
import { AppConfigType } from '../../config/configuration';

@Injectable()
export class EmailService {
  private readonly logger = new Logger(EmailService.name);
  private readonly transporter?: Transporter;
  private readonly from: string;

  constructor(configService: ConfigService<{ app: AppConfigType }, true>) {
    const appConfig = configService.getOrThrow<AppConfigType>('app');
    const { host, port, secure, user, pass, from } = appConfig.mail;
    this.from = from;

    if (!host || !user || !pass) {
      this.logger.warn('Mail service not configured, skip sending');
      return;
    }

    this.transporter = nodemailer.createTransport({
      host,
      port,
      secure,
      auth: { user, pass },
    });
  }

  async sendStockNotification(
    event: StockNotificationEvent,
    recipients: string[],
  ): Promise<void> {
    if (!this.transporter) return;

    const subject =
      event.type === 'low_stock'
        ? `低库存提醒:${event.itemName}`
        : `临期提醒:${event.itemName}`;

    const html = `
      <p>${subject}</p>
      <ul>
        <li>数量:${event.quantity}</li>
        <li>阈值:${event.minQuantity}</li>
        ${event.expiryDate ? `<li>过期:${event.expiryDate}</li>` : ''}
        <li>检测时间:${event.detectedAt}</li>
      </ul>
    `;

    await this.transporter.sendMail({
      from: this.from,
      to: recipients,
      subject,
      html,
    });

    this.logger.log(`Notification email sent for ${event.stockId}`);
  }
}

7.5 定时扫描

notification-scanner.service.ts

ts 复制代码
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationProducerService } from './notification-producer.service';
import { Stock } from '../stock/entities/stock.entity';
import { ConfigService } from '@nestjs/config';
import { AppConfigType } from '../../config/configuration';

@Injectable()
export class NotificationScannerService {
  private readonly logger = new Logger(NotificationScannerService.name);
  private readonly leadDays: number;

  constructor(
    @InjectRepository(Stock)
    private readonly stockRepository: Repository<Stock>,
    private readonly producerService: NotificationProducerService,
    configService: ConfigService<{ app: AppConfigType }, true>,
  ) {
    const appConfig = configService.getOrThrow<AppConfigType>('app');
    this.leadDays = appConfig.notification.leadDays;
  }

  @Cron(CronExpression.EVERY_HOUR)
  async scanAndDispatch(): Promise<void> {
    this.logger.debug('Running notification scan');
    const stocks = await this.stockRepository.find({
      relations: ['item', 'location'],
    });

    const now = new Date();
    const leadDate = new Date(now);
    leadDate.setDate(now.getDate() + this.leadDays);

    for (const stock of stocks) {
      if (stock.quantity <= stock.minQuantity) {
        this.producerService.publish({
          stockId: stock.id,
          itemName: stock.item.name,
          quantity: stock.quantity,
          minQuantity: stock.minQuantity,
          expiryDate: stock.expiryDate,
          type: 'low_stock',
          detectedAt: now.toISOString(),
        });
      }

      if (stock.expiryDate && new Date(stock.expiryDate) <= leadDate) {
        this.producerService.publish({
          stockId: stock.id,
          itemName: stock.item.name,
          quantity: stock.quantity,
          minQuantity: stock.minQuantity,
          expiryDate: stock.expiryDate,
          type: 'near_expiry',
          detectedAt: now.toISOString(),
        });
      }
    }
  }
}

8. 运行验证

  1. 启动服务

    bash 复制代码
    npm run start:dev

    控制台应出现:

    arduino 复制代码
    Notification microservice connected to RabbitMQ
  2. 准备测试数据

    创建一个库存数量小于阈值的记录,或让 expiryDate 接近当前日期。

  3. 等待 Cron / 手动扫描

    • 当前 Cron 表达式为 EVERY_HOUR,可修改为 EVERY_MINUTE 方便测试。

    • 或在开发环境手动执行:

      ts 复制代码
      await app.get(NotificationScannerService).scanAndDispatch();
  4. 观察日志 & 邮件

    • 控制台应依次打印 Published...Received...
    • QQ 邮箱(或指定收件人)应收到提醒邮件。

9. 常见问题排查

错误信息 解决方案
Missing "amqplib" or "amqp-connection-manager" npm install amqplib amqp-connection-manager
Error: There is no matching event handler defined... 确认使用 @EventPattern,并且通知服务注册在 Module 的 controllers
ECONNREFUSED / CONNECTION_FORCED 检查 CloudAMQP URL、队列名称是否正确;是否超过免费层限制
邮件发送失败、无日志 检查 SMTP 配置、授权码;确认 NOTIFICATION_EMAIL_ENABLEDtrue;查看是否进入垃圾邮件
QQ 邮箱"未启用 STMP" 在 QQ 邮箱后台开启 POP3/SMTP 服务,获取授权码

10. 后续扩展建议

  • 引入数据库表记录提醒历史、已读状态;
  • 对接 WebSocket(实时推送到前端);
  • 在库存更新(采购、使用)处发布事件,替代定时扫描;
  • 使用消息队列的死信队列、重试机制强化可靠性;
  • 将配置拆分到多个配置文件(开发/生产)。

11. 总结

通过本教程,你完成了:

  1. CloudAMQP 实例的创建与连接;
  2. NestJS 微服务(RabbitMQ)消息管道;
  3. 定时扫描库存 → 发布事件 → 消费 → 发送邮件;
  4. 配置、测试、排错的完整流程。

掌握这一套,用于库存预警、订单通知、日志异步处理等都能信手拈来。祝你写出一篇高质量的博客!🚀

复制代码
相关推荐
j***827016 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
android·前端·后端
小华同学ai16 小时前
终于有人帮你整理好了,火爆的“系统级提示词”支持ChatGPT、Claude、Gemini、xAI的
前端·后端·github
z***897116 小时前
Flask框架中SQLAlchemy的使用方法
后端·python·flask
s***45316 小时前
Springboot-配置文件中敏感信息的加密:三种加密保护方法比较
java·spring boot·后端
HashTang16 小时前
一个人就是一支队伍:从 Next.js 到显示器,聊聊我的“全栈续航”方案
前端·后端·程序员
Java水解16 小时前
Springboot | Spring Boot 3 纯 JDBC 实现宠物管理系统增删改查(无 ORM 框架)
spring boot·后端
q***484117 小时前
Redis Desktop Manager(Redis可视化工具)安装及使用详细教程
android·前端·后端
w***741717 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
chenyuhao202417 小时前
MySQL事务
开发语言·数据库·c++·后端·mysql
小马爱打代码17 小时前
Spring Boot:Service 层的正确写法 - 事务、幂等、聚合、拆分与业务抽象
spring boot·后端