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

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}`);
  }
});

关键改进点说明

  1. 分离关注点:业务逻辑(爬取、更新)和HTTP响应处理完全分离,既可以通过API手动触发,也可以通过定时任务后台执行。
  2. 修复异步遍历问题:原来的forEach(async ...)不会等待所有异步操作完成,改用Promise.all确保所有Track处理完成后再结束任务。
  3. 错误处理优化:定时任务中的错误用日志记录,不需要依赖res对象返回HTTP状态码。
  4. 可选链操作符:在page.evaluate中使用?.避免DOM元素不存在时的报错。

额外建议

  • 如果你的Track模型没有关联用户,可以根据实际需求调整processAllTracks的过滤条件(比如添加isAutoTrack: true字段,只处理开启自动爬取的Track)。
  • 可以考虑添加日志库(如winston)替代console.log,方便后续排查问题。
  • 定时任务表达式注意验证:*/60 * * * * *表示每秒执行一次(可能太频繁),如果需要每分钟执行一次,应该用* */1 * * *。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 08:42:30