NestJS @Cron 事务邮件 + Rovela(2026):@nestjs/schedule 与按窗口幂等键
用 NestJS @nestjs/schedule 的 @Cron 对接 Rovela POST /emails:服务端 sending_access,按调度窗口 Idempotency-Key,适合摘要与运维报告。
NestJS Schedule 负责调度 — Rovela 负责投递
NestJS Schedule(@nestjs/schedule)把 cron、interval、timeout 装饰器带进 Nest 应用。用 @Cron('0 9 * * *')(或 CronExpression.*)标注 provider 方法,导入 ScheduleModule.forRoot(),并保持 Nest 进程常驻。表达式匹配时 Nest 通过 DI 调用该方法——适合正文依赖触发时实时数据库状态的摘要,而不是提前用 Rovela scheduled_at 冻住的内容。
你的任务方法调用 POST https://api.rovela.dev/emails 时,Rovela 负责排队、DKIM 签名与投递(Send Email API、Quick Start、Authentication)。NestJS Schedule 管节奏与「是否到期」;任务方法管聚合、sending_access 密钥,以及按调度窗口稳定的 Idempotency-Key。Rovela 只负责投递。
本指南串联 NestJS Schedule → 任务方法 → Rovela:
- 在 provider 方法上声明
@Cron("0 9 * * *", { name: "daily-digest-email", timeZone: "UTC" }) - 将
ROVELA_API_KEY(sending_access)、MAIL_FROM、收件人放在仅服务端配置——绝不进浏览器、SPA 或公开 Git - 在任务方法内按当前窗口聚合摘要/报告
- 用窗口级
Idempotency-Key调用 Rovela - 在重试、进程重启、多 Worker 下避免重复发信
Rovela 不会创建 NestJS Schedule 的调度、Worker 或存储。 NestJS Schedule 管 schedule → 触发 → execute;你的方法管查询 → 映射字段 → POST /emails;Rovela 管投递。
与下列文章角度不同:
- Quartz 调度事务邮件 — Java Quartz
- node-cron 事务邮件 — node-cron
- Laravel Task Scheduler 邮件 — Laravel Schedule::
- APScheduler Cron 邮件 — APScheduler
- Spring @Scheduled 邮件 — Spring @Scheduled
- Resque-scheduler 邮件 — Resque-scheduler
- RQ Scheduler 邮件 — RQ Scheduler
- delayed_job 周期邮件 — delayed_job
- Solid Queue 周期邮件 — Solid Queue
- Hangfire Recurring 事务邮件 — Hangfire
- Sidekiq-Cron 事务邮件 — Sidekiq-Cron
- Celery Beat 事务邮件 — Celery Beat
- Agenda.js 定时任务邮件 — agenda
- BullMQ 可重复任务邮件 — BullMQ
- Temporal Schedule 邮件 — Temporal
- Trigger.dev Cron 邮件 — Trigger.dev
- Inngest Cron 事务邮件 — Inngest
- Kubernetes CronJob 邮件 — K8s CronJob
- Vercel Cron 摘要邮件 — Vercel cron
- 定时发送邮件 — scheduled_at
也不同于 Nest 之外的 node-cron,以及一次性 Nest 发信(OTP / magic link)——本文只讲 周期 @Cron。
NestJS Schedule 调度什么,Rovela 发送什么
| 关注点 | 归属 |
|---|---|
| Cron、name、时区 | Nest provider 上的 @Cron |
| 进程常驻 | Nest 宿主(PM2 / K8s / systemd) |
| DI(HttpService / 仓库) | Nest IoC |
ROVELA_API_KEY(sending_access) | 进程 / Worker 环境或密钥库 |
| 按窗口查库 / 调 API | 你的任务方法 |
| 渲染 HTML + text | 你的任务方法 |
POST /emails、DKIM、排队、退信 | Rovela |
Idempotency-Key = nest-cron-daily-digest:{yyyy-mm-dd} | 你的任务方法 |
Rovela 返回 200 表示已接受并入队,不是「已进收件箱」。
影响发信任务的 NestJS Schedule 约束
-
Nest 进程必须常驻。
@nestjs/schedule是进程内调度,不是独立 Beat。 -
多副本会双触发,除非只在 worker 角色部署上跑 cron,或加分布式锁。
-
@Cron使用name+timeZone。 -
默认五段 cron;秒字段以包文档为准。
-
邮件 cron 优先独立 worker 进程,不要挂在每个 API 副本上。
-
sending_access仅服务端。 放环境变量 / 密钥库,勿进客户端或 Git。 -
优先 HTTPS
POST /emails。 Worker 直连 SMTP 通常更差(Email API vs SMTP)。
何时用 NestJS Schedule + POST /emails(而不是 scheduled_at)
在下列情况用 NestJS Schedule → 即时发送:
- HTML 必须在触发时包含新鲜指标
- 已在跑 NestJS Schedule,希望调度与 Worker 同处
- 需要类 cron 摘要,但不想另上 SaaS 调度器
- 运行时查库得到的少量运维/创始人/客户地址
正文已知、只需「稍后投递」时,用 Rovela scheduled_at(定时发送邮件)。非 Nest 用 node-cron;需要跨多 Worker 的持久可重复任务时用 BullMQ。
前置条件
- Rovela 已验证发信域名;未验证
from会400。 re_…API Key,Worker 优先sending_access。- 已安装
@nestjs/schedule并调用ScheduleModule.forRoot()的 NestJS 应用。 - 配置:
ROVELA_API_KEY、MAIL_FROM、DIGEST_TO。 - Nest 可实例化的
@Injectable()provider(注入HttpService或使用fetch)。
注册调度
npm install @nestjs/schedule
@Module({ imports: [ScheduleModule.forRoot()], providers: [DigestService] })
export class AppModule {}
@Cron("0 9 * * *", { name: "daily-digest-email", timeZone: "UTC" })
async handleDailyDigest() { await this.sendDigest(); }
调用 Rovela 的任务方法
在 sendDigest 中构造 nest-cron-daily-digest:${day} 幂等键,查询实时统计,然后:
await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ROVELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ from, to, subject, html, text, tags }),
});
切勿记录 API Key。完整 TypeScript 示例见英文版。
窗口级 Idempotency-Key
Rovela 的 Idempotency-Key(最长 256 字符)让 24 小时内相同请求重试返回原响应、不再发第二封。对调度任务很重要:重试会重入方法;Rovela 已 200 但任务未标成功时进程崩溃可能再投递;队列延迟会让墙钟漂移——应按业务窗口键控。
推荐键:
| Key | 场景 |
|---|---|
nest-cron-daily-digest:{yyyy-mm-dd} | 每天一封运维摘要 |
nest-cron-ops-pulse:{yyyy-mm-dd}T{HH}-{MM} | /15 或每小时(分桶到窗口) |
nest-cron-tokyo-ops:{yyyy-mm-dd} | 本地时区晨间摘要 |
nest-cron-weekly-report:{yyyy}-W{ww} | ISO 周报 |
nest-cron-tenant-digest:{tenantId}:{yyyy-mm-dd} | 按租户参数 |
一次任务发多封个性化摘要时,优先每收件人一把 key(或对最多 100 封非模板消息用 POST /emails/batch)。
配置密钥并验证调度
- 在运行调度/Worker 的进程设置
ROVELA_API_KEY(sending_access)、MAIL_FROM、DIGEST_TO。 - 确认计划项的 cron 与时区。
- 确认 Worker 在线。
- 等下一次触发,或在非生产手动触发一次,同时保持窗口 key 稳定。
- 日志核对
idempotencyKey与 RovelaemailId。
生产发信优先 sending_access,不要用 full_access。勿记录 API Key。
故障对照
| 现象 | 可能原因 | 处理 |
|---|---|---|
| Cron 永不触发 | 调度/Worker 宕机 | 保持调度进程常开 |
| 双发摘要 | 多调度器/多副本 | 单一 leader 或分布式锁/持久存储 |
| 墙钟时间不对 | 默认时区错误 | 显式设置时区 |
| 重试后重复摘要 | 不稳定 key | 窗口级 key;24h 内幂等合并 |
Rovela 401 | Key 错误/缺失 | 修服务端环境 |
Rovela 400 提到域名 | MAIL_FROM 未验证 | 在 Rovela Domains 验证 |
Rovela 429 | 限流 | 重试并保持同一 Idempotency-Key |
| 密钥进 Git | 误提交 | 只放环境变量 / 密钥库 |
上线清单
- Rovela 发信域名已验证;
MAIL_FROM匹配 ROVELA_API_KEY为sending_access,仅在 Worker 环境/密钥库- NestJS Schedule 调度与 Worker 按文档常驻
- 文档化 cron + 时区
- Rovela 发送使用窗口级
Idempotency-Key - 日志有
emailId且不打印密钥 - 连续失败 / 调度宕机有告警
- 可选:摘要收件人的 bounce/complaint webhook
FAQ
调度器能不写任务方法直接调 Rovela 吗?没有实质意义——调度器只唤醒你的代码。聚合与 sending_access 必须在你的方法里。Rovela 不托管你的调度器或 Worker。
任务里还要用 scheduled_at 吗?通常不必。调度器已在发送时刻触发且正文是实时的,直接 POST /emails。若现在受理、稍后再投且不想再唤醒进程,才用 scheduled_at(定时发送邮件)。
sending_access 能做什么?足够 POST /emails / batch 入队。读信、改期、取消需要 full_access 或 Dashboard session。Worker 保持 sending_access。
200 等于创始人已收到摘要?否——只表示 Rovela 已接受入队。投递状态看 GET /emails/:id、events 或 webhook。
为何既要调度器重试又要 Idempotency-Key?调度器在代码失败时重试任务;Rovela 的 key 在同一窗口内 HTTP 成功两次(或超时但已接受)时合并重复发送。两层都要。
上线周期任务;让 Rovela 投递
用显式 cron 与时区注册 NestJS Schedule 调度,在任务方法里做实时聚合与 POST /emails,把 Idempotency-Key 限定在调度窗口(及租户),并仅在服务端保存 sending_access。NestJS Schedule 按时唤醒;Rovela 投递事务邮件。