概要
前面几章完成了,当日任务和长期目标的基础模块,现在我将要完成定时任务模块。就像我一开始介绍的那样,我要对我每天没有完成的任务,或者长期目标没有达成的情况下,发送电子邮件来提醒我。
如果大家时间充裕的话,可以看下相关的文章使用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();
}