You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Node.js中实现可跳过周末、按规则重发邮件的cron-job任务

Node.js定时邮件提醒任务实现方案

技术选型与前置准备

  • 定时调度工具:使用node-schedule库,支持动态创建、暂停、销毁定时任务,比原生crontab更适配业务灵活调度需求
  • 持久化存储:生产环境建议使用MySQL、MongoDB或Redis存储任务状态,避免服务重启导致任务丢失,需要存储的核心字段包含:任务唯一ID、剩余重试次数、是否已收到回复、任务启动时间、已累计计时时长、任务暂停状态、对应业务请求ID

核心逻辑实现思路

1. 工作日时长计算工具函数(核心)

提前封装工具函数实现仅统计工作日时长的逻辑,自动跳过周六、周日,所有任务的触发时间都通过该函数计算:

  • 从指定起始时间开始逐小时累加计数,遇到周六(getDay()返回6)、周日(getDay()返回0)的时间直接跳过不纳入计时
  • 如果计时过程中到周五23:59,自动跳转到下周一00:00后继续累计剩余时长
  • 累计满指定的24小时工作日时长后,返回最终的任务触发时间

2. 新请求初始化逻辑

新业务请求创建时执行以下操作:

  1. 立即调用邮件发送函数完成首次发送
  2. 初始化任务记录,剩余重试次数设置为3,标记为未回复状态
  3. 调用工具函数计算首次24小时倒计时的触发时间,创建对应定时任务,存入持久化存储

3. 定时任务触发逻辑

定时任务到触发时间后执行以下判断:

  • 如果该任务已经标记为已收到回复,直接销毁任务并删除存储记录
  • 如果剩余重试次数大于0:再次调用邮件发送函数,剩余重试次数减1,重新调用工具函数计算下一个24小时的触发时间,更新任务记录与定时任务
  • 如果剩余重试次数为0:标记任务为逾期结束,执行对应业务的逾期处理逻辑,销毁任务

4. 全局周末暂停兜底逻辑

额外配置两个全局定时任务做兜底,避免动态计算出现偏差:

  • 每周五23:59触发:暂停所有运行中的任务,记录每个任务已经累计的计时时长,标记任务为暂停状态
  • 每周日23:59触发:恢复所有暂停的任务,用剩余需要计时的时长重新计算触发时间,更新定时任务

核心代码示例

const schedule = require('node-schedule');
// 示例用内存存储,生产环境替换为持久化数据库
const taskStore = new Map();

// 自行实现业务邮件发送逻辑
const sendBusinessEmail = async (taskId, requestInfo) => {
  console.log(`向任务${taskId}对应的用户发送提醒邮件`);
};

/**
 * 计算仅统计工作日的结束时间
 * @param {Date} start 起始时间
 * @param {Number} needHours 需要累计的工作日时长(小时)
 * @returns {Date} 最终触发时间
 */
const calculateWorkEndTime = (start = new Date(), needHours = 24) => {
  let currentTime = new Date(start);
  let remainingHours = needHours;
  while (remainingHours > 0) {
    currentTime.setHours(currentTime.getHours() + 1);
    const weekDay = currentTime.getDay();
    // 周六周日跳过不统计
    if (weekDay === 0 || weekDay === 6) continue;
    remainingHours--;
  }
  return currentTime;
};

// 新请求创建时初始化任务
const initEmailTask = async (taskId, requestInfo) => {
  // 首次发送邮件
  await sendBusinessEmail(taskId, requestInfo);
  // 计算首次触发时间
  const firstTriggerTime = calculateWorkEndTime(new Date(), 24);
  // 定时任务回调
  const taskCallback = async () => {
    const task = taskStore.get(taskId);
    if (!task || task.replied) {
      task?.job?.cancel();
      taskStore.delete(taskId);
      return;
    }
    if (task.remainRetry > 0) {
      // 重试发送邮件
      await sendBusinessEmail(taskId, task.requestInfo);
      task.remainRetry--;
      // 计算下一次触发时间
      const nextTriggerTime = calculateWorkEndTime(new Date(), 24);
      task.job.cancel();
      task.job = schedule.scheduleJob(nextTriggerTime, taskCallback);
      task.triggerTime = nextTriggerTime;
      taskStore.set(taskId, task);
    } else {
      // 重试次数耗尽,执行逾期逻辑
      task.job.cancel();
      taskStore.delete(taskId);
      // 这里加你的业务逾期处理逻辑
    }
  };
  // 创建定时任务并存入存储
  const job = schedule.scheduleJob(firstTriggerTime, taskCallback);
  taskStore.set(taskId, {
    taskId,
    requestInfo,
    remainRetry: 3,
    replied: false,
    paused: false,
    triggerTime: firstTriggerTime,
    job,
    callback: taskCallback
  });
};

// 收到邮件回复时调用,销毁对应任务
const handleEmailReply = (taskId) => {
  const task = taskStore.get(taskId);
  if (task) {
    task.job.cancel();
    taskStore.delete(taskId);
  }
};

// 兜底定时:每周五23:59暂停所有运行中任务
schedule.scheduleJob('59 23 * * 5', () => {
  taskStore.forEach(task => {
    if (!task.paused && !task.replied) {
      const usedHours = (new Date() - task.triggerTime) / 1000 / 3600;
      task.usedHours = Math.max(0, 24 - usedHours);
      task.job.cancel();
      task.paused = true;
    }
  });
});

// 兜底定时:每周日23:59恢复所有暂停任务
schedule.scheduleJob('59 23 * * 0', () => {
  taskStore.forEach(task => {
    if (task.paused && !task.replied) {
      const newTriggerTime = calculateWorkEndTime(new Date(), task.usedHours);
      task.job = schedule.scheduleJob(newTriggerTime, task.callback);
      task.triggerTime = newTriggerTime;
      task.paused = false;
    }
  });
});

生产环境注意事项

  • 服务重启时需要扫描持久化存储中未完成的任务,重新生成对应的定时调度,避免任务丢失
  • 业务量大的场景建议将定时调度模块与业务处理模块拆分,通过消息队列异步触发邮件发送逻辑,避免调度阻塞
  • 所有时间计算统一时区,推荐使用UTC时间避免服务器时区差异导致的触发时间错误
  • 可以添加任务执行日志,方便排查发送失败、触发时间偏差等问题

内容的提问来源于stack exchange,提问作者Jithin Jimmy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 21:18:03