简介
本项目的题目为设计一个类似12306的网络列车购票系统后端。通过上一篇文章的分析,我们已经的出了系统的数据原型和需求。下面我将通过给出分解视图、依赖视图、执行视图、实现视图、部署视图和数据库实现来描述项目的完整设计方案。
分解视图
项目采用微服务架构,所以先对模块进行水平拆分,然后进行一些垂直才分得到分解视图。

- PRC为公共的远程调用接口包,其中还包含了每一个模块的远程调用接口
- TicketServer为购票服务,负责接受前端的请求然后进行处理
- ReTicketServer为退票服务
- StaticSearchServer是静态搜索功能,负责处理用户需要获得的不会实时变化或者变化比较慢的数据
- UserServer是用户相关的服务
- CandidateServer是候补服务,通过轮询访问票池是否还有余票,或者是退票
- DynamicSearchServer动态数据的查询,负责处理经常变化的数据的查询,例如余票的查询
- PayServer负责支付和退款服务
- TicketPool票池,用来存储和计算余票情况
依赖视图
模块直接使用gPRC作为远程调用协议,从而对模块进行解耦。

执行视图&API数据
对几个复杂的执行过程做时序图。
支付


流程一:
接受来自另一个服务的请求数据如下:
{
"username": ,// 发出业务的用户名
"money": ,// 付款金额
"affair_number": //业务号
}
然后支付服务通过http请求获得支付宝的orderInfo,然后创建订单,存储在redis中。然后再返回订单号和orderInfo,主要是为了让另一个服务器给用户传回支付信息。
返回数据如下:
{
"code": ,// 返回代码
"msg": ,// 返回信息
"data": {//返回数据
"affair_number", //业务编号
"orderInfo": , //订单信息
}
}
流程二:
用户支付完成后,通知支付服务支付完成,通知数据如下:
{
"username": ,// 发出业务的用户名
"token": ,// 验证信息
"affair_number": ,//业务号
"orderInfo": //订单信息
}
接收到通知后,支付服务器将通过支付宝查询订单状态,然后如果查到已支付完成,将会向相应的服务发出通知然后回传最终数据给用户。
退票


用户选中需要退的票,然后点击退票后前端将向服务器发送退票请求,请求数据如下:
{
"username": ,//用户名称
"token": ,// 验证信息
"ticket_outside_id": // 票号
}
退票服务,查询到这张票对应的订单,然后计算要退款的金额。调用支付服务进行退款,发送数据如下:
{
"order_id": ,// 订单号
"money": // 退款金额
}
购票


用户通过前端点击购票,前端向服务器发送购票请求,请求数据如下:
{
"username":, // 用户名
"token":,//验证信息
"date":, // 发车日期
"train_number":,//车次
"start_station":,//上车站
"end_station":,//下车站
"passengers":[ // 乘客数据,数组
{
"passenger_seq":, // 乘客序号,已经存储在用户信息中
"seat_class":, //座位等级,如一等座,二等座
"seat_tpye": // 座位类型A,B,C等
}
]
}
服务器接收到请求后将检查用户已有的订单中是否存在冲突例如时间重叠,如果没有冲突,购票服务将通过票池获取用户所需的票,通过票信息生成订单返回给用户,用户根据订单查看自己是否要购买,因为用户所选择的座位编号可能是要变化的。返回的订单数据如下:
{
"code":,// 返回代码
"msg": ,// 返回信息
"data":{
"order_outer_id":,//订单外部id
"train_number":,//车次号
"start_station":,// 上车站
"end_station":,//下车站
"arrival_time":,//到达下车站时间
"duration":, // 中间用时
"start_date":,//发车日期
"total_money":,//总金额
"tickets":[// 乘客的票信息
{
"passenger_name":,//乘车人姓名
"passenger_id":,//乘车人身份证号
"carriage_number":,//车厢号
"seat":,//座位号
"money"://票价
}
]
}
}
用户选择取消订单,票将退回票池,如果用户支付订单,系统将进入支付服务。用户支付完成后,返回给用户与订单类似的票数据。
实现视图
12306A/ 12306后端A小组
|------rpc grpc相关的接口和协议文件
| |------pay pay服务器的rpc代码, 同理如果是user服务应该在该文件夹下建立user文件夹
| |------proto .proto文件存放
| |------client grpc客户端, grpc服务再server中自己实现
|------server 每个微服务项目
| |------candidate 候补服务器
| |------controller 控制层,数据的接受的校验
| |------service 服务层,业务逻辑
| |------model 模型层,与数据库连接
| |------redis 缓存连接
| |------setting 配置服务
| |------config 配置文件存放
| |------pay 支付服务器
| |------reticket 退票服务器
| |------search 搜索
| |------dynamic 动态搜索
| |------static 静态搜索
| |------ticket 购票服务器
| |------user 用户服务器
|------ticketPool 线程池服务,主要是对内提供服务
部署视图

