├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── db ├── init.sql └── ticket.db ├── docker ├── .env └── docker-compose.yml ├── pom.xml ├── python ├── 12306.image.model.h5 ├── LICENSE ├── README.md ├── __pycache__ │ ├── mlearn_for_image.cpython-36.pyc │ └── pretreatment.cpython-36.pyc ├── baidu.py ├── category_images.py ├── main.py ├── mlearn.py ├── mlearn_for_image.py ├── model.h5 ├── pretreatment.py ├── requirements.txt ├── run.sh ├── texts.txt └── verify_image_hash.py ├── src ├── main │ ├── java │ │ └── com │ │ │ └── qianxunclub │ │ │ └── ticket │ │ │ ├── TicketApplication.java │ │ │ ├── config │ │ │ ├── ApiConfig.java │ │ │ ├── Config.java │ │ │ ├── CookiesConfig.java │ │ │ ├── NoticeConfig.java │ │ │ ├── SwaggerConfig.java │ │ │ └── UserConfig.java │ │ │ ├── constant │ │ │ ├── SeatLevelEnum.java │ │ │ └── StatusEnum.java │ │ │ ├── controller │ │ │ └── IndexController.java │ │ │ ├── filter │ │ │ └── LogFilter.java │ │ │ ├── ip │ │ │ ├── AddIpThread.java │ │ │ ├── CheckIpThread.java │ │ │ └── PingIpThread.java │ │ │ ├── model │ │ │ ├── BuyTicketInfoModel.java │ │ │ ├── LogdeviceModel.java │ │ │ ├── LoggerMessage.java │ │ │ ├── NoticeModel.java │ │ │ ├── PassengerModel.java │ │ │ ├── Result.java │ │ │ ├── SeatModel.java │ │ │ ├── TicketModel.java │ │ │ ├── UserModel.java │ │ │ ├── UserTicketStore.java │ │ │ ├── request │ │ │ │ ├── PassengerRequest.java │ │ │ │ └── TicketRequest.java │ │ │ └── response │ │ │ │ ├── PassengerResponse.java │ │ │ │ └── TicketInfoResponse.java │ │ │ ├── repository │ │ │ ├── dao │ │ │ │ ├── IpsDao.java │ │ │ │ ├── ProxyIpDao.java │ │ │ │ └── TicketDao.java │ │ │ ├── entity │ │ │ │ ├── Ips.java │ │ │ │ ├── ProxyIp.java │ │ │ │ └── Ticket.java │ │ │ └── mapper │ │ │ │ ├── IpsMapper.java │ │ │ │ ├── ProxyIpMapper.java │ │ │ │ └── TicketMapper.java │ │ │ ├── service │ │ │ ├── ApiRequestService.java │ │ │ ├── IpsService.java │ │ │ ├── NoticeService.java │ │ │ ├── ProxyIpService.java │ │ │ ├── TicketService.java │ │ │ └── WeChatNotice.java │ │ │ ├── ticket │ │ │ ├── BuyTicket.java │ │ │ ├── DoHandle.java │ │ │ ├── Login.java │ │ │ ├── PassengerService.java │ │ │ ├── QueryTicket.java │ │ │ ├── Station.java │ │ │ └── Task.java │ │ │ └── util │ │ │ ├── ApplicationContextHelper.java │ │ │ ├── CaptchaImageForPy.java │ │ │ ├── CommonUtil.java │ │ │ ├── CookieUtil.java │ │ │ ├── HttpUtil.java │ │ │ ├── LogUtil.java │ │ │ ├── LogdeviceUtil.java │ │ │ └── StaticUtil.java │ └── resources │ │ ├── application-cookie.yml │ │ ├── application-sms.yml │ │ ├── application-user.yml │ │ ├── application.yml │ │ └── logback.xml └── test │ └── java │ └── com │ └── qianxunclub │ └── ticket │ ├── GetLogdeviceTest.java │ ├── GetPassengerTest.java │ ├── LoginTest.java │ ├── NoticeTest.java │ ├── TimeTest.java │ ├── YzmTest.java │ └── constant │ └── Constant.java └── temp └── index.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /build/ 27 | 28 | ### VS Code ### 29 | .vscode/ 30 | python/venv/ 31 | temp 32 | log 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | RUN yum install -y git vim wget curl java-1.8.0-openjdk.x86_64 maven python36-setuptools python36-pip 3 | RUN git clone https://github.com/qianxunclub/ticket.git 4 | RUN cd ticket && mvn clean package 5 | RUN cp ticket/target/ticket-0.0.1-SNAPSHOT.jar app.jar 6 | RUN cp -R ticket/python python 7 | RUN cd python && python3 -m venv venv 8 | RUN cd python && source venv/bin/activate && pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 9 | VOLUME /tmp 10 | ENTRYPOINT ["java","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://travis-ci.com/qianxunclub/ticket.svg?branch=master) 2 | # 12306抢票 3 | 这是一个牛逼的全自动购票系统,该系统为 `Spring Boot` 编写的后端服务,就不需要天天盯着 `12306` 官网查询余票了,用起来很爽,保证不收集任何敏感信息,真的。 4 | 5 | > 马云 Gitee:https://gitee.com/qianxunclub/ticket 6 | > GitHub:https://github.com/qianxunclub/ticket 7 | # 功能介绍 8 | - 自动识别验证码 9 | - 多账号同步购票 10 | - 动态添加抢票账号 11 | - 定时刷新监控余票 12 | - 设置多座位优先抢票 13 | - 自动下单 14 | - 下单成功短息通知 15 | - 下单成功微信通知 16 | - 接口文档:http://localhost:9998/swagger-ui.html 17 | - 配置代理 18 | - 支持 docker 部署 19 | - 支持Mac、Linux、Windows 20 | 21 | # 说明 22 | - [配置文件说明](https://github.com/qianxunclub/ticket/wiki/%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) 23 | - [开发环境配置](https://github.com/qianxunclub/ticket/wiki/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) 24 | - [docker 使用](https://github.com/qianxunclub/ticket/wiki/docker-%E7%8E%AF%E5%A2%83) 25 | 26 | # 在线接口文档 27 | 项目集成了 `swagger` ,可以在线直接调用接口使用。 28 | 项目启动成功后,输入地址: 29 | ``` 30 | http://localhost:9998/swagger-ui.html 31 | ``` 32 | 33 | # 开发者讨论 34 | 有什么好建议或者想法的,可以添加QQ群一起讨论:852214454 35 | 36 | # 欢迎提交 PR 升级 37 | -------------------------------------------------------------------------------- /db/init.sql: -------------------------------------------------------------------------------- 1 | -- auto-generated definition 2 | create table ticket 3 | ( 4 | id INTEGER 5 | constraint ticket_pk primary key autoincrement, 6 | date varchar not null, 7 | "from" varchar not null, 8 | "to" varchar not null, 9 | train_number varchar not null, 10 | passenger_code varchar not null, 11 | mobile varchar not null, 12 | real_name varchar, 13 | seat varchar not null, 14 | username varchar not null, 15 | password varchar not null, 16 | server_sckey varchar not null 17 | ); 18 | 19 | 20 | create table ips 21 | ( 22 | id INTEGER 23 | constraint ips_pk primary key autoincrement, 24 | ip varchar not null 25 | ); 26 | 27 | 28 | create table proxy_ip 29 | ( 30 | id INTEGER 31 | constraint ips_pk primary key autoincrement, 32 | ip varchar not null, 33 | port integer not null 34 | ); 35 | -------------------------------------------------------------------------------- /db/ticket.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/db/ticket.db -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | # 日志级别 2 | logging_level_com.qianxunclub= 3 | # 代理配置 4 | config_enableProxy= 5 | config_proxyHost= 6 | config_proxyPort= 7 | # 随机查询时间,取值:min-max,范围随机 8 | config_queryTicketSleepTime_min= 9 | config_queryTicketSleepTime_max= 10 | # 短信配置 11 | notice_accessKeyId= 12 | notice_accessSecret= 13 | notice_templateCode= 14 | notice_signName= 15 | 16 | # 乘客配置 17 | 18 | 19 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | ticket: 4 | image: ${TICKET_IMAGE:-960339491/ticket:latest} 5 | ports: 6 | - "9998:9998" 7 | - "5005:5005" 8 | volumes: 9 | - ../db/ticket.db:/db/ticket.db 10 | - ../temp:/temp 11 | - ../src/main/resources/application.yml:/application.yml 12 | - ../src/main/resources/application-cookie.yml:/application-cookie.yml 13 | - ../src/main/resources/application-sms.yml:/application-sms.yml 14 | - ../src/main/resources/application-user.yml:/application-user.yml 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.5.RELEASE 9 | 10 | 11 | com.qianxunclub 12 | ticket 13 | 0.0.1-SNAPSHOT 14 | ticket 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | 39 | org.apache.httpcomponents 40 | httpclient 41 | 4.5.13 42 | 43 | 44 | 45 | org.projectlombok 46 | lombok 47 | 1.18.8 48 | provided 49 | 50 | 51 | 52 | com.google.code.gson 53 | gson 54 | 2.8.9 55 | 56 | 57 | 58 | commons-lang 59 | commons-lang 60 | 2.6 61 | 62 | 63 | 64 | org.apache.httpcomponents 65 | httpmime 66 | 4.5.3 67 | 68 | 69 | 70 | com.aliyun 71 | aliyun-java-sdk-core 72 | 4.0.3 73 | 74 | 75 | 76 | org.xerial 77 | sqlite-jdbc 78 | 3.41.2.2 79 | 80 | 81 | 82 | com.baomidou 83 | mybatis-plus-boot-starter 84 | 3.1.2 85 | 86 | 87 | 88 | io.springfox 89 | springfox-swagger2 90 | 2.9.2 91 | 92 | 93 | 94 | io.springfox 95 | springfox-swagger-ui 96 | 2.9.2 97 | 98 | 99 | 100 | com.google.guava 101 | guava 102 | [30.0-jre,) 103 | 104 | 105 | 106 | net.sourceforge.htmlunit 107 | htmlunit 108 | 2.37.0 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | nexus-aliyun 118 | Nexus aliyun 119 | http://maven.aliyun.com/nexus/content/groups/public 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.springframework.boot 127 | spring-boot-maven-plugin 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-javadoc-plugin 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-surefire-plugin 136 | 137 | true 138 | 139 | 140 | 141 | com.spotify 142 | docker-maven-plugin 143 | 1.2.1 144 | 145 | 960339491/ticket:latest 146 | ${project.basedir} 147 | 148 | 149 | ${project.build.directory} 150 | ${project.build.finalName}.jar 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /python/12306.image.model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/python/12306.image.model.h5 -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2015 zhaipro 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | 203 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # easy12306 2 | 3 | 两个必要的数据集: 4 | 5 | 1. 文字识别,model.h5 6 | 2. 图片识别,12306.image.model.h5 7 | 8 | 识别器数据的下载地址: 9 | 10 | 1. ~[百度网盘](https://pan.baidu.com/s/1OsBIBM4rl8EnpZt7VYiD9g)~ 11 | 1. https://drive.google.com/drive/folders/1GDCQyaHr36c7y1H-19pOKjc_EdAI1wn0 12 | 13 | `python3 main.py ` 14 | 15 | 我把设计思路写在维基中了:https://github.com/zhaipro/easy12306/wiki 16 | 17 | ### 如何? 18 | 19 | ![2](https://user-images.githubusercontent.com/8620842/51320752-d6f2cc00-1a9b-11e9-9d2d-7d1e25ddadc5.jpg) 20 | 21 | ``` 22 | ~$ python3 main.py 2.jpg 2> /dev/null 23 | 电子秤 24 | 风铃 # 要找的是以上两样东西 25 | 0 0 电子秤 # 第一行第一列就是电子秤 26 | 0 1 绿豆 27 | 0 2 蒸笼 28 | 0 3 蒸笼 29 | 1 0 风铃 30 | 1 1 电子秤 31 | 1 2 网球拍 32 | 1 3 网球拍 33 | ``` 34 | 35 | 识别前所未见的图片 36 | 37 | ![8](https://user-images.githubusercontent.com/8620842/51799645-a01c7300-225e-11e9-8214-296773112484.jpg) 38 | 39 | 具体的编号:[texts.txt](./texts.txt) 40 | 41 | ``` 42 | ~$ python3 mlearn_for_image.py 8.jpg 43 | [0.8991613] # 可信度 44 | [0] # 0 表示的就是打字机 45 | ``` 46 | 47 | ### 在线体验 48 | 49 | 识别验证码,暂不识别多标签。 50 | 51 | http://shell.teachx.cn:12306/ 52 | 53 | ![a](https://user-images.githubusercontent.com/8620842/51885312-e809da00-23c5-11e9-93a3-78d5e8b4ac18.png) 54 | 55 | 识别单个图片,可任意尺寸(总之由cv2简单的将其转为指定尺寸)。 56 | 57 | http://shell.teachx.cn:5000/ 58 | 59 | ![a](https://user-images.githubusercontent.com/8620842/51879603-21831b00-23af-11e9-8d16-9ae64866ca4c.jpg) 60 | -------------------------------------------------------------------------------- /python/__pycache__/mlearn_for_image.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/python/__pycache__/mlearn_for_image.cpython-36.pyc -------------------------------------------------------------------------------- /python/__pycache__/pretreatment.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/python/__pycache__/pretreatment.cpython-36.pyc -------------------------------------------------------------------------------- /python/baidu.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import base64 3 | 4 | import cv2 5 | import numpy as np 6 | import requests 7 | 8 | 9 | AK = 'unknown' 10 | SK = 'unknown' 11 | 12 | 13 | def get_token(ak, sk): 14 | # http://ai.baidu.com/docs#/Auth/top 15 | url = 'https://aip.baidubce.com/oauth/2.0/token' 16 | params = { 17 | 'grant_type': 'client_credentials', 18 | 'client_id': ak, 19 | 'client_secret': sk, 20 | } 21 | r = requests.post(url, params=params) 22 | return r.json()['access_token'] 23 | 24 | 25 | TOKEN = get_token(AK, SK) 26 | 27 | 28 | def ocr(img): 29 | # 文件名 30 | if isinstance(img, str): 31 | img = open(img, 'rb').read() 32 | # 或cv2图像 33 | elif isinstance(img, np.ndarray): 34 | _, img = cv2.imencode('.jpg', img) 35 | # https://ai.baidu.com/docs#/OCR-API/e1bd77f3 36 | url = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic' 37 | params = {'access_token': TOKEN} 38 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 39 | img = base64.b64encode(img) 40 | data = {'image': img} 41 | r = requests.post(url, data=data, params=params, headers=headers) 42 | # 该项目只需要一个词 43 | return r.json()['words_result'][0]['words'] 44 | 45 | 46 | def main(): 47 | import sys 48 | from pretreatment import load_data 49 | texts = load_data() 50 | fp = open('texts.log', 'w', encoding='utf-8') 51 | for idx, text in enumerate(texts): 52 | try: 53 | text = ocr(text) 54 | print(idx, text, file=fp) 55 | print(idx, text) 56 | except Exception as e: 57 | print(e, file=sys.stderr) 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /python/category_images.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import mlearn 4 | from pretreatment import load_data 5 | 6 | 7 | def learn(): 8 | texts, imgs = load_data() 9 | labels = mlearn.predict(texts) 10 | labels = labels.argmax(axis=1) 11 | imgs.dtype = np.uint64 12 | imgs.shape = (-1, 8) 13 | unique_imgs = np.unique(imgs) 14 | print(unique_imgs.shape) 15 | imgs_labels = [] 16 | for img in unique_imgs: 17 | idxs = np.where(imgs == img)[0] 18 | counts = np.bincount(labels[idxs], minlength=80) 19 | imgs_labels.append(counts) 20 | np.savez('images.npz', images=unique_imgs, labels=imgs_labels) 21 | 22 | 23 | if __name__ == '__main__': 24 | learn() 25 | -------------------------------------------------------------------------------- /python/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import sys 3 | 4 | import cv2 5 | import numpy as np 6 | from keras import models 7 | 8 | import pretreatment 9 | from mlearn_for_image import preprocess_input 10 | 11 | 12 | def get_text(img, offset=0): 13 | text = pretreatment.get_text(img, offset) 14 | text = cv2.cvtColor(text, cv2.COLOR_BGR2GRAY) 15 | text = text / 255.0 16 | h, w = text.shape 17 | text.shape = (1, h, w, 1) 18 | return text 19 | 20 | 21 | def main(fn): 22 | # 读取并预处理验证码 23 | img = cv2.imread(fn) 24 | text = get_text(img) 25 | imgs = np.array(list(pretreatment._get_imgs(img))) 26 | imgs = preprocess_input(imgs) 27 | 28 | # 识别文字 29 | model = models.load_model('model.h5') 30 | label = model.predict(text) 31 | label = label.argmax() 32 | fp = open('texts.txt', encoding='utf-8') 33 | texts = [text.rstrip('\n') for text in fp] 34 | text = texts[label] 35 | print(text) 36 | # 获取下一个词 37 | # 根据第一个词的长度来定位第二个词的位置 38 | if len(text) == 1: 39 | offset = 27 40 | elif len(text) == 2: 41 | offset = 47 42 | else: 43 | offset = 60 44 | text = get_text(img, offset=offset) 45 | if text.mean() < 0.95: 46 | label = model.predict(text) 47 | label = label.argmax() 48 | text = texts[label] 49 | print(text) 50 | 51 | # 加载图片分类器 52 | model = models.load_model('12306.image.model.h5') 53 | labels = model.predict(imgs) 54 | labels = labels.argmax(axis=1) 55 | for pos, label in enumerate(labels): 56 | print(pos // 4, pos % 4, texts[label]) 57 | 58 | 59 | if __name__ == '__main__': 60 | main(sys.argv[1]) 61 | -------------------------------------------------------------------------------- /python/mlearn.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pathlib 3 | 4 | import cv2 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from keras import backend as K 8 | from keras import layers 9 | from keras import models 10 | from keras.callbacks import ReduceLROnPlateau 11 | from keras.utils import to_categorical 12 | 13 | 14 | def load_data(fn='texts.npz', to=False): 15 | data = np.load(fn) 16 | texts, labels = data['texts'], data['labels'] 17 | texts = texts / 255.0 18 | _, h, w = texts.shape 19 | texts.shape = (-1, h, w, 1) 20 | if to: 21 | labels = to_categorical(labels) 22 | n = int(texts.shape[0] * 0.9) # 90%用于训练,10%用于测试 23 | return (texts[:n], labels[:n]), (texts[n:], labels[n:]) 24 | 25 | 26 | def savefig(history, fn='loss.jpg', start=2): 27 | # 忽略起点 28 | loss = history.history['loss'][start - 1:] 29 | val_loss = history.history['val_loss'][start - 1:] 30 | epochs = list(range(start, len(loss) + start)) 31 | plt.plot(epochs, loss, 'bo', label='Training loss') 32 | plt.plot(epochs, val_loss, 'b', label='Validation loss') 33 | plt.title('Training and validation loss') 34 | plt.xlabel('Epochs') 35 | plt.ylabel('Loss') 36 | plt.legend() 37 | plt.savefig(fn) 38 | 39 | 40 | def main(): 41 | (train_x, train_y), (test_x, test_y) = load_data() 42 | model = models.Sequential([ 43 | layers.Conv2D(64, (3, 3), padding='same', activation='relu', input_shape=(None, None, 1)), 44 | layers.MaxPooling2D(), # 19 -> 9 45 | layers.Conv2D(64, (3, 3), padding='same', activation='relu'), 46 | layers.MaxPooling2D(), # 9 -> 4 47 | layers.Conv2D(64, (3, 3), padding='same', activation='relu'), 48 | layers.MaxPooling2D(), # 4 -> 2 49 | layers.GlobalAveragePooling2D(), 50 | layers.Dropout(0.25), 51 | layers.Dense(64, activation='relu'), 52 | layers.Dense(80, activation='softmax'), 53 | ]) 54 | model.summary() 55 | model.compile(optimizer='rmsprop', 56 | loss='sparse_categorical_crossentropy', 57 | metrics=['accuracy']) 58 | # 当标准评估停止提升时,降低学习速率 59 | reduce_lr = ReduceLROnPlateau(verbose=1) 60 | history = model.fit(train_x, train_y, epochs=100, 61 | validation_data=(test_x, test_y), 62 | callbacks=[reduce_lr]) 63 | savefig(history, start=10) 64 | model.save('model.v1.0.h5', include_optimizer=False) 65 | 66 | 67 | def load_data_v2(): 68 | (train_x, train_y), (test_x, test_y) = load_data(to=True) 69 | # 这里是统计学数据 70 | (train_v2_x, train_v2_y), (test_v2_x, test_v2_y) = load_data('texts.v2.npz') 71 | # 合并 72 | train_x = np.concatenate((train_x, train_v2_x)) 73 | train_y = np.concatenate((train_y, train_v2_y)) 74 | test_x = np.concatenate((test_x, test_v2_x)) 75 | test_y = np.concatenate((test_y, test_v2_y)) 76 | return (train_x, train_y), (test_x, test_y) 77 | 78 | 79 | def acc(y_true, y_pred): 80 | return K.cast(K.equal(K.argmax(y_true + y_pred, axis=-1), 81 | K.argmax(y_pred, axis=-1)), 82 | K.floatx()) 83 | 84 | 85 | def main_v19(): # 1.9 86 | (train_x, train_y), (test_x, test_y) = load_data_v2() 87 | model = models.load_model('model.v1.0.h5') 88 | model.compile(optimizer='RMSprop', 89 | loss='categorical_hinge', 90 | metrics=[acc]) 91 | reduce_lr = ReduceLROnPlateau(verbose=1) 92 | history = model.fit(train_x, train_y, epochs=100, 93 | validation_data=(test_x, test_y), 94 | callbacks=[reduce_lr]) 95 | savefig(history) 96 | model.save('model.v1.9.h5', include_optimizer=False) 97 | 98 | 99 | def main_v20(): 100 | (train_x, train_y), (test_x, test_y) = load_data() 101 | model = models.Sequential([ 102 | layers.Conv2D(64, (3, 3), activation='relu', padding='same', input_shape=(None, None, 1)), 103 | layers.MaxPooling2D(), # 19 -> 9 104 | layers.Conv2D(64, (3, 3), activation='relu', padding='same'), 105 | layers.MaxPooling2D(), # 9 -> 4 106 | layers.Conv2D(64, (3, 3), activation='relu', padding='same'), 107 | layers.MaxPooling2D(), # 4 -> 2 108 | layers.Conv2D(64, (3, 3), activation='relu', padding='same'), 109 | layers.Conv2D(64, (3, 3), activation='relu', padding='same'), 110 | layers.MaxPooling2D(), # 2 -> 1 111 | layers.GlobalAveragePooling2D(), 112 | layers.Dropout(0.25), 113 | layers.Dense(64, activation='relu'), 114 | layers.Dense(80, activation='softmax'), 115 | ]) 116 | model.summary() 117 | model.compile(optimizer='rmsprop', 118 | loss='sparse_categorical_crossentropy', 119 | metrics=['accuracy']) 120 | model.fit(train_x, train_y, epochs=10, 121 | validation_data=(test_x, test_y)) 122 | (train_x, train_y), (test_x, test_y) = load_data_v2() 123 | model.compile(optimizer='rmsprop', 124 | loss='categorical_hinge', 125 | metrics=[acc]) 126 | reduce_lr = ReduceLROnPlateau(verbose=1) 127 | history = model.fit(train_x, train_y, epochs=100, 128 | validation_data=(test_x, test_y), 129 | callbacks=[reduce_lr]) 130 | savefig(history) 131 | # 保存,并扔掉优化器 132 | model.save('model.v2.0.h5', include_optimizer=False) 133 | 134 | 135 | def predict(texts): 136 | model = models.load_model('model.h5') 137 | texts = texts / 255.0 138 | _, h, w = texts.shape 139 | texts.shape = (-1, h, w, 1) 140 | labels = model.predict(texts) 141 | return labels 142 | 143 | 144 | def _predict(): 145 | texts = np.load('data.npy') 146 | labels = predict(texts) 147 | np.save('labels.npy', labels) 148 | 149 | 150 | def show(): 151 | texts = np.load('data.npy') 152 | labels = np.load('labels.npy') 153 | labels = labels.argmax(axis=1) 154 | pathlib.Path('classify').mkdir(exist_ok=True) 155 | for idx, (text, label) in enumerate(zip(texts, labels)): 156 | # 使用聚类结果命名 157 | fn = f'classify/{label}.{idx}.jpg' 158 | cv2.imwrite(fn, text) 159 | 160 | 161 | if __name__ == '__main__': 162 | main() 163 | # main_v2() 164 | _predict() 165 | show() 166 | -------------------------------------------------------------------------------- /python/mlearn_for_image.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import sys 3 | 4 | import cv2 5 | import numpy as np 6 | from keras import models 7 | from keras import layers 8 | from keras import optimizers 9 | from keras.applications import VGG16 10 | from keras.callbacks import ReduceLROnPlateau 11 | from keras.preprocessing.image import ImageDataGenerator 12 | 13 | 14 | def preprocess_input(x): 15 | x = x.astype('float32') 16 | # 我是用cv2来读取的图片,其已经是BGR格式了 17 | mean = [103.939, 116.779, 123.68] 18 | x -= mean 19 | return x 20 | 21 | 22 | def load_data(): 23 | # 这是统计学专家提供的训练集 24 | data = np.load('captcha.npz') 25 | train_x, train_y = data['images'], data['labels'] 26 | train_x = preprocess_input(train_x) 27 | # 由于是统计得来的信息,所以在此给定可信度 28 | sample_weight = train_y.max(axis=1) / np.sqrt(train_y.sum(axis=1)) 29 | sample_weight /= sample_weight.mean() 30 | train_y = train_y.argmax(axis=1) 31 | 32 | # 这是人工提供的验证集 33 | data = np.load('captcha.test.npz') 34 | test_x, test_y = data['images'], data['labels'] 35 | test_x = preprocess_input(test_x) 36 | return (train_x, train_y, sample_weight), (test_x, test_y) 37 | 38 | 39 | def learn(): 40 | (train_x, train_y, sample_weight), (test_x, test_y) = load_data() 41 | datagen = ImageDataGenerator(horizontal_flip=True, 42 | vertical_flip=True) 43 | train_generator = datagen.flow(train_x, train_y, sample_weight=sample_weight) 44 | base = VGG16(weights='imagenet', include_top=False, input_shape=(None, None, 3)) 45 | for layer in base.layers[:-4]: 46 | layer.trainable = False 47 | model = models.Sequential([ 48 | base, 49 | layers.BatchNormalization(), 50 | layers.Conv2D(64, (3, 3), activation='relu', padding='same'), 51 | layers.GlobalAveragePooling2D(), 52 | layers.BatchNormalization(), 53 | layers.Dense(64, activation='relu'), 54 | layers.BatchNormalization(), 55 | layers.Dropout(0.20), 56 | layers.Dense(80, activation='softmax') 57 | ]) 58 | model.compile(optimizer=optimizers.RMSprop(lr=1e-5), 59 | loss='sparse_categorical_crossentropy', 60 | metrics=['accuracy']) 61 | model.summary() 62 | reduce_lr = ReduceLROnPlateau(verbose=1) 63 | model.fit_generator(train_generator, epochs=400, 64 | steps_per_epoch=100, 65 | validation_data=(test_x[:800], test_y[:800]), 66 | callbacks=[reduce_lr]) 67 | result = model.evaluate(test_x, test_y) 68 | print(result) 69 | model.save('12306.image.model.h5', include_optimizer=False) 70 | 71 | 72 | def predict(imgs): 73 | imgs = preprocess_input(imgs) 74 | model = models.load_model('12306.image.model.h5') 75 | labels = model.predict(imgs) 76 | return labels 77 | 78 | 79 | def _predict(fn): 80 | imgs = cv2.imread(fn) 81 | imgs = cv2.resize(imgs, (67, 67)) 82 | imgs.shape = (-1, 67, 67, 3) 83 | labels = predict(imgs) 84 | print(labels.max(axis=1)) 85 | print(labels.argmax(axis=1)) 86 | 87 | 88 | if __name__ == '__main__': 89 | if len(sys.argv) >= 2: 90 | _predict(sys.argv[1]) 91 | else: 92 | learn() 93 | -------------------------------------------------------------------------------- /python/model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/python/model.h5 -------------------------------------------------------------------------------- /python/pretreatment.py: -------------------------------------------------------------------------------- 1 | #! env python 2 | # coding: utf-8 3 | # 功能:对图像进行预处理,将文字部分单独提取出来 4 | # 并存放到ocr目录下 5 | # 文件名为原验证码文件的文件名 6 | import hashlib 7 | import os 8 | import pathlib 9 | 10 | import cv2 11 | import numpy as np 12 | import requests 13 | import scipy.fftpack 14 | import json 15 | import base64 16 | 17 | PATH = 'imgs' 18 | 19 | 20 | def download_image(): 21 | # 抓取验证码 22 | # 存放到指定path下 23 | # 文件名为图像的MD5 24 | url = 'https://kyfw.12306.cn/passport/captcha/captcha-image64' 25 | r = requests.get(url) 26 | fn = hashlib.md5(r.content).hexdigest() 27 | img_str = json.loads(r.content)['image'] 28 | with open(f'{PATH}/{fn}.jpg', 'wb') as fp: 29 | fp.write(base64.b64decode(img_str)) 30 | 31 | 32 | def download_images(): 33 | pathlib.Path(PATH).mkdir(exist_ok=True) 34 | for idx in range(40000): 35 | download_image() 36 | print(idx) 37 | 38 | 39 | def get_text(img, offset=0): 40 | # 得到图像中的文本部分 41 | return img[3:22, 120 + offset:177 + offset] 42 | 43 | 44 | def avhash(im): 45 | im = cv2.resize(im, (8, 8), interpolation=cv2.INTER_CUBIC) 46 | avg = im.mean() 47 | im = im > avg 48 | im = np.packbits(im) 49 | return im 50 | 51 | 52 | def phash(im): 53 | im = cv2.resize(im, (32, 32), interpolation=cv2.INTER_CUBIC) 54 | im = scipy.fftpack.dct(scipy.fftpack.dct(im, axis=0), axis=1) 55 | im = im[:8, :8] 56 | med = np.median(im) 57 | im = im > med 58 | im = np.packbits(im) 59 | return im 60 | 61 | 62 | def _get_imgs(img): 63 | interval = 5 64 | length = 67 65 | for x in range(40, img.shape[0] - length, interval + length): 66 | for y in range(interval, img.shape[1] - length, interval + length): 67 | yield img[x:x + length, y:y + length] 68 | 69 | 70 | def get_imgs(img): 71 | imgs = [] 72 | for img in _get_imgs(img): 73 | imgs.append(phash(img)) 74 | return imgs 75 | 76 | 77 | def pretreat(): 78 | if len(os.listdir(PATH)) < 40000: 79 | download_images() 80 | texts, imgs = [], [] 81 | for img in os.listdir(PATH): 82 | img = os.path.join(PATH, img) 83 | img = cv2.imread(img, cv2.IMREAD_GRAYSCALE) 84 | texts.append(get_text(img)) 85 | imgs.append(get_imgs(img)) 86 | return texts, imgs 87 | 88 | 89 | def load_data(path='./data/data.npz'): 90 | if not os.path.isfile(path): 91 | texts, imgs = pretreat() 92 | np.savez(path, texts=texts, images=imgs) 93 | f = np.load(path) 94 | return f['texts'], f['images'] 95 | 96 | 97 | if __name__ == '__main__': 98 | texts, imgs = load_data() 99 | print(texts.shape) 100 | print(imgs.shape) 101 | imgs = imgs.reshape(-1, 8) 102 | print(np.unique(imgs, axis=0).shape) 103 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | sklearn 3 | opencv-python 4 | keras>=2.2.4 5 | tensorflow 6 | matplotlib>=3.0.2 7 | numpy>=1.14.6 8 | scipy>=1.1.0 9 | -------------------------------------------------------------------------------- /python/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd python && source venv/bin/activate && PYTHONIOENCODING=utf-8 python3 main.py $1 2>/dev/null 3 | -------------------------------------------------------------------------------- /python/texts.txt: -------------------------------------------------------------------------------- 1 | 打字机 2 | 调色板 3 | 跑步机 4 | 毛线 5 | 老虎 6 | 安全帽 7 | 沙包 8 | 盘子 9 | 本子 10 | 药片 11 | 双面胶 12 | 龙舟 13 | 红酒 14 | 拖把 15 | 卷尺 16 | 海苔 17 | 红豆 18 | 黑板 19 | 热水袋 20 | 烛台 21 | 钟表 22 | 路灯 23 | 沙拉 24 | 海报 25 | 公交卡 26 | 樱桃 27 | 创可贴 28 | 牌坊 29 | 苍蝇拍 30 | 高压锅 31 | 电线 32 | 网球拍 33 | 海鸥 34 | 风铃 35 | 订书机 36 | 冰箱 37 | 话梅 38 | 排风机 39 | 锅铲 40 | 绿豆 41 | 航母 42 | 电子秤 43 | 红枣 44 | 金字塔 45 | 鞭炮 46 | 菠萝 47 | 开瓶器 48 | 电饭煲 49 | 仪表盘 50 | 棉棒 51 | 篮球 52 | 狮子 53 | 蚂蚁 54 | 蜡烛 55 | 茶盅 56 | 印章 57 | 茶几 58 | 啤酒 59 | 档案袋 60 | 挂钟 61 | 刺绣 62 | 铃铛 63 | 护腕 64 | 手掌印 65 | 锦旗 66 | 文具盒 67 | 辣椒酱 68 | 耳塞 69 | 中国结 70 | 蜥蜴 71 | 剪纸 72 | 漏斗 73 | 锣 74 | 蒸笼 75 | 珊瑚 76 | 雨靴 77 | 薯条 78 | 蜜蜂 79 | 日历 80 | 口哨 81 | -------------------------------------------------------------------------------- /python/verify_image_hash.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pathlib 3 | 4 | import cv2 5 | import numpy as np 6 | import scipy.fftpack 7 | 8 | 9 | def avhash(im): 10 | im = cv2.resize(im, (8, 8), interpolation=cv2.INTER_CUBIC) 11 | avg = im.mean() 12 | im = im > avg 13 | im = np.packbits(im) 14 | return im 15 | 16 | 17 | def phash(im): 18 | im = cv2.resize(im, (32, 32), interpolation=cv2.INTER_CUBIC) 19 | im = scipy.fftpack.dct(scipy.fftpack.dct(im, axis=0), axis=1) 20 | im = im[:8, :8] 21 | med = np.median(im) 22 | im = im > med 23 | im = np.packbits(im) 24 | return im 25 | 26 | 27 | def phash_simple(im): 28 | im = cv2.resize(im, (32, 32), interpolation=cv2.INTER_CUBIC) 29 | im = scipy.fftpack.dct(im) 30 | im = im[:8, 1:8 + 1] 31 | avg = im.mean() 32 | im = im > avg 33 | im = np.packbits(im) 34 | return im 35 | 36 | 37 | def dhash(im): 38 | im = cv2.resize(im, (8 + 1, 8), interpolation=cv2.INTER_CUBIC) 39 | im = im[:, 1:] > im[:, :-1] 40 | im = np.packbits(im) 41 | return im 42 | 43 | 44 | def dhash_vertical(im): 45 | im = cv2.resize(im, (8, 8 + 1), interpolation=cv2.INTER_CUBIC) 46 | im = im[1:, :] > im[:-1, :] 47 | im = np.packbits(im) 48 | return im 49 | 50 | 51 | def whash(image): 52 | """ 53 | Wavelet Hash computation. 54 | 55 | based on https://www.kaggle.com/c/avito-duplicate-ads-detection/ 56 | @image must be a PIL instance. 57 | """ 58 | ll_max_level = int(np.log2(min(image.shape))) 59 | image_scale = 2**ll_max_level 60 | 61 | level = 3 62 | dwt_level = ll_max_level - level 63 | 64 | image = cv2.resize(image, (image_scale, image_scale)) 65 | pixels = image / 255 66 | 67 | # Remove low level frequency LL(max_ll) if @remove_max_haar_ll using haar filter 68 | coeffs = pywt.wavedec2(pixels, 'haar', level = ll_max_level) 69 | coeffs[0][:] = 0 70 | pixels = pywt.waverec2(coeffs, 'haar') 71 | 72 | # Use LL(K) as freq, where K is log2(@hash_size) 73 | coeffs = pywt.wavedec2(pixels, 'haar', level = dwt_level) 74 | dwt_low = coeffs[0] 75 | 76 | # Substract median and compute hash 77 | med = np.median(dwt_low) 78 | diff = dwt_low > med 79 | 80 | diff = np.packbits(diff) 81 | return diff 82 | 83 | 84 | def verify(_hash): 85 | # 用验证集测试各哈希函数的效果 86 | data = np.load('captcha.npz') 87 | images, labels = data['images'], data['labels'] 88 | print(images.shape) 89 | himages = {} 90 | for idx, (img, label) in enumerate(zip(images, labels)): 91 | img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 92 | img = _hash(img) 93 | img.dtype = np.uint64 94 | img = img[0] 95 | if himages.get(img, (label,))[0] != label: 96 | cv2.imwrite(f'errors/{idx}.{label}.jpg', images[idx]) 97 | pre_label, pre_idx = himages[img] 98 | cv2.imwrite(f'errors/{idx}.{pre_label}.jpg', images[pre_idx]) 99 | else: 100 | himages[img] = label, idx 101 | print(len(himages)) 102 | 103 | 104 | if __name__ == '__main__': 105 | pathlib.Path('errors').mkdir(exist_ok=True) 106 | # verify(avhash) 107 | # 我觉得下面这个是最佳的 108 | verify(phash) 109 | # verify(phash_simple) 110 | # verify(dhash) 111 | # verify(dhash_vertical) 112 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/TicketApplication.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | import com.qianxunclub.ticket.config.UserConfig; 4 | import com.qianxunclub.ticket.ip.AddIpThread; 5 | import com.qianxunclub.ticket.ip.CheckIpThread; 6 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 7 | import com.qianxunclub.ticket.service.ApiRequestService; 8 | import com.qianxunclub.ticket.service.IpsService; 9 | import com.qianxunclub.ticket.service.ProxyIpService; 10 | import com.qianxunclub.ticket.service.TicketService; 11 | import com.qianxunclub.ticket.ticket.DoHandle; 12 | import com.qianxunclub.ticket.model.UserTicketStore; 13 | 14 | import com.qianxunclub.ticket.ticket.Station; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.boot.autoconfigure.SpringBootApplication; 17 | import org.springframework.boot.builder.SpringApplicationBuilder; 18 | import org.springframework.context.ApplicationContext; 19 | 20 | import java.util.List; 21 | 22 | 23 | /** 24 | * @author zhangbin 25 | */ 26 | @SpringBootApplication 27 | @Slf4j 28 | public class TicketApplication { 29 | 30 | public static void main(String[] args) { 31 | SpringApplicationBuilder builder = new SpringApplicationBuilder(TicketApplication.class); 32 | ApplicationContext applicationContext = builder.run(args); 33 | 34 | TicketApplication.init(applicationContext); 35 | 36 | } 37 | 38 | public static void init(ApplicationContext applicationContext) { 39 | 40 | // 初始化 IP 41 | IpsService ipsService = applicationContext.getBean(IpsService.class); 42 | ipsService.load(); 43 | log.info("初始化Ip完成"); 44 | // 初始化 代理 45 | ProxyIpService proxyIpservice = applicationContext.getBean(ProxyIpService.class); 46 | proxyIpservice.load(); 47 | log.info("初始化代理Ip完成"); 48 | // 初始化站点 49 | Station station = applicationContext.getBean(Station.class); 50 | ApiRequestService apiRequestService = applicationContext.getBean(ApiRequestService.class); 51 | station.load(apiRequestService); 52 | log.info("初始化站点信息完成"); 53 | // 配置文件获取购票信息 54 | List buyTicketInfoModelList = applicationContext 55 | .getBean(UserConfig.class).getTicketInfo(); 56 | buyTicketInfoModelList.forEach((buyTicketInfoModel) -> { 57 | if (buyTicketInfoModel != null) { 58 | UserTicketStore.add(buyTicketInfoModel); 59 | } 60 | }); 61 | log.info("初始化从配置文件初始化购票信息完成"); 62 | // 数据库获取取购票信息 63 | TicketService ticketService = applicationContext.getBean(TicketService.class); 64 | ticketService.getBuyTicketInfoModel().forEach(buyTicketInfoModel -> { 65 | if (buyTicketInfoModel != null) { 66 | UserTicketStore.add(buyTicketInfoModel); 67 | } 68 | }); 69 | log.info("初始化从数据库初始化购票信息完成"); 70 | DoHandle doHandle = applicationContext.getBean(DoHandle.class); 71 | doHandle.go(); 72 | log.info("开始执行抢票任务"); 73 | // 检查IP 74 | CheckIpThread checkIpThread = new CheckIpThread(ipsService); 75 | Thread checkIp = new Thread(checkIpThread); 76 | checkIp.start(); 77 | log.info("检查IP完成"); 78 | // 获取新IP 79 | AddIpThread addIpThread = new AddIpThread(ipsService); 80 | Thread addIp = new Thread(addIpThread); 81 | addIp.start(); 82 | log.info("获取IP完成"); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/ApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | 4 | import com.qianxunclub.ticket.util.StaticUtil; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-05-30 16:06 14 | * @description: TODO 15 | */ 16 | @Data 17 | @Component 18 | @ConfigurationProperties(prefix = "api") 19 | public class ApiConfig { 20 | 21 | @Autowired 22 | private Config config; 23 | 24 | private String station; 25 | 26 | private String leftTicketBaseUrl; 27 | 28 | private String leftTicket; 29 | 30 | private String loginConfig; 31 | 32 | private String captchaImage; 33 | 34 | private String uamtkStatic; 35 | 36 | private String login; 37 | 38 | private String passengers; 39 | 40 | private String captchaCheck; 41 | 42 | private String uamtk; 43 | 44 | private String uamauthclient; 45 | 46 | private String submitOrderRequest; 47 | 48 | private String checkUser; 49 | 50 | private String initDc; 51 | 52 | private String getPassengerDTOs; 53 | 54 | private String checkOrderInfo; 55 | 56 | private String checkRandCodeAnsyn; 57 | 58 | private String confirmSingleForQueue; 59 | 60 | private String getQueueCount; 61 | 62 | private String queryOrderWaitTime; 63 | 64 | private String notice; 65 | 66 | private String init; 67 | 68 | private String serverWechat; 69 | 70 | public String getHost() { 71 | String host = StaticUtil.ip(); 72 | if (host == null) { 73 | host = config.getBaseUrl(); 74 | } 75 | return host; 76 | } 77 | 78 | public String getStation() { 79 | return this.getHost().concat(station); 80 | } 81 | 82 | public String getLeftTicket() { 83 | return this.getHost().concat(leftTicket); 84 | } 85 | 86 | public String getLoginConfig() { 87 | return this.getHost().concat(loginConfig); 88 | } 89 | 90 | public String getCaptchaImage() { 91 | return this.getHost().concat(captchaImage); 92 | } 93 | 94 | public String getUamtkStatic() { 95 | return this.getHost().concat(uamtkStatic); 96 | } 97 | 98 | public String getLogin() { 99 | return this.getHost().concat(login); 100 | } 101 | 102 | public String getPassengers() { 103 | return this.getHost().concat(passengers); 104 | } 105 | 106 | public String getCaptchaCheck() { 107 | return this.getHost().concat(captchaCheck); 108 | } 109 | 110 | public String getUamtk() { 111 | return this.getHost().concat(uamtk); 112 | } 113 | 114 | public String getUamauthclient() { 115 | return this.getHost().concat(uamauthclient); 116 | } 117 | 118 | public String getSubmitOrderRequest() { 119 | return this.getHost().concat(submitOrderRequest); 120 | } 121 | 122 | public String getCheckUser() { 123 | return this.getHost().concat(checkUser); 124 | } 125 | 126 | public String getInitDc() { 127 | return this.getHost().concat(initDc); 128 | } 129 | 130 | public String getGetPassengerDTOs() { 131 | return this.getHost().concat(getPassengerDTOs); 132 | } 133 | 134 | public String getCheckOrderInfo() { 135 | return this.getHost().concat(checkOrderInfo); 136 | } 137 | 138 | public String getCheckRandCodeAnsyn() { 139 | return this.getHost().concat(checkRandCodeAnsyn); 140 | } 141 | 142 | public String getConfirmSingleForQueue() { 143 | return this.getHost().concat(confirmSingleForQueue); 144 | } 145 | 146 | public String getGetQueueCount() { 147 | return this.getHost().concat(getQueueCount); 148 | } 149 | 150 | public String getQueryOrderWaitTime() { 151 | return this.getHost().concat(queryOrderWaitTime); 152 | } 153 | 154 | public String getInit() { 155 | return this.getHost().concat(init); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/Config.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | import com.qianxunclub.ticket.repository.entity.ProxyIp; 4 | import com.qianxunclub.ticket.util.StaticUtil; 5 | import java.util.Map; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-05-30 15:57 14 | * @description: TODO 15 | */ 16 | @Data 17 | @Component 18 | @ConfigurationProperties(prefix = "config") 19 | public class Config { 20 | 21 | private String host; 22 | 23 | private String baseUrl; 24 | 25 | private Boolean enableProxy; 26 | 27 | private String proxyHost; 28 | 29 | private Integer proxyPort; 30 | 31 | private Map queryTicketSleepTime; 32 | 33 | private String pythonPath; 34 | 35 | private ProxyIp proxyIp; 36 | 37 | public Integer querySleep() { 38 | return (int) (queryTicketSleepTime.get("min") + Math.random() * ( 39 | queryTicketSleepTime.get("max") - queryTicketSleepTime.get("min") + 1)); 40 | } 41 | 42 | public ProxyIp getProxyIp() { 43 | proxyIp = StaticUtil.proxyIp(); 44 | if (proxyIp == null) { 45 | proxyIp = new ProxyIp(); 46 | proxyIp.setIp(proxyHost); 47 | proxyIp.setPort(proxyPort); 48 | } 49 | return proxyIp; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/CookiesConfig.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import lombok.Data; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-06-04 11:22 11 | * @description: TODO 12 | */ 13 | @Data 14 | @Component 15 | @ConfigurationProperties(prefix = "cookies") 16 | public class CookiesConfig { 17 | Boolean enable; 18 | String railExpiration; 19 | String railDeviceid; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/NoticeConfig.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import lombok.Data; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-05-30 15:57 11 | * @description: TODO 12 | */ 13 | @Data 14 | @Component 15 | @ConfigurationProperties(prefix = "notice") 16 | public class NoticeConfig { 17 | 18 | private String accessKeyId; 19 | private String accessSecret; 20 | private String templateCode; 21 | private String signName; 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 9 | 10 | import lombok.Data; 11 | import springfox.documentation.builders.ApiInfoBuilder; 12 | import springfox.documentation.builders.RequestHandlerSelectors; 13 | import springfox.documentation.service.ApiInfo; 14 | import springfox.documentation.service.Contact; 15 | import springfox.documentation.spi.DocumentationType; 16 | import springfox.documentation.spring.web.plugins.Docket; 17 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 18 | 19 | import static com.google.common.base.Predicates.not; 20 | import static springfox.documentation.builders.PathSelectors.regex; 21 | 22 | /** 23 | * @author zhangbin 24 | * @date 2019-07-19 10:57 25 | * @description: TODO 26 | */ 27 | @Configuration 28 | @EnableSwagger2 29 | @Data 30 | @Component 31 | @ConfigurationProperties(prefix = "swagger") 32 | public class SwaggerConfig extends WebMvcConfigurerAdapter { 33 | private Boolean enabled; 34 | 35 | private String title; 36 | 37 | private String description; 38 | 39 | private String webBasePackage; 40 | 41 | private String author; 42 | 43 | private String url; 44 | 45 | private String email; 46 | 47 | @Bean 48 | public Docket createRestApi() { 49 | return new Docket(DocumentationType.SWAGGER_2) 50 | .enable(this.getEnabled()) 51 | .apiInfo(apiInfo()) 52 | .select() 53 | .apis(RequestHandlerSelectors.basePackage(this.getWebBasePackage())) 54 | .paths(not(regex("/error.*"))) 55 | .build(); 56 | } 57 | 58 | private ApiInfo apiInfo() { 59 | String author = this.getAuthor(); 60 | String url = this.getUrl(); 61 | String email = this.getEmail(); 62 | return new ApiInfoBuilder() 63 | .title(this.getTitle()) 64 | .description(this.getDescription()) 65 | .contact(new Contact(author, url, email)) 66 | .build(); 67 | } 68 | 69 | @Override 70 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 71 | registry.addResourceHandler("swagger-ui.html") 72 | .addResourceLocations("classpath:/META-INF/resources/"); 73 | 74 | registry.addResourceHandler("/webjars/**") 75 | .addResourceLocations("classpath:/META-INF/resources/webjars/"); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/config/UserConfig.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.config; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import lombok.Data; 12 | 13 | /** 14 | * @author zhangbin 15 | * @date 2019-07-05 13:56 16 | * @description: TODO 17 | */ 18 | @Data 19 | @Component 20 | @ConfigurationProperties(prefix = "user") 21 | public class UserConfig { 22 | private List ticketInfo = new ArrayList<>(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/constant/SeatLevelEnum.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.constant; 2 | 3 | import com.google.common.base.Joiner; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * @author zhangbin 11 | * @date 2019-05-31 10:52 12 | * @description: TODO 13 | */ 14 | @Getter 15 | public enum SeatLevelEnum { 16 | 17 | YINGWO("硬卧",28,"3"), 18 | RUANWO("软卧",23,"4"), 19 | TWO("二等座",30,"O"), 20 | RUANZUO("软座",24,"2"), 21 | YINGZUO("硬座",29,"1"), 22 | ONE("一等座",31,"M"), 23 | SHANGWUZUO("商务座",32,"9"), 24 | GAOJIRUANWO("高级软卧",21,"6"), 25 | WUZUO("无座",26,"1") 26 | ; 27 | 28 | private String name; 29 | private int index; 30 | private String code; 31 | 32 | SeatLevelEnum(String name,int index,String code) { 33 | this.name = name; 34 | this.index = index; 35 | this.code = code; 36 | } 37 | 38 | public static String getSeatNameList(List seatLevelEnums){ 39 | List seatLevels = seatLevelEnums.stream().map(level->level.name).collect(Collectors.toList()); 40 | return Joiner.on(",").join(seatLevels); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/constant/StatusEnum.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.constant; 2 | 3 | /** 4 | * @author zhangbin 5 | * @date 2019-07-02 10:13 6 | * @description: TODO 7 | */ 8 | public enum StatusEnum { 9 | 10 | START("开始"), 11 | ING("正在抢票"), 12 | SUCCESS("抢票成功") 13 | ; 14 | 15 | private String status; 16 | 17 | StatusEnum(String status){ 18 | this.status = status; 19 | } 20 | 21 | public String getStatus() { 22 | return status; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.controller; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | import com.qianxunclub.ticket.model.PassengerModel; 5 | import com.qianxunclub.ticket.model.request.PassengerRequest; 6 | import com.qianxunclub.ticket.model.Result; 7 | import com.qianxunclub.ticket.model.request.TicketRequest; 8 | import com.qianxunclub.ticket.model.response.PassengerResponse; 9 | import com.qianxunclub.ticket.model.response.TicketInfoResponse; 10 | import com.qianxunclub.ticket.service.TicketService; 11 | import com.qianxunclub.ticket.model.UserTicketStore; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import org.springframework.beans.BeanUtils; 15 | import org.springframework.util.CollectionUtils; 16 | import org.springframework.util.StringUtils; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.bind.annotation.RequestBody; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.ResponseBody; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | import io.swagger.annotations.Api; 28 | import io.swagger.annotations.ApiOperation; 29 | import lombok.AllArgsConstructor; 30 | 31 | /** 32 | * @author zhangbin 33 | * @date 2019-06-08 19:31 34 | * @description: TODO 35 | */ 36 | @Api("接口文档") 37 | @RestController 38 | @AllArgsConstructor 39 | @RequestMapping(value = "/api", produces = "application/json") 40 | public class IndexController { 41 | 42 | private TicketService ticketService; 43 | 44 | 45 | @ApiOperation("登录") 46 | @ResponseBody 47 | @PostMapping("getPassenger") 48 | public Result getPassenger(@RequestBody PassengerRequest passengerRequest) { 49 | Result result = new Result("SUCCESS", "登录成功"); 50 | List passengerModelList = ticketService 51 | .login(passengerRequest.toUserModel()); 52 | if (CollectionUtils.isEmpty(passengerModelList)) { 53 | result = new Result("ERROR", "登录失败"); 54 | } 55 | List passengerResponseList = new ArrayList<>(); 56 | passengerModelList.forEach(passengerModel -> { 57 | PassengerResponse passengerResponse = passengerModel.toPassengerResponse(); 58 | passengerResponseList.add(passengerResponse); 59 | }); 60 | result.setData(passengerResponseList); 61 | return result; 62 | } 63 | 64 | @ApiOperation("正在抢票中的用户") 65 | @ResponseBody 66 | @GetMapping("buying") 67 | public Object buying(HttpServletRequest request) { 68 | String username = request.getParameter("username"); 69 | if (StringUtils.isEmpty(username)) { 70 | List ticketInfoResponseList = new ArrayList<>(); 71 | UserTicketStore.buyTicketInfoModelList.forEach(buyTicketInfoModel -> { 72 | TicketInfoResponse ticketInfoResponse = new TicketInfoResponse(); 73 | BeanUtils.copyProperties(buyTicketInfoModel, ticketInfoResponse); 74 | ticketInfoResponseList.add(ticketInfoResponse); 75 | }); 76 | return ticketInfoResponseList; 77 | } 78 | BuyTicketInfoModel buyTicketInfo = UserTicketStore.buyTicketInfoModelList.stream() 79 | .filter(buyTicketInfoModel -> 80 | buyTicketInfoModel.getUsername().equals(username) 81 | ).findFirst().get(); 82 | TicketInfoResponse ticketInfoResponse = new TicketInfoResponse(); 83 | BeanUtils.copyProperties(buyTicketInfo, ticketInfoResponse); 84 | return ticketInfoResponse; 85 | } 86 | 87 | @ApiOperation("添加抢票信息") 88 | @ResponseBody 89 | @PostMapping("ticket") 90 | public Result user(@RequestBody TicketRequest TicketRequest) { 91 | return ticketService.addTicketInfo(TicketRequest.toBuyTicketInfoModel()); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/filter/LogFilter.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.filter; 2 | 3 | import com.qianxunclub.ticket.model.LoggerMessage; 4 | import com.qianxunclub.ticket.util.LogUtil; 5 | 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.text.DateFormat; 9 | import java.util.Date; 10 | 11 | import ch.qos.logback.classic.spi.ILoggingEvent; 12 | import ch.qos.logback.core.filter.Filter; 13 | import ch.qos.logback.core.spi.FilterReply; 14 | 15 | /** 16 | * @author zhangbin 17 | * @date 2019-07-01 14:21 18 | * @description: TODO 19 | */ 20 | @Component 21 | public class LogFilter extends Filter { 22 | @Override 23 | public FilterReply decide(ILoggingEvent event) { 24 | LoggerMessage loggerMessage = new LoggerMessage(event.getMessage(), DateFormat.getDateTimeInstance().format(new Date(event.getTimeStamp())), event.getThreadName(), event.getLoggerName(), event.getLevel().levelStr); 25 | // LogUtil.push(loggerMessage.toString()); 26 | return FilterReply.ACCEPT; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ip/AddIpThread.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ip; 2 | 3 | import com.qianxunclub.ticket.service.IpsService; 4 | import com.qianxunclub.ticket.util.StaticUtil; 5 | import org.springframework.util.StringUtils; 6 | 7 | public class AddIpThread implements Runnable{ 8 | 9 | private IpsService ipsService; 10 | 11 | public AddIpThread(IpsService ipsService){ 12 | this.ipsService = ipsService; 13 | } 14 | 15 | @Override 16 | public void run() { 17 | while (true) { 18 | try { 19 | // 每十分钟获取更新一次 IP 20 | Thread.sleep(10 * 1000 * 60); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | // 获取 IP 25 | String ip = ""; 26 | if(!StringUtils.isEmpty(ip)){ 27 | ipsService.addIp(ip); 28 | StaticUtil.addIp(ip); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ip/CheckIpThread.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ip; 2 | 3 | import com.qianxunclub.ticket.service.IpsService; 4 | import com.qianxunclub.ticket.util.StaticUtil; 5 | 6 | public class CheckIpThread implements Runnable { 7 | 8 | private IpsService ipsService; 9 | 10 | 11 | public CheckIpThread(IpsService ipsService) { 12 | this.ipsService = ipsService; 13 | } 14 | 15 | @Override 16 | public void run() { 17 | while (true) { 18 | try { 19 | // 每十分钟检查一次 IP 20 | Thread.sleep(10 * 1000 * 60); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | StaticUtil.ips.forEach(ip -> { 25 | Thread thread = new Thread(new PingIpThread(ipsService, ip)); 26 | thread.start(); 27 | try { 28 | Thread.sleep(500); 29 | } catch (InterruptedException e) { 30 | e.printStackTrace(); 31 | } 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ip/PingIpThread.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ip; 2 | 3 | import com.qianxunclub.ticket.service.IpsService; 4 | import com.qianxunclub.ticket.util.StaticUtil; 5 | 6 | public class PingIpThread implements Runnable { 7 | 8 | private IpsService ipsService; 9 | 10 | private String ip; 11 | 12 | public PingIpThread(IpsService ipsService,String ip) { 13 | this.ip = ip; 14 | this.ipsService = ipsService; 15 | } 16 | 17 | @Override 18 | public void run() { 19 | boolean s = true; 20 | // 这里校验 IP 21 | 22 | 23 | // 如果 IP 失效 24 | if(!s){ 25 | ipsService.delIp(ip); 26 | StaticUtil.rmIp(ip); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/BuyTicketInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 4 | import com.qianxunclub.ticket.constant.StatusEnum; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-05-30 17:00 14 | * @description: TODO 15 | */ 16 | @Data 17 | public class BuyTicketInfoModel extends UserModel { 18 | 19 | private int id; 20 | 21 | private String date; 22 | 23 | private String from; 24 | 25 | private String to; 26 | 27 | private String trainNumber; 28 | 29 | private String allEncStr; 30 | 31 | private String mobile; 32 | 33 | private String realName; 34 | 35 | /** 36 | * server酱微信通知秘钥 37 | */ 38 | private String serverSckey; 39 | 40 | private PassengerModel passengerModel; 41 | 42 | private List seat = new ArrayList<>(); 43 | 44 | private int queryNum; 45 | 46 | private StatusEnum status = StatusEnum.START; 47 | 48 | public String getSeatStr() { 49 | StringBuffer seatStr = new StringBuffer(); 50 | seat.forEach(seatLevelEnum -> { 51 | seatStr.append(seatLevelEnum.name() + ","); 52 | }); 53 | return seatStr.substring(0, seatStr.length() - 1); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/LogdeviceModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | /** 8 | * @author zhangbin 9 | * @date 2019-05-30 16:46 10 | * @description: TODO 11 | */ 12 | @Data 13 | @AllArgsConstructor 14 | public class LogdeviceModel { 15 | 16 | private String exp; 17 | private String dfp; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/LoggerMessage.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author zhangbin 8 | * @date 2019-07-01 14:24 9 | * @description: TODO 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class LoggerMessage { 14 | private String body; 15 | private String timestamp; 16 | private String threadName; 17 | private String className; 18 | private String level; 19 | 20 | @Override 21 | public String toString(){ 22 | return "[" + timestamp + "][" + threadName + "]:" + body; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/NoticeModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-06-08 16:11 11 | * @description: TODO 12 | */ 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class NoticeModel { 18 | private String phoneNumber; 19 | private String userName; 20 | private String password; 21 | private String orderId; 22 | private String name; 23 | private String trainNum; 24 | private String trainDate; 25 | private String from; 26 | private String to; 27 | private String serverSckey; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/PassengerModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import com.qianxunclub.ticket.model.response.PassengerResponse; 4 | 5 | import org.springframework.beans.BeanUtils; 6 | 7 | import java.util.Map; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-06-05 10:30 14 | * @description: TODO 15 | */ 16 | @Data 17 | public class PassengerModel { 18 | 19 | String passengerName; 20 | String sexCode; 21 | String sexName; 22 | String bornDate; 23 | String countryCode; 24 | String passengerIdTypeCode; 25 | String passengerIdTypeName; 26 | String passengerIdNo; 27 | String passengerType; 28 | String passengerFlag; 29 | String passengerTypeName; 30 | String mobileNo; 31 | String phoneNo; 32 | String email; 33 | String address; 34 | String postalcode; 35 | String firstLetter; 36 | String recordCount; 37 | String totalLimes; 38 | String indexId; 39 | String gatBornDate; 40 | String gatValidDateStart; 41 | String gatValidDateEnd; 42 | String gatVersi; 43 | String allEncStr; 44 | 45 | public PassengerModel() { 46 | super(); 47 | } 48 | 49 | public PassengerModel(Map passengerMap) { 50 | this.passengerName = passengerMap.get("passenger_name"); 51 | this.sexCode = passengerMap.get("sex_code"); 52 | this.sexName = passengerMap.get("sex_name"); 53 | this.bornDate = passengerMap.get("born_date"); 54 | this.countryCode = passengerMap.get("countryCode"); 55 | this.passengerIdTypeCode = passengerMap.get("passenger_id_type_code"); 56 | this.passengerIdTypeName = passengerMap.get("passenger_id_type_name"); 57 | this.passengerIdNo = passengerMap.get("passenger_id_no"); 58 | this.passengerType = passengerMap.get("passenger_type"); 59 | this.passengerFlag = passengerMap.get("passenger_flag"); 60 | this.passengerTypeName = passengerMap.get("passenger_type_name"); 61 | this.mobileNo = passengerMap.get("mobile_no"); 62 | this.phoneNo = passengerMap.get("phone_no"); 63 | this.email = passengerMap.get("email"); 64 | this.address = passengerMap.get("address"); 65 | this.postalcode = passengerMap.get("postalcode"); 66 | this.firstLetter = passengerMap.get("first_letter"); 67 | this.recordCount = passengerMap.get("record_count"); 68 | this.totalLimes = passengerMap.get("total_limes"); 69 | this.indexId = passengerMap.get("index_id"); 70 | this.gatBornDate = passengerMap.get("gat_born_date"); 71 | this.gatValidDateStart = passengerMap.get("gat_valid_date_start"); 72 | this.gatValidDateEnd = passengerMap.get("gat_valid_date_end"); 73 | this.gatVersi = passengerMap.get("gat_versi"); 74 | this.allEncStr = passengerMap.get("allEncStr"); 75 | } 76 | 77 | public String getPassengerTicketStr(BuyTicketInfoModel buyTicketInfoModel, 78 | TicketModel ticketModel) { 79 | PassengerModel passengerModel = buyTicketInfoModel.getPassengerModel(); 80 | return ticketModel.getSeat().get(0).getSeatLevel().getCode() + "," + 81 | "0,1," + passengerModel.getPassengerName() + "," + 82 | passengerModel.getPassengerIdTypeCode() + "," + 83 | passengerModel.getPassengerIdNo() + "," + 84 | passengerModel.getMobileNo() + "," + 85 | "N," + passengerModel.getAllEncStr(); 86 | } 87 | 88 | public String getOldPassengerStr(PassengerModel passengerModel) { 89 | return passengerModel.getPassengerName() + "," + 90 | passengerModel.getPassengerIdTypeCode() + "," + 91 | passengerModel.getPassengerIdNo() + "," + 92 | passengerModel.getPassengerType() + "_"; 93 | } 94 | 95 | public PassengerResponse toPassengerResponse() { 96 | PassengerResponse passengerResponse = new PassengerResponse(); 97 | BeanUtils.copyProperties(this, passengerResponse); 98 | return passengerResponse; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/Result.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author zhangbin 7 | * @date 2019-07-19 15:44 8 | * @description: TODO 9 | */ 10 | @Data 11 | public class Result { 12 | private String code; 13 | private String msg; 14 | private T data; 15 | 16 | public Result(String code, String msg) { 17 | this.code = code; 18 | this.msg = msg; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/SeatModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-05-31 10:50 11 | * @description: TODO 12 | */ 13 | @Data 14 | @AllArgsConstructor 15 | public class SeatModel { 16 | 17 | private SeatLevelEnum seatLevel; 18 | private String count; 19 | 20 | public SeatModel(SeatLevelEnum seatLevel,String[] info) { 21 | this.seatLevel = seatLevel; 22 | String n = info[seatLevel.getIndex()]; 23 | this.count = n; 24 | } 25 | 26 | 27 | @Override 28 | public String toString(){ 29 | return seatLevel.getName() + "-" + count + "票"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/TicketModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 4 | import com.qianxunclub.ticket.ticket.Station; 5 | 6 | import java.net.URLDecoder; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import lombok.Data; 11 | 12 | /** 13 | * @author zhangbin 14 | * @date 2019-05-31 10:46 15 | * @description: TODO 16 | */ 17 | @Data 18 | public class TicketModel { 19 | 20 | private String trainDate; 21 | private String secret; 22 | private String trainCode; 23 | private String trainNumber; 24 | private String from; 25 | private String to; 26 | private String trainLocation; 27 | private String departDate; 28 | private String arriveDate; 29 | private String interval; 30 | private String leftTicket; 31 | 32 | private List seat = new ArrayList<>(); 33 | 34 | public void setInfo(String[] info) { 35 | this.secret = URLDecoder.decode(info[0]); 36 | this.trainCode = info[2]; 37 | this.trainNumber = info[3]; 38 | this.from = info[6]; 39 | this.to = info[7]; 40 | this.trainLocation = info[15]; 41 | this.departDate = info[8]; 42 | this.arriveDate = info[9]; 43 | this.interval = info[10]; 44 | this.leftTicket = info[12]; 45 | 46 | for(SeatLevelEnum seatLevelEnum : SeatLevelEnum.values()){ 47 | SeatModel seatModel = new SeatModel(seatLevelEnum,info); 48 | seat.add(seatModel); 49 | } 50 | } 51 | 52 | @Override 53 | public String toString(){ 54 | return "✅车次[" + trainNumber + "][" + Station.getNameByCode(from) + "-" + Station.getNameByCode(to) + "][" + departDate + "-" + arriveDate + "]:" + seat.toString(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/UserModel.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author zhangbin 7 | * @date 2019-05-31 15:19 8 | * @description: TODO 9 | */ 10 | @Data 11 | public class UserModel { 12 | private String username; 13 | private String password; 14 | private String answer; 15 | private String uamtk; 16 | private LogdeviceModel logdeviceModel; 17 | private String globalRepeatSubmitToken; 18 | private String keyCheckIsChange; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/UserTicketStore.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model; 2 | 3 | import com.qianxunclub.ticket.util.HttpUtil; 4 | import org.apache.http.impl.client.BasicCookieStore; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-06-08 11:10 14 | * @description: TODO 15 | */ 16 | public class UserTicketStore { 17 | 18 | public static List buyTicketInfoModelList = new ArrayList<>(); 19 | public static Map httpUtilStore = new HashMap<>(); 20 | 21 | public static void add(BuyTicketInfoModel buyTicketInfoModel) { 22 | buyTicketInfoModelList.add(buyTicketInfoModel); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/request/PassengerRequest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model.request; 2 | 3 | import com.qianxunclub.ticket.model.UserModel; 4 | 5 | import org.springframework.beans.BeanUtils; 6 | 7 | import io.swagger.annotations.ApiModel; 8 | import io.swagger.annotations.ApiModelProperty; 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-07-23 09:32 14 | * @description: TODO 15 | */ 16 | @Data 17 | @ApiModel("登录信息") 18 | public class PassengerRequest { 19 | @ApiModelProperty("12306用户名") 20 | private String username; 21 | @ApiModelProperty("12306密码") 22 | private String password; 23 | 24 | public UserModel toUserModel() { 25 | UserModel userModel = new UserModel(); 26 | BeanUtils.copyProperties(this, userModel); 27 | return userModel; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/request/TicketRequest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model.request; 2 | 3 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 4 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 5 | 6 | import org.springframework.beans.BeanUtils; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import io.swagger.annotations.ApiModel; 12 | import io.swagger.annotations.ApiModelProperty; 13 | import lombok.Data; 14 | 15 | /** 16 | * @author zhangbin 17 | * @date 2019-07-25 12:01 18 | * @description: TODO 19 | */ 20 | @Data 21 | @ApiModel("购票信息") 22 | public class TicketRequest extends PassengerRequest { 23 | @ApiModelProperty(value = "乘车日期", example = "2019-07-08") 24 | private String date; 25 | @ApiModelProperty(value = "出发站点", example = "深圳") 26 | private String from; 27 | @ApiModelProperty(value = "到达站点", example = "三门峡") 28 | private String to; 29 | @ApiModelProperty(value = "车次", example = "G818") 30 | private String trainNumber; 31 | @ApiModelProperty(value = "乘客编号", example = "登录完成后会乘客信息allEncStr的值") 32 | private String allEncStr; 33 | @ApiModelProperty(value = "乘车人手机号", example = "1234567890") 34 | private String mobile; 35 | @ApiModelProperty(value = "乘车人真实姓名", example = "XXX") 36 | private String realName; 37 | @ApiModelProperty(value = "座位级别", example = "ONE,RUANWO") 38 | private String seat; 39 | 40 | public List toSeatList() { 41 | List seatList = new ArrayList<>(); 42 | String[] seats = seat.split(","); 43 | for (int i = 0; i < seats.length; i++) { 44 | SeatLevelEnum seatLevelEnum = SeatLevelEnum.valueOf(seats[i]); 45 | seatList.add(seatLevelEnum); 46 | } 47 | return seatList; 48 | } 49 | 50 | public BuyTicketInfoModel toBuyTicketInfoModel() { 51 | BuyTicketInfoModel buyTicketInfoModel = new BuyTicketInfoModel(); 52 | BeanUtils.copyProperties(this, buyTicketInfoModel); 53 | buyTicketInfoModel.setSeat(this.toSeatList()); 54 | return buyTicketInfoModel; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/response/PassengerResponse.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model.response; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | /** 8 | * @author zhangbin 9 | * @date 2019-07-26 16:51 10 | * @description: TODO 11 | */ 12 | @Data 13 | @ApiModel("乘客信息") 14 | public class PassengerResponse { 15 | @ApiModelProperty("乘客编号") 16 | String allEncStr; 17 | @ApiModelProperty("乘客姓名") 18 | String passengerName; 19 | @ApiModelProperty("乘客性别") 20 | String sexName; 21 | @ApiModelProperty("证件类型") 22 | String passengerIdTypeName; 23 | @ApiModelProperty("证件号码") 24 | String passengerIdNo; 25 | @ApiModelProperty("电话号码") 26 | String mobileNo; 27 | @ApiModelProperty("手机号码") 28 | String phoneNo; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/model/response/TicketInfoResponse.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.model.response; 2 | 3 | import com.qianxunclub.ticket.constant.StatusEnum; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class TicketInfoResponse { 8 | 9 | private String username; 10 | private String date; 11 | private String from; 12 | private String to; 13 | private String trainNumber; 14 | private String realName; 15 | private int queryNum; 16 | private StatusEnum status = StatusEnum.START; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/dao/IpsDao.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.dao; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 4 | import com.qianxunclub.ticket.repository.entity.Ips; 5 | import com.qianxunclub.ticket.repository.mapper.IpsMapper; 6 | import java.util.List; 7 | import lombok.AllArgsConstructor; 8 | import org.springframework.stereotype.Repository; 9 | 10 | /** 11 | * @author zhangbin 12 | * @date 2019-07-19 10:19 13 | * @description: TODO 14 | */ 15 | @Repository 16 | @AllArgsConstructor 17 | public class IpsDao { 18 | 19 | private IpsMapper ipsMapper; 20 | 21 | public List list() { 22 | return ipsMapper.selectList(null); 23 | } 24 | 25 | public Ips getIp(String ip) { 26 | QueryWrapper queryWrapper = new QueryWrapper(); 27 | queryWrapper.eq("ip", ip); 28 | return ipsMapper.selectOne(queryWrapper); 29 | } 30 | 31 | public int add(Ips ips) { 32 | return ipsMapper.insert(ips); 33 | } 34 | 35 | public int deleteById(Integer id) { 36 | return ipsMapper.deleteById(id); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/dao/ProxyIpDao.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.dao; 2 | 3 | import com.qianxunclub.ticket.repository.entity.ProxyIp; 4 | import com.qianxunclub.ticket.repository.mapper.ProxyIpMapper; 5 | import java.util.List; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.stereotype.Repository; 8 | 9 | /** 10 | * @author zhangbin 11 | * @date 2019-07-19 10:19 12 | * @description: TODO 13 | */ 14 | @Repository 15 | @AllArgsConstructor 16 | public class ProxyIpDao { 17 | 18 | private ProxyIpMapper proxyIpMapper; 19 | 20 | public List list() { 21 | return proxyIpMapper.selectList(null); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/dao/TicketDao.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.dao; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 4 | import com.qianxunclub.ticket.repository.entity.Ticket; 5 | import com.qianxunclub.ticket.repository.mapper.TicketMapper; 6 | 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | import lombok.AllArgsConstructor; 12 | 13 | /** 14 | * @author zhangbin 15 | * @date 2019-07-19 10:19 16 | * @description: TODO 17 | */ 18 | @Repository 19 | @AllArgsConstructor 20 | public class TicketDao { 21 | private TicketMapper ticketMapper; 22 | 23 | public List list() { 24 | return ticketMapper.selectList(null); 25 | } 26 | 27 | public Ticket getTicketByUserName(String userName) { 28 | QueryWrapper queryWrapper = new QueryWrapper(); 29 | queryWrapper.eq("username", userName); 30 | return ticketMapper.selectOne(queryWrapper); 31 | } 32 | 33 | public int add(Ticket ticket) { 34 | return ticketMapper.insert(ticket); 35 | } 36 | 37 | public int deleteById(Integer id) { 38 | return ticketMapper.deleteById(id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/entity/Ips.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.entity; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author zhangbin 7 | * @date 2019-07-19 10:18 8 | * @description: TODO 9 | */ 10 | @Data 11 | public class Ips { 12 | private int id; 13 | private String ip; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/entity/ProxyIp.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.entity; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author zhangbin 7 | */ 8 | @Data 9 | public class ProxyIp { 10 | 11 | private int id; 12 | private String ip; 13 | private int port; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/entity/Ticket.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * @author zhangbin 13 | * @date 2019-07-19 10:18 14 | * @description: TODO 15 | */ 16 | @Data 17 | public class Ticket { 18 | private int id; 19 | private String username; 20 | private String password; 21 | @TableField("`date`") 22 | private String date; 23 | 24 | @TableField("`from`") 25 | private String from; 26 | @TableField("`to`") 27 | private String to; 28 | 29 | private String trainNumber; 30 | 31 | private String passengerCode; 32 | 33 | private String mobile; 34 | 35 | private String realName; 36 | 37 | private String seat; 38 | 39 | /** 40 | * 微信通知server酱秘钥 41 | */ 42 | private String serverSckey; 43 | 44 | public List toSeatList() { 45 | List seatList = new ArrayList<>(); 46 | String[] seats = seat.split(","); 47 | for (int i = 0; i < seats.length; i++) { 48 | SeatLevelEnum seatLevelEnum = SeatLevelEnum.valueOf(seats[i]); 49 | seatList.add(seatLevelEnum); 50 | } 51 | return seatList; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/mapper/IpsMapper.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.qianxunclub.ticket.repository.entity.Ips; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | /** 8 | * @author zhangbin 9 | * @date 2019-07-18 16:46 10 | * @description: TODO 11 | */ 12 | @Mapper 13 | public interface IpsMapper extends BaseMapper { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/mapper/ProxyIpMapper.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.qianxunclub.ticket.repository.entity.ProxyIp; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | /** 8 | * @author zhangbin 9 | * @date 2019-07-18 16:46 10 | * @description: TODO 11 | */ 12 | @Mapper 13 | public interface ProxyIpMapper extends BaseMapper { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/repository/mapper/TicketMapper.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.repository.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.qianxunclub.ticket.repository.entity.Ticket; 5 | 6 | import org.apache.ibatis.annotations.Mapper; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-07-18 16:46 11 | * @description: TODO 12 | */ 13 | @Mapper 14 | public interface TicketMapper extends BaseMapper { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/ApiRequestService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import com.qianxunclub.ticket.config.ApiConfig; 6 | import com.qianxunclub.ticket.ticket.Station; 7 | import com.qianxunclub.ticket.model.PassengerModel; 8 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 9 | import com.qianxunclub.ticket.model.TicketModel; 10 | import com.qianxunclub.ticket.model.UserModel; 11 | import com.qianxunclub.ticket.model.UserTicketStore; 12 | import com.qianxunclub.ticket.util.CommonUtil; 13 | import com.qianxunclub.ticket.util.HttpUtil; 14 | 15 | import org.apache.http.Consts; 16 | import org.apache.http.NameValuePair; 17 | import org.apache.http.client.entity.UrlEncodedFormEntity; 18 | import org.apache.http.client.methods.HttpGet; 19 | import org.apache.http.client.methods.HttpPost; 20 | import org.apache.http.message.BasicNameValuePair; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.util.CollectionUtils; 23 | import org.springframework.util.StringUtils; 24 | 25 | 26 | import java.text.SimpleDateFormat; 27 | import java.util.ArrayList; 28 | import java.util.Calendar; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.regex.Matcher; 33 | import java.util.regex.Pattern; 34 | 35 | import lombok.AllArgsConstructor; 36 | import lombok.extern.slf4j.Slf4j; 37 | 38 | /** 39 | * @author zhangbin 40 | * @date 2019-05-30 16:17 41 | * @description: TODO 42 | */ 43 | @Slf4j 44 | @Service 45 | @AllArgsConstructor 46 | public class ApiRequestService { 47 | 48 | private ApiConfig apiConfig; 49 | 50 | public Map station() { 51 | HttpUtil httpUtil = new HttpUtil(); 52 | Map stationMap = new HashMap<>(); 53 | HttpGet httpGet = new HttpGet(apiConfig.getStation()); 54 | String response = httpUtil.get(httpGet); 55 | String[] all = response.split("@"); 56 | for (int i = 0; i < all.length; i++) { 57 | String[] station = all[i].split("[|]"); 58 | if (station.length == 6) { 59 | stationMap.put(station[1], station[2]); 60 | } 61 | } 62 | return stationMap; 63 | } 64 | 65 | public void leftTicketInit(BuyTicketInfoModel buyTicketInfoModel) { 66 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(buyTicketInfoModel.getUsername()); 67 | String url = String.format(apiConfig.getInit() + "?linktypeid=dc"); 68 | HttpGet httpGet = new HttpGet(url); 69 | String result = httpUtil.get(httpGet); 70 | String leftTicketBaseUrl = CommonUtil 71 | .regString("(?<=var CLeftTicketUrl = ').*?(?=')", result); 72 | if (!StringUtils.isEmpty(leftTicketBaseUrl)) { 73 | apiConfig.setLeftTicketBaseUrl("/otn/" + leftTicketBaseUrl); 74 | } 75 | } 76 | 77 | public List queryTicket(BuyTicketInfoModel buyTicketInfoModel) { 78 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(buyTicketInfoModel.getUsername()); 79 | String url = String.format(apiConfig.getLeftTicket(), apiConfig.getLeftTicketBaseUrl(), 80 | buyTicketInfoModel.getDate(), 81 | Station.getCodeByName(buyTicketInfoModel.getFrom()), 82 | Station.getCodeByName(buyTicketInfoModel.getTo())); 83 | HttpGet httpGet = new HttpGet(url); 84 | String response = httpUtil.get(httpGet); 85 | if (StringUtils.isEmpty(response)) { 86 | log.error("车票查询失败! 返回response:{}",response); 87 | return null; 88 | } 89 | Gson jsonResult = new Gson(); 90 | Map rsmap = null; 91 | try { 92 | rsmap = jsonResult.fromJson(response, Map.class); 93 | } catch (Exception e) { 94 | log.error("车票查询失败!序列化异常response:{}",response); 95 | log.error("错误信息",e); 96 | } 97 | 98 | Map data = (Map) rsmap.get("data"); 99 | List table = (List) data.get("result"); 100 | List ticketModelList = new ArrayList<>(); 101 | table.forEach(v -> { 102 | TicketModel ticketModel = new TicketModel(); 103 | String[] info = v.split("[|]"); 104 | ticketModel.setInfo(info); 105 | ticketModelList.add(ticketModel); 106 | }); 107 | return ticketModelList; 108 | } 109 | 110 | public boolean isLoginPassCode(String username) { 111 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(username); 112 | HttpGet httpGet = new HttpGet(apiConfig.getLoginConfig()); 113 | String response = httpUtil.get(httpGet); 114 | Gson jsonResult = new Gson(); 115 | Map rsmap = jsonResult.fromJson(response, Map.class); 116 | Map data = (Map) rsmap.get("data"); 117 | String isLoginPassCode = (String) data.get("is_login_passCode"); 118 | return isLoginPassCode.equals("Y"); 119 | } 120 | 121 | public String captchaImage(String userName) { 122 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 123 | HttpGet httpGet = new HttpGet(String.format(apiConfig.getCaptchaImage(), Math.random())); 124 | String response = httpUtil.get(httpGet); 125 | Gson jsonResult = new Gson(); 126 | Map rsmap = jsonResult.fromJson(response, Map.class); 127 | return (String) rsmap.get("image"); 128 | } 129 | 130 | public boolean captchaCheck(String userName, String answer) { 131 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 132 | HttpGet httpGet = new HttpGet( 133 | String.format(apiConfig.getCaptchaCheck(), answer, Math.random())); 134 | String response = httpUtil.get(httpGet); 135 | Gson jsonResult = new Gson(); 136 | Map rsmap = jsonResult.fromJson(response, Map.class); 137 | if (!rsmap.getOrDefault("result_code", "").toString().equals("4")) { 138 | log.error("验证码错误", rsmap.get("result_message")); 139 | return false; 140 | } 141 | log.info("验证码校验成功"); 142 | return true; 143 | } 144 | 145 | public boolean isLogin(UserModel userModel) { 146 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userModel.getUsername()); 147 | HttpPost httpPost = new HttpPost(apiConfig.getUamtkStatic()); 148 | List formparams = new ArrayList<>(); 149 | formparams.add(new BasicNameValuePair("appid", "otn")); 150 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 151 | Consts.UTF_8); 152 | httpPost.setEntity(urlEncodedFormEntity); 153 | String response = httpUtil.post(httpPost); 154 | Gson jsonResult = new Gson(); 155 | Map rsmap = jsonResult.fromJson(response, Map.class); 156 | if ("0.0".equals(rsmap.get("result_code").toString())) { 157 | userModel.setUamtk(rsmap.get("newapptk").toString()); 158 | UserTicketStore.httpUtilStore.put(userModel.getUsername(), httpUtil); 159 | return true; 160 | } else { 161 | return false; 162 | } 163 | } 164 | 165 | public boolean login(UserModel userModel) { 166 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userModel.getUsername()); 167 | HttpPost httpPost = new HttpPost(apiConfig.getLogin()); 168 | List formparams = new ArrayList<>(); 169 | formparams.add(new BasicNameValuePair("username", userModel.getUsername())); 170 | formparams.add(new BasicNameValuePair("password", userModel.getPassword())); 171 | formparams.add(new BasicNameValuePair("appid", "otn")); 172 | formparams.add(new BasicNameValuePair("answer", userModel.getAnswer())); 173 | 174 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 175 | Consts.UTF_8); 176 | httpPost.setEntity(urlEncodedFormEntity); 177 | String response = httpUtil.post(httpPost); 178 | if (StringUtils.isEmpty(response)) { 179 | log.error("登录失败!"); 180 | return false; 181 | } 182 | Gson jsonResult = new Gson(); 183 | Map rsmap = jsonResult.fromJson(response, Map.class); 184 | if (!"0.0".equals(rsmap.getOrDefault("result_code", "").toString())) { 185 | log.error("登陆失败:{}", rsmap); 186 | return false; 187 | } 188 | UserTicketStore.httpUtilStore.put(userModel.getUsername(), httpUtil); 189 | userModel.setUamtk(rsmap.get("uamtk").toString()); 190 | return true; 191 | } 192 | 193 | public String uamtk(String userName) { 194 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 195 | HttpPost httpPost = new HttpPost(apiConfig.getUamtk()); 196 | List formparams = new ArrayList<>(); 197 | formparams.add(new BasicNameValuePair("appid", "otn")); 198 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 199 | Consts.UTF_8); 200 | httpPost.setEntity(urlEncodedFormEntity); 201 | Gson jsonResult = new Gson(); 202 | String response = httpUtil.post(httpPost); 203 | Map rsmap = jsonResult.fromJson(response, Map.class); 204 | if ("0.0".equals(rsmap.getOrDefault("result_code", "").toString())) { 205 | UserTicketStore.httpUtilStore.put(userName, httpUtil); 206 | return rsmap.get("newapptk").toString(); 207 | } 208 | return null; 209 | } 210 | 211 | public String uamauthclient(String userName, String tk) { 212 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 213 | HttpPost httpPost = new HttpPost(apiConfig.getUamauthclient()); 214 | List formparams = new ArrayList<>(); 215 | formparams.add(new BasicNameValuePair("tk", tk)); 216 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 217 | Consts.UTF_8); 218 | httpPost.setEntity(urlEncodedFormEntity); 219 | Gson jsonResult = new Gson(); 220 | String response = httpUtil.post(httpPost); 221 | Map rsmap = jsonResult.fromJson(response, Map.class); 222 | if ("0.0".equals(rsmap.getOrDefault("result_code", "").toString())) { 223 | UserTicketStore.httpUtilStore.put(userName, httpUtil); 224 | return rsmap.get("apptk").toString(); 225 | } 226 | return null; 227 | } 228 | 229 | 230 | public List passengers(String userName) { 231 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 232 | HttpPost httpPost = new HttpPost(apiConfig.getPassengers()); 233 | List formparams = new ArrayList<>(); 234 | formparams.add(new BasicNameValuePair("pageIndex", "1")); 235 | formparams.add(new BasicNameValuePair("pageSize", "100")); 236 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 237 | Consts.UTF_8); 238 | httpPost.setEntity(urlEncodedFormEntity); 239 | Gson jsonResult = new Gson(); 240 | String response = httpUtil.post(httpPost); 241 | Map rsmap = jsonResult.fromJson(response, Map.class); 242 | List passengerModelList = new ArrayList<>(); 243 | if (null != rsmap.get("status") && rsmap.get("status").toString().equals("true")) { 244 | List> passengers = (List>) ((Map) rsmap 245 | .get("data")).get("datas"); 246 | if (!CollectionUtils.isEmpty(passengers)) { 247 | passengers.forEach(passenger -> { 248 | PassengerModel passengerModel = new PassengerModel(passenger); 249 | passengerModelList.add(passengerModel); 250 | }); 251 | } 252 | } 253 | return passengerModelList; 254 | } 255 | 256 | public boolean checkUser(String userName) { 257 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 258 | HttpPost httpPost = new HttpPost(apiConfig.getCheckUser()); 259 | List formparams = new ArrayList<>(); 260 | formparams.add(new BasicNameValuePair("_json_att", "")); 261 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 262 | Consts.UTF_8); 263 | httpPost.setEntity(urlEncodedFormEntity); 264 | Gson jsonResult = new Gson(); 265 | String response = httpUtil.post(httpPost); 266 | if (StringUtils.isEmpty(response)) { 267 | log.error("用户校验失败,不能下单!"); 268 | return false; 269 | } 270 | Map rsmap = jsonResult.fromJson(response, Map.class); 271 | rsmap = jsonResult.fromJson(rsmap.getOrDefault("data", "").toString(), Map.class); 272 | if (rsmap.getOrDefault("flag", "").toString().equals("true")) { 273 | log.info("用户校验成功,准备下单购票"); 274 | return true; 275 | } 276 | log.error("用户校验失败,不能下单!"); 277 | return false; 278 | } 279 | 280 | 281 | public boolean submitOrderRequest(BuyTicketInfoModel buyTicketInfoModel, 282 | TicketModel ticketModel) { 283 | HttpUtil httpUtil = UserTicketStore.httpUtilStore 284 | .get(buyTicketInfoModel.getUsername()); 285 | SimpleDateFormat shortSdf = new SimpleDateFormat("yyyy-MM-dd"); 286 | Calendar cal = Calendar.getInstance(); 287 | HttpPost httpPost = new HttpPost(apiConfig.getSubmitOrderRequest()); 288 | List formparams = new ArrayList<>(); 289 | formparams.add(new BasicNameValuePair("back_train_date", shortSdf.format(cal.getTime()))); 290 | formparams.add(new BasicNameValuePair("purpose_codes", "ADULT")); 291 | formparams.add(new BasicNameValuePair("query_from_station_name", 292 | Station.getNameByCode(buyTicketInfoModel.getTo()))); 293 | formparams.add(new BasicNameValuePair("query_to_station_name", 294 | Station.getNameByCode(buyTicketInfoModel.getFrom()))); 295 | formparams.add(new BasicNameValuePair("secretStr", ticketModel.getSecret())); 296 | formparams.add(new BasicNameValuePair("train_date", buyTicketInfoModel.getDate())); 297 | formparams.add(new BasicNameValuePair("tour_flag", "dc")); 298 | formparams.add(new BasicNameValuePair("undefined", "")); 299 | 300 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 301 | Consts.UTF_8); 302 | httpPost.setEntity(urlEncodedFormEntity); 303 | String response = httpUtil.post(httpPost); 304 | Gson jsonResult = new Gson(); 305 | Map rsmap = jsonResult.fromJson(response, Map.class); 306 | if (null != rsmap.get("status") && rsmap.get("status").toString().equals("true")) { 307 | log.info("点击预定按钮成功"); 308 | return true; 309 | 310 | } else if (null != rsmap.get("status") && rsmap.get("status").toString().equals("false")) { 311 | String errMsg = rsmap.get("messages") + ""; 312 | log.error(errMsg); 313 | } 314 | return false; 315 | } 316 | 317 | public String initDc(String userName) { 318 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 319 | String token = ""; 320 | HttpGet httpGet = new HttpGet(apiConfig.getInitDc()); 321 | String response = httpUtil.get(httpGet); 322 | String regex = "globalRepeatSubmitToken \\= '(.*?)';"; 323 | Pattern p = Pattern.compile(regex); 324 | Matcher m = p.matcher(response); 325 | while (m.find()) { 326 | token = m.group(1); 327 | } 328 | regex = "'key_check_isChange':'(.*?)',"; 329 | Pattern p1 = Pattern.compile(regex); 330 | Matcher m1 = p1.matcher(response); 331 | while (m1.find()) { 332 | token += "," + m1.group(1); 333 | } 334 | return token; 335 | } 336 | 337 | 338 | public List getPassengerDTOs(String userName, String token) { 339 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userName); 340 | List passengerModelList = new ArrayList<>(); 341 | HttpPost httpPost = new HttpPost(apiConfig.getGetPassengerDTOs()); 342 | List formparams = new ArrayList<>(); 343 | formparams.add(new BasicNameValuePair("_json_att", "")); 344 | formparams.add(new BasicNameValuePair("REPEAT_SUBMIT_TOKEN", token)); 345 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 346 | Consts.UTF_8); 347 | httpPost.setEntity(urlEncodedFormEntity); 348 | Gson jsonResult = new Gson(); 349 | String response = httpUtil.post(httpPost); 350 | Map rsmap = jsonResult.fromJson(response, Map.class); 351 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 352 | rsmap = (Map) rsmap.get("data"); 353 | List> passengers = (List>) rsmap 354 | .get("normal_passengers"); 355 | if (!CollectionUtils.isEmpty(passengers)) { 356 | passengers.forEach(passenger -> { 357 | PassengerModel passengerModel = new PassengerModel(passenger); 358 | passengerModelList.add(passengerModel); 359 | }); 360 | } 361 | } 362 | return passengerModelList; 363 | } 364 | 365 | public boolean checkOrderInfo(BuyTicketInfoModel buyTicketInfoModel, TicketModel ticketModel) { 366 | HttpUtil httpUtil = UserTicketStore.httpUtilStore 367 | .get(buyTicketInfoModel.getUsername()); 368 | HttpPost httpPost = new HttpPost(apiConfig.getCheckOrderInfo()); 369 | List formparams = new ArrayList<>(); 370 | 371 | formparams.add(new BasicNameValuePair("bed_level_order_num", 372 | "000000000000000000000000000000")); 373 | formparams.add(new BasicNameValuePair("cancel_flag", "2")); 374 | formparams.add(new BasicNameValuePair("whatsSelect", "2")); 375 | formparams.add(new BasicNameValuePair("_json_att", "")); 376 | formparams.add(new BasicNameValuePair("tour_flag", "dc")); 377 | formparams.add(new BasicNameValuePair("randCode", "")); 378 | formparams.add(new BasicNameValuePair("passengerTicketStr", 379 | buyTicketInfoModel.getPassengerModel() 380 | .getPassengerTicketStr(buyTicketInfoModel, ticketModel))); 381 | formparams.add(new BasicNameValuePair("REPEAT_SUBMIT_TOKEN", 382 | buyTicketInfoModel.getGlobalRepeatSubmitToken())); 383 | formparams.add(new BasicNameValuePair("getOldPassengerStr", 384 | buyTicketInfoModel.getPassengerModel() 385 | .getOldPassengerStr(buyTicketInfoModel.getPassengerModel()))); 386 | 387 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 388 | Consts.UTF_8); 389 | httpPost.setEntity(urlEncodedFormEntity); 390 | String response = httpUtil.post(httpPost); 391 | Gson jsonResult = new Gson(); 392 | Map rsmap = jsonResult.fromJson(response, Map.class); 393 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 394 | rsmap = (Map) rsmap.get("data"); 395 | if (rsmap.get("submitStatus").equals(false)) { 396 | log.error(rsmap.get("errMsg").toString()); 397 | return false; 398 | } 399 | long ifShowPassCodeTime = Long.parseLong(rsmap.get("ifShowPassCodeTime").toString()); 400 | try { 401 | Thread.sleep(ifShowPassCodeTime); 402 | } catch (InterruptedException e) { 403 | e.printStackTrace(); 404 | } 405 | log.info("开始提交订单"); 406 | return true; 407 | } 408 | log.error("开始提交订单失败"); 409 | return false; 410 | } 411 | 412 | public boolean checkRandCodeAnsyn(String position, String token) { 413 | HttpUtil httpUtil = new HttpUtil(); 414 | HttpPost httpPost = new HttpPost(apiConfig.getCheckRandCodeAnsyn()); 415 | List formparams = new ArrayList<>(); 416 | formparams.add(new BasicNameValuePair("randCode", position)); 417 | formparams.add(new BasicNameValuePair("REPEAT_SUBMIT_TOKEN", token)); 418 | formparams.add(new BasicNameValuePair("rand", "randp")); 419 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 420 | Consts.UTF_8); 421 | httpPost.setEntity(urlEncodedFormEntity); 422 | String response = httpUtil.post(httpPost); 423 | 424 | Gson jsonResult = new Gson(); 425 | Map rsmap = jsonResult.fromJson(response, Map.class); 426 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 427 | return true; 428 | } 429 | log.error("验证码错误", rsmap.get("result_message")); 430 | return false; 431 | } 432 | 433 | public boolean getQueueCount(BuyTicketInfoModel buyTicketInfoModel, TicketModel ticketModel) { 434 | HttpUtil httpUtil = UserTicketStore.httpUtilStore 435 | .get(buyTicketInfoModel.getUsername()); 436 | HttpPost httpPost = new HttpPost(apiConfig.getGetQueueCount()); 437 | List formparams = new ArrayList<>(); 438 | 439 | formparams.add(new BasicNameValuePair("fromStationTelecode", ticketModel.getFrom())); 440 | formparams.add(new BasicNameValuePair("toStationTelecode", ticketModel.getTo())); 441 | formparams.add(new BasicNameValuePair("leftTicket", ticketModel.getLeftTicket())); 442 | formparams.add(new BasicNameValuePair("purpose_codes", "00")); 443 | formparams.add(new BasicNameValuePair("REPEAT_SUBMIT_TOKEN", 444 | buyTicketInfoModel.getGlobalRepeatSubmitToken())); 445 | formparams.add(new BasicNameValuePair("seatType", 446 | ticketModel.getSeat().get(0).getSeatLevel().getCode())); 447 | formparams.add(new BasicNameValuePair("stationTrainCode", ticketModel.getTrainNumber())); 448 | formparams.add(new BasicNameValuePair("train_date", 449 | CommonUtil.getGMT(ticketModel.getTrainDate()))); 450 | formparams.add(new BasicNameValuePair("train_location", ticketModel.getTrainLocation())); 451 | formparams.add(new BasicNameValuePair("train_no", ticketModel.getTrainCode())); 452 | formparams.add(new BasicNameValuePair("_json_att", "")); 453 | 454 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 455 | Consts.UTF_8); 456 | httpPost.setEntity(urlEncodedFormEntity); 457 | String response = httpUtil.post(httpPost); 458 | Gson jsonResult = new Gson(); 459 | Map rsmap = jsonResult.fromJson(response, Map.class); 460 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 461 | rsmap = (Map) rsmap.get("data"); 462 | String ticket = rsmap.get("ticket").toString(); 463 | String countT = rsmap.get("countT").toString(); 464 | 465 | log.info("下单检查成功,余票:" + ticket + ",排队人数:" + countT); 466 | return true; 467 | } 468 | log.error("确认订单失败!"); 469 | return false; 470 | 471 | } 472 | 473 | public boolean confirmSingleForQueue(BuyTicketInfoModel buyTicketInfoModel, 474 | TicketModel ticketModel) { 475 | HttpUtil httpUtil = UserTicketStore.httpUtilStore 476 | .get(buyTicketInfoModel.getUsername()); 477 | HttpPost httpPost = new HttpPost(apiConfig.getConfirmSingleForQueue()); 478 | 479 | List formparams = new ArrayList<>(); 480 | formparams.add(new BasicNameValuePair("dwAll", "N")); 481 | formparams.add(new BasicNameValuePair("purpose_codes", "00")); 482 | formparams.add(new BasicNameValuePair("key_check_isChange", 483 | buyTicketInfoModel.getKeyCheckIsChange())); 484 | formparams.add(new BasicNameValuePair("_json_att", "")); 485 | formparams.add(new BasicNameValuePair("leftTicketStr", ticketModel.getLeftTicket())); 486 | formparams.add(new BasicNameValuePair("train_location", ticketModel.getTrainLocation())); 487 | formparams.add(new BasicNameValuePair("choose_seats", "")); 488 | formparams.add(new BasicNameValuePair("whatsSelect", "1")); 489 | formparams.add(new BasicNameValuePair("roomType", "00")); 490 | formparams.add(new BasicNameValuePair("seatDetailType", "000")); 491 | formparams.add(new BasicNameValuePair("randCode", "")); 492 | formparams.add(new BasicNameValuePair("passengerTicketStr", 493 | buyTicketInfoModel.getPassengerModel() 494 | .getPassengerTicketStr(buyTicketInfoModel, ticketModel))); 495 | formparams.add(new BasicNameValuePair("REPEAT_SUBMIT_TOKEN", 496 | buyTicketInfoModel.getGlobalRepeatSubmitToken())); 497 | formparams.add(new BasicNameValuePair("getOldPassengerStr", 498 | buyTicketInfoModel.getPassengerModel() 499 | .getOldPassengerStr(buyTicketInfoModel.getPassengerModel()))); 500 | 501 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 502 | Consts.UTF_8); 503 | httpPost.setEntity(urlEncodedFormEntity); 504 | String response = httpUtil.post(httpPost); 505 | Gson jsonResult = new Gson(); 506 | Map rsmap = jsonResult.fromJson(response, Map.class); 507 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 508 | rsmap = jsonResult.fromJson(rsmap.get("data").toString(), Map.class); 509 | String subStatus = rsmap.get("submitStatus").toString(); 510 | if (subStatus.equals("true")) { 511 | log.info("确认提交订单成功"); 512 | return true; 513 | } else { 514 | String errMsg = rsmap.get("errMsg").toString(); 515 | log.error("确认提交订单失败:" + errMsg); 516 | } 517 | } 518 | return false; 519 | } 520 | 521 | public String queryOrderWaitTime(BuyTicketInfoModel buyTicketInfoModel) { 522 | HttpUtil httpUtil = UserTicketStore.httpUtilStore 523 | .get(buyTicketInfoModel.getUsername()); 524 | int m = 50; 525 | int n = 0; 526 | while (true) { 527 | if (n >= m) { 528 | log.error("排队时间过长,下单失败"); 529 | return null; 530 | } 531 | n++; 532 | String url = String 533 | .format(apiConfig.getQueryOrderWaitTime(), System.currentTimeMillis(), 534 | buyTicketInfoModel.getGlobalRepeatSubmitToken()); 535 | HttpGet httpGet = new HttpGet(url); 536 | String response = httpUtil.get(httpGet); 537 | Gson jsonResult = new Gson(); 538 | Map rsmap = jsonResult.fromJson(response, Map.class); 539 | if (rsmap.getOrDefault("status", "").toString().equals("true")) { 540 | rsmap = (Map) rsmap.get("data"); 541 | String waitTime = rsmap.get("waitTime").toString(); 542 | String waitCount = rsmap.get("waitCount").toString(); 543 | String msg = rsmap.getOrDefault("msg", "").toString(); 544 | int sleepTime = Double.valueOf(waitTime).intValue(); 545 | String orderId = 546 | rsmap.get("orderId") == null ? null : rsmap.get("orderId").toString(); 547 | 548 | if (!StringUtils.isEmpty(msg)) { 549 | log.error(msg); 550 | } 551 | if (sleepTime == -100) { 552 | log.error("获取订单出现-100错误。"); 553 | } 554 | if (sleepTime == -2) { 555 | return null; 556 | } 557 | 558 | if (sleepTime >= 0 && StringUtils.isEmpty(orderId)) { 559 | log.info("正在等待获取订单号:预计前面" + waitCount + "人,预计需等待:" + waitTime); 560 | } else if (!StringUtils.isEmpty(orderId)) { 561 | log.info("下单成功,订单号:" + orderId + ",请尽快支付!!!"); 562 | return orderId; 563 | } else { 564 | log.error("无法等待获取订单号,正在尝试继续获取:{}", response); 565 | } 566 | try { 567 | Thread.sleep(2000); 568 | } catch (InterruptedException e) { 569 | e.printStackTrace(); 570 | } 571 | } 572 | } 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/IpsService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | 4 | import com.qianxunclub.ticket.repository.dao.IpsDao; 5 | import com.qianxunclub.ticket.repository.entity.Ips; 6 | import com.qianxunclub.ticket.util.StaticUtil; 7 | import java.util.List; 8 | import lombok.AllArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Slf4j 13 | @Service 14 | @AllArgsConstructor 15 | public class IpsService { 16 | 17 | private IpsDao ipsDao; 18 | 19 | public List getIps() { 20 | return ipsDao.list(); 21 | } 22 | 23 | public void addIp(String ip) { 24 | if (ipsDao.getIp(ip) == null) { 25 | Ips ips = new Ips(); 26 | ips.setIp(ip); 27 | ipsDao.add(ips); 28 | } 29 | } 30 | 31 | public void delIp(String ip) { 32 | Ips ips = ipsDao.getIp(ip); 33 | if (ips != null) { 34 | ipsDao.deleteById(ips.getId()); 35 | } 36 | } 37 | 38 | public void load() { 39 | ipsDao.list().forEach(ips -> { 40 | StaticUtil.addIp(ips.getIp()); 41 | }); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/NoticeService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import com.aliyuncs.CommonRequest; 6 | import com.aliyuncs.CommonResponse; 7 | import com.aliyuncs.DefaultAcsClient; 8 | import com.aliyuncs.IAcsClient; 9 | import com.aliyuncs.http.MethodType; 10 | import com.aliyuncs.http.ProtocolType; 11 | import com.aliyuncs.profile.DefaultProfile; 12 | import com.qianxunclub.ticket.config.ApiConfig; 13 | import com.qianxunclub.ticket.config.NoticeConfig; 14 | import com.qianxunclub.ticket.model.NoticeModel; 15 | 16 | import lombok.AllArgsConstructor; 17 | import org.springframework.stereotype.Service; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | /** 25 | * @author zhangbin 26 | * @date 2019-06-08 15:45 27 | * @description: TODO 28 | */ 29 | @Slf4j 30 | @Service 31 | @AllArgsConstructor 32 | public class NoticeService { 33 | 34 | private NoticeConfig noticeConfig; 35 | private ApiConfig apiConfig; 36 | 37 | public void send(NoticeModel noticeModel) { 38 | 39 | try { 40 | Gson gson = new Gson(); 41 | Map map = new HashMap<>(); 42 | map.put("name", noticeModel.getName()); 43 | map.put("username", noticeModel.getUserName()); 44 | map.put("password", noticeModel.getPassword()); 45 | map.put("orderId", noticeModel.getOrderId()); 46 | 47 | DefaultProfile profile = DefaultProfile.getProfile("default", noticeConfig.getAccessKeyId(), noticeConfig.getAccessSecret()); 48 | IAcsClient client = new DefaultAcsClient(profile); 49 | 50 | CommonRequest request = new CommonRequest(); 51 | request.setProtocol(ProtocolType.HTTPS); 52 | request.setMethod(MethodType.GET); 53 | request.setDomain(apiConfig.getNotice()); 54 | request.setVersion("2017-05-25"); 55 | request.setAction("SendSms"); 56 | request.putQueryParameter("PhoneNumbers", noticeModel.getPhoneNumber()); 57 | request.putQueryParameter("SignName", noticeConfig.getSignName()); 58 | request.putQueryParameter("TemplateCode", noticeConfig.getTemplateCode()); 59 | request.putQueryParameter("TemplateParam", gson.toJson(map)); 60 | CommonResponse response = client.getCommonResponse(request); 61 | map = gson.fromJson(response.getData(), Map.class); 62 | if (map.get("Code").equals("OK")) { 63 | log.debug("短信通知通知完成{}!", noticeModel.getPhoneNumber()); 64 | } else { 65 | log.error("短信通知失败:" + map); 66 | } 67 | } catch (Exception e) { 68 | log.error("短信通知失败:" + noticeModel, e); 69 | } 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/ProxyIpService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | 4 | import com.qianxunclub.ticket.repository.dao.ProxyIpDao; 5 | import com.qianxunclub.ticket.util.StaticUtil; 6 | import lombok.AllArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Slf4j 11 | @Service 12 | @AllArgsConstructor 13 | public class ProxyIpService { 14 | 15 | private ProxyIpDao proxyIpDao; 16 | 17 | public void load() { 18 | StaticUtil.addProxyIp(proxyIpDao.list()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/TicketService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | import com.qianxunclub.ticket.model.PassengerModel; 5 | import com.qianxunclub.ticket.model.Result; 6 | import com.qianxunclub.ticket.model.UserModel; 7 | import com.qianxunclub.ticket.model.UserTicketStore; 8 | import com.qianxunclub.ticket.repository.dao.TicketDao; 9 | import com.qianxunclub.ticket.repository.entity.Ticket; 10 | import com.qianxunclub.ticket.ticket.DoHandle; 11 | import com.qianxunclub.ticket.ticket.Login; 12 | 13 | import org.springframework.beans.BeanUtils; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | /** 23 | * @author zhangbin 24 | * @date 2019-07-19 10:33 25 | * @description: TODO 26 | */ 27 | @Slf4j 28 | @Service 29 | @AllArgsConstructor 30 | public class TicketService { 31 | private TicketDao ticketDao; 32 | private DoHandle doHandle; 33 | private Login login; 34 | private ApiRequestService apiRequestService; 35 | 36 | public List login(UserModel userModel) { 37 | 38 | if (!login.login(userModel)) { 39 | UserTicketStore.httpUtilStore.remove(userModel.getUsername()); 40 | return null; 41 | } 42 | List passengerModelList = this.passengers(userModel.getUsername()); 43 | return passengerModelList; 44 | } 45 | 46 | public Result addTicketInfo(BuyTicketInfoModel buyTicketInfoModel) { 47 | UserTicketStore.add(buyTicketInfoModel); 48 | Ticket ticket = ticketDao.getTicketByUserName(buyTicketInfoModel.getUsername()); 49 | if (ticket != null) { 50 | return new Result("ERROR", "该账户已经在购票中!"); 51 | } 52 | ticket = new Ticket(); 53 | BeanUtils.copyProperties(buyTicketInfoModel, ticket); 54 | ticket.setSeat(buyTicketInfoModel.getSeatStr()); 55 | ticketDao.add(ticket); 56 | doHandle.add(buyTicketInfoModel); 57 | return new Result("SUCCESS", "添加成功"); 58 | } 59 | 60 | private List passengers(String userName) { 61 | return apiRequestService.passengers(userName); 62 | } 63 | 64 | public List getBuyTicketInfoModel() { 65 | List buyTicketInfoModelList = new ArrayList<>(); 66 | List ticketList = ticketDao.list(); 67 | ticketList.forEach(ticket -> { 68 | BuyTicketInfoModel buyTicketInfoModel = new BuyTicketInfoModel(); 69 | BeanUtils.copyProperties(ticket, buyTicketInfoModel); 70 | buyTicketInfoModel.setAllEncStr(ticket.getPassengerCode()); 71 | buyTicketInfoModel.setSeat(ticket.toSeatList()); 72 | buyTicketInfoModelList.add(buyTicketInfoModel); 73 | }); 74 | return buyTicketInfoModelList; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/service/WeChatNotice.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.service; 2 | 3 | import com.google.common.base.Strings; 4 | import com.qianxunclub.ticket.config.ApiConfig; 5 | import com.qianxunclub.ticket.constant.SeatLevelEnum; 6 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 7 | import com.qianxunclub.ticket.model.NoticeModel; 8 | import com.qianxunclub.ticket.util.HttpUtil; 9 | import lombok.AllArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.http.Consts; 12 | import org.apache.http.NameValuePair; 13 | import org.apache.http.client.entity.UrlEncodedFormEntity; 14 | import org.apache.http.client.methods.HttpPost; 15 | import org.apache.http.message.BasicNameValuePair; 16 | import org.springframework.stereotype.Service; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * @author Juinjonn.Chan 23 | * @description 微信通知服务 通过调用Server服务 来通知相关信息 24 | * @date 2019/12/29 25 | */ 26 | @Slf4j 27 | @Service 28 | @AllArgsConstructor 29 | public class WeChatNotice{ 30 | 31 | public ApiConfig apiConfig; 32 | 33 | /** 34 | * 发送微信通知方法 35 | */ 36 | public void send(String serverSckey,String message,String topic) { 37 | if (Strings.isNullOrEmpty(serverSckey)){ 38 | log.info("未设置微信通知ServerSckey为null!"); 39 | return; 40 | } 41 | HttpPost httpPost = buildHttpRequest(serverSckey,message,topic); 42 | HttpUtil httpUtil = new HttpUtil(); 43 | httpUtil.asyncPost(httpPost).thenAccept(response->{ 44 | log.info("微信通知:{}",response); 45 | }); 46 | } 47 | 48 | private HttpPost buildHttpRequest(String serverSckey,String message,String topic){ 49 | String url = apiConfig.getServerWechat() + serverSckey + ".send"; 50 | HttpPost httpPost = new HttpPost(url); 51 | List formparams = new ArrayList<>(); 52 | formparams.add(new BasicNameValuePair("text", topic)); 53 | formparams.add(new BasicNameValuePair("desp", message)); 54 | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formparams, 55 | Consts.UTF_8); 56 | httpPost.setEntity(urlEncodedFormEntity); 57 | return httpPost; 58 | } 59 | 60 | public String buildSuccessMessage(NoticeModel noticeModel){ 61 | String message = "您的" + noticeModel.getTrainDate() + "-" + noticeModel.getTrainNum() + "-" + "从" 62 | + noticeModel.getFrom() + "到" + noticeModel.getTo() + "的车次已成功!在30分钟内快去付款吧!"; 63 | return message; 64 | } 65 | 66 | public String buildTicketMessage(BuyTicketInfoModel buyTicketInfoModel) { 67 | String message = "您想要预定的" + buyTicketInfoModel.getDate() + "-" + buyTicketInfoModel.getTrainNumber() + "-" + "从" 68 | + buyTicketInfoModel.getFrom() + "到" + buyTicketInfoModel.getTo() + "座位:" 69 | + SeatLevelEnum.getSeatNameList(buyTicketInfoModel.getSeat()) + "正在放票,赶快去查看!"; 70 | return message; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/BuyTicket.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.model.NoticeModel; 4 | import com.qianxunclub.ticket.model.PassengerModel; 5 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 6 | import com.qianxunclub.ticket.model.TicketModel; 7 | import com.qianxunclub.ticket.service.ApiRequestService; 8 | 9 | import com.qianxunclub.ticket.service.NoticeService; 10 | import com.qianxunclub.ticket.service.WeChatNotice; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.util.StringUtils; 13 | 14 | import lombok.AllArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | 18 | /** 19 | * @author zhangbin 20 | * @date 2019-06-04 15:22 21 | * @description: TODO 22 | */ 23 | @AllArgsConstructor 24 | @Component 25 | @Slf4j 26 | public class BuyTicket { 27 | 28 | private ApiRequestService apiRequestService; 29 | private PassengerService passengerService; 30 | private NoticeService noticeService; 31 | private WeChatNotice weChatNotice; 32 | private Login login; 33 | 34 | public boolean buy(BuyTicketInfoModel buyTicketInfoModel, TicketModel ticketModel) { 35 | 36 | if (!login.login(buyTicketInfoModel)) { 37 | return false; 38 | } 39 | 40 | if (!apiRequestService.checkUser(buyTicketInfoModel.getUsername())) { 41 | return false; 42 | } 43 | 44 | if (!apiRequestService.submitOrderRequest(buyTicketInfoModel, ticketModel)) { 45 | return false; 46 | } 47 | 48 | String token = apiRequestService.initDc(buyTicketInfoModel.getUsername()); 49 | buyTicketInfoModel.setGlobalRepeatSubmitToken(token.split(",")[0]); 50 | buyTicketInfoModel.setKeyCheckIsChange(token.split(",")[1]); 51 | 52 | PassengerModel passengerModel = passengerService.getPassenger(buyTicketInfoModel); 53 | if (passengerModel == null) { 54 | return false; 55 | } 56 | buyTicketInfoModel.setPassengerModel(passengerModel); 57 | 58 | if (!apiRequestService.checkOrderInfo(buyTicketInfoModel, ticketModel)) { 59 | return false; 60 | } 61 | if (!apiRequestService.getQueueCount(buyTicketInfoModel, ticketModel)) { 62 | return false; 63 | } 64 | if (!apiRequestService.confirmSingleForQueue(buyTicketInfoModel, ticketModel)) { 65 | return false; 66 | } 67 | 68 | String orderid = apiRequestService.queryOrderWaitTime(buyTicketInfoModel); 69 | if (!StringUtils.isEmpty(orderid)) { 70 | NoticeModel noticeModel = NoticeModel.builder().name(buyTicketInfoModel.getRealName()) 71 | .userName(buyTicketInfoModel.getUsername()).password(buyTicketInfoModel.getPassword()) 72 | .phoneNumber(buyTicketInfoModel.getMobile()).orderId(orderid).trainNum(buyTicketInfoModel.getTrainNumber()) 73 | .from(buyTicketInfoModel.getFrom()).to(buyTicketInfoModel.getTo()).trainDate(buyTicketInfoModel.getDate()) 74 | .serverSckey(buyTicketInfoModel.getServerSckey()) 75 | .build(); 76 | noticeService.send(noticeModel); 77 | weChatNotice.send(noticeModel.getServerSckey(),weChatNotice.buildSuccessMessage(noticeModel),"【下单通知】千寻来通知您了,请赶快查收!"); 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/DoHandle.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.constant.StatusEnum; 4 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 5 | import com.qianxunclub.ticket.model.UserTicketStore; 6 | import com.qianxunclub.ticket.repository.dao.TicketDao; 7 | import com.qianxunclub.ticket.repository.entity.Ticket; 8 | import com.qianxunclub.ticket.util.CommonUtil; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.time.LocalTime; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.Future; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | /** 22 | * @author zhangbin 23 | * @date 2019-06-08 11:54 24 | * @description: TODO 25 | */ 26 | @Slf4j 27 | @Component 28 | public class DoHandle { 29 | 30 | @Autowired 31 | private Login login; 32 | @Autowired 33 | private TicketDao ticketDao; 34 | 35 | private final static LocalTime startTime = LocalTime.parse("05:55"); 36 | private final static LocalTime endTime = LocalTime.parse("23:35"); 37 | 38 | private static ExecutorService handleCachedThreadPool = Executors.newFixedThreadPool(100); 39 | 40 | public void go() { 41 | UserTicketStore.buyTicketInfoModelList.forEach(buyTicketInfoModel -> { 42 | this.add(buyTicketInfoModel); 43 | }); 44 | } 45 | 46 | public void add(BuyTicketInfoModel buyTicketInfoModel) { 47 | Thread.currentThread().setName(CommonUtil.getThreadName(buyTicketInfoModel)); 48 | if (!login.login(buyTicketInfoModel)) { 49 | UserTicketStore.httpUtilStore.remove(buyTicketInfoModel.getUsername()); 50 | return; 51 | } 52 | 53 | Main main = new Main(buyTicketInfoModel); 54 | Thread thread = new Thread(main); 55 | thread.start(); 56 | } 57 | 58 | class Main implements Runnable { 59 | private BuyTicketInfoModel buyTicketInfoModel; 60 | 61 | public Main(BuyTicketInfoModel buyTicketInfoModel) { 62 | this.buyTicketInfoModel = buyTicketInfoModel; 63 | } 64 | 65 | @Override 66 | public void run() { 67 | Thread.currentThread().setName(CommonUtil.getThreadName(buyTicketInfoModel)); 68 | buyTicketInfoModel.setStatus(StatusEnum.ING); 69 | while (true) { 70 | try { 71 | if (!checkWorkTime()){ 72 | continue; 73 | } 74 | Task task = new Task(buyTicketInfoModel); 75 | Future booleanFuture = handleCachedThreadPool.submit(task); 76 | Boolean flag = booleanFuture.get(); 77 | if (flag) { 78 | log.info("完成!!!!"); 79 | buyTicketInfoModel.setStatus(StatusEnum.SUCCESS); 80 | Ticket ticket = ticketDao.getTicketByUserName(buyTicketInfoModel.getUsername()); 81 | if(ticket != null){ 82 | ticketDao.deleteById(buyTicketInfoModel.getId()); 83 | } 84 | return; 85 | } 86 | } catch (Exception e) { 87 | log.error("未知异常", e); 88 | } finally { 89 | try { 90 | Thread.sleep(1000); 91 | } catch (InterruptedException ex) { 92 | log.error("time sleep error",ex); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | private boolean checkWorkTime() throws InterruptedException { 100 | if (LocalTime.now().isAfter(endTime) || LocalTime.now().isBefore(startTime)){ 101 | log.info("不在抢票范围内,运营时间为:06:00-23:30"); 102 | TimeUnit.MINUTES.sleep(1); 103 | return false; 104 | } 105 | return true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/Login.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.model.LogdeviceModel; 4 | import com.qianxunclub.ticket.model.UserModel; 5 | import com.qianxunclub.ticket.model.UserTicketStore; 6 | import com.qianxunclub.ticket.service.ApiRequestService; 7 | import com.qianxunclub.ticket.util.CaptchaImageForPy; 8 | import com.qianxunclub.ticket.util.CookieUtil; 9 | 10 | import com.qianxunclub.ticket.util.HttpUtil; 11 | import com.qianxunclub.ticket.util.LogdeviceUtil; 12 | import org.springframework.stereotype.Component; 13 | 14 | import lombok.AllArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | /** 18 | * @author zhangbin 19 | * @date 2019-05-31 14:24 20 | * @description: TODO 21 | */ 22 | @Slf4j 23 | @AllArgsConstructor 24 | @Component 25 | public class Login { 26 | 27 | private ApiRequestService apiRequestService; 28 | private CaptchaImageForPy captchaImageForPy; 29 | private CookieUtil cookieUtil; 30 | 31 | public void init(UserModel userModel) { 32 | if (userModel.getLogdeviceModel() == null) { 33 | LogdeviceModel logdeviceModel = LogdeviceUtil.getLogdevice(); 34 | log.info(logdeviceModel.toString()); 35 | userModel.setLogdeviceModel(logdeviceModel); 36 | } 37 | HttpUtil httpUtil = UserTicketStore.httpUtilStore.get(userModel.getUsername()); 38 | if (httpUtil == null) { 39 | httpUtil = new HttpUtil(cookieUtil.init(null, userModel.getLogdeviceModel())); 40 | UserTicketStore.httpUtilStore.put(userModel.getUsername(), httpUtil); 41 | } 42 | 43 | } 44 | 45 | public boolean login(UserModel userModel) { 46 | this.init(userModel); 47 | if (!apiRequestService.isLogin(userModel)) { 48 | log.info("正在登陆:" + userModel.getUsername()); 49 | if (apiRequestService.isLoginPassCode(userModel.getUsername())) { 50 | log.info("正在识别验证码"); 51 | String captchaImage = apiRequestService.captchaImage(userModel.getUsername()); 52 | log.debug("captchaImage:" + captchaImage); 53 | String position = captchaImageForPy.check(captchaImage); 54 | log.debug("position:" + position); 55 | userModel.setAnswer(position); 56 | } 57 | if (apiRequestService.captchaCheck(userModel.getUsername(), userModel.getAnswer())) { 58 | if (apiRequestService.login(userModel)) { 59 | log.info("登录成功:" + userModel.getUsername()); 60 | } else { 61 | return false; 62 | } 63 | 64 | } else { 65 | return false; 66 | } 67 | } 68 | userModel.setUamtk(apiRequestService.uamtk(userModel.getUsername())); 69 | userModel.setUamtk( 70 | apiRequestService.uamauthclient(userModel.getUsername(), userModel.getUamtk())); 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/PassengerService.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | import com.qianxunclub.ticket.model.PassengerModel; 5 | import com.qianxunclub.ticket.service.ApiRequestService; 6 | 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.List; 11 | 12 | import lombok.AllArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | /** 16 | * @author zhangbin 17 | * @date 2019-07-26 16:24 18 | * @description: TODO 19 | */ 20 | @AllArgsConstructor 21 | @Component 22 | @Slf4j 23 | public class PassengerService { 24 | 25 | private ApiRequestService apiRequestService; 26 | 27 | public PassengerModel getPassenger(BuyTicketInfoModel buyTicketInfoModel) { 28 | List passengerModelList = apiRequestService 29 | .getPassengerDTOs(buyTicketInfoModel.getUsername(), 30 | buyTicketInfoModel.getGlobalRepeatSubmitToken()); 31 | PassengerModel passengerModel = passengerModelList.stream().filter(model -> { 32 | if (!StringUtils.isEmpty(buyTicketInfoModel.getAllEncStr())) { 33 | return model.getAllEncStr().equals(buyTicketInfoModel.getAllEncStr()); 34 | } 35 | return false; 36 | }).findFirst().orElse(null); 37 | if (passengerModel == null) { 38 | log.error("没有找到对应的乘客信息:" + buyTicketInfoModel.getRealName() + ",allEncStr:" 39 | + buyTicketInfoModel.getAllEncStr()); 40 | } 41 | return passengerModel; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/QueryTicket.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | import com.qianxunclub.ticket.model.SeatModel; 5 | import com.qianxunclub.ticket.model.TicketModel; 6 | import com.qianxunclub.ticket.service.ApiRequestService; 7 | 8 | import com.qianxunclub.ticket.service.WeChatNotice; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.springframework.beans.BeanUtils; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.util.CollectionUtils; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.concurrent.atomic.AtomicReference; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | /** 22 | * @author zhangbin 23 | * @date 2019-06-04 14:04 24 | * @description: TODO 25 | */ 26 | @AllArgsConstructor 27 | @Component 28 | @Slf4j 29 | public class QueryTicket { 30 | 31 | private WeChatNotice weChatNotice; 32 | private ApiRequestService apiRequestService; 33 | 34 | public TicketModel getMyTicket(BuyTicketInfoModel buyTicketInfoModel) { 35 | TicketModel ticketModel = this.getTicket(buyTicketInfoModel); 36 | List seat = new ArrayList<>(); 37 | buyTicketInfoModel.getSeat().forEach(seatLevelEnum -> { 38 | ticketModel.getSeat().forEach(seatModel -> { 39 | if (seatModel.getSeatLevel().equals(seatLevelEnum)) { 40 | seat.add(seatModel); 41 | } 42 | }); 43 | }); 44 | ticketModel.setSeat(seat); 45 | return ticketModel; 46 | } 47 | 48 | 49 | public TicketModel getTicket(BuyTicketInfoModel buyTicketInfoModel) { 50 | AtomicReference ticketModel = new AtomicReference<>(new TicketModel()); 51 | List ticketModelList = this.getCanBuyTicket(buyTicketInfoModel); 52 | ticketModelList.forEach(ticket -> { 53 | ticket.setTrainDate(buyTicketInfoModel.getDate()); 54 | if (buyTicketInfoModel.getTrainNumber().contains(ticket.getTrainNumber())) { 55 | List seat = new ArrayList<>(); 56 | ticket.getSeat().forEach(seatModel -> { 57 | if (buyTicketInfoModel.getSeat().contains(seatModel.getSeatLevel())) { 58 | seat.add(seatModel); 59 | } 60 | }); 61 | BeanUtils.copyProperties(ticket,ticketModel.get()); 62 | ticketModel.get().setSeat(seat); 63 | } 64 | }); 65 | return ticketModel.get(); 66 | } 67 | 68 | private List getCanBuyTicket(BuyTicketInfoModel buyTicketInfoModel) { 69 | 70 | apiRequestService.leftTicketInit(buyTicketInfoModel); 71 | 72 | List ticketModelList = apiRequestService.queryTicket(buyTicketInfoModel); 73 | List canBuy = new ArrayList<>(); 74 | ticketModelList.forEach(ticketModel -> { 75 | if (StringUtils.isNotBlank(ticketModel.getSecret())) { 76 | List seatModelList = ticketModel.getSeat(); 77 | List canBuySeatModelList = new ArrayList<>(); 78 | seatModelList.forEach(seatModel -> { 79 | if (StringUtils.isNotBlank(seatModel.getCount()) && !seatModel.getCount().equals("无")) { 80 | if(buyTicketInfoModel.getSeat().contains(seatModel.getSeatLevel())&& buyTicketInfoModel.getTrainNumber().contains(ticketModel.getTrainNumber())) { 81 | log.info("✅第"+buyTicketInfoModel.getQueryNum()+"次搜索 车次[" + ticketModel.getTrainNumber() + "]「" + seatModel.getSeatLevel().getName() + "-" + seatModel.getCount() + "」(" + Station.getNameByCode(ticketModel.getFrom()) 82 | + ticketModel.getDepartDate() + "-" + Station.getNameByCode(ticketModel.getTo()) + ticketModel.getArriveDate() + "):可以预定"); 83 | weChatNotice.send(buyTicketInfoModel.getServerSckey(),weChatNotice.buildTicketMessage(buyTicketInfoModel),"【放票通知】千寻来通知您了,请赶快查收!"); 84 | } 85 | canBuySeatModelList.add(seatModel); 86 | } else { 87 | if(buyTicketInfoModel.getSeat().contains(seatModel.getSeatLevel()) && buyTicketInfoModel.getTrainNumber().contains(ticketModel.getTrainNumber())) { 88 | log.debug("❌第"+buyTicketInfoModel.getQueryNum()+"次搜索 车次[" + ticketModel.getTrainNumber() + "]「" + seatModel.getSeatLevel().getName() + "-" + seatModel.getCount() + "」(" + Station.getNameByCode(ticketModel.getFrom()) 89 | + ticketModel.getDepartDate() + "-" + Station.getNameByCode(ticketModel.getTo()) + ticketModel.getArriveDate() + "):无票"); 90 | } 91 | } 92 | }); 93 | if (!CollectionUtils.isEmpty(canBuySeatModelList)) { 94 | ticketModel.setSeat(canBuySeatModelList); 95 | canBuy.add(ticketModel); 96 | } 97 | } else { 98 | log.debug("⚠️车次[" + ticketModel.getTrainNumber() + "]" + "(" + Station.getNameByCode(ticketModel.getFrom()) + "-" + Station.getNameByCode(ticketModel.getTo()) + "):未开售"); 99 | } 100 | }); 101 | 102 | return canBuy; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/Station.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | 4 | import com.qianxunclub.ticket.service.ApiRequestService; 5 | 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | 12 | /** 13 | * @author zhangbin 14 | * @date 2019-05-31 10:11 15 | * @description: TODO 16 | */ 17 | @Component 18 | public class Station { 19 | 20 | private static Map stations; 21 | 22 | public void load(ApiRequestService apiRequestService){ 23 | stations = apiRequestService.station(); 24 | } 25 | 26 | 27 | public static String getCodeByName(String name){ 28 | return stations.get(name); 29 | } 30 | 31 | public static String getNameByCode(String c){ 32 | AtomicReference result = new AtomicReference<>(""); 33 | stations.forEach((name, code) -> { 34 | if(code.equals(c)){ 35 | result.set(name); 36 | } 37 | }); 38 | return result.get(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/ticket/Task.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.ticket; 2 | 3 | import com.qianxunclub.ticket.config.Config; 4 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 5 | import com.qianxunclub.ticket.model.TicketModel; 6 | import com.qianxunclub.ticket.util.ApplicationContextHelper; 7 | import com.qianxunclub.ticket.util.CommonUtil; 8 | 9 | 10 | import org.springframework.util.CollectionUtils; 11 | 12 | import java.util.concurrent.Callable; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | /** 18 | * @author zhangbin 19 | * @date 2019-06-08 11:08 20 | * @description: TODO 21 | */ 22 | @Slf4j 23 | public class Task implements Callable { 24 | 25 | private Config config; 26 | private BuyTicket buyTicket; 27 | private QueryTicket queryTicket; 28 | private BuyTicketInfoModel buyTicketInfoModel; 29 | 30 | public Task(BuyTicketInfoModel buyTicketInfoModel) { 31 | this.buyTicketInfoModel = buyTicketInfoModel; 32 | this.buyTicket = ApplicationContextHelper.getBean(BuyTicket.class); 33 | this.queryTicket = ApplicationContextHelper.getBean(QueryTicket.class); 34 | this.config = ApplicationContextHelper.getBean(Config.class); 35 | } 36 | 37 | @Override 38 | public Boolean call() { 39 | Thread.currentThread().setName(CommonUtil.getThreadName(buyTicketInfoModel)); 40 | log.info("正在查询车票"); 41 | while (true) { 42 | buyTicketInfoModel.setQueryNum(buyTicketInfoModel.getQueryNum() + 1); 43 | try { 44 | TicketModel ticketModel = queryTicket.getMyTicket(buyTicketInfoModel); 45 | if (ticketModel == null || CollectionUtils.isEmpty(ticketModel.getSeat())) { 46 | log.debug("没有查询到购买的票"); 47 | Thread.sleep(config.querySleep() * 1000); 48 | continue; 49 | } 50 | log.info("有票啦,开始抢!"); 51 | for (int i = 0; i<=5 ; i++){ 52 | log.info("第{}次开始下单!!!"); 53 | if (buyTicket.buy(buyTicketInfoModel, ticketModel)){ 54 | return true; 55 | } 56 | TimeUnit.MILLISECONDS.sleep(500); 57 | } 58 | return false; 59 | } catch (Exception e) { 60 | log.error("出现错误", e); 61 | try { 62 | Thread.sleep(config.querySleep() * 1000); 63 | } catch (InterruptedException ex) { 64 | ex.printStackTrace(); 65 | } 66 | return null; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/ApplicationContextHelper.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author zhangbin 10 | * @date 2019-05-30 17:37 11 | * @description: TODO 12 | */ 13 | @Component 14 | public class ApplicationContextHelper implements ApplicationContextAware { 15 | private static ApplicationContext applicationContext; 16 | 17 | public ApplicationContextHelper() { 18 | } 19 | 20 | @Override 21 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 22 | ApplicationContextHelper.applicationContext = applicationContext; 23 | } 24 | 25 | public static Object getBean(String beanName) { 26 | return applicationContext != null ? applicationContext.getBean(beanName) : null; 27 | } 28 | 29 | public static T getBean(Class t) { 30 | return applicationContext != null ? applicationContext.getBean(t) : null; 31 | } 32 | 33 | public static ApplicationContext getApplicationContext() { 34 | return applicationContext; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/CaptchaImageForPy.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import com.qianxunclub.ticket.config.Config; 4 | 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import org.apache.commons.lang.StringUtils; 8 | import org.apache.tomcat.util.http.fileupload.IOUtils; 9 | import org.springframework.stereotype.Component; 10 | 11 | import sun.misc.BASE64Decoder; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.ByteArrayInputStream; 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.io.InputStream; 18 | import java.io.InputStreamReader; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.UUID; 22 | 23 | import lombok.AllArgsConstructor; 24 | import lombok.Data; 25 | import lombok.extern.slf4j.Slf4j; 26 | 27 | /** 28 | * @author zhangbin 29 | * @date 2019-06-11 14:12 30 | * @description: TODO 31 | */ 32 | @Component 33 | @AllArgsConstructor 34 | @Slf4j 35 | public class CaptchaImageForPy { 36 | 37 | private Config config; 38 | 39 | public String check(String base64String) { 40 | String filename = UUID.randomUUID() + ".jpg"; 41 | File folder = new File("temp"); 42 | if (!folder.exists()) { 43 | folder.mkdirs(); 44 | } 45 | File file = new File(folder, filename); 46 | try { 47 | BASE64Decoder decoder = new BASE64Decoder(); 48 | byte[] data = decoder.decodeBuffer(base64String); 49 | IOUtils.copy(new ByteArrayInputStream(data), new FileOutputStream(file)); 50 | Runtime runtime = Runtime.getRuntime(); 51 | String os = System.getProperty("os.name"); 52 | Process process; 53 | if (os.toLowerCase().startsWith("win")) { 54 | String[] cmd = new String[]{"cmd", "/c", "cd python & set PYTHONIOENCODING=UTF-8 & python main.py " + "..\\temp\\" + filename}; 55 | process = runtime.exec(cmd); 56 | } else { 57 | String bash = System.getProperty("user.dir") + "/" + config.getPythonPath() + "/run.sh " + System.getProperty("user.dir") + "/temp/" + filename; 58 | process = runtime.exec(bash); 59 | } 60 | process.waitFor(); 61 | InputStream inputStream = process.getInputStream(); 62 | String r = this.get(inputStream); 63 | if(StringUtils.isEmpty(r)){ 64 | inputStream = process.getErrorStream(); 65 | r = this.get(inputStream); 66 | } 67 | return r; 68 | } catch (Exception e) { 69 | log.error("", e); 70 | return "系统出错,联系QQ:960339491"; 71 | } finally { 72 | if (file.exists() && file.isFile()) { 73 | file.delete(); 74 | } 75 | } 76 | } 77 | 78 | public String get(InputStream inputStream) throws IOException { 79 | InputStreamReader inputStreamReader = new InputStreamReader(inputStream, 80 | StandardCharsets.UTF_8); 81 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 82 | String line; 83 | PredictVO predictVO = new PredictVO(); 84 | while ((line = bufferedReader.readLine()) != null) { 85 | if (StringUtils.isEmpty(line.trim())) { 86 | continue; 87 | } 88 | String[] parts = line.split("\\s"); 89 | if (parts.length == 1) { 90 | predictVO.getQuestions().add(parts[0]); 91 | } else { 92 | PictureVO pictureVO = new PictureVO(Integer.valueOf(parts[1]), Integer.valueOf(parts[0]), parts[2]); 93 | predictVO.getPictures().add(pictureVO); 94 | } 95 | } 96 | return this.getResult(predictVO); 97 | } 98 | 99 | private String getResult(PredictVO predictVO) { 100 | final int[][][] offset = { 101 | { 102 | {40, 77}, 103 | {112, 77}, 104 | {184, 77}, 105 | {256, 77}, 106 | }, 107 | { 108 | {40, 149}, 109 | {112, 149}, 110 | {184, 149}, 111 | {256, 149}, 112 | }, 113 | }; 114 | 115 | List integerList = new ArrayList<>(); 116 | for (String question : predictVO.getQuestions()) { 117 | for (PictureVO pictureVO : predictVO.getPictures()) { 118 | if (question.equalsIgnoreCase(pictureVO.getDesc())) { 119 | int[] pic = offset[pictureVO.getY()][pictureVO.getX()]; 120 | integerList.add(pic[0]); 121 | integerList.add(pic[1]); 122 | } 123 | } 124 | } 125 | return StringUtils.join(integerList, ","); 126 | } 127 | 128 | @Data 129 | public static class PredictVO { 130 | private List questions = new ArrayList<>(); 131 | private List pictures = new ArrayList<>(); 132 | } 133 | 134 | @Data 135 | @AllArgsConstructor 136 | public static class PictureVO { 137 | private Integer x; 138 | private Integer y; 139 | private String desc; 140 | } 141 | } 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/CommonUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import com.qianxunclub.ticket.model.BuyTicketInfoModel; 4 | 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | import java.util.Locale; 10 | import java.util.SimpleTimeZone; 11 | import java.util.TimeZone; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * @author zhangbin 17 | * @date 2019-06-06 17:36 18 | * @description: TODO 19 | */ 20 | public class CommonUtil { 21 | 22 | public static String getThreadName(BuyTicketInfoModel buyTicketInfoModel) { 23 | String name = "" + 24 | "👤" + buyTicketInfoModel.getUsername() + 25 | "[" + buyTicketInfoModel.getRealName() + 26 | "-" + buyTicketInfoModel.getTrainNumber() + "]" + 27 | "-" + buyTicketInfoModel.getMobile(); 28 | return name; 29 | } 30 | 31 | public static String regString(String regex, String str) 32 | { 33 | Pattern pa = Pattern.compile(regex); 34 | Matcher ma = pa.matcher(str); 35 | if (ma.find()) 36 | { 37 | return ma.group(); 38 | } 39 | return ""; 40 | } 41 | 42 | public static String getGMT(String date) { 43 | String str = ""; 44 | TimeZone tz = TimeZone.getTimeZone("ETC/GMT-8"); 45 | TimeZone.setDefault(tz); 46 | Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); 47 | Date dd; 48 | SimpleDateFormat shortSdf = new SimpleDateFormat("yyyy-MM-dd"); 49 | try { 50 | SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'", Locale.US); 51 | sdf.setTimeZone(TimeZone.getTimeZone("GMT")); 52 | dd = shortSdf.parse(date); 53 | cal.setTime(dd); 54 | str = sdf.format(cal.getTime()); 55 | return str + "+0800 (中国标准时间)"; 56 | } catch (ParseException e) { 57 | e.printStackTrace(); 58 | } 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import com.qianxunclub.ticket.config.Config; 4 | import com.qianxunclub.ticket.model.LogdeviceModel; 5 | 6 | import org.apache.http.impl.client.BasicCookieStore; 7 | import org.apache.http.impl.cookie.BasicClientCookie; 8 | import org.springframework.stereotype.Component; 9 | 10 | import lombok.AllArgsConstructor; 11 | 12 | /** 13 | * @author zhangbin 14 | * @date 2019-06-05 13:08 15 | * @description: TODO 16 | */ 17 | @AllArgsConstructor 18 | @Component 19 | public class CookieUtil { 20 | 21 | 22 | private Config config; 23 | 24 | public BasicCookieStore init(BasicCookieStore basicCookieStore, LogdeviceModel logdeviceModel) { 25 | 26 | if (basicCookieStore == null) { 27 | basicCookieStore = new BasicCookieStore(); 28 | } 29 | BasicClientCookie expCookie = new BasicClientCookie("RAIL_EXPIRATION", logdeviceModel.getExp()); 30 | expCookie.setDomain(config.getHost()); 31 | expCookie.setPath("/"); 32 | basicCookieStore.addCookie(expCookie); 33 | 34 | BasicClientCookie dfCookie = new BasicClientCookie("RAIL_DEVICEID", logdeviceModel.getDfp()); 35 | dfCookie.setDomain(config.getHost()); 36 | dfCookie.setPath("/"); 37 | basicCookieStore.addCookie(dfCookie); 38 | 39 | 40 | return basicCookieStore; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | 4 | import com.qianxunclub.ticket.config.Config; 5 | import org.apache.http.HttpEntity; 6 | import org.apache.http.HttpHeaders; 7 | import org.apache.http.HttpHost; 8 | import org.apache.http.HttpResponse; 9 | import org.apache.http.HttpStatus; 10 | import org.apache.http.client.HttpClient; 11 | import org.apache.http.client.methods.HttpGet; 12 | import org.apache.http.client.methods.HttpPost; 13 | import org.apache.http.client.methods.HttpRequestBase; 14 | import org.apache.http.impl.client.BasicCookieStore; 15 | import org.apache.http.impl.client.HttpClients; 16 | import org.apache.http.impl.conn.DefaultProxyRoutePlanner; 17 | import org.apache.http.message.BasicHeader; 18 | import org.apache.http.util.EntityUtils; 19 | 20 | import java.io.IOException; 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | /** 26 | * @author zhangbin 27 | * @date 2019-05-30 16:34 28 | * @description: TODO 29 | */ 30 | @Slf4j 31 | public class HttpUtil { 32 | 33 | private HttpClient httpClient; 34 | private HttpClient httpPureClient; 35 | private BasicCookieStore basicCookieStore; 36 | private Config config; 37 | 38 | public HttpUtil() { 39 | this.init(); 40 | } 41 | 42 | public HttpUtil(BasicCookieStore basicCookieStore) { 43 | init(basicCookieStore); 44 | } 45 | 46 | public void init() { 47 | BasicCookieStore basicCookieStore = new BasicCookieStore(); 48 | this.init(basicCookieStore); 49 | } 50 | 51 | public void init(BasicCookieStore basicCookieStore) { 52 | this.basicCookieStore = basicCookieStore; 53 | this.config = ApplicationContextHelper.getBean(Config.class); 54 | httpClient = HttpClients.custom().setDefaultCookieStore(basicCookieStore).build(); 55 | if (config != null && config.getEnableProxy()) { 56 | HttpHost proxy = new HttpHost(config.getProxyIp().getIp(), config.getProxyIp().getPort()); 57 | DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); 58 | httpClient = HttpClients.custom().setRoutePlanner(routePlanner) 59 | .setDefaultCookieStore(basicCookieStore).build(); 60 | } 61 | httpPureClient = HttpClients.createDefault(); 62 | } 63 | 64 | public BasicCookieStore getBasicCookieStore() { 65 | return basicCookieStore; 66 | } 67 | 68 | public String get(HttpGet httpGet) { 69 | return this.doAction(httpGet,httpClient); 70 | } 71 | 72 | 73 | public String post(HttpPost httpPost) { 74 | return this.doAction(httpPost,httpClient); 75 | } 76 | 77 | private String doAction(HttpRequestBase httpRequestBase,HttpClient client) { 78 | httpRequestBase.addHeader(new BasicHeader("Origin",config.getBaseUrl())); 79 | httpRequestBase.addHeader(new BasicHeader(HttpHeaders.REFERER,config.getBaseUrl())); 80 | httpRequestBase.addHeader(new BasicHeader(HttpHeaders.HOST, config.getHost())); 81 | return sendRequest(httpRequestBase,client); 82 | } 83 | 84 | /** 85 | * 发送 http 请求 86 | * @param httpRequestBase request 87 | * @param client http client 88 | * @return response 89 | */ 90 | private String sendRequest(HttpRequestBase httpRequestBase,HttpClient client){ 91 | String result = null; 92 | try { 93 | HttpResponse response = client.execute(httpRequestBase); 94 | if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { 95 | HttpEntity httpEntity = response.getEntity(); 96 | result = EntityUtils.toString(httpEntity, "UTF-8"); 97 | EntityUtils.consume(httpEntity); 98 | } else { 99 | log.error("请求异常:{},{}", httpRequestBase.getURI(), response.getStatusLine()); 100 | } 101 | return result; 102 | } catch (IOException e) { 103 | log.error("请求异常:{},{}", httpRequestBase.getURI(), e.getMessage()); 104 | } 105 | return ""; 106 | } 107 | 108 | /** 109 | * 异步http Get请求 无12306cookies植入等 纯净版 110 | * @param httpGet 请求参数 111 | * @return future response 112 | */ 113 | public CompletableFuture asyncGet(HttpGet httpGet){ 114 | return CompletableFuture.supplyAsync(()->sendRequest(httpGet,httpPureClient)); 115 | } 116 | 117 | /** 118 | * 异步http Post请求 无12306cookies植入等 纯净版 119 | * @param httpPost 请求参数 120 | * @return future response 121 | */ 122 | public CompletableFuture asyncPost(HttpPost httpPost){ 123 | return CompletableFuture.supplyAsync(()->sendRequest(httpPost,httpPureClient)); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import java.util.concurrent.ConcurrentLinkedQueue; 4 | 5 | /** 6 | * @author zhangbin 7 | * @date 2019-07-01 12:10 8 | * @description: TODO 9 | */ 10 | public class LogUtil { 11 | private static ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue<>(); 12 | 13 | public static String getLog(){ 14 | return concurrentLinkedQueue.poll(); 15 | } 16 | 17 | public static void push(String log){ 18 | concurrentLinkedQueue.add(log); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/LogdeviceUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import com.gargoylesoftware.htmlunit.AjaxController; 4 | import com.gargoylesoftware.htmlunit.BrowserVersion; 5 | import com.gargoylesoftware.htmlunit.WebClient; 6 | import com.gargoylesoftware.htmlunit.WebRequest; 7 | import com.gargoylesoftware.htmlunit.WebResponse; 8 | import com.gargoylesoftware.htmlunit.html.HtmlPage; 9 | import com.gargoylesoftware.htmlunit.util.WebConnectionWrapper; 10 | import com.google.gson.Gson; 11 | import com.google.gson.JsonObject; 12 | import com.qianxunclub.ticket.config.Config; 13 | import com.qianxunclub.ticket.config.CookiesConfig; 14 | import com.qianxunclub.ticket.model.LogdeviceModel; 15 | import java.io.IOException; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.apache.http.client.methods.HttpGet; 19 | import org.springframework.util.StringUtils; 20 | 21 | @Slf4j 22 | public class LogdeviceUtil { 23 | 24 | private static String cookieUrl; 25 | 26 | public static LogdeviceModel getLogdevice() { 27 | 28 | Config config = ApplicationContextHelper.getBean(Config.class); 29 | CookiesConfig cookiesConfig = ApplicationContextHelper.getBean(CookiesConfig.class); 30 | if(cookiesConfig.getEnable()){ 31 | return new LogdeviceModel(cookiesConfig.getRailExpiration(), 32 | cookiesConfig.getRailDeviceid()); 33 | } 34 | String proxyHost = config.getEnableProxy() ? config.getProxyIp().getIp() : null; 35 | int proxyPort = config.getEnableProxy() ? config.getProxyIp().getPort() : 0; 36 | try { 37 | String url = LogdeviceUtil.getLogdeviceUrl(proxyHost, proxyPort); 38 | if (StringUtils.isEmpty(url)) { 39 | log.warn("获取cookieurl出错,稍后再试"); 40 | return null; 41 | } 42 | HttpUtil httpUtil = new HttpUtil(); 43 | HttpGet httpGet = new HttpGet(url); 44 | String msg = httpUtil.get(httpGet); 45 | msg = msg.replace("callbackFunction('", ""); 46 | msg = msg.replace("')", ""); 47 | Gson g = new Gson(); 48 | JsonObject obj = g.fromJson(msg, JsonObject.class); 49 | LogdeviceModel model = new LogdeviceModel(obj.get("exp").getAsString(), 50 | obj.get("dfp").getAsString()); 51 | return model; 52 | } catch (Exception e) { 53 | log.error("获取cookie出错", e); 54 | } 55 | return null; 56 | } 57 | 58 | public static String getLogdeviceUrl(String proxyHost, int proxyPort) { 59 | WebClient wc = new WebClient(BrowserVersion.CHROME, proxyHost, proxyPort); 60 | wc.getOptions().setTimeout(15000); 61 | wc.getOptions().setUseInsecureSSL(true); 62 | wc.getOptions().setJavaScriptEnabled(true); 63 | wc.getOptions().setCssEnabled(false); 64 | //当JS执行出错的时候是否抛出异常, 这里选择不需要 65 | wc.getOptions().setThrowExceptionOnScriptError(true); 66 | //当HTTP的状态非200时是否抛出异常 67 | wc.getOptions().setThrowExceptionOnFailingStatusCode(true); 68 | //很重要,设置支持AJAX 69 | wc.setAjaxController(new AjaxController() { 70 | @Override 71 | public boolean processSynchron(HtmlPage page, WebRequest settings, boolean async) { 72 | return super.processSynchron(page, settings, async); 73 | } 74 | }); 75 | wc.setWebConnection( 76 | new WebConnectionWrapper(wc) { 77 | @Override 78 | public WebResponse getResponse(WebRequest request) throws IOException { 79 | WebResponse response = super.getResponse(request); 80 | 81 | if (request.getUrl().toExternalForm().contains("/otn/HttpZF/logdevice")) { 82 | cookieUrl = request.getUrl().toExternalForm(); 83 | } 84 | return response; 85 | 86 | } 87 | } 88 | 89 | ); 90 | try { 91 | wc.getPage("https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc"); 92 | } catch (IOException e) { 93 | e.printStackTrace(); 94 | } finally { 95 | wc.waitForBackgroundJavaScript(3 * 1000); 96 | } 97 | while (StringUtils.isEmpty(cookieUrl)) { 98 | try { 99 | Thread.sleep(1000); 100 | } catch (InterruptedException e) { 101 | e.printStackTrace(); 102 | } 103 | } 104 | return cookieUrl; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/qianxunclub/ticket/util/StaticUtil.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.util; 2 | 3 | import com.qianxunclub.ticket.repository.entity.ProxyIp; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Random; 7 | 8 | public class StaticUtil { 9 | 10 | public static List ips = new ArrayList<>(); 11 | public static List proxyIp = new ArrayList<>(); 12 | 13 | 14 | public static void addIp(String ip){ 15 | ips.add(ip); 16 | } 17 | 18 | public static void rmIp(String ip){ 19 | ips.remove(ip); 20 | } 21 | 22 | public static void addIps(List ips){ 23 | StaticUtil.ips = ips; 24 | } 25 | 26 | public static String ip(){ 27 | if(ips.size() <= 0){ 28 | return null; 29 | } 30 | Random r = new Random(0); 31 | int i = r.nextInt(ips.size() - 1); 32 | return ips.get(i); 33 | } 34 | 35 | public static void addProxyIp(List proxyIps){ 36 | StaticUtil.proxyIp = proxyIps; 37 | } 38 | 39 | public static ProxyIp proxyIp(){ 40 | if(proxyIp.size() <= 0){ 41 | return null; 42 | } 43 | if (proxyIp.size() == 1) { 44 | return proxyIp.get(0); 45 | } 46 | Random r = new Random(0); 47 | int i = r.nextInt(proxyIp.size() - 1); 48 | return proxyIp.get(i); 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/application-cookie.yml: -------------------------------------------------------------------------------- 1 | 2 | cookies: 3 | # 是否使用这个 cookie,如果启用,不会自动获取最新 cookie,linux 上面为 true,因为不能打开网页获取😁 4 | enable: false 5 | rail_expiration: "1576330253758" 6 | rail_deviceid: "D0vKZrOYYR8LWwpDIMmErxMPQ_weK4SG8vBGv_hk-Hl7iOEpGACn8QqbxAPren7my5aAozndcRPaNV0lhBepXDUVe_AEWyYmahcm75ZViUV_Ty6NbfVO20fWgQhNPSkAj5anYugDWT1drqVO9GRLv6vfHrVSbGJE" 7 | -------------------------------------------------------------------------------- /src/main/resources/application-sms.yml: -------------------------------------------------------------------------------- 1 | notice: 2 | accessKeyId: "XXXX" 3 | accessSecret: "XXXX" 4 | templateCode: "XXXX" 5 | signName: "XXXX" -------------------------------------------------------------------------------- /src/main/resources/application-user.yml: -------------------------------------------------------------------------------- 1 | # 可配置多乘客 2 | user.ticket-info: 3 | - # 乘客编号 4 | # 获取方法1: 5 | # 调用 com.qianxunclub.ticket.GetPassengerTest 里面的 getPassenger 方法,可以打印出来对应的值 6 | # 7 | # 获取方法2: 8 | # 登录12306官网成功后 9 | # 访问:https://kyfw.12306.cn/otn/passengers/query?pageIndex=1&pageSize=100 10 | # 找对对应乘客的【allEncStr】的值 11 | allEncStr: 970237bc89e920ecd037252df041a3f568550cea5743b0812430404a31cf60e7367e35d15b3c7ed363d77d5874616e07 12 | 13 | # 登陆server酱官方网 http://sc.ftqq.com/3.version 14 | # 绑定github获取SCKEY 15 | # 微信绑定server酱, 16 | # 修改配置文件application-user.yml中的serverSckey,即可开启抢票成功微信通知功能 17 | serverSckey: 18 | # 12306用户名 19 | username: qiemengyan 20 | # 12306密码 21 | password: yan666666 22 | # 出发日期 23 | date: 2020-01-08 24 | # 出发站点 25 | from: 北京南 26 | # 到达站点 27 | to: 南京南 28 | # 车次,多车次使用,号隔开 29 | trainNumber: G101 30 | # 乘车人姓名 31 | realName: 郄梦岩 32 | # 乘车人手机号码 33 | mobile: 18810290339 34 | # 座位选择:从上往下,优先购买上面 35 | # YINGWO:硬卧 36 | # RUANWO:软卧 37 | # TWO:二等座 38 | # RUANZUO:软座 39 | # YINGZUO:硬座 40 | # ONE:一等座 41 | # SHANGWUZUO:商务座 42 | # GAOJIRUANWO:高级软卧 43 | # WUZUO:无座 44 | seat: 45 | - TWO 46 | # 多账号抢票配置 47 | # - allEncStr: 1 48 | # username: XXXX 49 | # password: XXXX 50 | # date: 2019-08-14 51 | # from: 深圳 52 | # to: 三门峡南 53 | # trainNumber: XXXX 54 | # realName: XXXX 55 | # mobile: XXXX 56 | # seat: 57 | # - ONE 58 | # - WUZUO 59 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9998 3 | 4 | config: 5 | # 是否启用代理 6 | enableProxy: false 7 | # 代理 HOST 8 | proxyHost: 127.0.0.1 9 | # 代理端口 10 | proxyPort: 12639 11 | host: "kyfw.12306.cn" 12 | base_url: "https://kyfw.12306.cn" 13 | # 随机查询时间,取值:min-max,范围随机 14 | queryTicketSleepTime: 15 | min: 1 16 | max: 3 17 | pythonPath: "python" 18 | 19 | api: 20 | station: "/otn/resources/js/framework/station_name.js?station_version=1.9126" 21 | init: "/otn/leftTicket/init" 22 | leftTicketBaseUrl: "/otn/leftTicket/query" 23 | leftTicket: "%s?leftTicketDTO.train_date=%s&leftTicketDTO.from_station=%s&leftTicketDTO.to_station=%s&purpose_codes=ADULT" 24 | loginConfig: "/otn/login/conf" 25 | passengers: "/otn/passengers/query" 26 | captchaImage: "/passport/captcha/captcha-image64?login_site=E&module=login&rand=sjrand&_=%s" 27 | uamtkStatic: "/passport/web/auth/uamtk-static" 28 | login: "/passport/web/login" 29 | captchaCheck: "/passport/captcha/captcha-check?answer=%s&rand=sjrand&login_site=E&_=%s" 30 | uamtk: "/passport/web/auth/uamtk" 31 | uamauthclient: "/otn/uamauthclient" 32 | checkUser: "/otn/login/checkUser" 33 | submitOrderRequest: "/otn/leftTicket/submitOrderRequest" 34 | initDc: "/otn/confirmPassenger/initDc" 35 | getPassengerDTOs: "/otn/confirmPassenger/getPassengerDTOs" 36 | checkOrderInfo: "/otn/confirmPassenger/checkOrderInfo" 37 | checkRandCodeAnsyn: "/otn/passcodeNew/checkRandCodeAnsy" 38 | getQueueCount: "/otn/confirmPassenger/getQueueCount" 39 | confirmSingleForQueue: "/otn/confirmPassenger/confirmSingleForQueue" 40 | queryOrderWaitTime: "/otn/confirmPassenger/queryOrderWaitTime?random=%s&tourFlag=dc&_json_att=&REPEAT_SUBMIT_TOKEN=%s" 41 | notice: "dysmsapi.aliyuncs.com" 42 | serverWechat : "https://sc.ftqq.com/" 43 | 44 | spring: 45 | datasource: 46 | username: "" 47 | password: "" 48 | url: "jdbc:sqlite:db/ticket.db" 49 | driver-class-name: org.sqlite.JDBC 50 | profiles: 51 | include: 52 | - cookie 53 | - user 54 | - sms 55 | 56 | swagger: 57 | enabled: true 58 | title: 12306抢票系统 59 | description: 12306抢票系统 60 | webBasePackage: com.qianxunclub.ticket.controller 61 | author: 千寻啊千寻 62 | url: qianxunclub.com 63 | email: qianxunclub@qq.com 64 | 65 | logging: 66 | level: 67 | com.qianxunclub: info 68 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr(%-40.40logger{39}){cyan} %clr([%50t]){faint} %clr(:){faint} %m%n%wEx 11 | 12 | utf8 13 | 14 | 15 | 16 | 17 | log/ticket.log 18 | 19 | 20 | 21 | 22 | 23 | log/%d/ticket.%d.%i.log 24 | 25 | 360 26 | 27 | 28 | 512KB 29 | 30 | 31 | 32 | 33 | 34 | %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr(%-40.40logger{39}){cyan} %clr([%50t]){faint} %clr(:){faint} %m%n%wEx 35 | 36 | 37 | UTF-8 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/GetLogdeviceTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | import com.qianxunclub.ticket.model.LogdeviceModel; 4 | import com.qianxunclub.ticket.util.LogdeviceUtil; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | @RunWith(SpringRunner.class) 11 | @SpringBootTest 12 | public class GetLogdeviceTest { 13 | 14 | @Test 15 | public void getLogdevice() { 16 | LogdeviceModel logdeviceModel = LogdeviceUtil.getLogdevice(); 17 | System.out.println("rail_expiration:" + logdeviceModel.getExp()); 18 | System.out.println("rail_deviceid:" + logdeviceModel.getDfp()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/GetPassengerTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | 4 | import com.qianxunclub.ticket.constant.Constant; 5 | import com.qianxunclub.ticket.model.PassengerModel; 6 | import com.qianxunclub.ticket.model.UserModel; 7 | import com.qianxunclub.ticket.service.TicketService; 8 | import java.util.List; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest 17 | public class GetPassengerTest { 18 | 19 | @Autowired 20 | private TicketService ticketService; 21 | 22 | @Test 23 | public void getPassenger() { 24 | 25 | UserModel userModel = new UserModel(); 26 | userModel.setUsername(Constant.USER_NAME); 27 | userModel.setPassword(Constant.PASSWORD); 28 | List passengerModelList = ticketService.login(userModel); 29 | passengerModelList.forEach(passengerModel -> { 30 | System.out.println("" + 31 | " 姓名:" + passengerModel.getPassengerName() + 32 | " 证件号:" + passengerModel.getPassengerIdNo() + 33 | " allEncStr:" + passengerModel.getAllEncStr() 34 | ); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/LoginTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | 4 | import com.qianxunclub.ticket.constant.Constant; 5 | import com.qianxunclub.ticket.model.UserModel; 6 | import com.qianxunclub.ticket.ticket.Login; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | @RunWith(SpringRunner.class) 14 | @SpringBootTest 15 | public class LoginTest { 16 | 17 | @Autowired 18 | private Login login; 19 | 20 | @Test 21 | public void login() { 22 | UserModel userModel = new UserModel(); 23 | userModel.setUsername(Constant.USER_NAME); 24 | userModel.setPassword(Constant.PASSWORD); 25 | login.login(userModel); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/NoticeTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | import com.qianxunclub.ticket.model.NoticeModel; 4 | import com.qianxunclub.ticket.service.NoticeService; 5 | import com.qianxunclub.ticket.service.WeChatNotice; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | 13 | @RunWith(SpringRunner.class) 14 | @SpringBootTest 15 | public class NoticeTest { 16 | 17 | @Autowired 18 | private NoticeService noticeService; 19 | @Autowired 20 | private WeChatNotice weChatNotice; 21 | 22 | @Test 23 | public void send(){ 24 | 25 | NoticeModel noticeModel = new NoticeModel(); 26 | noticeModel.setName("张斌"); 27 | noticeModel.setUserName("adsfgh"); 28 | noticeModel.setPassword("54324567"); 29 | noticeModel.setPhoneNumber("3456765432"); 30 | noticeModel.setOrderId("wsadhjkhgf"); 31 | noticeService.send(noticeModel); 32 | } 33 | 34 | @Test 35 | public void wechatSend(){ 36 | NoticeModel noticeModel = NoticeModel.builder().trainDate("2020-01-23").from("北京").to("哈尔滨") 37 | .serverSckey("****").trainNum("Z83").build(); 38 | // weChatNotice.send(noticeModel); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/TimeTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | import java.time.LocalTime; 9 | 10 | /** 11 | * @author Juinjonn.Chan 12 | * @description 13 | * @date 2019/12/31 14 | */ 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest 17 | public class TimeTest { 18 | @Test 19 | public void testTime(){ 20 | LocalTime startTime = LocalTime.parse("05:55"); 21 | LocalTime endTime = LocalTime.parse("23:35"); 22 | if (LocalTime.parse("11:30").isAfter(endTime) || LocalTime.parse("11:30").isBefore(startTime)){ 23 | System.out.println("不在抢票范围内,运营时间为:06:00-23:30"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/YzmTest.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket; 2 | 3 | import com.qianxunclub.ticket.config.Config; 4 | import com.qianxunclub.ticket.util.CaptchaImageForPy; 5 | 6 | import org.apache.commons.lang.StringUtils; 7 | import org.junit.Test; 8 | 9 | import java.io.InputStream; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | /** 16 | * @author zhangbin 17 | * @date 2019-06-27 16:50 18 | * @description: TODO 19 | */ 20 | @RunWith(SpringRunner.class) 21 | @SpringBootTest 22 | public class YzmTest { 23 | 24 | @Autowired 25 | private CaptchaImageForPy captchaImageForPy; 26 | @Autowired 27 | private Config config; 28 | 29 | @Test 30 | public void yz() throws Exception { 31 | Runtime runtime = Runtime.getRuntime(); 32 | String os = System.getProperty("os.name"); 33 | Process process; 34 | if (os.toLowerCase().startsWith("win")) { 35 | String[] cmd = new String[]{"cmd", "/c", "cd " + System.getProperty("user.dir") + "/" + config.getPythonPath() + " & set PYTHONIOENCODING=UTF-8 & python main.py " + "..\\temp\\index.jpg"}; 36 | process = runtime.exec(cmd); 37 | } else { 38 | String bash = System.getProperty("user.dir") + "/" + config.getPythonPath() + "/run.sh " + System.getProperty("user.dir") + "/temp/index.jpg"; 39 | process = runtime.exec(bash); 40 | } 41 | InputStream inputStream = process.getInputStream(); 42 | String r = captchaImageForPy.get(inputStream); 43 | if(StringUtils.isEmpty(r)){ 44 | inputStream = process.getErrorStream(); 45 | r = captchaImageForPy.get(inputStream); 46 | } 47 | System.out.println(r); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/qianxunclub/ticket/constant/Constant.java: -------------------------------------------------------------------------------- 1 | package com.qianxunclub.ticket.constant; 2 | 3 | public class Constant { 4 | public static final String USER_NAME = "XXXX"; 5 | public static final String PASSWORD = "XXXX"; 6 | } 7 | -------------------------------------------------------------------------------- /temp/index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianxunclub/ticket/143a48c5f014242ed7c5dcf19cc5396ffddc698d/temp/index.jpg --------------------------------------------------------------------------------