Repository: kalvinGit/J12306
Branch: master
Commit: 69298ea5c070
Files: 42
Total size: 281.0 KB
Directory structure:
gitextract_po6zbghf/
├── README.md
├── lib/
│ ├── activation-1.1.1.jar
│ ├── hutool-all-4.6.6.jar
│ ├── javax.mail-1.5.4.jar
│ └── snakeyaml-1.25.jar
└── src/
├── com/
│ └── kalvin/
│ └── J12306/
│ ├── AI/
│ │ ├── Easy12306AI.java
│ │ └── ImageAI.java
│ ├── Go12306.java
│ ├── Main.java
│ ├── api/
│ │ ├── AlternateOrder.java
│ │ ├── Captcha.java
│ │ ├── CheckCaptcha.java
│ │ ├── CheckOrderInfo.java
│ │ ├── CheckRandCodeAsync.java
│ │ ├── CheckUser.java
│ │ ├── ConfirmSingleForQueue.java
│ │ ├── GetJS.java
│ │ ├── GetPassCodeNew.java
│ │ ├── GetQueueCount.java
│ │ ├── Login.java
│ │ ├── MyOrder.java
│ │ ├── PassengerDTOS.java
│ │ ├── QueryOrderWaitTime.java
│ │ ├── RepeatSubmitToken.java
│ │ ├── SubmitOrderRequest.java
│ │ └── Ticket.java
│ ├── cache/
│ │ └── TicketCache.java
│ ├── config/
│ │ ├── Constants.java
│ │ ├── TicketSeatType.java
│ │ ├── UrlConfig.java
│ │ ├── Urls.java
│ │ └── UrlsEnum.java
│ ├── dto/
│ │ ├── SubmitTicketInfoDTO.java
│ │ ├── TicketInfoDTO.java
│ │ └── UserInfoDTO.java
│ ├── exception/
│ │ └── J12306Exception.java
│ ├── http/
│ │ └── Session.java
│ └── utils/
│ ├── EmailUtil.java
│ ├── J12306Util.java
│ ├── StationUtil.java
│ └── YmlUtil.java
└── config.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: README.md
================================================
# J12306抢票助手
12306抢票程序JAVA版,自动登录-验证-查票-购票/自动候补。只需简单的配置即可运行进行快捷抢票。
### 使用说明
##### 引入jar依赖
* 手动添加项目lib文件夹中的依赖包
##### 配置文件config.yml
```
# 请修改相关配置
# 12306账号密码配置(暂时没用到)
j12306:
user: 182xxxx
password: 123456
ticket:
queryspeed: 2000 # 刷票速度(单位毫秒)。默认2秒。温馨提示:刷票频率不要过快,避免封IP(暂未测试过)
alternate: true # 开启自动候补
queryp: Z # 查票默认接口(可选值:A、Z)。说明:由于12306官方查票接口经常在A和Z两个接口中变更,所以为了方便,在此处加了默认接口配置。
# 通知配置
notice:
# 电子邮件配置
email:
sender:
from: hutool@yeah.net # 发件人(必须正确,否则发送失败)
host: smtp.yeah.net # 邮件服务器的SMTP地址,可选,默认为smtp.<发件人邮箱后缀>
port: 25 # 邮件服务器的SMTP端口,可选,默认25
user: hutool # 用户名
pass: qlw2e3 # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
receiver: 1481397688@qq.com # 接收人邮箱
```
##### 配置抢票信息
* Main.java中,直接配置用户名密码及乘车相关信息即可
##### 开始抢票
* 直接运行Main函数开始抢票。就是这么简单粗暴!
##### 程序运行log
```
[2019-09-22 12:42:33] [INFO] com.kalvin.J12306.api.Login: 进入12306登录页,状态码:200
[2019-09-22 12:42:36] [INFO] com.kalvin.J12306.AI.Easy12306AI: 验证码:3,4
[2019-09-22 12:42:37] [INFO] com.kalvin.J12306.api.Login: 验证码通过,开始密码登录
[2019-09-22 12:42:37] [INFO] com.kalvin.J12306.api.Login: 登录成功
[2019-09-22 12:42:40] [INFO] com.kalvin.J12306.api.Ticket: 进入查询车票页面,开始查票...
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2804,出发时间:07:06,到达时间:08:10,座席:一等座1、二等座12、无座有
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D1849,出发时间:07:23,到达时间:08:37,座席:一等座4、二等座有、无座无
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D7551,出发时间:09:23,到达时间:11:02,座席:一等座有、二等座有、无座有
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2962,出发时间:09:35,到达时间:10:41,座席:一等座8、二等座14、无座有
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2812,出发时间:10:05,到达时间:11:11,座席:一等座无、二等座2、无座无
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D1822,出发时间:11:00,到达时间:12:06,座席:一等座3、二等座无、无座无
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2948,出发时间:11:34,到达时间:12:42,座席:一等座无、二等座无、无座有
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2834,出发时间:15:15,到达时间:16:27,座席:一等座2、二等座2、无座有
[2019-09-22 12:42:42] [INFO] com.kalvin.J12306.Go12306: 可预订车票信息:发车日期:2019-09-26,车次:D2980,出发时间:17:19,到达时间:18:25,座席:一等座2、二等座20、无座有
[2019-09-22 12:42:46] [INFO] com.kalvin.J12306.api.CheckOrderInfo: 车票提交通过,正在尝试排队...
[2019-09-22 12:42:46] [INFO] com.kalvin.J12306.api.GetQueueCount: 排队成功,你当前排在6位,当前余票还有101张
[2019-09-22 12:42:46] [INFO] com.kalvin.J12306.api.ConfirmSingleForQueue: 不需要订单验证码,直接提交
[2019-09-22 12:42:46] [INFO] com.kalvin.J12306.api.ConfirmSingleForQueue: 开始正式下单...
[2019-09-22 12:42:48] [INFO] com.kalvin.J12306.api.QueryOrderWaitTime: 下单ing...正在第1次排队ing...
[2019-09-22 12:42:48] [INFO] com.kalvin.J12306.api.QueryOrderWaitTime: 订票成功!
[2019-09-22 12:42:48] [INFO] com.kalvin.J12306.api.QueryOrderWaitTime: 恭喜您订票成功,订单号为:EF71508610, 请立即打开浏览器登录12306,访问‘未完成订单’,在30分钟内完成支付!
[2019-09-22 12:42:48] [INFO] com.kalvin.J12306.api.QueryOrderWaitTime: 以邮件方式通知抢票人
[2019-09-22 12:42:48] [INFO] com.kalvin.J12306.Go12306: 抢票程序结束:STOP
```
### 更新日志
##### 2019-12-24
* 新增座席选择,现在支持座席【商务特等座(P)、一等座(M)、二等座(O)、无座(N)、软卧(4)、硬卧(3)、软座(2)、硬座(1)】
* 优化抢票逻辑及代码
##### 2019-12-22
* 更改刷票频率(config.yml[j12306.ticket.queryspeed])粒度为毫秒(ms), 原秒(s)。
* config.yml新增配置项:[j12306.ticket.queryp];由于12306官方查票接口经常在A和Z两个接口中变更,
现在支持配置默认查票接口(不是必要的),另外程序也会自动识别无法访问的查票接口,并自动切换,如:queryZ -> queryA
* 更新错误日志输出级别
### 问题反馈
如有疑问,可在项目上issues!
### 常见问题解决
* RAIL_EXPIRATION值已失效
有时候网络原因导致的,请务必多重试几次,如果还是这种情况就更新下logdevice接口的参数;更新步骤:
* 以下顺序一定要对,不然找不到logdevice
* 1.浏览器访问:https://kyfw.12306.cn/otn/login/init
* 2.按f12进入调试模式并点击Network选项
* 3.清除浏览器缓存的有关12306.cn和kyfw.12306.cn的Cookie(谷歌浏览器点击浏览器地址栏的小锁)
* 4.按f5重新刷新(只有第1次刷新才有出现,所以不要刷新2次)
* 5.在Network选项下找到logdevice请求,点击它,在Headers选项下拉到最下面就可以找到如下几个参数,复制替换它即可
* 其它情况登录失败或验证码验证失败
可能的解决方案:
* 请重试登录多次
* 确保更新到最新的代码
* 线程【main】无法获取车票信息,状态码:302
可能的解决方案:
* 确保你IP没被封(在浏览器上12306官网是否能正常查票)
* 更新最新代码
================================================
FILE: src/com/kalvin/J12306/AI/Easy12306AI.java
================================================
package com.kalvin.J12306.AI;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.exception.J12306Exception;
import java.io.File;
/**
* 此图片识别AI由https://github.com/zhaipro/easy12306提供
* Create by Kalvin on 2019/9/19.
*/
public class Easy12306AI implements ImageAI {
private static final Log log = LogFactory.get();
private String aiUrl;
private String imgPath;
public Easy12306AI(String aiUrl, String imgPath) {
this.aiUrl = aiUrl;
this.imgPath = imgPath;
}
@Override
public String printCode() {
try {
// 此AI不支持验证多个标签的图片验证码
HttpRequest request = HttpUtil.createPost(this.aiUrl);
request.form("file", new File(this.imgPath));
String body = request.execute().body(); // text: 辣椒酱 , images: 调色板 , 雨靴 , 绿豆 , 辣椒酱 , 雨靴 , 雨靴 , 档案袋 , 辣椒酱
body = body.replaceAll(" ", "");
// log.info("printCode body = {}", body);
String tagTxt = body.substring(body.indexOf("text:") + 5, body.indexOf(",images"));
String imagesTxt = body.substring(body.indexOf("images:") + 7);
String[] imageTxtArr = imagesTxt.split(",");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < imageTxtArr.length; i++) {
if (tagTxt.equals(imageTxtArr[i])) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(i + 1);
}
}
log.info("验证码:{}", sb.toString());
return sb.toString();
} catch (StringIndexOutOfBoundsException e) {
log.info("图片验证码识别AI异常,无法为您自动识别验证码,请重试");
throw new J12306Exception("图片验证码识别AI异常,无法为您自动识别验证码,请重试:" + e.getMessage());
}
}
}
================================================
FILE: src/com/kalvin/J12306/AI/ImageAI.java
================================================
package com.kalvin.J12306.AI;
/**
* 图片验证码识别AI接口
* Create by Kalvin on 2019/9/19.
*/
public interface ImageAI {
/**
* 打码
* @return 图片识别码(如12306图片识别码:1,3,5。分别代码横排数起图片位置)
*/
String printCode();
}
================================================
FILE: src/com/kalvin/J12306/Go12306.java
================================================
package com.kalvin.J12306;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONException;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.api.*;
import com.kalvin.J12306.cache.TicketCache;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.TicketSeatType;
import com.kalvin.J12306.dto.TicketInfoDTO;
import com.kalvin.J12306.dto.UserInfoDTO;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import com.kalvin.J12306.utils.J12306Util;
import com.kalvin.J12306.utils.StationUtil;
import com.kalvin.J12306.utils.YmlUtil;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 12306抢票程序
* Create by Kalvin on 2019/9/18.
*/
public class Go12306 {
private static final Log log = LogFactory.get();
private Session session; // 会话保持
private String username; // 12306用户账号
private String password; // 密码
private TicketCache ticketCache = TicketCache.getInstance();
/*乘客订票相关参数*/
private String trainDate; // 乘车日期(2019-10-01)
private String fromStation; // 出发站(IZQ)
private String toStation; // 到达站(FAQ)
private String trainNums; // 列车编号(D2834)。多个使用英文半角逗号分隔。目前暂时只能人工看列车编号啦
private String seats; // 列车座席。M,O,N分别代表:一等座、二等座、无座。目前只支持这三种选择
private int queryCount = 0; // 刷票次数
public static Go12306 newInstance() {
return new Go12306();
}
public Go12306 initUser(String username, String password) {
this.username = username;
this.password = password;
return this;
}
/**
* 初始化订票参数信息
* @param trainDate 乘车日期
* @param fromStation 始发站
* @param toStation 终点站
* @param trainNums 列表车次
* @param seats 座席类型:M、O、N
*/
public Go12306 initBookTicketInfo(String trainDate, String fromStation, String toStation, String trainNums, String seats) {
this.trainDate = J12306Util.formatDateStr(trainDate);
final String fromStationCode = StationUtil.getStationCode(fromStation);
if (fromStationCode == null) {
throw new J12306Exception("无法找到始发站站点【" + fromStation + "】,请确保始发站点名正确。");
}
final String toStationCode = StationUtil.getStationCode(toStation);
if (toStationCode == null) {
throw new J12306Exception("无法找到到达站站点【" + fromStation + "】,请确保到达站点名正确。");
}
this.fromStation = fromStationCode;
this.toStation = toStationCode;
this.trainNums = trainNums;
this.seats = seats;
return this;
}
public void run() {
// 构建会话
this.session = new Session();
// 开始登录
Login login = new Login(this.session, this.username, this.password);
UserInfoDTO userInfo = login.send();
int tryLoginCount = 0;
while (true) {
if (userInfo == null) {
if (tryLoginCount >= 5) {
throw new J12306Exception("无法登录,程序已终止,请手动重试登录");
}
log.error("登录失败,正在第{}次尝试登录", tryLoginCount++);
userInfo = login.send();
} else {
break;
}
}
// 用户信息保存到缓存中
this.ticketCache.put(Constants.USER_INFO_KEY, userInfo);
// 开始查询余票
Ticket ticket = new Ticket(this.session, this.trainDate, this.fromStation, this.toStation);
int querySpeed = (Integer) YmlUtil.get("j12306.ticket.queryspeed");
// 计算刷票粒度
// int intervalTime = querySpeed / threadPoolSize;
// ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
int queryA302Count = 0, queryZ302Count = 0;
String usingQuery = (String) YmlUtil.get("j12306.ticket.queryp");
boolean isPutTime = false;
stopLop: while (true) {
HttpResponse httpResponse;
try {
if ("A".equals(usingQuery)) {
httpResponse = ticket.queryA();
} else if ("Z".equals(usingQuery)) {
httpResponse = ticket.queryZ();
} else {
throw new J12306Exception("查票接口异常,请确认config.yml[j12306.ticket.queryp]配置正确");
}
} catch (HttpException e) {
log.error("请求超时,或无法访问,错误信息:{}", e.getMessage());
continue;
}
String body = httpResponse.body();
// log.info("query tickets status = {},body={}", httpResponse.getStatus(), body);
if (httpResponse.getStatus() == Constants.REQ_SUCCESS_STATUS) {
List ticketInfoDTOS;
try {
ticketInfoDTOS = J12306Util.parseTicketInfo(body);
} catch (JSONException e) {
log.error("查询车票发生未知异常:{}", e.getMessage());
continue;
}
for (TicketInfoDTO ticketInfoDTO : ticketInfoDTOS) {
if (!ticketInfoDTO.isOnSale()) {
continue;
}
String trainNum = ticketInfoDTO.getTrainNum();
String canNotAlternateSeatType = ticketInfoDTO.getCanNotAlternateSeatType();
final LinkedHashMap seatsTicketInfo = J12306Util.getSeatsTicketInfo(this.seats, ticketInfoDTO);
// 先进行一次解码。避免提交后再编码一次导致参数失效
String secretStr = J12306Util.urlDecode(ticketInfoDTO.getSecretStr());
String leftTicket = J12306Util.urlDecode(ticketInfoDTO.getLeftTicket());
String trainNo = ticketInfoDTO.getTrainNo();
String fromStationCode = ticketInfoDTO.getFormStationTelecode();
String toStationCode = ticketInfoDTO.getToStationTelecode();
String trainLocation = ticketInfoDTO.getTrainLocation();
if (!J12306Util.noNeedTicket(seatsTicketInfo)) {
log.info("可预订车票信息:发车日期【{}】,车次【{}】,出发时间:{},到达时间:{},座席:",
trainDate, trainNum, ticketInfoDTO.getGoOffTime(), ticketInfoDTO.getArrivalTime());
}
// 跳过不是购票意向的车次
if (!this.trainNums.contains(trainNum)) {
continue;
}
// 候补订单
if (J12306Util.noNeedTicket(seatsTicketInfo)) {
if ((boolean) YmlUtil.get("j12306.ticket.alternate")) {
// 判断是否能候补订单
if (this.ticketCache.get(trainNo) != null) {
continue;
}
if (ticketInfoDTO.isCanAlternate()) {
final String allEncStr = new PassengerDTOS(this.session).getPassengerEncStr();
for (String seatCode : seatsTicketInfo.keySet()) {
if ("".equals(canNotAlternateSeatType) || !canNotAlternateSeatType.contains(seatCode)) {
try {
log.info("准备提交候补订单:车次【{}】,发车日期【{}】,座席类型:{}", trainNum, trainDate, seatCode);
AlternateOrder alternateOrder = new AlternateOrder(this.session, ticketInfoDTO.getSecretStr(), allEncStr, seatCode, trainNo);
if (alternateOrder.checkFace()) {
alternateOrder.getSuccessRate();
}
} catch (Exception e) {
if (e instanceof J12306Exception) {
log.info("抢票程序结束");
break stopLop;
}
log.error("候补异常:{},列车【】加入小黑屋关闭3分钟", e.getMessage(), trainNum);
ticketCache.put(trainNo, trainNo, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
}
}
}
}
}
continue;
}
// 跳过小黑屋中的车次
if (ticketCache.get(trainNum) != null) {
continue;
}
try {
for (String seatCode : seatsTicketInfo.keySet()) {
if (seatsTicketInfo.get(seatCode)) {
log.info("提交订单:车次【{}】,发车日期【{}】,座席类型:{}", trainNum, trainDate, TicketSeatType.get(seatCode).getName());
new SubmitOrderRequest(this.session, secretStr, seatCode, trainDate, fromStationCode, toStationCode, trainNo, trainNum, trainLocation)
.send();
}
}
} catch (J12306Exception e) {
log.info("抢票程序结束:{}", e.getMsg());
break stopLop;
}
}
this.queryCount++;
log.info("-------线程【{}】已为账号{}刷票{}次,启程日期:{}--------", Thread.currentThread().getName(), this.username, this.queryCount, trainDate);
} else {
log.error("-------线程【{}】无法获取车票信息,状态码:{};程序会在{}次访问302后切换到另一个查询接口", Thread.currentThread().getName(), httpResponse.getStatus(), Constants.MAX_302);
if (httpResponse.getStatus() == 302) {
if ("A".equals(usingQuery)) {
queryA302Count++;
}
if ("Z".equals(usingQuery)) {
queryZ302Count++;
}
if (queryA302Count >= Constants.MAX_302) {
usingQuery = "Z";
}
if (queryZ302Count >= Constants.MAX_302) {
usingQuery = "A";
}
}
}
String puttime = (String) YmlUtil.get("j12306.ticket.puttime");
if (!StrUtil.isNullOrUndefined(puttime)) {
if (J12306Util.getCurrAftOneMinuteTime().equals(puttime.replace("-", ":"))) {
isPutTime = true;
querySpeed = (Integer) YmlUtil.get("j12306.ticket.puttimespeed");
}
}
if (isPutTime && J12306Util.getAfter5MinuteTime(puttime.replace("-", ":")).equals(J12306Util.getCurrTime())) {
isPutTime = false;
querySpeed = (Integer) YmlUtil.get("j12306.ticket.queryspeed");
}
J12306Util.sleepM(querySpeed);
}
}
}
================================================
FILE: src/com/kalvin/J12306/Main.java
================================================
package com.kalvin.J12306;
import com.kalvin.J12306.utils.EmailUtil;
import com.kalvin.J12306.utils.J12306Util;
public class Main {
public static void main(String[] args) {
// 测试配置邮件是否能成功发送(收件人能成功接收到测试邮件证明配置正确)
// EmailUtil.sendTest();
// 可以在控制台打印指定日期、出发站点、达到站点的所有车次,主要为了方便填写车次信息
// 你可以启动抢票前执行此方法(不是必要的),获取指定日期车次后,填写抢票信息,再启动抢票
// J12306Util.printlnLeftTicket("2020-01-21", "广州", "怀集");
// 开始抢票
selectTicket1();
}
public static void selectTicket1() {
Go12306.newInstance()
.initUser("182xxxx", "123456") // 用户名/密码
.initBookTicketInfo("2020-01-21", // 乘车日期
"广州", // 始发站
"怀集", // 到达站
"D1818,D1822,D2948,D1870,G2904,D2972,D1872,D2936", // 列车编号(D2834)。多个使用英文半角逗号分隔。目前暂时只能人工看列车编号啦
"P,M,O,N,4,3,2,1") // 列车座席。P,M,O,N,4,3,2,1分别代表:商务特等座(P)、一等座(M)、二等座(O)、无座(N)、软卧(4)、硬卧(3)、软座(2)、硬座(1)。
.run();
}
public static void selectTicket2() {
Go12306.newInstance()
.initUser("182xxxx", "123456") // 用户名/密码
.initBookTicketInfo("2020-01-22", // 乘车日期
"广州", // 始发站
"怀集", // 到达站
"D1882,D7551,D2962,D1818,D1754,D1822", // 列车编号(D2834)。多个使用英文半角逗号分隔。目前暂时只能人工看列车编号啦
"P,M,O,N,4,3,2,1") // 列车座席。P,M,O,N,4,3,2,1分别代表:商务特等座(P)、一等座(M)、二等座(O)、无座(N)、软卧(4)、硬卧(3)、软座(2)、硬座(1)。
.run();
}
}
================================================
FILE: src/com/kalvin/J12306/api/AlternateOrder.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.cache.TicketCache;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.UserInfoDTO;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import com.kalvin.J12306.utils.EmailUtil;
import com.kalvin.J12306.utils.J12306Util;
import java.util.HashMap;
/**
* 候补订单
* Create by Kalvin on 2019/9/25.
*/
public class AlternateOrder {
private final static Log log = LogFactory.get();
private Session session;
private String secretStr;
private String allEncStr;
private String seatType;
private String trainNo;
private String jzdhDateE;
private String jzdhHourE;
public AlternateOrder(Session session, String secretStr, String allEncStr, String seatType, String trainNo) {
this.session = session;
this.secretStr = secretStr;
this.allEncStr = allEncStr;
this.seatType = seatType;
this.trainNo = trainNo;
}
public boolean checkFace() {
TicketCache ticketCache = TicketCache.getInstance();
HashMap formData = new HashMap<>();
formData.put("secretList", this.getSecretList());
formData.put("_json_att", "");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CHECK_FACE, formData);
String body = httpResponse.body();
// log.info("checkFace body ={}", body);
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status") && parse.getByPath("data") != null) {
if ((boolean) parse.getByPath("data.face_flag")) {
log.info("已通过人脸核验,可以进行候补车票!");
return true;
} else {
log.info("你未通过人脸核验,通过人证一致性核验的用户及激活的“铁路畅行”会员可以提交候补需求,请您按照操作说明在铁路12306app.上完成人证核验");
return false;
}
} else if (parse.getByPath("messages") != null) {
ticketCache.put(this.trainNo, this.trainNo, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
log.info(((JSONArray) parse.getByPath("messages")).get(0).toString());
return false;
}
ticketCache.put(this.trainNo, this.trainNo, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
return false;
}
public void getSuccessRate() {
HashMap formData = new HashMap<>();
formData.put("successSecret", this.getSuccessSecret());
formData.put("_json_att", "");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.GET_SUCCESS_RATE, formData);
String body = httpResponse.body();
// log.info("getSuccessRate body ={}", body);
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status") && parse.getByPath("data") != null) {
// this.trainNo = (String) parse.getByPath("data.flag[0].train_no");
String info = (String) parse.getByPath("data.flag[0].info");
log.info("开始提交候补订单,{}", info);
this.submitOrderRequestAN();
} else {
log.info("getSuccessRate message:{}{}", ((JSONArray) parse.getByPath("messages")).get(0).toString(), parse.getByPath("validateMessages").toString());
}
}
private void submitOrderRequestAN() {
HashMap formData = new HashMap<>();
formData.put("secretList", this.getSecretList());
formData.put("_json_att", "");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.SUBMIT_ORDER_REQUEST_AN, formData);
String body = httpResponse.body();
// log.info("submitOrderRequestAN body ={}", body);
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status") && parse.getByPath("data.flag") != null) {
this.lineUpToPayInit();
this.passengerInitApi();
this.confirmHB();
} else {
log.info("提交候补订单失败:{}{}", ((JSONArray) parse.getByPath("messages")).get(0).toString(), parse.getByPath("validateMessages").toString());
}
}
private void lineUpToPayInit() {
this.session.httpClient.send(UrlsEnum.LINE_UP_TO_PAY);
}
private void passengerInitApi() {
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.PASSENGER_INIT_API);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status") && parse.getByPath("data") != null) {
this.jzdhDateE = (String) parse.getByPath("data.jzdhDateE");
this.jzdhHourE = (String) parse.getByPath("data.jzdhHourE");
} else {
log.info("passengerInitApi message:{}{}", ((JSONArray) parse.getByPath("messages")).get(0).toString(), parse.getByPath("validateMessages").toString());
}
}
private void confirmHB() {
HashMap formData = this.getConfirmHBParams();
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CONFIRM_HB, formData);
String body = httpResponse.body();
// log.info("confirmHB body ={}", body);
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status")) {
if (parse.getByPath("data.flag") != null) {
log.info("候补订单排队");
this.queryQueue();
} else {
log.info("confirmHB error:{}", parse.getByPath("data.msg"));
}
} else {
log.info("confirmHB message:{}{}", ((JSONArray) parse.getByPath("messages")).get(0).toString(), parse.getByPath("validateMessages").toString());
}
}
private void queryQueue() {
int maxNum = 10;
int i = 0;
while (i < maxNum) {
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.QUERY_QUEUE);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
if ((boolean) parse.getByPath("status")) {
log.info("恭喜您候补订单成功,请立即打开浏览器登录12306,访问‘候补订单’,在30分钟内完成支付!");
EmailUtil.send("12306候补成功", "恭喜您候补订单成功,请立即打开浏览器登录12306,访问‘候补订单’,在30分钟内完成支付!");
throw new J12306Exception(Constants.THREAD_STOP);
} else {
log.info(parse.getByPath("messages") + parse.getByPath("validateMessages").toString());
}
i++;
log.info("正在候补排队{}次", i);
J12306Util.sleep(1);
}
}
private String getSecretList() {
String secretList = "{secretStr}#{seatType}|";
return secretList
.replace("{secretStr}", this.secretStr)
.replace("{seatType}", this.seatType);
}
private String getSuccessSecret() {
String successSecret = "{secretStr}#{seatType}";
return successSecret
.replace("{secretStr}", this.secretStr)
.replace("{seatType}", this.seatType);
}
private HashMap getConfirmHBParams() {
HashMap formData = new HashMap<>();
formData.put("passengerInfo", this.getPassengerInfo());
formData.put("jzParam", "{0}#{1}"
.replace("{0}", this.jzdhDateE)
.replace("{1}", this.jzdhHourE)
.replace(":", "#"));
formData.put("hbTrain", "{0},{1}#"
.replace("{0}", this.trainNo)
.replace("{1}", this.seatType));
formData.put("lkParam", "");
formData.put("sessionId", "");
formData.put("sig", "");
formData.put("scene", "nc_login");
return formData;
}
private String getPassengerInfo() {
TicketCache ticketCache = TicketCache.getInstance();
UserInfoDTO userInfoDTO = (UserInfoDTO) ticketCache.get(Constants.USER_INFO_KEY);
String passengerInfo = "1#{name}#1#{idNo}#{encStr};";
return passengerInfo
.replace("{name}", userInfoDTO.getName())
.replace("{idNo}", userInfoDTO.getIdNo())
.replace("{encStr}", this.allEncStr);
}
}
================================================
FILE: src/com/kalvin/J12306/api/Captcha.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.kalvin.J12306.AI.Easy12306AI;
import com.kalvin.J12306.AI.ImageAI;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlConfig;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import com.kalvin.J12306.utils.J12306Util;
import java.io.File;
import java.util.HashMap;
/**
* 验证码
* Create by Kalvin on 2019/9/18.
*/
public class Captcha {
private Session session;
private String loginCaptchaImageName;
private String orderCaptchaImageName;
public Captcha(Session session) {
this.session = session;
}
/**
* 登录验证码
*/
public void getLoginCaptchaImg() {
UrlConfig urlConfig = UrlsEnum.CAPTCHA.getUrlConfig();
urlConfig.setUrl(urlConfig.getUrl().replace("{0}", J12306Util.genRandNumber()));
UrlsEnum.CAPTCHA.setUrlConfig(urlConfig);
HttpResponse httpResponse = session.httpClient.sendAsync(UrlsEnum.CAPTCHA);
this.loginCaptchaImageName = this.getNewLoginCaptchaImgFileName();
httpResponse.writeBody(new File(Constants.CAPTCHA_IMG_PRE_PATH + this.loginCaptchaImageName));
}
/**
* 订单页面验证码
*/
public String getOrderCaptchaImg() {
UrlConfig urlConfig = UrlsEnum.GET_PASSCODE_NEW.getUrlConfig();
urlConfig.setUrl(urlConfig.getUrl().replace("{0}", J12306Util.genRandNumber()));
UrlsEnum.GET_PASSCODE_NEW.setUrlConfig(urlConfig);
HttpResponse httpResponse = session.httpClient.sendAsync(UrlsEnum.GET_PASSCODE_NEW);
this.orderCaptchaImageName = this.getNewOrderCaptchaImgFileName();
httpResponse.writeBody(new File(Constants.CAPTCHA_IMG_PRE_PATH + this.orderCaptchaImageName));
return this.orderCaptchaImageName;
}
public boolean checkLoginCaptchaImg() {
J12306Util.sleep(2);
// 若需要使用其它打码平台AI,在AI包下新增一个类实现ImageAI接口并更换下面图片AI实例即可
ImageAI imageAI = new Easy12306AI(Constants.IMAGE_AI_URL, Constants.CAPTCHA_IMG_PRE_PATH + this.loginCaptchaImageName);
String code = imageAI.printCode();
// 转化为图片坐标点
String answerCode = J12306Util.getCaptchaPos(code);
// System.out.println("answerCode = " + answerCode);
HashMap hashMap = new HashMap<>();
hashMap.put("answer", answerCode);
hashMap.put("login_site", "E");
hashMap.put("rand", "sjrand");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CHECK_CAPTCHA, hashMap);
String body = httpResponse.body();
if (StrUtil.isBlank(body)) {
throw new J12306Exception(Constants.UPDATE_LOG_DEVICE_ERROR_MSG);
}
// System.out.println("checkLoginCaptchaImg body = " + body);
JSONObject jsonObject = JSONUtil.parseObj(body);
String resultCode = jsonObject.get("result_code").toString();
return "4".equals(resultCode);
}
private String getNewLoginCaptchaImgFileName() {
return "login" + RandomUtil.randomString(5) + ".png";
}
private String getNewOrderCaptchaImgFileName() {
return "order" + RandomUtil.randomString(5) + ".png";
}
}
================================================
FILE: src/com/kalvin/J12306/api/CheckCaptcha.java
================================================
package com.kalvin.J12306.api;
/**
* 校验验证码
* Create by Kalvin on 2019/9/19.
*/
public class CheckCaptcha {
}
================================================
FILE: src/com/kalvin/J12306/api/CheckOrderInfo.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.SubmitTicketInfoDTO;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import java.util.HashMap;
/**
* 检查订单
* Create by Kalvin on 2019/9/20.
*/
public class CheckOrderInfo {
private final static Log log = LogFactory.get();
private Session session;
private String repeatSubmitToken;
private String seatType;
private String passengerTicketStr;
private String oldPassengerStr;
private String trainDate;
private String trainNo;
private String trainNum;
private String fromStationCode;
private String toStationCode;
private String trainLocation;
private SubmitTicketInfoDTO submitTicketInfoDTO;
public CheckOrderInfo(Session session, String repeatSubmitToken, String seatType,
String passengerTicketStr, String oldPassengerStr, String trainDate,
String trainNo, String trainNum, String fromStationCode, String toStationCode,
String trainLocation, SubmitTicketInfoDTO submitTicketInfoDTO) {
this.session = session;
this.repeatSubmitToken = repeatSubmitToken;
this.seatType = seatType;
this.passengerTicketStr = passengerTicketStr;
this.oldPassengerStr = oldPassengerStr;
this.trainDate = trainDate;
this.trainNo = trainNo;
this.trainNum = trainNum;
this.fromStationCode = fromStationCode;
this.toStationCode = toStationCode;
this.trainLocation = trainLocation;
this.submitTicketInfoDTO = submitTicketInfoDTO;
}
public void send() {
HashMap formData = new HashMap<>();
formData.put("cancel_flag", 2);
formData.put("bed_level_order_num", "000000000000000000000000000000");
formData.put("passengerTicketStr", this.passengerTicketStr);
formData.put("oldPassengerStr", this.oldPassengerStr);
formData.put("tour_flag", "dc"); // 单程
formData.put("randCode", "");
formData.put("whatsSelect", 1); // 1-成人票 2-学生票
formData.put("_json_att", "");
formData.put("REPEAT_SUBMIT_TOKEN", this.repeatSubmitToken);
try {
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CHECK_ORDER_INFO, formData);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
// log.info("checkOrderInfo body = {}", body);
if (parse.getByPath("data.submitStatus") != null && (boolean) parse.getByPath("data.submitStatus")) {
log.info("车票提交通过,正在尝试排队...");
int ifShowPassCodeTime = Integer.parseInt((String) parse.getByPath("data.ifShowPassCodeTime"));
String ifShowPassCode = (String) parse.getByPath("data.ifShowPassCode");
boolean isNeedCode = "Y".equals(ifShowPassCode);
// 获取排队队列位置
new GetQueueCount(
this.session,
this.repeatSubmitToken,
this.trainDate,
this.trainNo,
this.trainNum,
this.passengerTicketStr,
this.oldPassengerStr,
this.seatType,
this.fromStationCode,
this.toStationCode,
this.trainLocation,
ifShowPassCodeTime,
isNeedCode,
this.submitTicketInfoDTO
).send();
} else {
log.info("车票提交失败,正在重试...");
}
} catch (Exception e) {
if (e instanceof J12306Exception) {
throw new J12306Exception(e.getMessage());
} else {
log.error("车票提交异常,正在重试...错误信息:{}", e.getMessage());
}
}
}
}
================================================
FILE: src/com/kalvin/J12306/api/CheckRandCodeAsync.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.http.Session;
import java.util.HashMap;
/**
* 检查订单验证码
* Create by Kalvin on 2019/9/20.
*/
public class CheckRandCodeAsync {
private static final Log log = LogFactory.get();
private Session session;
private String randCode;
private String repeatSubmitToken;
public CheckRandCodeAsync(Session session, String randCode, String repeatSubmitToken) {
this.session = session;
this.randCode = randCode;
this.repeatSubmitToken = repeatSubmitToken;
}
public boolean send() {
HashMap formData = new HashMap<>();
formData.put("randCode", this.randCode);
formData.put("rand", "randp");
formData.put("_json_att", "");
formData.put("REPEAT_SUBMIT_TOKEN", this.repeatSubmitToken);
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CHECK_RAND_CODE_ASYNC, formData);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
log.info("CheckRandCodeAsync body = {}", body);
String checked = (String) parse.getByPath("data.msg");
return checked.equals("TRUE");
}
}
================================================
FILE: src/com/kalvin/J12306/api/CheckUser.java
================================================
package com.kalvin.J12306.api;
/**
* 检查用户状态
* Create by Kalvin on 2019/9/19.
*/
public class CheckUser {
}
================================================
FILE: src/com/kalvin/J12306/api/ConfirmSingleForQueue.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.AI.Easy12306AI;
import com.kalvin.J12306.AI.ImageAI;
import com.kalvin.J12306.cache.TicketCache;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.SubmitTicketInfoDTO;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import com.kalvin.J12306.utils.J12306Util;
import java.util.HashMap;
/**
* 真正下单
* Create by Kalvin on 2019/9/20.
*/
public class ConfirmSingleForQueue {
private final static Log log = LogFactory.get();
private Session session;
private String repeatSubmitToken;
private String passengerTicketStr;
private String oldPassengerStr;
private String trainNum;
private String trainLocation;
private int ifShowPassCodeTime;
private boolean isNeedCode;
private SubmitTicketInfoDTO submitTicketInfoDTO;
public ConfirmSingleForQueue(Session session, String repeatSubmitToken, String passengerTicketStr,
String oldPassengerStr, String trainNum, String trainLocation, int ifShowPassCodeTime,
boolean isNeedCode, SubmitTicketInfoDTO submitTicketInfoDTO) {
this.session = session;
this.repeatSubmitToken = repeatSubmitToken;
this.passengerTicketStr = passengerTicketStr;
this.oldPassengerStr = oldPassengerStr;
this.trainNum = trainNum;
this.trainLocation = trainLocation;
this.ifShowPassCodeTime = ifShowPassCodeTime;
this.isNeedCode = isNeedCode;
this.submitTicketInfoDTO = submitTicketInfoDTO;
}
public void send() {
TicketCache ticketCache = TicketCache.getInstance();
String randCode = ""; // 订单验证码,默认空
try {
if (this.isNeedCode) {
log.info("需要订单验证码,正在打印验证码...");
Captcha captcha = new Captcha(this.session);
// 获取订单验证码
String orderCaptchaImg = captcha.getOrderCaptchaImg();
for (int i = 0; i < 3; i++) {
// 若需要使用其它打码平台AI,在AI包下新增一个类实现ImageAI接口并更换下面图片AI实例即可
ImageAI imageAI = new Easy12306AI(Constants.IMAGE_AI_URL, Constants.CAPTCHA_IMG_PRE_PATH + orderCaptchaImg);
String code = imageAI.printCode();
// 转化为图片坐标点
randCode = J12306Util.getCaptchaPos(code);
// 检查验证码
boolean checkCode = new CheckRandCodeAsync(this.session, randCode, this.repeatSubmitToken).send();
if (checkCode) {
log.info("验证码通过,正在提交订单...");
break;
} else {
log.info("验证码不通过,{}次重试中...", i + 1);
}
}
} else {
log.info("不需要订单验证码,直接提交");
}
log.info("开始正式下单...");
J12306Util.sleep(ifShowPassCodeTime / 1000);
HashMap formData = new HashMap<>();
formData.put("passengerTicketStr", this.passengerTicketStr);
formData.put("oldPassengerStr", this.oldPassengerStr);
formData.put("randCode", randCode);
formData.put("purpose_codes", this.submitTicketInfoDTO.getPurposeCodes());
formData.put("key_check_isChange", this.submitTicketInfoDTO.getKeyCheckIsChange());
formData.put("leftTicketStr", this.submitTicketInfoDTO.getLeftTicketStr()); // 这个参数不需要解码的。
formData.put("train_location", this.trainLocation);
formData.put("choose_seats", "");
formData.put("seatDetailType", "000"); // 选择座位,不选默认000
formData.put("whatsSelect", 1); // 1-成人票 2-学生票
formData.put("roomType", "00"); // 好像是根据一个id来判断选中的,两种 第一种是00,第二种是10,但是我在12306的页面没找到该id,目前写死是00
formData.put("dwAll", "N");
formData.put("_json_att", "");
formData.put("REPEAT_SUBMIT_TOKEN", this.repeatSubmitToken);
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CONFIRM_SINGLE_FOR_QUEUE, formData);
String body = httpResponse.body();
// log.info("confirmSingleForQueue body = {}", body);
JSON parse = JSONUtil.parse(body);
if (parse.getByPath("status") != null && (boolean) parse.getByPath("status")) {
if (parse.getByPath("status") != null && (boolean) parse.getByPath("data.submitStatus")) {
new QueryOrderWaitTime(
this.session,
this.repeatSubmitToken,
this.trainNum
).send();
} else {
ticketCache.put(this.trainNum, this.trainNum, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
log.error("正式下单失败,错误信息:{}。此列车{}加入小黑屋闭关3分钟", parse.getByPath("data.errMsg"), this.trainNum);
}
} else {
ticketCache.put(this.trainNum, this.trainNum, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
log.error("正式下单失败,错误信息:{}。此列车{}加入小黑屋闭关3分钟", parse.getByPath("messages"), this.trainNum);
}
} catch (Exception e) {
if (e instanceof J12306Exception) {
throw new J12306Exception(e.getMessage());
} else {
log.error("正式下单异常:{}", e.getMessage());
}
}
}
}
================================================
FILE: src/com/kalvin/J12306/api/GetJS.java
================================================
package com.kalvin.J12306.api;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.http.Session;
/**
* 获取js脚本
* Create by Kalvin on 2019/9/20.
*/
public class GetJS {
private Session session;
public GetJS(Session session) {
this.session = session;
}
public void send() {
this.session.httpClient.send(UrlsEnum.GET_JS);
}
}
================================================
FILE: src/com/kalvin/J12306/api/GetPassCodeNew.java
================================================
package com.kalvin.J12306.api;
/**
* 获取订单页面验证码
* Create by Kalvin on 2019/9/20.
*/
public class GetPassCodeNew {
}
================================================
FILE: src/com/kalvin/J12306/api/GetQueueCount.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.cache.TicketCache;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.SubmitTicketInfoDTO;
import com.kalvin.J12306.http.Session;
import com.kalvin.J12306.utils.J12306Util;
import java.util.Arrays;
import java.util.HashMap;
/**
* 获取队列位置
* Create by Kalvin on 2019/9/20.
*/
public class GetQueueCount {
private final static Log log = LogFactory.get();
private Session session;
private String repeatSubmitToken;
private String trainDate;
private String trainNo;
private String trainNum;
private String passengerTicketStr;
private String oldPassengerStr;
private String seatType;
private String fromStationCode;
private String toStationCode;
private String trainLocation;
private int ifShowPassCodeTime;
private boolean isNeedCode;
private SubmitTicketInfoDTO submitTicketInfoDTO;
public GetQueueCount(Session session, String repeatSubmitToken, String trainDate, String trainNo,
String trainNum, String passengerTicketStr, String oldPassengerStr, String seatType,
String fromStationCode, String toStationCode, String trainLocation, int ifShowPassCodeTime,
boolean isNeedCode, SubmitTicketInfoDTO submitTicketInfoDTO) {
this.session = session;
this.repeatSubmitToken = repeatSubmitToken;
this.trainDate = trainDate;
this.trainNo = trainNo;
this.trainNum = trainNum;
this.passengerTicketStr = passengerTicketStr;
this.oldPassengerStr = oldPassengerStr;
this.seatType = seatType;
this.fromStationCode = fromStationCode;
this.toStationCode = toStationCode;
this.trainLocation = trainLocation;
this.ifShowPassCodeTime = ifShowPassCodeTime;
this.isNeedCode = isNeedCode;
this.submitTicketInfoDTO = submitTicketInfoDTO;
}
public void send() {
HashMap formData = new HashMap<>();
formData.put("train_date", J12306Util.formatDateGMT(this.trainDate));
formData.put("train_no", this.trainNo);
formData.put("stationTrainCode", this.trainNum);
formData.put("seatType", this.seatType);
formData.put("fromStationTelecode", this.fromStationCode);
formData.put("toStationTelecode", this.toStationCode);
formData.put("leftTicket", submitTicketInfoDTO.getLeftTicketStr()); // todo 是否解码
formData.put("purpose_codes", submitTicketInfoDTO.getPurposeCodes());
formData.put("train_location", this.trainLocation);
formData.put("_json_att", "");
formData.put("REPEAT_SUBMIT_TOKEN", this.repeatSubmitToken);
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.GET_QUEUE_COUNT, formData);
String body = httpResponse.body();
// log.info("getQueueCount body = {}", body);
TicketCache ticketCache = TicketCache.getInstance();
// 余票数
int ticketCount;
JSONObject object = JSONUtil.parseObj(body);
JSONObject dataObj = (JSONObject) object.get("data");
if (object.get("status") != null && (boolean) object.get("status")) {
String ticket = dataObj.get("ticket").toString();
if (dataObj.get("count") != null) {
Integer count = Integer.valueOf(dataObj.get("count").toString());
if (!ticket.contains(",")) {
ticketCount = Integer.parseInt(ticket);
} else {
String[] ticketSplit = ticket.split(",");
ticketCount = Arrays.stream(ticketSplit).map(Integer::valueOf).reduce(0, Integer::sum);
}
log.info("排队成功,你当前排在{}位,当前余票还有{}张", count, ticketCount);
// 正式下单
new ConfirmSingleForQueue(
this.session,
this.repeatSubmitToken,
this.passengerTicketStr,
this.oldPassengerStr,
this.trainNum,
this.trainLocation,
this.ifShowPassCodeTime,
this.isNeedCode,
this.submitTicketInfoDTO
).send();
} else {
// 将此列车加入小黑屋3分钟
ticketCache.put(this.trainNum, this.trainNum, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
log.error("排队失败,错误信息:{},将此列车{}加入小黑屋3分钟", object.get("messages"), this.trainNum);
}
} else {
ticketCache.put(this.trainNum, this.trainNum, Constants.BLACK_ROOM_CACHE_EXP_TIME * 60);
log.error("排队失败,错误信息:{},将此列车{}加入小黑屋3分钟", object.get("messages"), this.trainNum);
}
}
}
================================================
FILE: src/com/kalvin/J12306/api/Login.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlConfig;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.UserInfoDTO;
import com.kalvin.J12306.exception.J12306Exception;
import com.kalvin.J12306.http.Session;
import java.net.HttpCookie;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 登录
* Create by Kalvin on 2019/9/18.
*/
public class Login {
private static final Log log = LogFactory.get();
private Session session;
private String username;
private String password;
public Login(Session session, String username, String password) {
this.session = session;
this.username = username;
this.password = password;
}
public UserInfoDTO send() {
HttpResponse initRes = this.session.httpClient.send(UrlsEnum.LOGIN_INIT);
this.session.setCookie(initRes.getCookies());
log.info("进入12306登录页,状态码:{}", initRes.getStatus());
Captcha captcha = new Captcha(this.session);
// 获取登录验证码
captcha.getLoginCaptchaImg();
// 获取deviceId
this.initLogDevice();
// 校验登录验证码
if (captcha.checkLoginCaptchaImg()) {
log.info("验证码通过,开始密码登录");
// 开始密码登录
HashMap formData = new HashMap<>();
formData.put("username", this.username);
formData.put("password", this.password);
formData.put("appid", "otn");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.LOGIN, formData);
String body = httpResponse.body();
// log.info("login body={}", body);
if (StrUtil.isBlank(body)) {
throw new J12306Exception(Constants.UPDATE_LOG_DEVICE_ERROR_MSG);
}
JSONObject jsonObject = JSONUtil.parseObj(body);
Integer resultCode = (Integer) jsonObject.get("result_code");
if (resultCode == 0) { // 登录成功,获取tk
log.info("登录成功");
this.userLogin();
this.passport();
new GetJS(this.session).send();
boolean uamtk = this.postUamTK((String) jsonObject.get("uamtk"));
if (uamtk) {
return this.getUserInfo();
}
} else {
log.info("登录失败,请检查12306账号或密码是否正确。");
throw new J12306Exception("登录失败,请检查12306账号或密码是否正确。");
}
} else {
log.info("登录失败,验证码不通过,请重试...");
}
return null;
}
private void initLogDevice() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36";
Session ldSession = new Session();
UrlConfig urlConfig = UrlsEnum.LOG_DEVICE.getUrlConfig();
/*urlConfig.setUrl(urlConfig.getUrl()
.replace("{0}", userAgent)
.replace("{1}", String.valueOf(System.currentTimeMillis())));*/
urlConfig.setUrl(this.fillLogDeviceUrlParams(urlConfig.getUrl()));
UrlsEnum.LOG_DEVICE.setUrlConfig(urlConfig);
HttpResponse httpResponse = ldSession.httpClient.send(UrlsEnum.LOG_DEVICE);
String body = httpResponse.body();
// log.info("deviceInfo body = {}", body);
String startIdxStr = "{";
String endIdxStr = "}";
body = body.substring(body.indexOf(startIdxStr), body.indexOf(endIdxStr) + 1);
JSONObject jsonObject = JSONUtil.parseObj(body);
// 设置到session的cookie中
String railExpiration = jsonObject.get("exp").toString();
String railDeviceId = jsonObject.get("dfp").toString();
// log.info("railDeviceId={}", railDeviceId);
this.session.setCookie("RAIL_EXPIRATION=" + railExpiration);
this.session.setCookie("RAIL_DEVICEID=" + railDeviceId);
// this.session.httpClient.setHeader(new HashMap() {{put("User-Agent", userAgent);}});
}
private String fillLogDeviceUrlParams(String url) {
final StringBuilder sb = new StringBuilder();
/**
* 如果RAIL_DEVICEID失效了,以下参数需要更新(顺序一定要对,不然找不到logdevice)
* 更新步骤:
* 1.浏览器访问:https://kyfw.12306.cn/otn/login/init
* 2.按f12进入调试模式并点击Network选项
* 3.清除浏览器缓存的有关12306.cn和kyfw.12306.cn的Cookie(谷歌浏览器点击浏览器地址栏的小锁)
* 4.按f5重新刷新(只有第1次刷新才有出现,所以不要刷新2次)
* 5.在Network选项下找到logdevice请求,点击它,在Headers选项下拉到最下面就可以找到如下几个参数,复制替换它即可
*/
final String algID = "PyvGQGRrn7";
final String hashCode = "zgwkwwmfXov0h0OiTVGEm5O3x8wCUon2_s6JFyCUmFE";
final String EOQP = "8f58b1186770646318a429cb33977d8c";
final String jp76 = "52d67b2a5aa5e031084733d5006cc664";
final String q5aJ = "-8";
final String Oaew = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36";
final String E3gR = "662690cce73ebaa8bae40c90cd3a15d5";
new LinkedHashMap() {{
put("algID", algID);
put("hashCode", hashCode);
put("FMQw", "0");
put("q4f3", "zh-CN");
put("VPIf", "1");
put("custID", "133");
put("VEek", "unknown");
put("dzuS", "0");
put("yD16", "0");
put("EOQP", EOQP);
put("jp76", jp76);
put("hAqN", "Win32");
put("platform", "WEB");
put("ks0Q", "d22ca0b81584fbea62237b14bd04c866");
put("TeRS", "1040x1920");
put("tOHY", "24xx1080x1920");
put("Fvje", "i1l1o1s1");
put("q5aJ", q5aJ);
put("wNLf", "99115dfb07133750ba677d055874de87");
put("0aew", Oaew);
put("E3gR", E3gR);
put("timestamp", String.valueOf(System.currentTimeMillis()));
}}.forEach((k, v) -> {
if (sb.length() == 0) {
sb.append("?");
} else {
sb.append("&");
}
sb.append(k).append("=").append(v);
});
return url + sb.toString();
}
private void userLogin() {
this.session.httpClient.send(UrlsEnum.USER_LOGIN);
}
private void passport() {
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.PASSPORT);
List cookies = httpResponse.getCookies();
// 设置cookies
this.session.setCookie(cookies);
}
/**
* 获取登录token
* @param uamTK uamTK令牌
*/
private boolean postUamTK(String uamTK) {
/*if (StrUtil.isNotBlank(uamTK)) {
HashMap headers = new HashMap() {{
// put("uamtk", uamTK);
}};
this.session.httpClient.setHeader(headers);
}*/
HashMap formData = new HashMap() {{
put("appid", "otn");
}};
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.UAM_TK, formData);
log.info("postUamTK http status = {}", httpResponse.getStatus());
String body = httpResponse.body();
// log.info("postUamTK body = {}", body);
try {
JSONObject jsonObject = JSONUtil.parseObj(body);
Integer resultCode = (Integer) jsonObject.get("result_code");
if (resultCode == 0) {
this.session.token = (String) jsonObject.get("newapptk");
return this.postUamAuthClient();
}
} catch (JSONException je) {
return false;
}
return false;
}
/**
* 获取权限
*/
private boolean postUamAuthClient() {
HashMap formData = new HashMap<>();
formData.put("tk", this.session.token);
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.UAM_AUTH_CLIENT, formData);
List cookies = httpResponse.getCookies();
this.session.setCookie(cookies);
String body = httpResponse.body();
try {
JSONObject jsonObject = JSONUtil.parseObj(body);
Integer resultCode = (Integer) jsonObject.get("result_code");
return resultCode == 0;
} catch (JSONException e) {
return false;
}
}
/**
* 获取用户信息
*/
private UserInfoDTO getUserInfo() {
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.GET_USER_INFO);
String body = httpResponse.body();
// log.info("queryPassengerInfo body={}", body);
JSON parse = JSONUtil.parse(body);
JSONObject object = (JSONObject) parse.getByPath("data.userDTO.loginUserDTO");
UserInfoDTO userInfo = new UserInfoDTO();
userInfo.setIdNo(object.get("id_no").toString());
userInfo.setName(object.get("name").toString());
userInfo.setUsername(object.get("user_name").toString());
userInfo.setIdTypeCode(object.get("id_type_code").toString());
userInfo.setIdTypeName(object.get("id_type_name").toString());
userInfo.setEmail(parse.getByPath("data.userDTO.email").toString());
return userInfo;
}
}
================================================
FILE: src/com/kalvin/J12306/api/MyOrder.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.http.Session;
import java.util.HashMap;
/**
* 我的订单
* Create by Kalvin on 2019/9/24.
*/
public class MyOrder {
private final static Log log = LogFactory.get();
private Session session;
public MyOrder(Session session) {
this.session = session;
}
public void init() {
HashMap formData = new HashMap<>();
formData.put("_json_att", "");
this.session.httpClient.send(UrlsEnum.INIT_NO_COMPLETE, formData);
}
/**
* 查询未完成的订单
* @return httpResponse
*/
public HttpResponse queryMyNoComplete() {
HashMap formData = new HashMap<>();
formData.put("_json_att", "");
try {
return this.session.httpClient.send(UrlsEnum.QUERY_MY_ORDER_NO_COMPLETE, formData);
} catch (Exception e) {
return null;
}
}
/**
* 检查订单状态
* @return State
*/
public State check() {
HttpResponse httpResponse = this.queryMyNoComplete();
if (httpResponse == null) {
return State.fail();
}
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
if (parse.getByPath("data") != null && parse.getByPath("data.orderDBList") != null) {
JSONArray objects = JSONUtil.parseArray(parse.getByPath("data.orderDBList"));
JSON parseODB = JSONUtil.parse(objects.get(0));
String sequenceNo = (String) parseODB.getByPath("sequence_no");
if (parseODB.getByPath("tickets.ticket_status_code").equals("i")) { // 待支付状态
return State.noPay(sequenceNo);
} else {
return State.queue(sequenceNo);
}
}
return State.fail();
}
/**
* 取消订单
* @param sequenceNo 订单ID
*/
public void cancelNoComplete(String sequenceNo) {
HashMap formData = new HashMap<>();
formData.put("sequence_no", sequenceNo);
formData.put("cancel_flag", "cancel_order");
formData.put("_json_att", "");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.CANCEL_NO_COMPLETE_MY_ORDER, formData);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
if (parse.getByPath("data") != null && parse.getByPath("data.existError").equals("N")) {
log.info("订单【{}】取消成功", sequenceNo);
} else {
log.info("订单【{}】取消失败", sequenceNo);
}
}
/**
* 订单状态
*/
public static class State {
/**
* 待支付状态
*/
public final static String NO_PAY_CODE = "NO_PAY";
/**
* 排队状态
*/
public final static String QUEUE_CODE = "QUEUE";
/**
* 失败状态
*/
public final static String FAIL_CODE = "FAIL";
private String code;
private String sequenceNo;
private State(String code, String sequenceNo) {
this.code = code;
this.sequenceNo = sequenceNo;
}
private static State noPay(String sequenceNo) {
return new State(State.NO_PAY_CODE, sequenceNo);
}
private static State queue(String sequenceNo) {
return new State(State.QUEUE_CODE, sequenceNo);
}
private static State fail() {
return new State(State.FAIL_CODE, "");
}
public String getCode() {
return code;
}
public String getSequenceNo() {
return sequenceNo;
}
}
}
================================================
FILE: src/com/kalvin/J12306/api/PassengerDTOS.java
================================================
package com.kalvin.J12306.api;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.kalvin.J12306.cache.TicketCache;
import com.kalvin.J12306.config.Constants;
import com.kalvin.J12306.config.UrlsEnum;
import com.kalvin.J12306.dto.UserInfoDTO;
import com.kalvin.J12306.http.Session;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
/**
* 乘客信息
* Create by Kalvin on 2019/9/20.
*/
public class PassengerDTOS {
private Session session;
private String repeatSubmitToken;
private String seatType;
public PassengerDTOS(Session session) {
this.session = session;
}
public PassengerDTOS(Session session, String repeatSubmitToken, String seatType) {
this.session = session;
this.repeatSubmitToken = repeatSubmitToken;
this.seatType = seatType;
}
/**
* 获取乘客购票信息
* @return passengerTicketStr
*/
public String getPassengerTicketStr() {
String passengerTicketStr = "{seatType},0,1,{name},1,{passengerIdCard},,N,{allEncStr}";
// 从缓存中获取用户信息
TicketCache ticketCache = TicketCache.getInstance();
UserInfoDTO userInfo = (UserInfoDTO) ticketCache.get(Constants.USER_INFO_KEY);
String idNo = userInfo.getIdNo();
String name = userInfo.getName();
HashMap formData = new HashMap<>();
formData.put("REPEAT_SUBMIT_TOKEN", repeatSubmitToken);
formData.put("_json_att", "");
HttpResponse httpResponse = this.session.httpClient.send(UrlsEnum.GET_PASSENGERDTOS, formData);
String body = httpResponse.body();
JSON parse = JSONUtil.parse(body);
JSONArray jsonArray = JSONUtil.parseArray(parse.getByPath("data.normal_passengers"));
List