如何在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:
- 安装依赖:
npm install node-schedule
- 在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
相关产品推荐
相关产品推荐

