Nest.js寫一個定時發郵件任務?太簡單了!
概要
前面幾章完成了,當日任務和長期目標的基礎模塊,現在我將要完成定時任務模塊。就像我一開始介紹的那樣,我要對我每天沒有完成的任務,或者長期目標沒有達成的情況下,發送電子郵件來提醒我。
如果大家時間充裕的話,可以看下相關的文章使用Cron Jobs和NestJS實現任務自動化[1]和通過工作隊列發送郵件[2]。重點要看下Cron Jobs,里面有對時間設置的具體說明。
由于個人管理項目,沒有什么特別需要處理高并發的需求,所以我只寫了普通的郵件發送就足夠了,不需要通過工作隊列來處理。
定時任務介紹
NestJS 提供了一種非常方便的方式來創建定時任務,通常用于執行周期性的后臺任務,例如數據同步、數據清理、報告生成等等。
以下是如何在 NestJS 中創建和使用定時任務的步驟:
- 安裝相關依賴:
$ pnpm install --save @nestjs/schedule
- 在app.module.ts中注冊定時任務:
// app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot()
],
})
export class AppModule {}
- 創建定時任務服務
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron('45 * * * * *')
handleCron() {
this.logger.debug('Called when the current second is 45');
}
}
詳細內容請參照官方文檔[3]。
郵件發送
在 NestJS 中發送郵件通常涉及使用郵件發送庫,如 Nodemailer。下面是如何在 NestJS 中發送郵件的一般步驟:
- 安裝 Nodemailer:
首先,你需要在你的 NestJS 項目中安裝 Nodemailer。可以使用以下命令安裝:
$ pnpm install nodemailer
- 創建一個郵件服務:
在你的 NestJS 應用程序中,創建一個專門的郵件服務,該服務負責配置和發送郵件。你可以創建一個自定義的郵件服務類,也可以將郵件配置添加到現有的服務中。
import { Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from './enum/config.enum';
@Injectable()
export class EmailService {
private transporter;
constructor(private readonly configService: ConfigService) {
const mailConfig = this.configService.get(ConfigEnum.MAIL_CONFIG);
this.transporter = nodemailer.createTransport({
host: mailConfig.host,
port: mailConfig.port,
secure: true, // 如果是 SMTPS 連接,設置為 true
auth: {
user: mailConfig.authUser,
pass: mailConfig.authPass,
},
});
}
async sendMail(to: string, subject: string, text: string): Promise<void> {
const mailOptions = {
from: this.configService.get(ConfigEnum.MAIL_CONFIG).authUser,
to,
subject,
text,
};
await this.transporter.sendMail(mailOptions);
}
}
在這個示例中,我們創建了一個 EmailService,它接收郵件配置信息并初始化 Nodemailer 的傳輸器。然后,它提供了一個 sendMail 方法,用于發送郵件。
- 注冊郵件服務:
將 EmailService 添加到你的 NestJS 模塊的 providers 數組中,以便在整個應用程序中使用它。
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
@Module({
providers: [EmailService],
// ...
})
export class AppModule {}
- 使用郵件服務:
現在,你可以在你的控制器或其他服務中使用 EmailService 來發送郵件。例如,在你的控制器中:
import { Controller, Get } from '@nestjs/common';
import { EmailService } from './email.service';
@Controller('email')
export class EmailController {
constructor(private readonly emailService: EmailService) {}
@Get('send')
async sendEmail() {
try {
await this.emailService.sendMail('recipient@example.com', 'Hello', 'This is the email body.');
return 'Email sent successfully!';
} catch (error) {
return 'Email sending failed: ' + error;
}
}
}
這是一個簡單的示例,說明如何在 NestJS 中發送郵件。你可以根據你的需求擴展和自定義郵件服務,以適應不同的郵件發送場景。確保你的配置信息正確,以及你的郵件服務提供了適當的錯誤處理來處理可能的發送失敗情況。
定時任務做成
- 首先,因為需要用到當日任務的長期目標模塊里面的方法。先導入這2個模塊
import { Module } from '@nestjs/common';
import { TasksCronService } from './tasks-cron.service';
import { TasksModule } from '../tasks/tasks.module';
import { LongTeamGoalsModule } from '../long-team-goals/long-team-goals.module';
@Module({
imports: [TasksModule, LongTeamGoalsModule],
providers: [TasksCronService],
})
export class TasksCronModule {}
- 然后都要用到郵件發送,所以要在構造函數中初期化transporter。并且發送成功或者失敗,希望能在log日志中能看到,所以loggerError個loggerNomal2個函數。
import * as nodemailer from 'nodemailer';
import { TasksService } from '../tasks/tasks.service';
import { LongTeamGoalsService } from '../long-team-goals/long-team-goals.service';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from '../enum/config.enum';
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class TasksCronService {
private transporter;
private readonly logger = new Logger(TasksCronService.name);
constructor(
private readonly configService: ConfigService,
private readonly tasksService: TasksService,
private readonly longsService: LongTeamGoalsService,
) {
const MAIL_CONFIG = this.configService.get(ConfigEnum.MAIL_CONFIG);
this.transporter = nodemailer.createTransport({
host: MAIL_CONFIG.host,
port: MAIL_CONFIG.port,
secure: true,
auth: {
user: MAIL_CONFIG.authUser,
pass: MAIL_CONFIG.authPass,
},
});
}
loggerError(message: string, error?: any) {
this.logger.error(message, error);
}
loggerNomal(message: string, info?: any) {
this.logger.log(message, info);
}
// 發送郵件
async sendReminderEmail(taskTitles: string) {
const MAIL_CONFIG = this.configService.get(ConfigEnum.MAIL_CONFIG);
const mailOptions = {
from: MAIL_CONFIG.authUser,
to: MAIL_CONFIG.destUser,
subject: '您有未完成的任務',
text: `以下是您今天尚未完成的任務:\n\n${taskTitles}`,
};
this.transporter.sendMail(mailOptions, (error, info) => {
if (error) {
this.loggerError(`郵件發送失敗: ${error}`);
} else {
this.loggerNomal(`郵件已發送: ${info.response}`);
}
});
}
}
- 簡單的介紹下,項目中定時任務的內容。
- 每天下午17:30點,檢查當日任務里面有沒有沒完成的任務。如果有就發送電子郵件
@Cron('30 17 * * *') // 每天下午7點30分執行
async handleCron() {
this.loggerNomal('開始檢查未完成的任務');
const currentDate = dayjs().format('YYYY-MM-DD');
const startDate = currentDate;
const endDate = currentDate;
const tasks = await this.tasksService.search({
startDate,
endDate,
});
// 過濾當日任務中已完成的任務。
const incompleteTasks = tasks.data.filter((task) => !task.isCompleted);
if (incompleteTasks.length) {
const titles = incompleteTasks.map((task) => task.title).join('\n');
await this.sendReminderEmail(titles);
}
}
- 每天晚上22:00點,檢查長期目標里面有沒有臨近的任務還沒有完成。如果有就發送電子郵件
@Cron('0 22 * * *')
async handleLongCron() {
const nearlyExpiredGoals = await this.longsService.findNearlyExpiredGoals(
3,
);
if (nearlyExpiredGoals.length) {
const titles = nearlyExpiredGoals.map((long) => long.title).join('\n');
await this.sendReminderEmail(titles);
await this.longsService.setWarned(nearlyExpiredGoals);
}
}
- 長期目標模塊中的方法
// 在你的目標服務中
async findNearlyExpiredGoals(days: number): Promise<any[]> {
const deadlineDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
return this.longTermGoalModel
.find({
deadline: { $lte: deadlineDate },
isWarned: false,
})
.exec();
}