12306订票系统的关键就是票务信息的查询和票务订单处理。我们先抛开其余人性化功能,把基本功做好。因此,我们对12306订票系统做几个基本要求:
- 订票期间能随时登陆上去;
- 查询余票时能基本准确地显示各类余票信息;
- 订票支持备选方案,主方案订票失败时及时启用备选方案订票;
- 想退票就退票。
- 还有什么要求呢?再想想吧…
12306JMNA版
- Gitcafe项目主页:https://gitcafe.com/12306
- 项目交流论坛:http://www.angularjs.cn/forums/forum/ngapp
- 项目设计展现页:http://www.angularjs.cn/docs/blogs/541.html
本方案从查询和票务订单处理这两个根本性问题着手,重新构建一个合理的构架。由于票务系统处理的主要事件就是数据库信息查询/修改,不涉及高负荷运算,所以本构架决定采用JavaScript+MongoDB+NodeJS+AngularJS来实现(12306JMNA版)。有人就会提出,NoSQL数据库不能保证事务一致性,不能用于订票系统。我认为恰恰是事务一致性的设计让12306系统烂到了家。
对于NodeJS和MongoDB,程序员一般都很熟悉了,配合下面的专为12306设计的构架图及数据结构,你会发现,处理票务系统对他们来说就是天赋本能,小菜一碟。至于AngularJS,是Google专为前端技术、WEB应用打造出来的新武器。AngularJS必将成为前端工程师们的绝招。AngularJS也将成为开发移动WEB应用的主将。感兴趣的同学不妨去AngularJS中文社区看看,有教程!AngularJS在本系统的作用就是把UI视图运算量从服务器分离,客户端浏览器来承担。如此一来,客户端与服务器的对话几乎变成了纯JSON数据交换对话。不吹了,看构架图:
如图,我们把订票系统分解,得到位池、余票池、订票池、取票池四个票务处理系统,以及独立的前台伺服系统、支付系统和通知系统,其中的精华就是位池、余票池系统。售票窗和电话取票内部原理与网上订票一样,只是多了一个人工客服或电话客服。将订票系统分解,目的是使订票事件异步处理,独立系统处理专业事件,实现简单,效率更高。子系统对应的数据结构也更符合实际需求,数据处理更快。每个子系统的处理任务不同,对服务器的要求也不同,从而实现服务器的灵活配置。
分解系统的依据是使票务信息数据库结构更简单、查询链更短、“静态化”。如此以来,我们查询票务信息时面对的仿佛就是一张简单的表格,根据我们提供的列车车次或者出发站/目的站直接就可以定位到相应列车的余票信息。而且这个余票信息还是实时更新的——后台驱动在默默地为您服务。
先简单描述一下12306JMNA系统实现流程及异步处理原理:
用户进入12306JMNA系统时,前台服务器返回一个纯静态用AngularJS构建的UI视图数据和一对“通信ID : 通信密钥”。客户端,AngularJS根据用户的需求生成相应界面,如查票界面、购票界面、通知反馈界面等。用户发出查余票、订票等请求时,AngularJS把请求打包成JSON数据包,用通信密钥加密,向服务器发出请求。服务器收到JSON请求包后,根据通信ID,用相应密钥解密,然后把请求派发给相应子系统。服务器不等反馈结果,直接处理下一位用户的请求。当子系统把请求处理完毕,向前台服务器返回处理结果JSON数据包。前台服务器收到结果数据包,根据通信ID,用相应密钥加密,向对应用户推送。该用户的AngularJS收到请求的结果数据包后,先解密、再解析编译到用户的UI视图界面中。这就是订票系统的异步处理原理。
前台服务器实际上还包含了在线用户数据池,消息队列池等,这些未在构架图中标出。当然,还有用户账号系统等也未标出。
下面我们来一一分析这些数据池结构及对应驱动的功能。(先上数据结构)
一、位池
位池系统是余票查询与订票的基础。位池数据库记录所有列车每一个票位在沿途各站点的使用情况,大家可以看看下面的数据结构,很简单,但也很重要。
{ /** 位池文档结构,订票系统的根基,发生订票时,向余票池请求减去或增加相应票位,向待支付票池请求生成订单。
一个文档对应列车中的一个位,如座位或者卧铺位,甚至站位。
文档的集合形成一个特定车次的列车。
一般一趟车次有不到2000车位,即集合里面文档树不到2000个,如开放站票可能会多点。
一天内所有列车集合形成当日所有列车位池数据库,当日列车位池形成当日位池数据库。
全国有不到5000次列车,即表示每日的数据库中约有不到5000个集合,每日一个数据库*/
"_id" : 列车位唯一ID, //由‘日期+列车ID+列车位ID’编码而成。
"seat_type" : 位类型, //如上铺、中铺、下铺、座位、站票、一等座、二等座等,该字段建索引。
"station" : { //途经站点,标记该位车票在哪一个站点被谁订购,下面以G1011武汉-深圳北(高铁)站点数据示例。
"武汉" : null, //null表示该位在该站点无人订购。
"咸宁北" : null,
"岳阳东" : 410xxxxx10, //表示身份证(或其它证件号)为410xxxxx10的顾客订购了岳阳东到株洲西的票。
"长沙南" : 410xxxxx10,
"株洲西" : 430xxxxx30, //表示身份证(或其它证件号)为430xxxxx30的顾客订购了郴州西到深圳北的票。
"郴州西" : 430xxxxx30,
"广州南" : 430xxxxx30,
"虎门" : 430xxxxx30,
"深圳北" : null
}
}
位池驱动程序专门伺服位池数据库,功能如下:
- 根据票务字典,每日出票阶段进行数据初始化;
- 接受来自订票池的请求。对于下单事件所有列车票请求(一个订单有一张或多张列车票),在位池数据库中查找特定列车车次的某个符合位类型的列车位文档,检查相应站段是否为null。如果所有要求的列车票均存在,则在相应请求站段写入顾客证件号。写入成功后向余票池发出请求,要求余票池减去相应票位ID,同时向订票池反馈下单成功。
- 接受来自订票池的退订请求和来自取票池的退票请求,根据请求,把位池中相应的车位重置为null,请求余票池增加相应的票位ID,同时向订票池或取票池反馈请求成功。
二、订票池
订票池系统以最简约的方式受理订票事件。支持同时订多张票和备选订票方案。
{ /** 订票池文档结构,处理过期未支付的无效订单,为支付系统提供数据。
一个订单文档对应一个下单人的下单事件。*/
"_id" : 订单唯一ID, //由‘下单人编号+下单时间+订单编号’编码而成。
"gen_time" : null, //下单时为null,下单成功后填入订单生成时间值,也标记了有效支付期的开始。
"pay_time" : null, //订单支付时间,驱动程序扫描该字段,null表示未支付,存在时间值表示已支付或者已被锁定请求释放中。
"pay_info" : {订单支付详细信息}, //支付信息对象,初始值为null,支付成功后即写入数据。
"order" : [{ //主订单列车票对象数组,数组中每个对象代表一张预定的列车票信息
"seat_type" : 位类型, //如上铺、中铺、下铺、座位、站票、一等座、二等座等,该字段建索引。
"seat_num" : null, //即位池中的列车位唯一ID,下单时为null,下单成功后填入位ID。
"price" : 票价, //根据订票站段信息从字典表查询而来。
"customer" : { //订票人信息
"name" : 姓名,
"cer" : 证件,
"cer_num" : 证件号,
"phone" : 手机号,
"email" : 电子邮箱
},
"s_station" : 出发站名称, //出发站点,如上面430xxxxx30顾客,该值为“株洲西”。
"d_station" : 到达站名称, //到达站点,如上面430xxxxx30顾客,该值为“深圳北”。
},
{第二张列车票}, //订单中第二张列车票对象。
{...}], //更多。
"re_order" : [{ //备用订单列车票对象数组,结构同上,主订单失败时启用备用订单。
"seat_type" : 位类型, //如上铺、中铺、下铺、座位、站票、一等座、二等座等,该字段建索引。
"seat_num" : null, //即位池中的列车位唯一ID,下单时为null,下单成功后填入位ID。
"price" : 票价, //根据订票站段信息从字典表查询而来。
"customer" : { //订票人信息
"name" : 姓名,
"cer" : 证件,
"cer_num" : 证件号,
"phone" : 手机号,
"email" : 电子邮箱
},
"s_station" : 出发站名称, //出发站点,如上面430xxxxx30顾客,该值为“株洲西”。
"d_station" : 到达站名称, //到达站点,如上面430xxxxx30顾客,该值为“深圳北”。
},
{第二张列车票}, //订单中第二张列车票对象。
{...}] //更多。
}
订票池驱动程序专门伺服订票池数据库,功能如下:
- 接受顾客订单请求,生成订单文档。然后向位池系统发出订单请求,位池系统反馈订单成功后向订单文档每个列车票对象写入成功获得的列车位ID和订单正时生成时间。然后向顾客反馈下单成功,同时向支付系统发出支付请求。如果下单失败,启用备用订单,再次向位池系统发出订单请求。如果备用订单仍失败,则删除该订票文档,同时向顾客反馈下单失败。
- 若支付系统反馈支付成功,则向取票池发出请求生成取票文档,同时在订票池中删除该已支付的订票文档。
- 对于超过有效支付期的订票文档,向位池系统发出退单请求,位池系统反馈退单成功后即删除该订票文档。
三、取票池
取票池系统生成取票文档,受理查询、退票、出票、记录存档等事件。
{ /** 取票池文档结构,处理退票、出票事件。与订票池文档结构不同,一个取票池文档对应一张列车票。
由于取票操作是以证件号为依据,需对[customer][cer_num]字段建索引。
根据出发站点将取票文档分成不同集合不同数据库,提高查询效率。
按照前面假设的数据最大化估算,取票池总文档数量可能达到亿级。*/
"_id" : 列车票唯一ID, //由‘列车位唯一ID+订票人证件类型+订票人证件号’编码而成,记录至订票系统个人账户中。
"type" : 位类型, //如上铺、中铺、下铺、座位、站票、一等座、二等座等。
"pay_time" : 支付时间, //订单支付时间,也是车票确认时间。
"pay_interface" : 订单支付接口, //订单支付接口。
"pay_info" : {订单支付详细信息}, //支付信息对象,为退票提供退款目标。
"ticket_time" : 出票时间, //null表示未取票。
"ticket_place" : 出票地点, //null表示未取票。
"price" : 票价, //根据订票信息从字典表查询而来。
"seat" : 位置编号, //如“XX车厢XX号上铺”,根据“_id”中列车位唯一ID,查询票务字典得到。
"customer" : { //订票人信息
"name" : 姓名,
"cer" : 证件类型,
"cer_num" : 证件号, //该字段建索引。
"phone" : 手机号,
"email" : 电子邮箱
},
"s_station" : 出发站名称,
"d_station" : 到达站名称,
"s_time" : 出发时间, //根据订票信息查询票务字典得到,人性化。
"d_time" : 到达时间, //根据订票信息查询票务字典得到,人性化。
}
取票池驱动程序专门伺服取票池数据库,功能如下:
- 根据订票池的请求生成相应的取票文档,若一个订票文档包含多张列车票,则会生成多个取票文档,同时将取票文档记录到用户账号,方便用户查询或退票;
- 若用户发出某张列车票退票请求,向位池发出退票请求;
- 获得退票请求成功反馈后向支付系统请求退还相应款项;
- 获得退款成功反馈后将该退票文档永久存档,同时在取票池中删除相应的取票文档。
- 响应现场取票机的查询及取票请求,取票成功后向该文档写入取票时间、地点,然后将该取票文档永久存档,同时在取票池中删除相应的取票文档。
- 对于超过取票期限的取票文档,执行退票请求(退款还是不退款?或退几成款?看铁老大啦)。
四、余票池
余票池系统是订票系统的关键,对用户而言,余票池系统实现了伪静态化特征。它还能体现某一列车位在不同站点空置情况。
{ /** 余票池文档结构,处理余票查询事件。
位池发生订票行为时,向余票池发出信息,余票池根据订票日期、列车ID、列车位ID,出发站和到达站信息,在余票池中删除相应的余票信息。
一个文档对应一趟列车的余票信息,所有列车当日余票信息组成一个集合,订票期内(如12天)每天所有列车集合共形成一个余票池数据库。
针对春节的返程票订购,可另外生成一个返程票余票池数据库。*/
"_id" : 列车唯一ID, //由‘日期+列车ID’编码而成。以下字段属性名为该列车所经站点,对应值是可能目的站点的组合对象。以G1011为例。
"武汉" : { //出发车站
"咸宁北" : { //表示武汉到咸宁北有以下类型车票及相应列车位ID数组。
"位类型1" : [列车位唯一ID,,], //表示位类型1,如一等座,剩余列车位ID数组。数组长度即剩余数量。
"位类型2" : [列车位唯一ID,,], //如,二等座。。。
... //其它位类型,如中铺、上铺等,省略表示。
},
"岳阳东" : {,,}, //武汉到岳阳东段的余票,省略表示,下同。
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
},
"咸宁北" : { //出发车站
"岳阳东" : {,,},
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
},
"岳阳东" : {
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
},
"长沙南" : ...
"株洲西" : ...
"郴州西" : ...
"广州南" : ...
"虎门" : {
"深圳北" : {
"位类型1" : [列车位唯一ID,,],
"位类型2" : [列车位唯一ID,,],
...
}
},
"深圳北" : null
}
余票池驱动程序专门伺服余票池数据库,功能如下:
- 根据票务字典对余票池初始化,未出票时,余票池包含所有的列车位ID。
- 响应位池的请求,增加或减去指定列车位ID。以G1011为例,假设430xxxxx30顾客订购郴州西到深圳北的一等座。位池处理后得到了该车票的列车位唯一ID,假设是“20121001-G1011-0117”号(2012年10月1日G1011次1号车厢17座),向余票池发出减票请求。余票池收到请求,找到该趟G1011次列车的余票文档。从始发站字段“武汉”开始,在其中郴州西到深圳北的“一等座”属性对应的数组中,删除“20121001-G1011-0117”,然后依次在“咸宁北”、“岳阳东”、“长沙南”…“虎门”做类似操作。其中,深圳北是到达站,订票时位池系统并未占位写入顾客身份证,这里也无需做相应删除操作。诸位也许觉得,操作有点多~,但这是必须的,为了最短的查询路径。查询事件总是远大于订票事件的。
- 处理查询请求。例如,410xxxxx10顾客查询10月1日从岳阳东到株洲西的票。余票池驱动先根据票务字典查找出10月1日所有从岳阳东和株洲西的列车ID,再根据列车ID查询对应的余票文档,在余票文档中找到主键“岳阳东”,再在其对象中找到目的地“株洲西”,最后返回[岳阳东][株洲西]对应的对象值:位类型和列车位ID数组长度的键值对。今后若提供选座功能,则该查询列车位ID数组。这应该是最短查询路径。
五、票务字典(静态数据库)
列车趟次——途径站名——到达/出发时间——票价字典
{ /*列车趟次——途径站名——到达/出发时间——票价字典,静态值。
与下面的车站名——列车趟次字典等组成票务字典数据库。为位池、票池、余票池等提供基础信息。*/
"_id" : 列车ID, //由‘车次’编码而成。以下字段属性名为该列车所经站点,对应值是可能目的站点的组合对象。以G1011为例。
"武汉" : [
到达时间, //始发站为null
发车时间, //发车时间
{
"咸宁北" : { //表示武汉到咸宁北有以下类型车票及票价。
"位类型1" : 票价, //表示位类型1,如一等座,及对应票价。
"位类型2" : 票价,
... //其它位类型,如中铺、上铺等,省略表示。
},
"岳阳东" : {,,},
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
}
],
"咸宁北" : [
到达时间, //到达咸宁北的时间
发车时间, //发车时间
{
"岳阳东" : {,,},
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
}
],
"岳阳东" : [
到达时间, //到达岳阳东的时间
发车时间, //发车时间
{
"长沙南" : {,,},
"株洲西" : {,,},
"郴州西" : {,,},
"广州南" : {,,},
"虎门" : {,,},
"深圳北" : {,,}
],
"长沙南" : [,,],
"株洲西" : [,,],
"郴州西" : [,,],
"广州南" : [,,],
"虎门" : [,,],
"深圳北" : null
}
列车趟次——位类型——列车位ID字典
{ /*列车趟次——位类型——列车位ID字典,静态值。*/
"_id" : 列车ID,
"位类型1" : [列车位ID数组],
"位类型2" : [列车位ID数组],
...
}
###车站名——列车趟次字典
{ /*车站名——列车趟次字典,一般车票查询基于车站到车站.
该字典迅速提供车站到车站的可用列车车次,然后根据列车车次查询相关余票。*/
"武汉" : [靠站列车1唯一ID, 靠站列车2唯一ID,,,,], //表示在武汉站停车的所有列车车次。
"株洲" : [,,,],
... //全国所有列车站的数据字典。
}