一、系统概述
二、需求分析
2.1 系统功能分析
2.2 系统数据分析
2.3 系统非功能分析
三、系统设计
3.1 应用程序设计
3.2 数据库设计
3.2.1 概念设计
3.2.2 逻辑设计
四、系统实现
4.1 关键技术实现
4.2 功能实现
五、系统测试
六、问题记录
一、系统概述
飞机票售票系统,分为两个角色,系统管理员和用户
系统管理员:查询航班、添加航班、取消航班、查询订单信息等功能
用户:查询航班、预订机票、添加乘客信息、删除乘客信息、查看已支付/已退款的订单、改签、退款、查看账单等功能
二、需求分析
2.1 系统功能分析
飞机票售票系统,分为两个角色,系统管理员和用户
系统管理员:
①可以查询航班、飞机票相关信息,包括飞机票的起止地、时间、价格、折扣、余票数等。
②可以添加航班
③可以取消航班
④可以查询订单信息
用户:
①可以查询航班,每个航班多种有不同折扣的飞机票,不同折扣对应着不同的价格和舱位。
②可以预订机票,预订机票时可以选择输入新的乘客信息,或勾选已有乘客信息。
③在个人页可以添加乘客信息、删除乘客信息。
④在个人页可以查看已支付/已退款的订单。
⑤在订单页已支付的订单可以改签、退款。
⑥可以查看账单
2.2 系统数据分析
(一)登录系统时,需要一个用户账户
①需要一个表保存用户信息
(二)在查询航班时,页面上需要显示指定日期和指定起始点的航班,此时需要显示航班的起始地,日期,时间,最低价等相关信息。点击某航班后的预订按钮,跳转到当前航班所包含的所有价位的飞机票界面。
②需要一个表来保存航班相关信息。包括起止地、日期、时间、原价等
③需要一个表来保存航班所含的所有飞机票信息。包括折扣、总票数、余票数等
联系:每个航班对应多种不同折扣飞机票
(三)在处理最短中转问题时,需要根据不同城市的位置来选择中转城市,以实现最短距离中转。以及在处理国际时差问题时,需要根据不同城市所处国家的时差来进行时差转换。
④需要一个表来保存城市相关信息。
联系:每个航班的起止城市的信息都在城市表中。
(四)在预订机票时,形成一个订单,订单包括所预订的飞机票的飞机票ID , 订单时间,订单金额,订单属性 ( 直达或中转 ) ,支付状态 ( 已支付或已退款 ) ,订单所属的用户的用户ID,以及订单所属乘客的身份证号等。
⑤需要一个表来保存订单相关信息。
联系:一种飞机票可能属于多份订单,一份订单可能包含多于一种的飞机票 ( 如:中转订单 ) 。
一个用户可能拥有多份订单。
一个乘客可能使用多份订单。
(五)在用户的个人中心,可以查看已保存的乘客的信息
⑥需要一个表来保存乘客相关信息。
联系:个用户可能保存有多个乘客的信息,一个乘客的信息也可能保存在多个用户的账户中。
(六)航班取消时会向用户发布航班取消的通知。
⑦需要一个表保存通知的相关信息。
联系:一个用户可能收到多条通知。
(七)用户每笔收入、支出都记录在账单中。
⑧需要一个表保存账单信息。
联系:一个用户可能有多条收支记录。
2.3 系统非功能分析
(一)系统性能:合理建立索引,加快了查询数据的速度。
(二)安全性:前端采用js进行数据校验,非法的输入和请求不能成功传到
后端,且设置弹框提醒用户输入正确合法的输入。
(三)可用性:前端采用简洁且功能明确的界面,易于用户使用。
.
三、系统设计
3.1 应用程序设计
①架构:采用B/S架构,使用SSM+VUE技术,前后端分离。
②前端:vue展示界面,通过调用接口从后端获取数据。
③后端:数据库使用mybatis作为持久层框架,后端java使用spring框架,并加入springMvc架构,对请求和响应进行统一处理。同时使用maven进行项目管理。
④前后端通信:
前端:通过axios向后端发送请求和接收数据,
后端: ( 1 ) .Controller层接收前台数据和返回页面请求信息。
( 2 ) .service层接受controller层信息,用于业务处理和逻辑判断。Service 用于处理业务逻辑,会调用mapper层的API;
( 3 ) .mapper层用于和数据库交互,想要访问数据库并且操作,只能通过mapper层向数据库发送sql语句,将这些结果通过接口传给service层,对数据库进行数据持久化操作。
3.2 数据库设计
3.2.1 概念设计
3.2.2 逻辑设计
①航班(航班号,出发城市,起始机场,出发日期,出发时间,终点城市,到达机场,到达日期,到达时间,航班价格,存在性)
②飞机票(票号,航班号,折扣,总票数,余票数)
③订单(订单号,属性,总金额,支付状态,完成时间,用户ID,乘客身份证号)
④订单-飞机票(订单号,票号)
⑤用户(用户ID,用户名,用户密码)
⑥乘客(身份证号,姓名,电话)
⑦用户-乘客(用户ID,乘客身份证号)
⑧通知(通知号,用户ID,航班号,通知时间)
⑨账单(账单号,用户ID,收支金额,事项描述,时间)
⑩城市(城市ID,城市名,横坐标,纵坐标,地理位置,时间)
.
四、系统实现
4.1 关键技术实现
框架:前后端分离,前端使用vue框架,数据库使用mybatis作为持久层框架,
后端java使用spring框架,并加入springMvc架构,对请求和响应进行
统一处理。同时使用maven进行项目管理。
MVC架构分层的主要作用是解耦。采用分层架构的好处,普遍接受的是系
统分层有利于系统的维护,系统的扩展。就是增强系统的可维护性和可扩
展性。
( 1 ) .Controller层接收前台数据和返回页面请求信息。
( 2 ) .service层接受controller层信息,用于业务处理和逻辑判断。Service 用于处理业务逻辑,会调用mapper层的API;
( 3 ) .mapper层用于和数据库交互,想要访问数据库并且操作,只能通过mapper层向数据库发送sql语句,将这些结果通过接口传给service层,对数据库进行数据持久化操作。
插件:由于使用了maven进行项目管理,一些插件的使用可以通过在pom.xml文件中配置来实现,比如:tomcat插件
导入一些依赖,就可以使用相应的工具。
比如:Fastjson 是一个 Java 库 , 可以将 Java 对象转换为 JSON 格
式 , 当然它也可以将 JSON 字符串转换为 Java 对象。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
导入lombok的依赖,可以直接使用注解生成类的getter,setter方法及构造方法。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
导入junit,可以进行测试
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
4.2 功能实现
1.功能实现-最短中转
在查询某趟航班的时候,前端通过axios将出发城市和终点城市的城市名传给相应的后端接口,在城市表中查询所有城市的坐标位置,获得城市之间的距离矩阵。
然后进行一次复杂度为0 ( n ) 的遍历,找到 ( dik+dkj)最小时k的值,k对应的城市即为最短中转城市。
寻找合适的中转航班时还要考虑时差,限制第二趟航班的出发时间减第一趟航班的到达时间的值在两个小时和十二个小时之间。
源代码:
①前端通过axios将出发城市和终点城市的城市名传给相应的后端接口,后端先通过CityController进行处理,再将请求转发给PlaneController进行处理。
//获得中转城市名称
@RequestMapping ( value = "/transit" , method = RequestMethod.POST )
public String transit ( HttpServletRequest req ) throws IOException {
//获得对象
BufferedReader reader = new BufferedReader ( new InputStreamReader ( req.getInputStream ( ) ) ) ;
String s = reader.readLine ( ) ;
String str=s.substring ( 8 , s.length ( ) -1 ) ;
Plane f_plane= JSON.parseObject ( str , Plane.class , Feature.InitStringFieldAsEmpty ) ;
//解码
try {
f_plane.setStart_city ( java.net.URLDecoder.decode ( f_plane.getStart_city ( ) , "UTF-8" ) ) ;
f_plane.setEnd_city ( java.net.URLDecoder.decode ( f_plane.getEnd_city ( ) , "UTF-8" ) ) ;
} catch ( UnsupportedEncodingException e ) {
e.printStackTrace ( ) ;
}
System.out.println ( f_plane.getStart_city ( ) +" "+f_plane.getEnd_city ( ) +" "+f_plane.getStart_day ( ) ) ;
//获得中转城市名称
Map<String , Object> map=new HashMap<> ( ) ;
String transitCity=cityService.getTransitCity ( map , f_plane.getStart_city ( ) , f_plane.getEnd_city ( ) ) ;
req.setAttribute ( "transit_city" , transitCity ) ;
req.setAttribute ( "start_city" , f_plane.getStart_city ( ) ) ;
req.setAttribute ( "end_city" , f_plane.getEnd_city ( ) ) ;
req.setAttribute ( "start_day" , f_plane.getStart_day ( ) ) ;
return "forward:/search3" ;
}
CityContronller内调用获得中转城市的服务,即getTransitCity
↓
①获得中转城市:
public String getTransitCity ( Map map , String start_city , String end_city ) {
List<City> cities= cityMapper.getCityList ( map ) ;
int s=cities.size ( ) ;
int[][] distance=new int[s][s] ;
for ( int i = 0 ; i < s ; i++ ) {
for ( int j = 0 ; j < s ; j++ ) {
int xi=cities.get ( i ) .getX ( ) ;
int yi=cities.get ( i ) .getY ( ) ;
int xj=cities.get ( j ) .getX ( ) ;
int yj=cities.get ( j ) .getY ( ) ;
distance[i][j]=Math.abs ( xj-xi ) *Math.abs ( xj-xi ) +Math.abs ( yj-yi ) *Math.abs ( yj-yi )
}
}
Map<String , Object> startM=new HashMap ( ) ;
startM.put ( "city_name" , start_city ) ;
List<City> cc=cityMapper.getCityList ( startM ) ;
Map<String , Object> endM=new HashMap ( ) ;
endM.put ( "city_name" , end_city ) ;
List<City> ccc=cityMapper.getCityList ( endM ) ;
int end=ccc.get ( 0 ) .getCity_id ( ) ;
if ( cc.size ( ) >0&&ccc.size ( ) >0 ) {
int start=cc.get ( 0 ) .getCity_id ( ) ;
for ( int i = 0 ; i < s ; i++ ) {
if ( cities.get ( i ) .getCity_id ( ) ==start ) {
start=i ;
break ;
}
}
for ( int i = 0 ; i < s ; i++ ) {
if ( cities.get ( i ) .getCity_id ( ) ==end ) {
end=i ;
break ;
}
}
int minD=1000000 ;
int flag=0 ;
for ( int i = 0 ; i < s ; i++ ) {
if ( cities.get ( i ) .getLocation ( ) .equals ( "china" ) &&i!=start&&i!=end ) {
if ( distance[start][i]+distance[i][end]<minD ) {
minD=distance[start][i]+distance[i][end] ;
flag=i ;
}
}
}
return cities.get ( flag ) .getCity_name ( ) ;
}
else return "?" ;
}
获得中转城市后,再将请求转发给PlaneController,在PlaneController中调用getPlaneListWithLowestPrice获得中转第一程和第二程的航班信息,航班信息中包括航班的最低价,最低价通过plane表和ticket表联表查询获得。
//找到合适的中转航班,中转时间大于1h,小于12h,且在同一个机场
List<Planes2> mm=new ArrayList<> ( ) ;
SimpleDateFormat sdf1= new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss" ) ;
for ( int i = 0 ; i < planes1. size ( ) ; i++ ) {
for ( int j = 0 ; j < planes2. size ( ) ; j++ ) {
if ( !planes1. get ( i). getArrival_airfield ( ). equals ( planes2. get ( j ). getDeparture_airfield ( ))){
continue ;
}
System. out. println ( 1 ) ;
String day1=planes1. get ( i ) . getStart_day ( )+" "+planes1. get ( i ) . getEnd_time ( ) ;
String day2=planes2. get ( j ) . getStart_day ( )+" "+planes2. get ( j ) . getStart_time ( ) ;
System. out. println ( day1 ) ;
System. out. println ( day2 ) ;
long ms=sdf1. parse ( day2). getTime ( )-sdf1. parse ( day1). getTime ( ) ;
System. out. println ( ms ) ;
if ( ms >= 3600000 && ms <= 43200000) {
Planes2 pp=new Planes2 ( planes1. get ( i ),planes2. get ( j ) ) ;
mm. add ( pp ) ;
break ;
}
}
}
在xml文件中映射接口
<select id="getPlaneListWithLowestPrice" parameterType="map" resultType="ds . pojo . Plane">
select e . plane_id , e . start_time , e . end_time , e . start_city , e . end_city , e . start_day , e . end_day ,
e . departure_airfield , e . arrival_airfield , lowest_price
from
( select a . plane_id,min ( price*coun ) lowest_price
from (
select * from planes_system . plane where
start_city = #{ start_city }
and end_city = #{ end_city }
and start_day = #{ start_day }) a,
planes_system . ticket b
where a . plane_id = b . plane_id
group by a . plane_id) d,planes_system . plane e
where d . plane_id = e . plane_id and e . exist= 'YES'
order by start_time asc
</select>
功能演示:
2.功能实现-改签
①在deal_ticket表中,删除原飞机票与该订单的绑定,添加改签后的飞机
票与该订单的绑定。
②需要更新订单的价格。一般改签的价格更新原则是多不退少补。
③在更新价格的时候,要考虑到直达的订单和中转的订单有一定的区别。
④更新票的数量。
直达:改签后的飞机票价格和原飞机票价格中较高的一项+机场建设费、燃油费
作为改签后的价格。
中转:若改签后的价格高于原价格,原订单价格原飞机票价格+改签后飞机票价格。
源代码:
当订单性质不同(“直达”或“中转”),前端传回的数据不同。
change ( deal, plane, ticket, index ) {
console . log ( deal ) ;
if ( deal . attribute === 'direct' ) {
if ( ( ticket . coun*plane . price/100+190 ) > deal . price ) {
axios . post ( '/addDealRecord', {
data:{
amount:encodeURI ( '-' . concat ( ( ticket . coun*plane . price/100+190 ) -deal . price ) ) ,
id:encodeURI ( sessionStorage . getItem ( "id" ) ) ,
description:encodeURI ( "改签" ) ,
time:new Date ( ) . Format ( "yyyy-MM-dd hh:mm:ss" )
}
} ) . then ( ( response ) => {
console . log ( response . data ) ;
} ) . catch ( function ( error ) {
console . log ( error ) ;
} ) ;
}
axios . post ( '/change', {
data:{
price: ( ticket . coun*plane . price/100+190 ) >deal . price? ( ticket . coun*plane . price/100+190 ) :deal . price,
old_ticket_id:deal . tickets[index] . ticket_id,
new_ticket_id:ticket . ticket_id,
deal_id:deal . deal_id
}
} ) . then ( ( response ) => {
if ( response . data ) {
this . $alert ( '改签成功', '信息', {
confirmButtonText: '确定'
} ) ;
this . $router . push ( { path:"/dealPaid" } ) ;
}
} ) . catch ( function ( error ) {
console . log ( error ) ;
} ) ;
}else if ( deal . attribute==='transit' ) {
if ( ( ticket . coun*plane . price ) >deal . tickets[index] . coun*deal . tickets[index] . plane . price ) {
axios . post ( '/addDealRecord', {
data:{
amount:encodeURI ( '-' . concat ( ( ticket . coun*plane . price ) /100-deal . tickets[index] . coun*deal . tickets[index] . plane . price/100 ) ) ,
id:encodeURI ( sessionStorage . getItem ( "id" ) ) ,
description:encodeURI ( "改签" ) ,
time:new Date ( ) . Format ( "yyyy-MM-dd hh:mm:ss" )
}
} ) . then ( ( response ) => {
console . log ( response . data ) ;
} ) . catch ( function ( error ) {
console . log ( error ) ;
} ) ;
}
axios . post ( '/change', {
data:{
price: ( ticket . coun*plane . price ) >deal . tickets[index] . coun*deal . tickets[index] . plane . price ?
( deal . price-deal . tickets[index] . coun*deal . tickets[index] . plane . price/100
+ticket . coun*plane . price/100 ) :deal . price,
old_ticket_id:deal . tickets[index] . ticket_id,
new_ticket_id:ticket . ticket_id,
deal_id:deal . deal_id
}
} ) . then ( ( response ) => {
if ( response . data ) {
this . $alert ( '改签成功', '信息', {
confirmButtonText: '确定'
} ) ;
this . $router . push ( { path:"/dealPaid" } ) ;
}
} ) . catch ( function ( error ) {
console . log ( error ) ;
} ) ;
}
}
因主要流程仍是前端传数据,后端接收,相应的controller调用service方法进行处理,故此处后端只分析关键代码。
先在DealController进行如下操作:
//更新改签后订单的价格,一般改签的原则是:多不退少补
HashMap<String, Object> map = new HashMap<String, Object> () ;
map.put ("deal_id", deal.getDeal_id () ) ;
map.put ("price", deal.getPrice () ) ;
dealService.updateDeal (map) ;
//在deal_ticket中删除原绑定,添加新绑定
JSONObject oo= JSON.parseObject (str) ;
String old_ticket_id=JSON.toJSONString (oo.get ("old_ticket_id") ) ;
String new_ticket_id=JSON.toJSONString (oo.get ("new_ticket_id") ) ;
HashMap<String, Object> map1 = new HashMap<String, Object> () ;
map1.put ("ticket_id", Integer.parseInt (old_ticket_id) ) ;
map1.put ("deal_id", deal.getDeal_id () ) ;
dealService.deleteDealTicket (map1) ;
deal.setTicket_id (Integer.parseInt (new_ticket_id) ) ;
dealService.addDealTicket (deal) ;
req.setAttribute ("old_ticket_id", Integer.parseInt ( old_ticket_id ) ) ;
req.setAttribute ("new_ticket_id", Integer.parseInt ( new_ticket_id ) ) ;
return "forward:ticketChange";
再转发请求到TicketController中更新票的数量
int ticket_id1 = (int) req.getAttribute ( "old_ticket_id" ) ;
int ticket_id2 = (int) req.getAttribute ( "new_ticket_id" ) ;
HashMap<String, Object> map = new HashMap <String, Object> () ;
map.put ("ticket_id", ticket_id1) ;
ticketService.updateTicketAdd1 (map) ;
HashMap<String, Object> map1 = new HashMap <String, Object> () ;
map1.put ( "ticket_id", ticket_id2 ) ;
ticketService.updateTicketMinus1 (map1) ;
功能演示:
改签成功,价格改变,机票时间改变。
3.功能实现一退票
中转:
①前端将deal_id传回, 在deal表中找到该订单,将订单的支付状态改为‘NO’
②通过该订单获得订单绑定的ticket_id , 在ticket表中找到对应的飞机票,将余票数加一。
非中转:
①将订单的价格,减去当前退票的飞机票的价格,再减去机场建设费和燃油费,作为订单的新价格。
②在deal_ticket表中将原订单与已退票的绑定删除。
③在ticket表中将已退票对应的飞机票余票数加一。
④新建一笔支付状态为’NO’的订单,该订单在deal_ticket表中和已退票绑定。
源代码:
同样是按步骤对数据进行增删改查的操作,与上面结构相同,只是操作的对象不同和执行的操作不同,故不再粘贴代码。
功能演示:
4.功能实现-取消航班
①在plane表中,将该航班的存在状态改为‘NO’
②将plane、ticket、deal_ticket和deal多表连接进行复杂查询,对涉
及到的飞机票执行退票操作。
③通过在deal表中查到的用户ID,向涉及到的用户发布航班取消的通知
源代码:
同样是按步骤对数据进行增删改查的操作,与上面结构相同,只是操作的对象不同和执行的操作不同,故不再粘贴代码。
功能演示:
然后在预订了此趟航班的用户界面可以收到航班取消的通知。
五、系统测试
除了上一点中提到的几个功能的演示外,还有交易记录、国际时差转换、添加航班等功能。
交易记录:
可以看到航班取消、退票、购票等原因的收支记录。
国际时差转换:
航班显示默认的北京时间,点击可以切换当地时间和本地时间
添加航班:
点击按钮可以减少/添加机票类型
可以查到刚才添加的航班
订单查询:
写在最后:
项目源码已上传到csdn资源,有需要的可以下载。
https://github.com/Barnes3255/PlaneSystem
也可以私信获取项目源码+数据库文件+项目设计报告+项目ppt