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 实例
-
访问 www.cloudamqp.com/ 注册并登录
-
Create new instance→ 选择 "Little Lemur" → 选择离你最近的 Region -
创建成功后,在实例详情页找到
AMQP URL,格式类似:perlamqps://用户名:密码@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. 运行验证
-
启动服务
bashnpm run start:dev控制台应出现:
arduinoNotification microservice connected to RabbitMQ -
准备测试数据
创建一个库存数量小于阈值的记录,或让
expiryDate接近当前日期。 -
等待 Cron / 手动扫描
-
当前 Cron 表达式为
EVERY_HOUR,可修改为EVERY_MINUTE方便测试。 -
或在开发环境手动执行:
tsawait app.get(NotificationScannerService).scanAndDispatch();
-
-
观察日志 & 邮件
- 控制台应依次打印
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_ENABLED 为 true;查看是否进入垃圾邮件 |
| QQ 邮箱"未启用 STMP" | 在 QQ 邮箱后台开启 POP3/SMTP 服务,获取授权码 |
10. 后续扩展建议
- 引入数据库表记录提醒历史、已读状态;
- 对接 WebSocket(实时推送到前端);
- 在库存更新(采购、使用)处发布事件,替代定时扫描;
- 使用消息队列的死信队列、重试机制强化可靠性;
- 将配置拆分到多个配置文件(开发/生产)。
11. 总结
通过本教程,你完成了:
- CloudAMQP 实例的创建与连接;
- NestJS 微服务(RabbitMQ)消息管道;
- 定时扫描库存 → 发布事件 → 消费 → 发送邮件;
- 配置、测试、排错的完整流程。
掌握这一套,用于库存预警、订单通知、日志异步处理等都能信手拈来。祝你写出一篇高质量的博客!🚀