数据库

user表用来存储用户的信息,该表是很多信息的基础,例如候补、订单和购票都要根据该表的用户信息进行。user表数据如下表:
字段 | 类型 | 内容 |
---|---|---|
id | unsigned int | 主键 |
created_at | timestamp | 创建时间 |
modified_at | timestamp | 最近一次修改时间 |
deleted_at | timestamp | 删除时间用于软删除 |
created_by | varchar(100) | 创建人 |
modified_by | varchar(100) | 最近一次修改人 |
deleted_by | varchar(100) | 删除人 |
username | varchar(100) | 用户名 |
password | varchar(500) | 密码 |
state | varchar(100) | 状态 |
token | varchar(500) | 验证信息 |
tokenExpire | timestamp | 验证信息过期时间 |
passenger_01_name | varchar(100) | 常用联系人1名称 |
passenger_01_id | varchar(100) | 常用联系人1身份证号 |
passenger_02_name | varchar(100) | 常用联系人2名称 |
passenger_02_id | varchar(100) | 常用联系人2身份证号 |
passenger_03_name | varchar(100) | 常用联系人3名称 |
passenger_03_id | varchar(100) | 常用联系人3身份证号 |
passenger_04_name | varchar(100) | 常用联系人4名称 |
passenger_04_id | varchar(100) | 常用联系人4身份证号 |
passenger_05_name | varchar(100) | 常用联系人5名称 |
passenger_05_id | varchar(100) | 常用联系人5身份证号 |
后面的每个表中都会含有id,created_at,modifided_at,deleted_at,created_by,modified_by,deleted_by字段表中不在进行描述。
order表,用来记录用户的下单情况,只要用户创建了订单不管订单有没有支付,或者已完成或者过期都会在该表中进行记录,表字段和内容如下:
字段 | 类型 | 内容 |
---|---|---|
user_id | unsigned int | 用户id 外键 |
alipay_order_info | varchar(500) | 支付宝订单号 |
money | varchar(100) | 支付金额 |
affair_id | varchar(100) | 对外订单号 |
expire_duration | int | 过期时间 |
state | int | 状态,0未支付,1已支付,2已过期 |
order_outer_id | varhar(100) | 订单外部id |
station表,用来存储站点信息,例如:name:北京北,city:北京,spell:bjb,state:0。字段信息如下表:
字段 | 类型 | 内容 |
---|---|---|
name | varchar(50) | 站名 |
city | varchar(50) | 所在城市 |
spell | varchar(20) | 拼音缩写 |
state | int | 状态 |
train表,用来存储车次信息。字段内容如下表:
字段 | 类型 | 内容 |
---|---|---|
number | varchar(50) | 车次号 |
start_station | varchar(100) | 车次起点站 |
end_station | varchar(100) | 车次终点站 |
train_type | unsigned int | 列车类型 外键 |
state | int | 状态 |
stop_info表用来记录列车停靠站信息,作为station和train的关联表。
字段 | 类型 | 内容 |
---|---|---|
train_id | unsigned int | 车次id 外键 |
station_id | unsigned int | 车站id 外键 |
train_number | varchar(50) | 车次编号 |
station_name | varchar(50) | 车站名 |
arrived_time | varchar(50) | 到达时间 |
stay_duration | unsigned int | 停留时间 |
stay_num | unsigned int | 停留序号 |
candidate表用于存储候补信息,当用户选择候补操作时就会在该表中生成一个候补信息。
字段 | 类型 | 内容 |
---|---|---|
date | timestamp | 候补时间 |
train_id | unsigned int | 候补车次id 外键 |
user_id | unsigned int | 候补的用户id 外键 |
passenger_number | int | 候补人数 |
passenger_id_tag | varchar(20) | 候补乘车人的身份证标识,用逗号分隔 |
state | int | 状态,0正在候补,1候补成功,2为候补失败 |
ticket表用来存储用户已经购买的票。
字段 | 类型 | 内容 |
---|---|---|
user_id | unsigned int | 用户id 外键 |
start_station | varchar(100) | 等待车站名称 |
end_station | varchar(100) | 下车站名称 |
start_time | timestamp | 发车时间 |
train_id | unsigned int | 车次id 外键 |
ticket_outer_id | varchar(100) | 票外部id |
train_num | varchar(50) | 车次号 |
passager_name | varchar(100) | 乘客名 |
passager_id | varchar(100) | 乘客身份证号 |
state | int | 状态,0为未发车,1为已经发车,2为以退票 |
train_run_info表,记录的时列车运行时的情况,例如列车当前已经到那个车站了,方用户查看自己所乘列车的情况,字段如下表。
字段 | 类型 | 内容 |
---|---|---|
train_id | unsigned int | 车次id 外键 |
now_station | varchar(100) | 当前停靠车站名 |
state | int | 状态,1为在行驶,2为在停靠站 |
train_type表用来存储列车类型。虽然有很车次但是,大部分车其实都是一样的,有着相同的车箱数,相同的速度,外观等。我们所以用一个train_type来存储他们之间的共性,这样可以减少数据冗余。字段如下表。
字段 | 类型 | 内容 |
---|---|---|
type_number | varchar(50) | 列车类型编号 |
carriage_list | int | 车厢id 逗号分隔 |
carriage_num | int | 车厢数 |
max_speed | int | 最高速度 |
wifi_state | int | 0为没有wifi,1有wifi |
fool_carriages | varchar(100) | 餐车车厢号,用逗号分隔 |
max_passager | int | 定员数 |
length | int | 列车长度 |
carriage_type存储的是车厢类型。在一辆列车或者不同列车中往往会有相同的车厢,而且存储车厢信息的最主要目的是记录车厢有什么类型的作为、每种作为可以坐多少人、每种座位从什么编号开始卖起。比如车厢中有二等座A、B、C、E、F五种,如果一个乘客购买了一张A的票那就出票0A,当前A的记录加一,下一个乘客购买A座位就出票1A,以此类推。
字段 | 类型 | 内容 |
---|---|---|
soft_berth_number | int | 软卧数量 |
hard_berth_number | int | 硬卧数量 |
senior_soft_benth_number | int | 高级软卧数量 |
hard_seat_number | int | 硬座数量 |
second_seat_number | int | 二等座数量 |
first_seat_number | int | 一等座数量 |
business_seat_number | int | 商务座数量 |
business_seat | varchar(200) | 商务座的全部编号 |
first_seat | varchar(50) | 一等座开始编号,如A、B、E****、F |
second_seat | varchar(50) | 二等座开始编号 |
hard_seat | varchar(50) | 硬座开始编号 |
hard_berth | varchar(50) | 硬卧开始编号 |
soft_berth | varchar(50) | 软卧开始编号 |
senior_soft_berth | varchar(50) | 高级软卧开始编号 |
技术选型说明
开发方法:
在需求分析阶段,使用面向对象的方式对系统进行分析,得到项目的数据库原型。在实现阶段采用面向接口的开发方式,使用web框架后小组成员实现网络接口,并且在不同的模块直接也采用面向接口编程方式,这样能够很好的保持软件的可维护性和可扩展性。
保证软件的健壮性
首先,使用gin网络框架自带的validator对所有传入的数据进行验证,拒绝非法的数据。
其次,使用JWT技术对前端请求进行验证。前端进行登录后,服务器会发送一个带有时效的token字符串,然后在一些请求中都需要带有这段token,否则服务器将拒绝请求。若token过期了服务器会要求重新登录。
保证时延要求
这里的时延并不包括网络时延,主要是请求到达服务器后再返回数据的时间。使用zipkin工具记录请求进入到服务器后到返回请求数据的时间差。
使用redis保证快速响应请求。redis是一个K-V内存数据库,由于将数据存储在内存当中所以速度比普通的数据库快很多,也因为在内存中所以redis主要还是当缓存使用。
将余票信息存在在内存中,这样可以快速计算余票。
开发主要语言:Golang
开发环境:Windows10,MacOS
部署环境:Docker+Ubuntu
开发工具:Goland,VSCode
测试方案:wrk性能测试,配合前端进行黑盒测试