900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > (实战)Node.js 实现抢票小工具短信通知提醒

(实战)Node.js 实现抢票小工具短信通知提醒

时间:2023-03-30 02:39:30

相关推荐

(实战)Node.js 实现抢票小工具短信通知提醒

作者:西岚

https://juejin.im/post/5dadd0236fb9a04de04d968e

写在前言

要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。

尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”

想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!

....

咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【xxxxxx】

但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具

获取接口信息

查看页面结构

这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选

如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:

我们打算这么做,

定时抓取返回的接口信息

根据接口返回值判断是否有余票

好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是

使用chrome 调试微信公众号网页页面

首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去

所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄

借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:

获取本机IP地址和端口

设置代理手机上网

依次执行上面两步

获取本机IP地址和端口

第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888

然后找到Charleshelp/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步

设置代理手机上网

首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:

输入上一步获取主机名,端口号就ok了

输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。

用手机访问目标网页

我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。

仔细分析了下,大概明白了业务逻辑:

整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery

当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,

而我们想要的核心内容:日历表,一开始是不显示的

因为还要在请求一次

第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容

下面这个是请求当月票务信息,然而发现他返回的是一堆html节点

好吧...估计是获取到之后直接appenddiv里面的,然后渲染生成日历表内容

接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:

看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式

在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息

记录所需要的信息内容

根据上面的分析,总结下内容: 整个项目用户身份验证是使用cookiesession方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的

所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。

我们找个记事本,记录下信息,记录的内容有:

请求余票接口和购票接口的url地址

cookie信息

各自的request参数字段

user-Agent信息

各自的response返回内容

设置chrome

有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions

user-Agent填入到Custom里面

Charles抓包本地请求

因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie

首先我们需要开启macOS Proxy,抓包我们的http请求

打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试

然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute

这时候,能够正确跳到目标页面了。

大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块

审查了下元素发现:

**小方块的结构为:**

<tdclass="b"><span>这里为日期</span><span>如果有余票则显示余票数量</span></td>

td的样式名为a代表不可选

样式名为e代表已满

样式名为d代表已购

样式名为b则是我们要找的,代表可选,也就是有余票

到这一步,整个购票流程就清楚了

到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了

Node.js 请求目标接口

分析需要开发的功能点

写代码之前我们需要想好功能点,我们需要什么功能:

请求余票接口

定时请求任务

有余票则自动请求购票接口下订单

调用腾讯云短信api接口发送短信通知

多个用户抢票功能

抢某个日期的票

首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。 下面开始安装依赖,根据上面的功能需求,我们大概需要:

请求工具,这里看个人习惯,你也可以使用原生的`http.request`,我这里选择用的是`axios`,毕竟`axios`在node端底层也是调用`http.request`

cnpminstallaxios--save

定时任务 `node-schedule`

cnpminstallnode-schedule--save

node端选择dom节点工具 `cheerio`

cnpminstallcheerio--save

腾讯发短信的依赖包 `qcloudsms_js`

cnpminstallqcloudsms_js

热更新包,诺豆的妈妈,`nodemon` (其实不用也可以)

cnpminstallnodemon--save-dev

开发请求余票接口

接着touch index.js创建核心js文件,开始编码:

首先引入所有依赖

constaxios=require('axios')constquerystring=require("querystring");//序列化对象,用qs也行,都一样letQcloudSms=require("qcloudsms_js");letcheerio=require('cheerio');letschedule=require('node-schedule');

然后我们先定义请求参数,来一个obj

