Node.js实战:京东商品到货监控与自动下单的完整流程与踩坑记录
发布时间:2026/9/26 11:58:14来源:尧图网络
简介这是一份基于 Node.js 的京东商品监控与到货自动下单爬虫源码包适合熟悉 JavaScript、对电商自动化或爬虫实践感兴趣的开发者。项目因京东接口更新已标注废弃但按地区查询库存、库存大于 0 自动下单、抢购商品支持、本地缓存登录状态等思路依然有参考价值尤其适合作为学习 Node 爬虫与 Web 端自动化交互的示例。压缩包共 11 个文件以 5 个 js 文件为主体分别承担入口、参数解析、工具函数、日志与主流程另含配置与说明文档、演示动图及授权文件整体约 1.7MB已有 818 人学习。源码演示了扫码登录、初始化浏览器、抓取页面、分析页面参数、请求扫码及二维码状态轮询等完整流程可帮助读者理解 Node 爬虫的请求构造、页面参数分析与状态持久化等常见实现方式。1. jd-happy 是面向“有明确购买意图”的 Node 爬虫先看懂它在解决什么问题jd-happy 属于那种“一听就懂、一跑就翻车”的 Node 爬虫项目轮询京东商品详情接口发现商品从“无货”变成“有货”立刻触发一次下单请求。它解决的痛点不是“爬数据”而是“盯状态”——让你从每隔几分钟刷新一次商品页的重复劳动里解脱出来。这个项目被标记 DEPRECATED 并不奇怪京东的 H5 和 PC 端接口一直在变风控策略也在变今天能用的请求头组合过两周就可能被滑块验证码截住。它适合两类人一类是想给自己抢购显卡、PS5、限量球鞋的 Node 开发者另一类是想研究“接口轮询 状态变化触发动作”这套通用自动化流程的人。本文不讨论任何绕过登录或攻击京东的做法只讲公开接口的正常调用与合理的自动化边界。2. 监控到货这件事的原理JD 的库存与价格是怎么被看到的2.1 网页端与移动端接口的差异为什么选 H5 接口而不是 PC 页面做 jd-happy 这类监控第一个要判断的是请求走哪个端。PC 网页端的商品详情页是服务端渲染和异步混着的HTML 结构里塞了大量模板变量、埋点脚本和推荐位数据直接抓 HTML 再正则抠库存字段等于在烂泥里捞针。而移动端 H5 的接口是纯 JSON 返回字段语义清晰stock状态、价格、促销信息都在一个扁平结构里解析成本低一个数量级。常见做法是直接请求 H5 的商品详情接口。这个接口只需要两个参数skuId是商品 IDarea是收货地区的编码串。请求里带一个常规的User-Agent不用带登录态也能看到基础库存状态。返回的 JSON 里有一个stockInfo结构里面有类似wareStockStatus之类的状态值不同状态下值不同比如有货、无货、仅配送。这个值就是 jd-happy 轮询时要比较的核心信号。提示京东接口的字段名会随版本调整不要写死字符串比较建议把“有货”的判断条件收敛成一个函数方便改。为什么不直接调 PC 端的购物车或下单接口PC 端下单流程长要经过加购、结算页、订单确认页好几跳每一跳都有页面级的参数校验写起来像在做 UI 自动化。H5 的结算流程相对短但这几年也在收紧。所以 jd-happy 这类项目一旦把监控和下单做在一起就会高频踩到风控这也是它最终被弃用的核心原因之一——不是轮询写不好是下单这一步太容易触发验证。2.2 Node 在爬虫任务里的定位短平快的轮询脚本 vs Python 重管线同类需求在 Python 里也有大量实现用requests库轮询、用 SQLAlchemy 存商品历史价格、用定时任务框架调度一套组合拳很成熟。那为什么 jd-happy 选 Node核心理由是 Node 的事件循环模型非常适合“定时轮询 异步下单”这种 IO 密集型任务。爬虫任务的特点是绝大部分时间在等网络响应Node 的异步非阻塞模型可以用很小的内存撑起并发请求不需要像 Python 那样为并发引入额外的线程池或 asyncio 复杂度。另外npm 生态里有大量现成的请求库和工具链。老一点的写法会用node request这个库现在它已经弃用更常见的是undici或者 Node 18 之后内置的fetch。如果你的监控目标就几个 SKU根本不需要上分布式爬虫一个 Node 进程、一个setInterval、两个异步函数就够了。这也是 jd-happy 这类项目能吸引人的地方代码量可以压缩到很小前后端同一种语言部署也简单。如果硬要拿它和 Python 比Python 在“数据落库和分析”这条链路上更顺手SQLAlchemy 这类 ORM 成熟适合做长周期价格趋势统计。但 jd-happy 的核心诉求是“快”——发现到货、立刻下单。在这个场景里数据落库反而成了次要任务用 JSON 文件或 SQLite 记录一下点击时间就够用了。选型结论监控下单用 Node历史分析用 Python两件事不要混在一个脚本里。2.3 jd-happy 的核心状态模型无货、有货、可下单三个状态把 jd-happy 的业务逻辑抽象出来它其实是一个三状态机无货态、有货态、已下单态。正常轮询时脚本处于“无货态”每一轮请求详情接口拿到库存状态后和上一轮比较。如果从无货变成有货就进入“有货态”这时可以选择直接下单或者先推送通知等用户确认。下单请求发出去之后脚本进入“已下单态”停止轮询该 SKU或者降低轮询频率只查订单状态。状态机看似简单但坑在于边界的处理。比如商品从“有货”变成“仅配送”再到“无货”中间可能只持续几十秒。如果轮询间隔是 30 秒漏掉这一波就需要再等补货所以轮询间隔直接决定监控的有效性。另一个边界是“有货但不可下单”有些商品需要预约有些限制了地区详情接口返回“有货”但结算接口报错这种情况要把状态拆成“可加购”和“可下单”两层不能混为一谈。注意状态机里一定要有“冷却时间”概念。下单失败不要立刻重试先回到无货态并暂停 60 秒避免高频请求把风控阈值打爆。3. 落地一套可运行的 jd-happy 流程从 Cookie 登录到订单提交3.1 环境准备用 nvm 锁 Node 版本避免 node-gyp 编译问题先把环境说清楚。jd-happy 这类老项目基本跑在 Node 14 或 16 时代而你现在机器上可能装的是 Node 20 甚至更高。高版本 Node 运行老项目最常见的翻车点有两个一是某些老依赖用了废弃 API启动时直接报 warning 或者崩溃二是没有预编译二进制包的原生模块比如数据库驱动安装时要现场编译这时候node-gyp和 Node 版本不对应就会出问题。所以第一步不是写代码是装一个 Node 版本管理器。Windows 用户用nvm-windowsmacOS/Linux 用nvm先把版本锁到项目需要的范围。# 用 nvm 安装并切换到 16.x老项目兼容性最好的版本区间 nvm install 16.20.2 nvm use 16.20.2 # 验证当前版本 node -v npm -v逻辑说明nvm install会从官方二进制源下载对应版本的 Nodenvm use切换当前 shell 的默认版本。注意nvm use只对当前终端生效新开一个终端窗口需要重新切换或者用nvm alias default 16.20.2固定默认版本。node -v和npm -v两个命令必须一起看因为 npm 是随 Node 版本绑定的版本太旧会导致后续依赖安装时 lockfile 解析失败。Windows 上还有一个高频坑npm 脚本被执行策略拦截报错长这样npm : 无法加载文件 D:\Program Files (x86)\nodejs\npm.ps1因为在此系统上禁止运行脚本。解决方案是用管理员权限打开 PowerShell执行下面的命令放开 CurrentUser 的脚本执行策略# 允许当前用户执行 npm.ps1 脚本只影响当前用户不影响系统安全策略 Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser参数说明RemoteSigned表示本地创建的脚本可以运行从网上下载的脚本必须有签名才能运行这是开发机器上比较平衡的策略。-Scope CurrentUser限定作用域不会动系统级策略。执行完重新打开终端npm 就能正常跑了。3.2 第一步把登录态转成可复用的 Cookie 配置jd-happy 的监控部分可以匿名请求但下单部分必须有登录态。京东的下单流程要求的 Cookie 字段比普通爬虫多常见的包括记住登录身份的字段和用户标识字段。手工复制是最可靠的因为你用浏览器正常登录经过的验证码和风控检查都已经过了拿到的 Cookie 是干净的。具体做法用 Chrome 无痕窗口打开京东手动完成登录这一步可能会遇到滑块验证这是正常流程说明该地区需要额外校验。登录完成后打开开发者工具的 Network 面板随便点一个商品页请求从请求头里复制完整的Cookie值粘贴到项目配置里。// config.js - 把浏览器里复制的 Cookie 粘贴到这里不要提交到 Git module.exports { cookie: pt_keyxxx; pt_pinxxx; whwswswwsxxx, area: 1_72_4137_0, // 京东地址编码省_市_区_街道按实际地址抓包获取 defaultSkuId: 100012345, // 要监控的商品 ID pollInterval: 15000, // 轮询间隔单位毫秒 maxRetry: 3 // 下单失败后的重试次数 }参数说明pt_key和pt_pin是京东登录态的核心字段分别保存会话凭证和用户标识缺失这两个字段几乎一定会被 302 重定向到登录页。area编码串直接影响库存判断同一件商品在北京有货、在上海无货是常态这个值从浏览器里某个含area参数的请求里复制最准确。pollInterval建议不要小于 10000低于 10 秒的轮询频率会给账号带来风险。拿到 Cookie 后先验证有效性写一个简单的请求访问购物车或用户中心接口// check-cookie.js - 验证 Cookie 是否有效 const config require(./config); async function checkLogin() { const res await fetch(https://api.m.jd.com/client.action?functionIdGetUserInfo, { headers: { Cookie: config.cookie, User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) } }); const data await res.json(); console.log(登录状态:, JSON.stringify(data)); } checkLogin();逻辑说明这个请求如果返回正常的用户信息 JSON说明 Cookie 有效。如果返回空的userInfo或者直接跳到登录跳转逻辑说明 Cookie 已失效。functionIdGetUserInfo是移动端常用的用户信息接口在 H5 端可以匿名调用但拿不到完整字段带上 Cookie 才能看到用户标识。提示Cookie 是有有效期的jd-happy 里要加一个“失效检测”逻辑。每次请求如果返回的结果里出现明显的未登录特征立即停止下单流程并通知你手动更新 Cookie不要闷头重试。3.3 第二步到货监控的轮询与判定逻辑到货监控是整个项目里代码量最少但最容易写错的部分。核心逻辑就三步请求详情接口、解析库存字段、和上一次状态比较。用setInterval做轮询就行不需要引入额外的定时任务库。// monitor.js - 商品到货监控主逻辑 const config require(./config); let lastStatus OUT_OF_STOCK; // 记录上一次的库存状态初始假设无货 async function fetchStock(skuId) { const url https://api.m.jd.com/client.action?functionIdGetItemStockskuId${skuId}area${config.area}; const res await fetch(url, { headers: { Cookie: config.cookie, User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X), Referer: https://item.m.jd.com/product/${skuId}.html } }); return res.json(); } function parseStock(rawData) { // 京东的库存状态在不同接口版本里字段名不一样 // 这里做一层映射把原始数据规整成统一的三态 const stockInfo rawData.stockInfo || rawData.stock || {}; const rawStatus stockInfo.wareStockStatus || stockInfo.stockStatus; if (rawStatus 1 || rawStatus 1) return IN_STOCK; if (rawStatus 2 || rawStatus 2) return OUT_OF_STOCK; return UNKNOWN; } async function tick() { try { const rawData await fetchStock(config.defaultSkuId); const currentStatus parseStock(rawData); if (currentStatus IN_STOCK lastStatus ! IN_STOCK) { console.log([${new Date().toISOString()}] 商品 ${config.defaultSkuId} 到货); // 触发下单流程见下一步 await placeOrder(config.defaultSkuId); lastStatus IN_STOCK; // 下单成功后切换状态避免重复下单 } else { console.log([${new Date().toISOString()}] 当前状态: ${currentStatus}); lastStatus currentStatus; } } catch (err) { console.error(轮询异常:, err.message); // 网络抖动或接口变更时不要立刻改变 lastStatus下次轮询再继续判断 } } // 启动轮询间隔从 config 读取 setInterval(tick, config.pollInterval);逻辑说明fetchStock负责网络请求parseStock负责把接口返回的原始库存状态映射成统一枚举tick是每次轮询的入口。这里故意把lastStatus放在函数体外因为setInterval每次调用tick时都需要记住上一次的状态闭包变量比全局变量更安全。判断逻辑里有一个关键点只有当currentStatus IN_STOCK且lastStatus ! IN_STOCK时才触发下单这叫“上升沿触发”可以避免持续上报到货导致重复下单。参数说明Referer头在 H5 接口里很重要部分接口会校验来源页面。area参数拼接在 URL 里但如果地址编码里有特殊字符需要先用encodeURIComponent转义。pollInterval的单位是毫秒15000 就是 15 秒一次。这个频率配合三个 SKU 以内是安全的超过这个数量建议把轮询间隔拉到 30 秒以上。3.4 第三步下单接口的调用与订单确认下单是整条链路上最敏感的一步。jd-happy 这类项目通常直接调 H5 的结算接口流程比 PC 端短但参数里的坑很多。一个比较稳妥的做法是到货后先调“预下单”接口做一次校验确认商品真的可以结算再提交最终订单。// order.js - 下单流程先校验再提交 const config require(./config); async function checkBeforeOrder(skuId, num) { const body new URLSearchParams({ skuId: String(skuId), num: String(num), area: config.area }); const res await fetch(https://api.m.jd.com/client.action?functionIdCheckInterim, { method: POST, headers: { Cookie: config.cookie, Content-Type: application/x-www-form-urlencoded, User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) }, body: body.toString() }); const data await res.json(); if (data.code ! 0 || data.data.noStock) { throw new Error(预下单校验失败: JSON.stringify(data)); } return data.data.priceInfo || {}; } async function submitOrder(skuId, num) { const checkResult await checkBeforeOrder(skuId, num); const body new URLSearchParams({ skuId: String(skuId), num: String(num), area: config.area, price: String(checkResult.price || ) }); const res await fetch(https://api.m.jd.com/client.action?functionIdSubmitOrder, { method: POST, headers: { Cookie: config.cookie, Content-Type: application/x-www-form-urlencoded, User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X), Referer: https://trade.m.jd.com/ }, body: body.toString() }); const data await res.json(); if (data.orderId) { console.log(下单成功订单号:, data.orderId); return data.orderId; } throw new Error(下单失败: JSON.stringify(data)); } async function placeOrder(skuId) { for (let i 0; i config.maxRetry; i) { try { return await submitOrder(skuId, 1); } catch (err) { console.error(第 ${i 1} 次下单失败:, err.message); // 重试间隔要递增第一次等 5 秒第二次等 10 秒 await new Promise(resolve setTimeout(resolve, 5000 * (i 1))); } } console.error(重试耗尽放弃下单转入人工通知流程); } module.exports { placeOrder };逻辑说明checkBeforeOrder和submitOrder是分开的两个函数。CheckInterim这个预下单校验接口的主要作用是确认当前库存还能锁住返回的priceInfo里会带一个当前价格。提交订单时把这个价格原样带上如果提交瞬间价格变了服务端会报价格不一致的错误——这是正常保护机制说明你的轮询速度已经赶不上价格波动需要把策略从“直接下单”改成“下单前二次确认”。参数说明submitOrder里的functionId在不同时期可能叫SubmitOrder或CreateOrder要以你抓包看到的实际值为准。price字段带上前一步校验接口返回的价格值是为了减少服务端校验差异。重试逻辑里的退避时间用了5000 * (i 1)第一次失败等 5 秒第二次等 10 秒不递增退避会让风控系统更容易判定你是脚本。注意下单接口的参数和请求头随版本变动很大如果请求返回“功能不存在”或“参数错误”优先去抓包看新版调用了什么参数而不是在旧参数里找原因。3.5 数据要不要落库从 console.log 到结构化日志监控脚本跑起来之后控制台输出只是给人看的。商品什么时候到货、轮询到了多少次、下单失败的原因是什么这些数据如果不落盘出问题时只能靠回忆排查。如果你有 Python 背景习惯用 SQLAlchemy 存数据那在 Node 这边也可以沿用类似思路但 jd-happy 这种轻量项目不需要上重型 ORM。常见做法是写一个简单的 JSON 日志文件每轮轮询追加一条记录。到货事件和下单事件单独记录方便后续复盘。// logger.js - 极简结构化日志避免单文件无限增长 const fs require(fs); const path require(path); function appendEvent(eventType, payload) { const logLine JSON.stringify({ time: new Date().toISOString(), type: eventType, ...payload }) \n; const filePath path.join(__dirname, events.jsonl); fs.appendFileSync(filePath, logLine); } module.exports { appendEvent };逻辑说明用appendFileSync同步写入每次写入打开一次文件句柄然后关闭。轮询频率 15 秒一次一小时也就 240 条记录同步写入的性能损耗完全可以忽略。JSON.stringify保证每行是一个完整的 JSON后续可以用任意语言按行解析。文件格式用.jsonl而不是.json因为它是逐行追加的单行损坏不影响其他行。如果日志量增长太快在轮询回调里加一个简单的行数检查超过 10000 行就滚动一次文件名。提示不要用console.log代替日志文件。控制台输出在进程崩溃后就没有了而events.jsonl能让你在排查“为什么下单没触发”时看到完整的时间线。4. jd-happy 踩坑记录Cookie 失效、滑块验证与订单被取消的 3 类高频问题4.1 现象Cookie 半小时就失效轮询突然开始返回 302脚本前一天还跑得好好的第二天早上一看日志从凌晨 3 点起所有请求都返回登录跳转。手动打开浏览器京东还活着但脚本里的 Cookie 就是不行。原因京东的登录态不是单点失效的它分为几个层级的 token。浏览器里你每天都在用、Cookie 看起来很稳定但脚本里固定的 Cookie 缺失了某些续期字段的更新逻辑。特别是pt_key这类会话凭证服务端会定期强制刷新而脚本每次请求都用旧的、不更新一旦触发刷新策略就立刻过期。解决给 Cookie 加“有效期快照”。在config.js里记录粘贴 Cookie 的时间每次启动时检查是否已使用超过 12 小时超过就提醒你更新。更省事的方案是改用手动触发一次浏览器登录然后用浏览器插件的 Cookie 导出功能把最新 Cookie 写回配置文件。不要想着程序自动续期在页面上更新登录态比接口里续期安全得多。4.2 现象轮询频率不高但还是触发了滑块验证明明把轮询间隔设成了 20 秒四个 SKU 轮流查QPS 不算高可跑了两小时后接口开始返回一个特殊的 HTML 页面里面包含滑块验证逻辑。原因风控的维度不止是频率。你的请求特征如果一直不变比如User-Agent固定同一个 iPhone 型号、每次请求的Referer都是同一个商品页、请求间隔还稳定得像节拍器风控模型很容易把这种流量归为程序化请求。更隐蔽的触发点是你同时请求了商品详情和结算校验接口这两类接口在正常用户行为里是间隔很远的脚本却把它们放在同一个 tick 前后 100 毫秒内。解决做两件事。第一请求头随机化准备三到五个User-Agent轮流使用Referer跟着当前 SKU 的商品页走。第二把“监控”和“下单”拆成两条执行链监控轮询只碰详情接口下单单独作为一个动作触发触发瞬间不做其他请求模拟真人“看到到货→点购买”的行为节奏。4.3 现象下单返回成功订单号也拿到了但半小时后订单被取消这是最让人血压飙升的坑。SubmitOrder接口返回了正常的订单号日志里也记录了“下单成功”结果订单被系统自动取消取消原因写着“超时未支付”或“库存不足”。原因京东的下单接口返回订单号只代表订单创建成功不代表锁库存成功。部分商品是“先创建订单、后锁库存”的流程如果锁库存失败订单会被自动取消。另外价格校验在前一步通过但提交时正好赶上价格变动服务端也会拒掉。解决下单成功后增加“订单核验”步骤。拿到订单号后轮询订单详情接口确认订单状态是“待支付”而不是“已取消”。如果发现被取消立即回到监控逻辑重新等下一轮补货。注意核验轮询的间隔不能复用pollInterval要拉长到 60 秒以上因为订单状态的更新有延迟查太早只会看到中间态。4.4 现象npm 安装依赖时 node-gyp 编译失败报各种 Python 或 Visual Studio 错误换了台新电脑npm install执行到一半某个带着 C 扩展的依赖开始编译然后报错说找不到 Python 或者 MSBuild。原因部分 npm 包没有预编译的二进制安装时会触发node-gyp从源码编译。node-gyp需要 Python 2.7 或 3.x 以及 C 编译工具链不同 Node 版本对编译器版本的要求还不一样。JD 项目本身不一定用原生模块但它的间接依赖里可能带了sqlite3或sharp这类需要编译的包。解决先看报错日志里的目标是哪个包判断它是不是必需依赖。如果只是间接依赖考虑升级 Node 版本到 18 以上新版 npm 会自动优先选择预编译二进制只有找不到时才现场编译。如果必须现场编译Windows 上安装 Visual Studio Build Tools勾选 C 桌面开发工作负载然后重新安装。macOS 上确保xcode-select --install已执行。5. 把监控调稳的进阶做法到货检测状态机与下单后的订单核验前面写的轮询逻辑其实是一个隐式的状态机。把状态机显式化之后整个脚本的行为会变得清晰很多也更容易扩展。我的做法是这样的// state-machine.js - 显式的三态流转 const STATES { WATCHING: watching, // 监控中等待到货 ORDERING: ordering, // 已触发下单等待结果 VERIFYING: verifying // 下单成功核验订单状态 }; let currentState STATES.WATCHING; async function onStateChanged(nextState, context) { console.log(状态切换: ${currentState} - ${nextState}, context); currentState nextState; }状态切换的规则只有三条WATCHING下检测到有货切到ORDERING并触发下单ORDERING下收到订单号切到VERIFYING并启动订单核验VERIFYING下确认订单是待支付状态任务完成停止该 SKU 的轮询。如果ORDERING下单抛异常回到WATCHING但附加一个 60 秒的冷却期。订单核验的轮询频率要单独调用 60 秒一次最稳妥。核验请求本身也要带上 Cookie 和订单相关的参数因为订单详情接口必须登录态。看到订单状态变成“待支付”才算真正结束如果看到“已取消”回到WATCHING继续等下一轮补货。这个核验动作很重要它让整个项目从“发了请求就算成功”变成“确认订单有效才算成功”。部署层面本地电脑跑轮询脚本有个隐患电脑休眠、网络断开都会中断监控。把脚本部署到云服务器是更稳的做法用 PM2 做进程守护崩溃后自动拉起再用pm2 save pm2 startup配置开机自启。分布式轮询我不建议做除非你有多个账号且每个账号轮询不同 SKU否则单账号跑多个节点反而会触发风控。现在有人用 Codex 这类 AI 辅助生成爬虫脚本但监控下单这种强状态流程手写代码能让你清楚每一处重试和状态切换的逻辑AI 生成的代码在边界处理上容易埋雷。我做这类监控项目最大的教训是永远不要把下单逻辑的健壮性寄托在“接口不会变”上。jd-happy 被弃用不是因为它本身写得差而是接口变了、风控变了维护成本超过了项目价值。所以你自己做的时候把配置和代码分离、把监控和下单拆开、把关键事件写进日志文件这三点守住即使接口变动你也能在半小时内改完重新上线。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网