Nest.js寫一個(gè)定時(shí)發(fā)郵件任務(wù)?太簡單了!
概要
前面幾章完成了,當(dāng)日任務(wù)和長期目標(biāo)的基礎(chǔ)模塊,現(xiàn)在我將要完成定時(shí)任務(wù)模塊。就像我一開始介紹的那樣,我要對(duì)我每天沒有完成的任務(wù),或者長期目標(biāo)沒有達(dá)成的情況下,發(fā)送電子郵件來提醒我。
如果大家時(shí)間充裕的話,可以看下相關(guān)的文章使用Cron Jobs和NestJS實(shí)現(xiàn)任務(wù)自動(dòng)化[1]和通過工作隊(duì)列發(fā)送郵件[2]。重點(diǎn)要看下Cron Jobs,里面有對(duì)時(shí)間設(shè)置的具體說明。
由于個(gè)人管理項(xiàng)目,沒有什么特別需要處理高并發(fā)的需求,所以我只寫了普通的郵件發(fā)送就足夠了,不需要通過工作隊(duì)列來處理。
定時(shí)任務(wù)介紹
NestJS 提供了一種非常方便的方式來創(chuàng)建定時(shí)任務(wù),通常用于執(zhí)行周期性的后臺(tái)任務(wù),例如數(shù)據(jù)同步、數(shù)據(jù)清理、報(bào)告生成等等。
以下是如何在 NestJS 中創(chuàng)建和使用定時(shí)任務(wù)的步驟:
- 安裝相關(guān)依賴:
$ pnpm install --save @nestjs/schedule
- 在app.module.ts中注冊(cè)定時(shí)任務(wù):
// app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot()
],
})
export class AppModule {}
- 創(chuàng)建定時(shí)任務(wù)服務(wù)
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');
}
}
詳細(xì)內(nèi)容請(qǐng)參照官方文檔[3]。
郵件發(fā)送
在 NestJS 中發(fā)送郵件通常涉及使用郵件發(fā)送庫,如 Nodemailer。下面是如何在 NestJS 中發(fā)送郵件的一般步驟:
- 安裝 Nodemailer:
首先,你需要在你的 NestJS 項(xiàng)目中安裝 Nodemailer。可以使用以下命令安裝:
$ pnpm install nodemailer
- 創(chuàng)建一個(gè)郵件服務(wù):
在你的 NestJS 應(yīng)用程序中,創(chuàng)建一個(gè)專門的郵件服務(wù),該服務(wù)負(fù)責(zé)配置和發(fā)送郵件。你可以創(chuàng)建一個(gè)自定義的郵件服務(wù)類,也可以將郵件配置添加到現(xiàn)有的服務(wù)中。
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 連接,設(shè)置為 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);
}
}
在這個(gè)示例中,我們創(chuàng)建了一個(gè) EmailService,它接收郵件配置信息并初始化 Nodemailer 的傳輸器。然后,它提供了一個(gè) sendMail 方法,用于發(fā)送郵件。
- 注冊(cè)郵件服務(wù):
將 EmailService 添加到你的 NestJS 模塊的 providers 數(shù)組中,以便在整個(gè)應(yīng)用程序中使用它。
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
@Module({
providers: [EmailService],
// ...
})
export class AppModule {}
- 使用郵件服務(wù):
現(xiàn)在,你可以在你的控制器或其他服務(wù)中使用 EmailService 來發(fā)送郵件。例如,在你的控制器中:
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;
}
}
}
這是一個(gè)簡單的示例,說明如何在 NestJS 中發(fā)送郵件。你可以根據(jù)你的需求擴(kuò)展和自定義郵件服務(wù),以適應(yīng)不同的郵件發(fā)送場景。確保你的配置信息正確,以及你的郵件服務(wù)提供了適當(dāng)?shù)腻e(cuò)誤處理來處理可能的發(fā)送失敗情況。
定時(shí)任務(wù)做成
- 首先,因?yàn)樾枰玫疆?dāng)日任務(wù)的長期目標(biāo)模塊里面的方法。先導(dǎo)入這2個(gè)模塊
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 {}
- 然后都要用到郵件發(fā)送,所以要在構(gòu)造函數(shù)中初期化transporter。并且發(fā)送成功或者失敗,希望能在log日志中能看到,所以loggerError個(gè)loggerNomal2個(gè)函數(shù)。
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);
}
// 發(fā)送郵件
async sendReminderEmail(taskTitles: string) {
const MAIL_CONFIG = this.configService.get(ConfigEnum.MAIL_CONFIG);
const mailOptions = {
from: MAIL_CONFIG.authUser,
to: MAIL_CONFIG.destUser,
subject: '您有未完成的任務(wù)',
text: `以下是您今天尚未完成的任務(wù):\n\n${taskTitles}`,
};
this.transporter.sendMail(mailOptions, (error, info) => {
if (error) {
this.loggerError(`郵件發(fā)送失敗: ${error}`);
} else {
this.loggerNomal(`郵件已發(fā)送: ${info.response}`);
}
});
}
}
- 簡單的介紹下,項(xiàng)目中定時(shí)任務(wù)的內(nèi)容。
- 每天下午17:30點(diǎn),檢查當(dāng)日任務(wù)里面有沒有沒完成的任務(wù)。如果有就發(fā)送電子郵件
@Cron('30 17 * * *') // 每天下午7點(diǎn)30分執(zhí)行
async handleCron() {
this.loggerNomal('開始檢查未完成的任務(wù)');
const currentDate = dayjs().format('YYYY-MM-DD');
const startDate = currentDate;
const endDate = currentDate;
const tasks = await this.tasksService.search({
startDate,
endDate,
});
// 過濾當(dāng)日任務(wù)中已完成的任務(wù)。
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點(diǎn),檢查長期目標(biāo)里面有沒有臨近的任務(wù)還沒有完成。如果有就發(fā)送電子郵件
@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);
}
}
- 長期目標(biāo)模塊中的方法
// 在你的目標(biāo)服務(wù)中
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();
}