簡介
本項目的題目為設計一個類似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性能測試,配合前端進行黑盒測試
