基于Nest.js(Typescript)+Mongodb+TS定时任务实现发送邮件功能(qq邮箱)
基于Nest.js(Typescript)+Mongodb+TS定时任务实现发送邮件功能(qq邮箱)
一、背景与问题
在现代Web应用中,邮件通知功能是常见的业务需求。例如用户注册后发送验证邮件、订单支付成功后发送通知邮件等场景。传统做法是通过同步方式调用邮件服务,但存在以下问题:
- 同步调用阻塞:在高并发场景下,邮件发送可能成为性能瓶颈
- 可靠性不足:网络波动或服务异常可能导致邮件丢失
- 资源浪费:每次请求都建立SMTP连接会消耗大量资源
- 调度困难:定时任务需要复杂的时间管理机制
本方案通过Nest.js的定时任务功能,结合MongoDB存储邮件记录,实现异步、可靠的邮件发送系统。特别适用于需要定时处理邮件发送、需要记录发送状态、需要处理邮件重试等场景。
二、基本原理
整个系统分为三个核心模块:
- 邮件接收模块:接收用户请求,存储邮件记录到MongoDB
- 定时任务模块:定时从MongoDB中获取待发送邮件
- 邮件发送模块:通过SMTP协议发送邮件,并记录发送结果
关键原理包括:
- 异步处理:通过队列机制解耦邮件接收和发送过程
- 持久化存储:使用MongoDB记录邮件状态,防止数据丢失
- 重试机制:支持发送失败后的自动重试
- 定时调度:使用CronJob模块实现精确的定时任务
三、环境准备
1. 技术栈
- Nest.js(基于TypeScript)
- MongoDB
- nodemailer(邮件发送)
- cron(定时任务)
- dotenv(环境变量管理)
2. 依赖安装
npm install @nestjs/cron @nestjs/common @nestjs/core mongoose dotenv nodemailer3. 环境配置
创建.env文件:
MONGO_URI=mongodb://localhost:27017/email_service
SMTP_HOST=smtp.qq.com
SMTP_PORT=465
SMTP_USER=your@qq.com
SMTP_PASS=your_authorization_code四、核心实现
1. 邮件接收接口
// src/email/email.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailRequest } from './dto/email.request';
@Controller('email')
export class EmailController {
constructor(private readonly emailService: EmailService) {}
@Post('send')
@HttpCode(HttpStatus.CREATED)
async sendEmail(@Body() request: EmailRequest) {
const result = await this.emailService.saveEmail({
...request,
status: 'pending',
createdAt: new Date()
});
return { id: result._id };
}
}关键点说明:
- 使用
HttpCode保证接口返回201状态码 - 邮件内容存储为
pending状态 - 返回邮件ID用于后续查询
2. 邮件发送服务
// src/email/email.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { EmailDocument, Email } from './schema/email.schema';
import { EmailRequest } from './dto/email.request';
import { Cron, CronExpression } from '@nestjs/schedule';
import { MailOptions, Transporter } from 'nodemailer';
@Injectable()
export class EmailService {
private transporter: Transporter;
constructor(
@InjectModel(Email.name) private emailModel: Model<EmailDocument>
) {
this.transporter = this.createTransporter();
}
private createTransporter(): Transporter {
return nodemailer.createTransport({
service: 'qq',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
}
@Cron(CronExpression.EVERY_5_MINUTES)
async sendPendingEmails() {
const emails = await this.emailModel.find({ status: 'pending' }).limit(10);
for (const email of emails) {
try {
await this.sendEmail(email);
await this.emailModel.findByIdAndUpdate(email._id, { status: 'sent' });
} catch (error) {
await this.emailModel.findByIdAndUpdate(email._id, { status: 'failed' });
console.error(`Failed to send email to ${email.to}`, error);
}
}
}
async sendEmail(email: Email) {
const mailOptions: MailOptions = {
from: process.env.SMTP_USER,
to: email.to,
subject: email.subject,
html: email.html
};
await this.transporter.sendMail(mailOptions);
}
}关键点说明:
- 使用
@Cron装饰器创建定时任务 - 每次处理最多10封邮件(防止资源耗尽)
- 错误处理机制确保发送失败的邮件状态更新
- 使用
nodemailer的sendMail方法发送邮件
3. 邮件存储模型
// src/email/schemas/email.schema.ts
import { Schema, Document, Types } from 'mongoose';
export interface EmailDocument extends Document {
_id: Types.ObjectId;
to: string;
subject: string;
html: string;
status: 'pending' | 'sent' | 'failed';
createdAt: Date;
}
const EmailSchema = new Schema({
to: { type: String, required: true },
subject: { type: String, required: true },
html: { type: String, required: true },
status: { type: String, enum: ['pending', 'sent', 'failed'], default: 'pending' },
createdAt: { type: Date, default: Date.now }
});
export default EmailSchema;关键点说明:
- 使用MongoDB的
enum类型限制状态值 - 添加
createdAt字段用于时间排序 - 使用
default设置默认值
五、完整案例
1. 邮件发送接口测试
创建test-email接口用于测试:
// src/email/email.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailRequest } from './dto/email.request';
@Controller('email')
export class EmailController {
constructor(private readonly emailService: EmailService) {}
@Post('send')
@HttpCode(HttpStatus.CREATED)
async sendEmail(@Body() request: EmailRequest) {
const result = await this.emailService.saveEmail({
...request,
status: 'pending',
createdAt: new Date()
});
return { id: result._id };
}
@Post('test')
@HttpCode(HttpStatus.CREATED)
async testEmail() {
const email = {
to: 'test@qq.com',
subject: 'Test Email',
html: '<h1>This is a test email</h1>'
};
await this.emailService.saveEmail(email);
return { message: 'Test email saved' };
}
}2. 定时任务日志记录
在定时任务中添加日志记录:
@Cron(CronExpression.EVERY_5_MINUTES)
async sendPendingEmails() {
const now = new Date();
const logs = [];
const emails = await this.emailModel.find({ status: 'pending' }).limit(10);
for (const email of emails) {
try {
await this.sendEmail(email);
await this.emailModel.findByIdAndUpdate(email._id, { status: 'sent' });
logs.push({
timestamp: now,
emailId: email._id,
status: 'success',
message: 'Email sent successfully'
});
} catch (error) {
await this.emailModel.findByIdAndUpdate(email._id, { status: 'failed' });
logs.push({
timestamp: now,
emailId: email._id,
status: 'error',
message: 'Failed to send email',
error: error.message
});
}
}
// 将日志保存到MongoDB
await this.emailModel.create(logs);
}3. 邮件状态查询接口
// src/email/email.controller.ts
import { Controller, Get, Query, HttpCode, HttpStatus } from '@nestjs/common';
import { EmailService } from './email.service';
@Controller('email')
export class EmailController {
constructor(private readonly emailService: EmailService) {}
@Get('status')
@HttpCode(HttpStatus.OK)
async getEmailStatus(@Query('id') id: string) {
const email = await this.emailService.getEmailById(id);
return email;
}
}六、源码解析
1. 定时任务调度机制
@Cron装饰器底层使用node-schedule库实现,其核心原理是:
- 基于时间间隔的事件驱动机制
- 使用线程池处理任务队列
- 支持多种调度表达式(如CronExpression.EVERY_5_MINUTES)
2. 邮件发送流程
graph TD
A[用户请求发送邮件] --> B[保存邮件记录到MongoDB]
B --> C{是否定时发送?}
C -->|是| D[定时任务触发]
C -->|否| E[立即发送]
D --> F[从MongoDB获取待发送邮件]
F --> G[发送邮件]
G --> H{发送成功?}
H -->|是| I[更新邮件状态为"sent"]
H -->|否| J[更新邮件状态为"failed"]3. 错误处理机制
- 使用try-catch块捕获异常
- 邮件状态更新为失败
- 记录错误日志
- 可扩展重试机制(如使用
retry-axios)
七、进阶使用
1. 重试机制实现
// src/email/email.service.ts
async sendEmail(email: Email, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await this.transporter.sendMail({
...email,
subject: `(${i + 1}) ${email.subject}`
});
await this.emailModel.findByIdAndUpdate(email._id, { status: 'sent' });
return;
} catch (error) {
await this.emailModel.findByIdAndUpdate(email._id, {
status: 'failed',
retryCount: (email.retryCount || 0) + 1
});
console.error(`Attempt ${i + 1} failed: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, 5000 * (i + 1)));
}
}
}2. 邮件模板系统
// src/email/email.service.ts
async sendEmailWithTemplate(email: Email, template: string, data: any) {
const rendered = await this.renderTemplate(template, data);
await this.sendEmail({
...email,
html: rendered,
subject: `${email.subject} - Template ${template}`
});
}
private async renderTemplate(template: string, data: any) {
// 使用Handlebars或EJS模板引擎渲染
return await this.templateEngine.render(template, data);
}3. 邮件分类处理
// src/email/email.service.ts
async sendEmailWithCategory(email: Email, category: string) {
const categoryConfig = await this.configService.getCategoryConfig(category);
const finalEmail = {
...email,
subject: `${categoryConfig.prefix} ${email.subject}`,
html: `${categoryConfig.header}${email.html}${categoryConfig.footer}`
};
await this.sendEmail(finalEmail);
}八、性能与工程实践
1. 性能优化策略
| 优化措施 | 说明 |
|---|---|
| 连接池配置 | 配置SMTP连接池大小(默认10) |
| 批处理发送 | 每次处理最多10封邮件 |
| 缓存模板 | 使用Redis缓存模板内容 |
| 分页处理 | 限制每次查询的邮件数量 |
| 异步处理 | 使用队列系统(如RabbitMQ) |
2. 异常处理机制
- 使用
try-catch捕获异常 - 邮件状态更新为失败
- 记录错误日志
- 可扩展重试机制
3. 安全实践
- 敏感信息保护:使用
.env文件存储SMTP凭证 - 输入验证:使用
class-validator校验邮件参数 - XSS防护:对邮件内容进行HTML转义
- 日志安全:避免记录敏感信息到日志
4. 高可用方案
- 使用MongoDB副本集保证数据可靠性
- 部署多个Nest.js实例并使用Redis共享队列
- 配置负载均衡器
- 使用云服务的自动扩展功能
九、常见问题与踩坑
1. 常见错误及解决方案
| 错误类型 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 10002 | SMTP身份验证失败 | SMTP配置错误 | 检查QQ邮箱SMTP设置 |
| 429 | 请求过多 | 频繁发送邮件 | 增加定时任务间隔 |
| 550 | 邮件服务器拒绝 | 邮件内容不符合规范 | 检查邮件内容格式 |
| 500 | 内部服务器错误 | 代码逻辑错误 | 检查日志输出 |
| 11003 | 邮件内容过大 | 邮件内容超出限制 | 简化邮件内容 |
2. 高级问题
- 邮件发送延迟:检查定时任务调度策略
- 邮件丢失:检查MongoDB的持久化配置
- 资源耗尽:限制每次处理的邮件数量
- 安全漏洞:防止邮件内容被恶意篡改
十、最佳实践
1. 推荐方案
- 定时任务:使用
@nestjs/schedule的@Cron装饰器 - 邮件存储:使用MongoDB的文档模型存储
- 邮件发送:使用
nodemailer的SMTP协议 - 错误处理:实现重试机制和日志记录
- 扩展性:设计可扩展的邮件模板系统
2. 使用场景建议
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 定时发送通知 | ✅ | 适合需要定时处理的场景 |
| 高并发邮件发送 | ✅ | 通过队列机制保证可靠性 |
| 邮件内容需要模板 | ✅ | 支持动态内容生成 |
| 需要记录发送状态 | ✅ | 自动记录邮件状态 |
| 需要重试机制 | ✅ | 内置重试机制 |
| 需要快速开发 | ✅ | 简化开发流程 |
3. 不适用场景
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 实时邮件发送 | ❌ | 无法保证实时性 |
| 需要复杂路由规则 | ❌ | 不支持复杂的路由逻辑 |
| 需要处理大量附件 | ❌ | 需要额外处理附件 |
| 需要集成第三方邮件服务商 | ❌ | 需要额外配置 |
十一、总结
本方案通过Nest.js的定时任务功能,结合MongoDB的持久化存储,实现了可靠的邮件发送系统。关键点包括:
- 异步处理:通过队列机制解耦邮件接收和发送
- 持久化存储:确保邮件状态不会丢失
- 重试机制:处理发送失败的情况
- 定时调度:精确控制发送时间
- 安全防护:防止敏感信息泄露
适用场景包括定时通知、邮件验证、订单通知等场景,不适用需要实时响应或复杂路由规则的场景。开发过程中需要注意SMTP配置、错误处理和性能优化,确保系统的稳定性和可靠性。通过合理的设计,可以构建一个可扩展、可维护的邮件发送系统。
评论已关闭