【飞机票售票系统】山东大学大二暑期数据库课程设计项目SSM+VUE2前后端分离(含源码)

一、系统概述
二、需求分析
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 概念设计

E-R图

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

  • 12
    点赞
  • 140
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 41
    评论
很高兴回答您的问题!Java项目员工管理系统是一个非常常见的实践项目。它通常涉及到前后端分离的架构,使用JavaEE作为后端框架,Vue作为前端框架,SSM作为整个项目的技术栈,MySQL作为数据库,同时使用Maven进行项目构建和依赖管理,Axios用于前后端数据交互,HTML用于构建前端页面。 以下是一个简单的步骤指南,以帮助您开始这个项目: 1. 确定项目需求:明确系统所需的功能和特性,例如员工信息的增删改查、权限管理等。 2. 创建数据库:使用MySQL或其他数据库管理系统创建一个数据库,定义员工表和其他相关表。 3. 后端开发: - 使用JavaEE作为后端框架,搭建项目结构。 - 使用SSM框架(SpringSpringMVC、MyBatis)进行后端开发。配置数据源、连接数据库,并编写DAO层接口和Mapper文件。 - 实现业务逻辑层和控制层,编写接口和请求处理方法。 4. 前端开发: - 使用Vue作为前端框架,搭建项目结构。 - 使用Axios进行前后端数据交互,发送HTTP请求。 - 使用HTML和CSS构建前端页面,实现员工信息的展示、增删改查等功能。 5. 前后端联调: - 后端提供接口,在前端使用Axios发送请求,获取后端数据。 - 前端通过Ajax获取数据,并进行展示和交互。 6. 项目打包部署: - 使用Maven进行项目构建和依赖管理。配置pom.xml文件,添加所需的依赖。 - 部署后端项目到服务器,配置数据库连接等相关配置。 - 将前端代码打包为静态文件,并部署到Web服务器中。 这只是一个简单的指南,实际开发过程中还需要考虑更多的细节和问题。希望以上信息对您有所帮助!如有任何进一步的问题,请随时提问。
评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头顶黑黑草原

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值