letobj={data:{lineId:111130,//路线idvehTime:0722,//发车时间,startTime:0751,//预计上车时间onStationId:564492,//预定的站点idoffStationId:17990,//到站idonStationName:'宝安交通运输局③',//预定的站点名称offStationName:"深港产学研基地",//预定到站名称tradePrice:0,//总金额saleDates:'17',//车票日期beginDate:'',//订票时间,滞空,用于抓取到余票后填入数据},phoneNumber:123123123,//用户手机号,接收短信的手机号cookie:'JSESSIONID=TESTCOOKIE',//抓取到的cookieday:"17"//定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}

接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,

同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop

编写代码:

classQueryTicket{/***CreatesaninstanceofQueryTicket.*@param{Object}{data,phoneNumber,cookie,day}*@paramdata{Object}请求余票接口的requery参数*@paramphoneNumber{Number}用户手机号,短信需要用到*@paramcookie{String}cookie信息*@paramsday{String}某日的票,如'18'*@memberofQueryTicket请求余票接口*/constructor({data,phoneNumber,cookie,day}){this.data=datathis.cookie=cookiethis.day=daythis.phoneNumber=phoneNumberthis.postData=querystring.stringify(data)this.times=0;//记录次数letstop=false//通过特定接口才能修改stop值,防止外部随意串改this.getStop=function(){//获取是否停止returnstop}this.setStop=function(ifStop){//设置是否停止stop=ifStop}}}

下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数

classQueryTicket{constructor({data,phoneNumber,cookie,day}){//constructor代码...}init(){}//初始化handleQueryTicket(){}//查询余票的逻辑requestTicket(){}//调用查询余票接口handleBuyTicket(){}//购票相关逻辑requestOrder(){}//调用购票接口handleInfoUser(){}//通知用户的逻辑sendMSg(){}//发短信接口}

所有数据都是基于查询余票的操作,因此我们先开发这部分功能

classQueryTicket{constructor({data,phoneNumber,cookie,day}){//constructor代码...}//初始化,因为涉及到异步请求,所以我们使用`asyncawait`asyncinit(){letticketList=awaitthis.handleQueryTicket()//返回查询到的余票数组}//查询余票的逻辑handleQueryTicket(){letticketList=[]//余票数组letres=awaitthis.requestTicket()this.times++//计数器,记录请求查询多少次letstr=res.data.replace(/\\/g,"")//格式化返回值let$=cheerio.load(`<divclass="main">${str}</div>`)//cheerio载入查询接口response的html节点数据letlist=$(".main").find(".b")//查找是否有余票的dom节点//如果没有余票,打印出请求多少次,然后返回,不执行下面的代码if(!list.length){console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)return}//如果有余票list.each((idx,item)=>{letstr=$(item).html()//str这时格式是<span>21</span><span>&$x4F59;0</span>//最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已//因此要在下一步对其进行格式化letarr=str.split(/<span>|<\/span>|\&\#x4F59\;/).filter(item=>!!item===true)letdata={day:arr[0],ticketLeft:arr[1]}//如果是要抢指定日期的票if(this.day){//如果有指定日期的余票if(parseInt(data.day)===parseInt(data.day)){ticketList.push(data)}}else{//如果不是,则返回查询到的所有余票ticketList.push(data)}})returnticketList}//调用查询余票接口requestTicket(){returnaxios.post('/ebus/front/wxQueryController.do?BcTicketCalendar',this.postData,{headers:{'Content-Type':'application/x-www-form-urlencoded','User-Agent':"Mozilla/5.0(iPhone;CPUiPhoneOS8_0likeMacOSX)AppleWebKit/600.1.4(KHTML,likeGecko)Mobile/12A365MicroMessenger/5.4.1NetType/WIFI","Cookie":this.cookie}})}handleBuyTicket(){}//购票相关逻辑requestOrder(){}//调用购票接口handleInfoUser(){}//通知用户的逻辑sendMSg(){}//发短信接口}

来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量

所以我们要把它格式化变成这种数组,也就是ticketList

开发购票功能

首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛

classQueryTicket{constructor({data,phoneNumber,cookie,day}){//constructor代码...}//初始化asyncinit(){letticketList=awaitthis.handleQueryTicket()//如果有余票if(ticketList.length){//把余票传入购票逻辑方法,返回短信通知所需要的数据letresParse=awaitthis.handleBuyTicket(ticketList)}}//查询余票的逻辑asynchandleQueryTicket(){//查询余票代码...}//调用查询余票接口requestTicket(){//调用查询余票接口代码...}//购票相关逻辑asynchandleBuyTicket(ticketList){letyear=newDate().getFullYear()//年份,letmonth=newDate().getMonth()+1//月份,拼接购票日期用得上,因为余票接口只返回几号let{onStationName,//起始站点名offStationName,//结束站点名lineId,//线路idvehTime,//发车时间startTime,//预计上车时间onStationId,//上车的站台idoffStationId//到站的站台id}=this.data//初始化的数据letstation=`${onStationName}-${offStationName}`//站点,发短信时候用到:"宝安交通局-深港产学研基地"letdateStr="";//车票日期lettickAmount=""//总张数ticketList.forEach(item=>{dateStr=dateStr+`${year}-${month}-${item.day},`tickAmount=tickAmount+`${item.ticketLeft}张,`})letbuyTicket={lineId,//线路idvehTime,//发车时间startTime,//预计上车时间onStationId,//上车的站点idoffStationId,//目标站点idtradePrice:'5',//金额saleDates:dateStr.slice(0,-1),payType:'2'//支付方式,微信支付}//调用购票接口letdata=querystring.stringify(buyTicket)letres=awaitthis.requestOrder(data)//返回json数据,是否购票成功等等//把发短信所需要数据都要传入returnObject.assign({},JSON.parse(res.data),{queryParam:{dateStr,tickAmount,startTime,station}})}//购票相关逻辑//调用购票接口requestOrder(obj){returnaxios.post('/ebus/front/wxQueryController.do?BcTicketBuy',obj,{headers:{'Content-Type':'application/x-www-form-urlencoded','User-Agent':"Mozilla/5.0(iPhone;CPUiPhoneOS8_0likeMacOSX)AppleWebKit/600.1.4(KHTML,likeGecko)Mobile/12A365MicroMessenger/5.4.1NetType/WIFI","Cookie":this.cookie}})}handleInfoUser(){}//通知用户的逻辑sendMSg(){}//发短信接口}

到这里,查询余票,购票这两个核心操作已经完成。

目前还剩下,如何通知用户是否购票成功。

之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。

加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。

腾讯云短信的相关内容

其实看看文档就行了,我也是copy文档,注意看短信单发那部分

/document/pr…

如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作

看下短信正文,{Number}这些里面的数字是变量。

就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义

调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推

提交审核,审核一般很快就通过,也就是几十万毫秒吧

开发通知功能

classQueryTicket{constructor({data,phoneNumber,cookie,day}){//constructor代码...}//初始化asyncinit(){letticketList=awaitthis.handleQueryTicket()//如果有余票if(ticketList.length){//把余票传入购票逻辑方法,返回短信通知所需要的数据letresParse=awaitthis.handleBuyTicket(ticketList)//执行通知逻辑this.handleInfoUser(resParse)}}//查询余票的逻辑asynchandleQueryTicket(){//查询余票代码...}//调用查询余票接口requestTicket(){//调用查询余票接口代码...}//购票相关逻辑asynchandleBuyTicket(ticketList){//购票代码...}//调用购票接口requestOrder(obj){//购票接口请求代码...}//通知用户的逻辑asynchandleInfoUser(parseData){//获取上一步购票的response数据和我们拼接的数据let{returnCode,returnData:{main:{lineName,tradePrice}},queryParam:{dateStr,tickAmount,startTime,station}}=parseData//如果购票成功,则返回500if(returnCode==="500"){letres=awaitthis.sendMsg({dateStr,//日期tickAmount:tickAmount.slice(0,-1),//总张数station,//站点lineName,//巴士名称/路线名称tradePrice,//总价startTime,//出发时间phoneNumber:this.phoneNumber,//手机号})//如果发信成功,则不再进行抢票操作if(res.result===0&&res.errmsg==="OK"){this.setStop(true)}else{//失败不做任何操作console.log(res.errmsg)}}else{//失败不做任何操作console.log(resParse['returnInfo'])}}//发短信接口sendMSg(){let{dateStr,tickAmount,station,lineName,phoneNumber,startTime,tradePrice}=objletappid=140034324;//SDKAppID以1400开头//短信应用SDKAppKeyletappkey="asdfdsvajwienin23493nadsnzxc";//短信模板ID,需要在短信控制台中申请lettemplateId=7839;//NOTE:这里的模板ID`7839`只是示例,真实的模板ID需要在短信控制台中申请//签名letsmsSign="测试短信";//NOTE:签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请//实例化QcloudSmsletqcloudsms=QcloudSms(appid,appkey);letssender=qcloudsms.SmsSingleSender();//这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容letparams=[dateStr,station,lineName,startTime,tickAmount,tradePrice];//用promise来封装下异步操作returnnewPromise((resolve,reject)=>{ssender.sendWithParam(86,phoneNumber,templateId,params,smsSign,"","",function(err,res,resData){if(err){reject(err)}else{resolve(resData)}});})}}

如果发信成功,返回result:0

到这里,大部分需求已经完成了,还剩下一个定时任务

定时任务

也声明一个类,这里我们用到的是schedule

//定时任务classSetInter{constructor({timer,fn}){this.timer=timer//每几秒执行this.fn=fn//执行的回调this.rule=newschedule.RecurrenceRule();//实例化一个对象this.rule.second=this.setRule()//调用原型方法,schedule的语法而已this.init()}setRule(){letrule=[];leti=1;while(i<60){rule.push(i)i+=this.timer}returnrule//假设传入的timer为5,则表示定时任务每5秒执行一次//[1,6,11,16,21,26,31,36,41,46,51,56]}init(){schedule.scheduleJob(this.rule,()=>{this.fn()//定时调用传入的回调方法});}}

多个用户抢票

假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket

data:{//用户1lineId:111130,vehTime:0722,startTime:0751,onStationId:564492,offStationId:17990,onStationName:'宝安交通运输局③',offStationName:"深港产学研基地",tradePrice:0,saleDates:'',beginDate:'',},phoneNumber:123123123,cookie:'JSESSIONID=TESTCOOKIE',day:"17"}letobj2={//用户2data:{lineId:134423,vehTime:1820,startTime:1855,onStationId:4322,offStationId:53231,onStationName:'百度国际大厦',offStationName:"裕安路口",tradePrice:0,saleDates:'',beginDate:'',},phoneNumber:175932123124,cookie:'JSESSIONID=TESTCOOKIE',day:""}letticket=newQueryTicket(obj)//用户1letticket2=newQueryTicket(obj2)//用户2newSetInter({timer:1,//每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图fn:function(){[ticket,ticket2].map(item=>{//同时进行两个用户的抢票if(!item.getStop()){//调用实例的原型方法,判断是否停止抢票,如果没有则继续抢item.init()}else{//如果抢到票了,则不继续抢票console.log('stop')}})}})

node index.js运行下,跑起来了

如果他抢到票的话,我就会收到短信通知:

打开手机,看下订单信息

搞定,收工

写在最后

其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的

值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑

如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章

https://juejin.im/post/5d8efeace51d45782b0c1bd6

希望各位能有所收获

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。