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

如何在MERN栈中实现每日10点自动发送逾期图书提醒邮件?

问题:实现每日自动发送逾期图书邮件提醒

我开发了一个基于MERN栈的图书馆管理系统,目前管理员可点击Notify按钮,向所有持有逾期图书的用户发送邮件——该功能通过将逾期用户数组作为请求体传入触发,管理员在对应组件初始渲染时从数据库获取用户列表。

现在需要实现每日10:00自动触发邮件提醒的功能。当前点击Notify按钮会调用notifyBookDefaulties控制器,我尝试用setTimeout和定时器调用路由,但没达到预期效果。


现有代码

1. notifyBookDefaulties控制器

const notifyBookDefaulties = asyncHandler(async (req, res) => {
  const admin = await Auth.findById(req.user.id);
  // 验证用户存在且为管理员(通过token)
  if (!admin && admin.admin !== true) {
    res.status(401);
    throw new Error("Not Authorized");
  }
  const { users, bookID, title } = req.body; // users为用户ID列表
  let emails = "";
  // 通过用户ID获取邮箱
  for (let user of users) {
    try {
      const defaulty = await Auth.findById(user);
      emails += defaulty.email + ",";
    } catch (error) {
      res.status(400);
      throw new Error(error);
    }
  }
  // 生成逗号分隔的邮箱列表
  const emailList = emails.slice(0, -1).toString();
  // 发送邮件
  try {
    var transporter = nodemailer.createTransport({
      service: process.env.SERVICE,
      auth: {
        user: process.env.USER,
        pass: process.env.PASS,
      },
    });
    var mailOptions = {
      from: process.env.USER,
      to: emailList,
      subject: "Return Book",
      html: `<!DOCTYPE html><html lang="en"><body>This is to remind you that the book titled ${title} and ID ${bookID} issued by you is due.</body></html>`,
    };
    transporter.sendMail(mailOptions, function (error, info) {
      if (error) {
        res.status(400).json({ msg: error });
      } else {
        res.status(200).json({ msg: "E-Mail Successfully sent" });
      }
    });
  } catch (error) {
    console.log(error);
    res.status(500).json({ msg: error });
  }
});

2. server.js

require("dotenv").config();
const express = require("express");
const { errorHandler } = require("./middleware/errorMiddleware");
const connectDB = require("./config/db");
const cors = require("cors");
const port = process.env.PORT || 5000;

connectDB();

const app = express();

const corsOptions = {
  origin: 'http://localhost:3000',
  optionsSuccessStatus: 204
};

app.use(cors(corsOptions))
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use("/api/admin", require("./routes/adminRoutes"));
app.use("/api/user", require("./routes/userRoutes"));
app.use("/api/actions", require("./routes/authRoute"));

app.use(errorHandler);

app.listen(port, () => {
  console.log(`Running on ${port}`);
});

3. 对应路由

router.post("/notify", protect, notifyBookDefaulties);

对应URL:http://localhost:5000/api/admin/notify

注:获取逾期图书用户ID列表的功能有单独控制器,待定时逻辑实现后会合并到当前控制器中。


解决方案

1. 重构邮件发送逻辑,分离可复用函数

首先把邮件发送和用户邮箱收集的逻辑抽成独立函数,避免和HTTP请求强绑定:

// 新增utils/emailService.js
const nodemailer = require("nodemailer");
const Auth = require("../models/authModel"); // 替换为你的Auth模型路径

// 全局初始化transporter,避免重复创建连接
const transporter = nodemailer.createTransport({
  service: process.env.SERVICE,
  auth: {
    user: process.env.USER,
    pass: process.env.PASS,
  },
});

// 获取用户邮箱列表
const getUserEmails = async (userIds) => {
  const emails = [];
  for (const userId of userIds) {
    const user = await Auth.findById(userId);
    if (user) emails.push(user.email);
  }
  return emails.join(",");
};

// 发送逾期提醒邮件
const sendOverdueReminder = async (userIds, bookID, title) => {
  try {
    const emailList = await getUserEmails(userIds);
    if (!emailList) return { success: false, msg: "No valid user emails found" };

    const mailOptions = {
      from: process.env.USER,
      to: emailList,
      subject: "Return Book",
      html: `<!DOCTYPE html><html lang="en"><body>This is to remind you that the book titled ${title} and ID ${bookID} issued by you is due.</body></html>`,
    };

    await transporter.sendMail(mailOptions);
    return { success: true, msg: "E-Mail Successfully sent" };
  } catch (error) {
    console.error("Email send error:", error);
    return { success: false, msg: error.message };
  }
};

module.exports = { sendOverdueReminder };

2. 使用node-schedule实现定时任务

原生setTimeout/setInterval在服务器重启后会失效,推荐用专门的定时任务库node-schedule:

  1. 安装依赖:
npm install node-schedule
  1. 在server.js中添加定时任务:
require("dotenv").config();
const express = require("express");
const { errorHandler } = require("./middleware/errorMiddleware");
const connectDB = require("./config/db");
const cors = require("cors");
const schedule = require("node-schedule");
const { sendOverdueReminder } = require("./utils/emailService");
// 导入你的逾期用户查询函数(替换为实际路径)
const { getOverdueUsersAndBooks } = require("./controllers/overdueController");

const port = process.env.PORT || 5000;

connectDB();

const app = express();

// ... 保留原有cors、中间件、路由配置 ...

// 每日10:00执行定时任务(表达式规则:秒 分 时 日 月 周)
schedule.scheduleJob("0 10 * * *", async () => {
  console.log("Starting daily overdue reminder job...");
  try {
    // 获取所有逾期图书及对应用户ID列表
    // 返回格式示例:[{ userIds: ['user1Id', 'user2Id'], bookID: 'book1', title: 'Book Title 1' }, ...]
    const overdueItems = await getOverdueUsersAndBooks();

    for (const item of overdueItems) {
      const result = await sendOverdueReminder(item.userIds, item.bookID, item.title);
      console.log(`Reminder for book ${item.bookID}: ${result.msg}`);
    }
  } catch (error) {
    console.error("Daily reminder job failed:", error);
  }
});

app.listen(port, () => {
  console.log(`Running on ${port}`);
});

3. 调整原控制器,复用邮件发送函数

修改notifyBookDefaulties控制器,使用抽离的函数,同时保留HTTP接口功能:

const asyncHandler = require("express-async-handler");
const Auth = require("../models/authModel");
const { sendOverdueReminder } = require("../utils/emailService");

const notifyBookDefaulties = asyncHandler(async (req, res) => {
  const admin = await Auth.findById(req.user.id);
  // 修正原逻辑的判断错误:原逻辑会导致管理员身份验证失效
  if (!admin || !admin.admin) {
    res.status(401);
    throw new Error("Not Authorized");
  }
  const { users, bookID, title } = req.body;
  if (!users || !bookID || !title) {
    res.status(400);
    throw new Error("Missing required fields: users, bookID, title");
  }

  const result = await sendOverdueReminder(users, bookID, title);
  if (result.success) {
    res.status(200).json({ msg: result.msg });
  } else {
    res.status(400).json({ msg: result.msg });
  }
});

module.exports = notifyBookDefaulties;

4. 补充说明

  • 服务器重启后,node-schedule会重新计算下次执行时间,无需额外处理
  • 若需要更可靠的定时任务(如服务器宕机后补执行),可以考虑用Linux的cron定时调用你的HTTP接口
  • 原控制器中的权限判断存在逻辑错误,已修正为!admin || !admin.admin,确保非管理员用户无法调用该接口

内容的提问来源于stack exchange,提问作者Roshan Shetty

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 08:56:06