Node.js+Express+MongoDB项目中为multiTrack爬取函数添加Cron定时任务的报错问题求助
解决方案:分离业务逻辑与HTTP控制器
你的问题核心在于**multiTrack原本是为Express HTTP请求设计的控制器函数,依赖req、res、next这些请求响应对象,但定时任务是后台触发的,没有这些对象**。解决思路是把业务逻辑(爬取商品、更新数据库)和HTTP响应处理拆分开,让两者可以独立调用。
步骤1:抽离核心业务逻辑
首先,把爬取和更新的核心代码从控制器中抽离成独立的函数,这些函数不依赖req/res,只负责处理业务逻辑并返回结果或抛出错误。
修改track.controller.js,添加以下独立函数:
const puppeteer = require('puppeteer'); const User = require('../models/User'); // 确保路径正确 const Track = require('../models/Track'); // 确保路径正确 // 处理单个商品Track的爬取与更新 const updateTrack = async (track) => { const browser = await puppeteer.launch(); const page = await browser.newPage(); try { console.log(`${track.name} re-crawling starts`); await page.goto(track.productUrl, { waitUntil: "networkidle2" }); const crawledProduct = await page.evaluate(() => { let actualPrice = 0; const prepOurPrice = document.querySelector("span.woocommerce-Price-amount.amount")?.innerText; const image = document.querySelector(".woocommerce-product-gallery__image a img")?.src; if (prepOurPrice) { actualPrice = parseFloat(prepOurPrice.replace(/[^0-9\.-]+/g, "")); } // 补充处理salePrice和dealPrice的逻辑(注意可选链避免null报错) const salePrice = document.querySelector("#priceblock_saleprice")?.innerText; const dealPrice = document.querySelector("#priceblock_dealprice")?.innerText; if (salePrice) actualPrice = parseFloat(salePrice.replace(/[^0-9\.-]+/g, "")); if (dealPrice) actualPrice = parseFloat(dealPrice.replace(/[^0-9\.-]+/g, "")); return { image, actualPrice }; }); // 更新Track数据 let updated = false; if (track.image !== crawledProduct.image) { track.image = crawledProduct.image; updated = true; } if (track.actualPrice !== crawledProduct.actualPrice) { track.actualPrice = crawledProduct.actualPrice; updated = true; } if (updated) await track.save(); console.log(`${track.name} re-crawling ends (${updated ? "updated" : "no changes"})`); } catch (err) { console.error(`Failed to crawl ${track.name}: ${err.message}`); throw err; // 抛出错误让上层处理 } finally { await browser.close(); // 确保浏览器始终关闭 } }; // 处理单个用户的所有Tracks const processUserTracks = async (userId) => { const user = await User.findById(userId); if (!user) throw new Error("User does not exist"); const userTracks = await Track.find({ user: userId }); // 假设Track模型有user字段关联用户 if (!userTracks.length) throw new Error("No tracks found for user"); // 逐个处理Track(用Promise.all替代forEach+async,避免异步问题) await Promise.all(userTracks.map(track => updateTrack(track))); return userTracks; }; // 处理系统中所有需要跟踪的Tracks(用于定时任务) const processAllTracks = async () => { const allTracks = await Track.find(); // 获取所有Track,可根据需求添加过滤条件(比如只启用自动爬取的) if (!allTracks.length) { console.log("No tracks to process"); return; } await Promise.all(allTracks.map(track => updateTrack(track))); console.log(`Completed re-crawling ${allTracks.length} tracks`); }; // 原来的Express控制器,现在只处理HTTP请求响应 exports.multiTrack = async (req, res, next) => { try { const { userId } = req.body; const tracks = await processUserTracks(userId); return res.status(201).json({ success: true, data: tracks }); } catch (err) { if (err.message === "User does not exist") { return res.status(401).json({ success: false, error: err.message }); } return res.status(500).json({ error: err.message }); } }; // 暴露定时任务需要的函数 exports.processAllTracks = processAllTracks;
步骤2:修改定时任务配置
在server.js中,直接调用抽离后的processAllTracks函数,不需要传入req/res:
const cron = require('node-cron'); const trackController = require('./controllers/track.controller.js'); // 每分钟执行一次(如需调整频率,可修改cron表达式) cron.schedule("* */1 * * *", async () => { console.log(`Starting scheduled re-crawling job`); try { await trackController.processAllTracks(); } catch (err) { console.error(`Scheduled job failed: ${err.message}`); } });
关键改进点说明
- 分离关注点:业务逻辑(爬取、更新)和HTTP响应处理完全分离,既可以通过API手动触发,也可以通过定时任务后台执行。
- 修复异步遍历问题:原来的
forEach(async ...)不会等待所有异步操作完成,改用Promise.all确保所有Track处理完成后再结束任务。 - 错误处理优化:定时任务中的错误用日志记录,不需要依赖
res对象返回HTTP状态码。 - 可选链操作符:在
page.evaluate中使用?.避免DOM元素不存在时的报错。
额外建议
- 如果你的Track模型没有关联用户,可以根据实际需求调整
processAllTracks的过滤条件(比如添加isAutoTrack: true字段,只处理开启自动爬取的Track)。 - 可以考虑添加日志库(如winston)替代
console.log,方便后续排查问题。 - 定时任务表达式注意验证:
*/60 * * * * *表示每秒执行一次(可能太频繁),如果需要每分钟执行一次,应该用* */1 * * *。
内容的提问来源于stack exchange,提问作者Pavel
相关产品推荐
相关产品推荐

