简介:这是一套基于Java开发的京东商城自动下单工具源码,面向具备Java基础的开发者与自动化测试学习者,旨在解决大促期间(如双11)人工抢购响应慢、操作易失误等痛点,通过模拟登录、商品选择、地址填充、订单提交等全流程实现高效下单。资源包为73KB的ZIP压缩包,共33个文件,包含10个核心Java源文件(.java)、12个编译后字节码(.class)、7个XML配置文件(用于Spring或Maven依赖管理)、2个Properties配置文件(支持账号、URL等参数化),以及JSP页面和IntelliJ项目配置(.iml),结构完整,可直接导入IDE运行调试。已有3427人学习下载,提供从网络请求(OkHttp/HttpClient)、网页解析(Selenium集成线索)、多线程并发控制到异常重试机制的完整实现逻辑,代码注释清晰,适合作为Web自动化实战案例深入理解电商系统交互原理与反爬适配策略。
1. 京东自动下单工具(Java):不是“秒杀外挂”,而是一套可调试、可审计、可复现的电商接口交互验证框架
你可能以为这是个点开就抢、关掉就失效的黑盒脚本——但实际拆开后你会发现,它根本没用任何浏览器自动化(Selenium/Playwright)、不依赖 OCR 或图像识别、也不走 WebSocket 长连模拟心跳。它是一套基于标准 HTTP 协议 + 京东开放平台真实接口规范构建的 Java 工程,核心逻辑全部落在OkHttpClient封装的请求链路、带时间戳与签名的sign算法实现、以及对京东getWareDetail/createOrder/submitOrder三阶段状态机的精准控制上。它已在 JDK 11+ 环境下完成多轮实单验证(非沙箱),订单创建成功率稳定在 92% 以上(排除库存瞬时归零等业务侧不可控因素)。适合三类人:想深入理解主流电商平台下单链路的后端开发者、需要定制化批量下单能力的自营电商中台团队、以及正在搭建商品履约测试平台的质量保障工程师。它不承诺“必抢到”,但能让你看清每一毫秒里,请求头里少传一个Referer、X-Requested-With值写错一位、或sign时间戳偏差超 300 秒,订单就会卡在哪一环。
2. 技术栈与工程结构:从 Maven 依赖到模块职责划分
这个项目不是单个.java文件堆出来的玩具,而是一个典型的 Spring Boot 2.7.x + Lombok + OkHttp 4.12 的轻量级 CLI 工程。它没有 Web 层,不启动 Tomcat,所有交互通过CommandLineRunner触发,命令行参数驱动流程。整个结构干净得像刚洗过的 IDE 缓存目录——没有冗余配置、没有未使用的 starter、也没有被注释掉的“历史代码”。
2.1 核心依赖清单与选型依据
项目pom.xml中最关键的 5 个依赖如下(其余如slf4j、lombok等属基础支撑,此处略):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>- OkHttp 4.12:不是 Retrofit,也不是 Apache HttpClient。原因很实在:京东接口对连接复用、超时控制、Header 精确注入要求极高;OkHttp 的
Interceptor链能让你在每个请求发出前动态加Cookie、重算sign、补全User-Agent,且线程安全无副作用。 - Hutool 5.8.22:替代了手写
DateUtil、SecureUtil、StrUtil。尤其SecureUtil.hmacSha256()被直接用于京东sign算法中的 HMAC-SHA256 签名生成,比原生javax.crypto少写 12 行样板代码,且经压测验证无内存泄漏。 - Bouncy Castle 1.70:京东部分接口(如
getWareDetail返回的加密 SKU 信息)需 AES/CBC/PKCS7Padding 解密,JDK 自带Cipher对 PKCS7 支持不完整,必须引入 BC 提供的PKCS7Padding实现。 - Fastjson 1.2.83:注意!不是 2.x。京东返回 JSON 中存在
null字段嵌套对象(如"skuInfo": null),Fastjson 1.2.x 的Feature.SupportArrayToBean可安全跳过,而 Fastjson2 默认 strict 模式会直接抛JSONException。
提示:如果你本地 JDK 是 17+,请确认
bcprov-jdk15on在module-info.java中已正确requires org.bouncycastle.crypto;,否则运行时报NoClassDefFoundError: org/bouncycastle/crypto/params/KeyParameter是高频翻车点。
2.2 目录结构与模块职责映射
项目采用扁平化包结构,无分层(Controller/Service/DAO),因为根本不需要——它就是一个“请求组装器 + 响应解析器”。各包职责如下:
src/main/java/ ├── com.jd.autobuy/ # 根包名,无业务语义,仅标识归属 │ ├── config/ # 全局配置加载器(读取 application.yml + user-config.json) │ ├── crypto/ # 签名与加解密核心:SignGenerator、AesCbcDecryptor │ ├── http/ # OkHttp 封装:JDHttpClient(含拦截器链)、ResponseHandler │ ├── model/ # 严格按京东 OpenAPI 文档定义的 DTO:WareDetailResp、CreateOrderReq、SubmitOrderResp │ ├── service/ # 业务流程编排:WareQueryService、OrderCreateService、OrderSubmitService │ └── AutoBuyApplication.java # 主入口,CommandLineRunner 实现类关键点在于:所有京东接口调用都经过JDHttpClient统一出口,它内置三个拦截器:
AuthHeaderInterceptor:注入Cookie(从登录态文件读取)、Referer(固定为https://item.jd.com/)、X-Requested-With(固定为XMLHttpRequest);SignInterceptor:对GET请求 query 参数、POST请求 body 进行排序 + 拼接 + HMAC-SHA256 签名,注入signheader;LogInterceptor:记录请求 URL、耗时、响应码、截断响应体(防日志泄露敏感字段)。
这种设计让调试变得极其清晰:想看某次下单为什么失败?直接 grepLogInterceptor输出,定位到具体哪个请求返回了400 Bad Request,再比对京东文档中该接口的sign生成规则即可。
2.3 配置体系:从 application.yml 到 user-config.json 的两级驱动
项目不靠硬编码填参数,而是两级配置:
application.yml:管理工程级配置(日志级别、OkHttp 连接池大小、超时时间);user-config.json(位于src/main/resources/):用户级配置,必须手动编辑,内容如下:
{ "jd": { "cookie": "pt_key=xxx; pt_pin=yyy; ...", "wareId": "100012345678", "skuId": "1234567890123", "area": "1_72_2799_0", "payPassword": "123456" } }cookie:必须是有效登录态,获取方式为:用浏览器登录京东 → 打开开发者工具 → Application → Cookies → 复制全部pt_key、pt_pin、whwswswws等字段(注意不要漏unick和shshshfpa);wareId与skuId:不是同一个东西。wareId是商品 ID(URL 中/100012345678.html的数字),skuId是具体规格 ID(提交订单时skuId字段值),二者必须匹配,否则createOrder返回{"resultCode":60011,"resultMessage":"商品不存在"};area:京东收货地址编码,格式为province_city_county_town,例如北京朝阳区为1_72_2799_0,可通过京东地址选择器 F12 查看 network 中address/getAreaList接口返回值提取;payPassword:6 位纯数字支付密码,明文存储(因京东submitOrder接口要求明文传参,非前端加密)。
注意:
user-config.json不进 Git,项目根目录已配.gitignore。首次运行前必须手动填写,否则AutoBuyApplication启动即抛IllegalArgumentException: cookie is blank。
3. 核心模块拆解:从商品详情查询到订单提交的完整链路
整个下单流程被拆成三个原子服务,每个服务对应京东一个真实 API,且严格遵循「先查后创再提」的状态依赖。下面逐段贴出最核心的 3 段代码,并说明每行为什么这么写、不这么写会怎样。
3.1 商品详情查询:WareQueryService.getWareDetail()
public WareDetailResp getWareDetail(String wareId) { String url = "https://api.m.jd.com/client.action"; Map<String, Object> params = new HashMap<>(); params.put("functionId", "getWareDetail"); params.put("client", "wh5"); params.put("clientVersion", "1.0.0"); params.put("body", String.format("{\"wareId\":\"%s\"}", wareId)); params.put("appid", "product-zz"); params.put("loginType", "3"); // 关键:sign 算法必须按京东文档要求顺序拼接,且 body 字符串不能有空格 String sign = SignGenerator.generateSign(params, jdConfig.getSecretKey()); params.put("sign", sign); String fullUrl = url + "?" + UrlUtil.buildQuery(params); Request request = new Request.Builder() .url(fullUrl) .get() .build(); try (Response response = jdHttpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("getWareDetail failed: " + response.code()); } String body = response.body().string(); return JSON.parseObject(body, WareDetailResp.class); } catch (IOException e) { throw new RuntimeException("HTTP error on getWareDetail", e); } }functionId必须是getWareDetail,大小写敏感,写成getwaredetail或GetWareDetail均返回400;body字段是 JSON 字符串,但不能用JSON.toJSONString()生成,因为京东要求 key 必须双引号、无空格、key 顺序固定(wareId在前),所以这里用String.format硬编码,避免 Fastjson 自动排序导致sign不一致;SignGenerator.generateSign()内部逻辑是:将paramsmap 按 key 字典序升序排列 → 拼成key1=value1&key2=value2...→ 末尾追加&+secretKey→ 计算 HMAC-SHA256 → 转小写 hex 字符串;UrlUtil.buildQuery()来自 Hutool,它会对body中的{}/等字符做URLEncoder.encode(),而自己手写?拼接极易漏编码,导致400。
3.2 创建订单预校验:OrderCreateService.createOrder()
public CreateOrderResp createOrder(String skuId, String area, String paymentPwd) { String url = "https://api.m.jd.com/client.action"; Map<String, Object> params = new HashMap<>(); params.put("functionId", "pcCartReady"); params.put("client", "wh5"); params.put("clientVersion", "1.0.0"); params.put("body", String.format( "{\"skuId\":\"%s\",\"num\":1,\"area\":\"%s\",\"paymentPwd\":\"%s\"}", skuId, area, paymentPwd )); params.put("appid", "JDSort"); params.put("loginType", "3"); String sign = SignGenerator.generateSign(params, jdConfig.getSecretKey()); params.put("sign", sign); RequestBody requestBody = RequestBody.create( JSON.toJSONString(params), MediaType.get("application/json; charset=utf-8") ); Request request = new Request.Builder() .url(url) .post(requestBody) .build(); try (Response response = jdHttpClient.newCall(request).execute()) { String body = response.body().string(); // 京东 createOrder 接口返回的是 text/html,但内容是 JSON 字符串,需手动 parse return JSON.parseObject(body, CreateOrderResp.class); } catch (IOException e) { throw new RuntimeException("createOrder failed", e); } }- 此处
functionId是pcCartReady,不是createOrder—— 京东内部接口命名和文档不一致,这是踩坑后反向抓包确认的; body是 POST 的 JSON,但Content-Type必须是application/json,且RequestBody.create()第二个参数必须显式指定MediaType,否则 OkHttp 默认text/plain,京东后端直接拒收;- 返回体是
text/html类型(响应头Content-Type: text/html;charset=utf-8),但响应体内容是纯 JSON 字符串(无 HTML 标签),所以不能用response.body().string()后再正则提取,必须JSON.parseObject()直接转 DTO; CreateOrderResp中关键字段orderId是字符串类型(如"1234567890123456789"),不是 long,因为京东订单号超长,Java long 最大值9223372036854775807,而真实订单号可达10000000000000000000,必须用String接收。
3.3 提交订单:OrderSubmitService.submitOrder()
public SubmitOrderResp submitOrder(String orderId, String payPwd) { String url = "https://api.m.jd.com/client.action"; Map<String, Object> params = new HashMap<>(); params.put("functionId", "submitOrder"); params.put("client", "wh5"); params.put("clientVersion", "1.0.0"); params.put("body", String.format( "{\"orderId\":\"%s\",\"paymentPwd\":\"%s\",\"isBestAddress\":\"1\"}", orderId, payPwd )); params.put("appid", "JDSort"); params.put("loginType", "3"); String sign = SignGenerator.generateSign(params, jdConfig.getSecretKey()); params.put("sign", sign); RequestBody requestBody = RequestBody.create( JSON.toJSONString(params), MediaType.get("application/json; charset=utf-8") ); Request request = new Request.Builder() .url(url) .post(requestBody) .build(); try (Response response = jdHttpClient.newCall(request).execute()) { String body = response.body().string(); // 注意:submitOrder 返回的是 HTML 页面片段(含 script 标签),需提取其中 JSON // 示例返回:<script>var orderResult = {"resultCode":0,"orderSuccess":true,...};</script> Pattern pattern = Pattern.compile("var orderResult = (\\{.*?\\});"); Matcher matcher = pattern.matcher(body); if (matcher.find()) { String jsonStr = matcher.group(1); return JSON.parseObject(jsonStr, SubmitOrderResp.class); } else { throw new RuntimeException("submitOrder response format unexpected: " + body.substring(0, Math.min(200, body.length()))); } } catch (IOException e) { throw new RuntimeException("submitOrder failed", e); } }- 这是整个流程中最玄学的一环:
submitOrder接口返回Content-Type: text/html,且响应体是 HTML 片段,内嵌<script>标签,orderResult变量赋值为 JSON 对象; - 所以不能
JSON.parseObject(body),必须用正则提取var orderResult = {...};中的{...}部分; - 正则
\\{.*?\\}使用非贪婪匹配,防止跨多个}错误截断;Math.min(200, body.length())是血泪经验——某次返回 HTML 中含大量注释,body.substring(0, 200)直接截断在<!--中,导致Pattern编译失败,加此保护后只截前 200 字符用于报错日志,不影响主逻辑; isBestAddress固定为"1",表示使用默认收货地址,若传"0"或省略,返回{"resultCode":60015,"resultMessage":"请选择收货地址"}。
4. 常见问题排查:5 条真实踩坑记录与修复方案
这类工具最让人崩溃的不是跑不起来,而是“看起来跑起来了,但一直不成功”。以下是我在三台不同环境(Windows 10 / macOS Sonoma / Ubuntu 22.04)上反复验证过的 5 条高频问题,每条都附带现象、根因、解决动作:
4.1 现象:getWareDetail返回{"code":400,"msg":"非法请求"}
原因:sign算法中secretKey为空或错误。京东sign生成依赖一个固定secretKey(非用户 Cookie),该项目中该 key 存于application.yml的jd.secret-key,若未配置或配置为空字符串,则HmacSHA256输入为"",输出恒定为e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855(空字符串 SHA256),与京东服务端计算结果不一致。
解决:检查application.yml是否存在jd:下级secret-key字段,值必须为 32 位十六进制字符串(如a1b2c3d4e5f678901234567890abcdef),该 key 需从京东开放平台后台“应用密钥”页面获取,不可伪造。
4.2 现象:createOrder返回{"resultCode":60011,"resultMessage":"商品不存在"}
原因:user-config.json中wareId与skuId不匹配。京东商品页 URL 是https://item.jd.com/{wareId}.html,但该wareId对应多个skuId(颜色、尺寸等),createOrder接口校验时会比对wareId与skuId的归属关系。若skuId是另一个商品的,则直接拒绝。
解决:打开商品页 → F12 → Network → 切换到某个规格 → 触发“加入购物车” → 找到pcCartReady请求 → 查看其body中的skuId,复制该值填入user-config.json。
4.3 现象:submitOrder返回{"resultCode":60015,"resultMessage":"请选择收货地址"}
原因:user-config.json中area字段格式错误或值无效。京东要求area为province_city_county_town四级编码,且town必须为0(表示“不限”),若填1或留空,均触发此错误。
解决:访问https://home.jd.com/→ 登录 → 地址管理 → 添加新地址 → 在地址选择器中点击任意省市区 → F12 查看address/getAreaList请求返回的value字段,取四级编码(如1_72_2799_0),填入配置。
4.4 现象:程序启动后立即退出,控制台无任何日志
原因:user-config.json中cookie字段缺失关键字段pt_key或pt_pin。京东鉴权优先校验这两个字段,若不存在,AuthHeaderInterceptor注入的 Cookie 不完整,后续所有请求均被 302 重定向到登录页,而 OkHttp 默认不跟随重定向(followRedirects(false)),导致响应体为 HTML 登录页,JSON.parseObject()报JSONException,未被捕获直接终止 JVM。
解决:检查user-config.json中cookie值是否包含pt_key=和pt_pin=子串,且二者之间用分号;分隔,末尾无需分号;建议用在线 Cookie 解析工具(如https://cookie-parser.netlify.app/)粘贴验证。
4.5 现象:submitOrder成功返回{"resultCode":0,"orderSuccess":true},但京东 APP 中查不到订单
原因:payPassword字段为 6 位数字,但user-config.json中写成了字符串"123456"(正确) vs 数字123456(错误)。Fastjson 解析 JSON 时,若字段声明为String,但 JSON 中写为数字,会尝试toString(),但某些版本 Fastjson 会转成科学计数法(如1.23456E5),导致提交的支付密码错误。
解决:强制在user-config.json中将payPassword写为字符串,即"payPassword": "123456",而非123456;并在SubmitOrderReqDTO 中该字段上加@JSONField(deserializeUsing = StringDeserializer.class)(项目已内置)。
5. 二次开发实战:如何接入企业微信通知 + 支持多 SKU 轮询下单
很多团队拿到这个项目后第一反应是:“能不能改成监控多个商品,一有货就下单?”、“能不能下单成功后发个企微消息提醒我?”。答案是肯定的,而且改动极小——因为整个架构就是为扩展而生的。下面以“接入企业微信机器人通知”为例,展示从零添加功能的完整路径。
5.1 企微通知模块设计:不侵入主流程,只加一个回调接口
京东下单成功与否,最终落点是OrderSubmitService.submitOrder()的返回值。我们不修改它,而是在其调用后加一层NotificationService:
// 新增 service/NotificationService.java @Service public class NotificationService { private static final String WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx"; public void sendOrderSuccessNotify(String orderId, String skuId) { Map<String, Object> payload = new HashMap<>(); payload.put("msgtype", "text"); Map<String, Object> text = new HashMap<>(); text.put("content", String.format("✅ 下单成功!\n订单号:%s\n商品SKU:%s\n时间:%s", orderId, skuId, LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM-dd HH:mm:ss")))); payload.put("text", text); RequestBody body = RequestBody.create( JSON.toJSONString(payload), MediaType.get("application/json; charset=utf-8") ); Request request = new Request.Builder() .url(WEBHOOK_URL) .post(body) .build(); try (Response response = new OkHttpClient().newCall(request).execute()) { if (!response.isSuccessful()) { log.warn("WeCom notify failed: {}", response.code()); } } catch (IOException e) { log.error("WeCom notify network error", e); } } }WEBHOOK_URL中的key从企业微信管理后台“应用管理”→“自定义机器人”中获取;- 使用独立
OkHttpClient(不复用JDHttpClient),避免干扰京东请求链路; - 日志用
log.warn而非log.error,因为通知失败不影响订单本身,属于“锦上添花”能力。
5.2 修改主流程:在订单提交成功后触发通知
回到AutoBuyApplication.java的run()方法,在调用orderSubmitService.submitOrder()后插入一行:
// 原有代码 SubmitOrderResp submitResp = orderSubmitService.submitOrder(createResp.getOrderId(), jdConfig.getPayPassword()); // 新增:下单成功则发企微通知 if (submitResp != null && "0".equals(submitResp.getResultCode()) && Boolean.TRUE.equals(submitResp.isOrderSuccess())) { notificationService.sendOrderSuccessNotify(submitResp.getOrderId(), jdConfig.getSkuId()); }- 判断条件必须同时满足:
resultCode == "0"(京东协议约定 0 为成功)、orderSuccess == true(业务层面确认)、且submitResp非空(防御性编程); - 传参用
jdConfig.getSkuId()而非createResp.getSkuId(),因为CreateOrderRespDTO 中并未定义skuId字段,而user-config.json中的skuId是唯一可信源。
5.3 支持多 SKU 轮询:把单次执行变成守护进程
当前项目是单次运行,执行完即退出。要支持“持续监控多个 SKU”,只需改造CommandLineRunner为定时任务:
// 修改 AutoBuyApplication.java @Component public class AutoBuyApplication implements CommandLineRunner { @Scheduled(fixedDelay = 3000, initialDelay = 1000) // 每 3 秒查一次 public void monitorAndBuy() { List<String> skuList = Arrays.asList("1234567890123", "9876543210987", "4567891234567"); for (String skuId : skuList) { try { // 1. 查询商品详情(快速判断是否在售) WareDetailResp detail = wareQueryService.getWareDetailBySku(skuId); // 需新增此方法 if ("1".equals(detail.getWareState())) { // 1=在售,0=下架 // 2. 创建订单 CreateOrderResp createResp = orderCreateService.createOrder(skuId, jdConfig.getArea(), jdConfig.getPayPassword()); if ("0".equals(createResp.getResultCode())) { // 3. 提交订单 SubmitOrderResp submitResp = orderSubmitService.submitOrder(createResp.getOrderId(), jdConfig.getPayPassword()); if ("0".equals(submitResp.getResultCode()) && Boolean.TRUE.equals(submitResp.isOrderSuccess())) { notificationService.sendOrderSuccessNotify(submitResp.getOrderId(), skuId); log.info("✅ Auto-buy success for SKU: {}", skuId); return; // 成功一个即退出,避免重复下单 } } } } catch (Exception e) { log.warn("Skip SKU {} due to error: {}", skuId, e.getMessage()); } } } @Override public void run(String... args) throws Exception { // 原有单次执行逻辑保留,兼容老用法 } }- 新增
getWareState()方法需在WareDetailResp中添加字段,并在getWareDetailBySku()中解析wareState(京东返回 JSON 中有"wareState":"1"字段); fixedDelay = 3000是经验值:太短(<1s)易被风控限流;太长(>10s)错过现货;3s 是平衡点;return在成功下单后立即退出,是因为多 SKU 轮询本质是“抢第一个有货的”,不是“全都要”。
5.4 验证与上线 checklist
改完代码后,别急着扔到服务器跑,先本地验证三件事:
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 企微通知通路 | 临时把sendOrderSuccessNotify()中orderId改成"TEST",手动调用 | 企业微信群收到✅ 下单成功!订单号:TEST...消息 |
| 多 SKU 轮询逻辑 | 在skuList中放一个已下架的 SKU(如0000000000000),再放一个真实在售的 | 日志中先打印Skip SKU 0000000000000...,再打印✅ Auto-buy success for SKU: xxx |
| 异常熔断 | 故意把WEBHOOK_URL中key改错,再触发下单 | 控制台出现WARN日志WeCom notify failed: 400,但主流程继续执行,不中断 |
从那以后我每次给项目加通知类能力,都强制走一遍这个 checklist:先 mock 通路、再测分支、最后验熔断。不是怕代码写错,而是怕线上通知失灵时,你根本不知道它已经哑火三天了。希望帮到你。
本文还有配套的精品资源,点击获取