大二小白的 Java 实战初体验day03:一个简易 12306 购票系统(车票的分页查询)

2025博客之星年度评选已开启 10w+人浏览 3.1k人参与

前言:上一篇文章讲述了作者在完成基础的登录注册功能的过程,本文章将讲述作者在实现车票查询页面的分页查询等一系列功能上的完成过程,希望各位大佬多多指点。

一、车票查询页面的制作

        作者首先在设计整个页面的时候,其实考虑了很多功能实现,参考了12306APP的一些功能特色,但是实际上有些功能开发起来对现阶段的我,或者是相似经历的开发者们来说,确实有一些难度和不现实,所以我决定制作了一个多条件查询车次的分页查询功能,主要运用的还是xml映射文件。首先我对网页先进行了一个设计,代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <a th:href="@{find_train_list(qry_current_page=${'1'},
                         qry_page_size=${'5'})}">车票预览</a>
    <a th:href="@{passenger(qry_current_page=${qry_current_page},
                         qry_page_size=${qry_page_size})}">乘车人</a>
    <a th:href="@{order(qry_current_page=${qry_current_page},
                         qry_page_size=${qry_page_size})}">订单</a>
    
</div>
<form action="/find_train_list">
<!--    添加隐藏的输入框,用于保存当前页码和页面大小-->
    <input type="hidden" th:name="qry_current_page" th:value="${qry_current_page}"/>
    <input type="hidden" th:name="qry_page_size" th:value="${qry_page_size}"/>
    始发地:
    <input type = "text" id="start_station" th:name="star_station">
    <button onclick="swapLocations()">交换</button>
    目的地:
    <input type = "text" id="end_station" th:name="end_station">
    日期:
    <input type="date" id="departDate" th:name="start_time">
    <div>
        <div>
            <label>车次类型</label>
            <span>全部</span>
            <input type="checkbox" name="train_type" value="高铁城际"> GC-高铁城际
            <input type="checkbox" name="train_type" value="动车"> D-动车
            <input type="checkbox" name="train_type" value="复兴号"> 复兴号
        </div>
        <div>
            <label>车次席别</label>
            <span>全部</span>
            <input type="checkbox" name="seat_type" value="商务座" > 商务座
            <input type="checkbox" name="seat_type" value="一等座"> 一等座
            <input type="checkbox" name="seat_type" value="二等座"> 二等座
            <input type="checkbox" name="seat_type" value="无座"> 无座
        </div>
    </div>
    <button type="submit">查询</button>
</form>

<table>
    <tr>
        <th>车次</th>
        <th>出发站</th>
        <th>到达站</th>
        <th>出发时间</th>
        <th>到达时间</th>
        <th>商务座</th>
        <th>一等座</th>
        <th>二等座</th>
        <th>无座</th>
        <th>详情</th>
        <th>操作</th>
    </tr>
    <tr th:each="train,itemStat:${trainList}">
        <td th:text="${train.train_id}">车次</td>
        <td th:text="${train.start_station}">出发站</td>
        <td th:text="${train.end_station}">到达站</td>
        <td th:text="${train.start_time}">出发时间</td>
        <td th:text="${train.end_time}">到达时间</td>
        <td th:text="${train.bussiness_seat}">商务座余票</td>
        <td th:text="${train.first_seat}">一等座余票</td>
        <td th:text="${train.second_seat}">二等座余票</td>
        <td th:text="${train.no_seat}">无座余票</td>
        <td th:text="${train.other}">详情</td>
        <td>
            <a th:href="@{/book(train_id=${train.train_id},
                                           qry_current_page=${'1'},
                                           qry_page_size=${'5'})}"
               th:text="预订"></a>
        </td>
<!--        <td><a href="#" th:href="@{/book(trainId=${train.id})}">预订</a></td>-->
    </tr>
</table>
<p>
    <a th:href="@{find_train_list(
                                 qry_current_page=${qry_current_page - 1},
                                 qry_page_size=${qry_page_size})}" th:text="上一页"></a>
    <a th:href="@{find_train_list(
                                 qry_current_page=${qry_current_page + 1},
                                 qry_page_size=${qry_page_size})}" th:text="下一页"></a>
</p>
</body>
</html>
<script>
    //交换两个输入框的值
    function swapLocations() {
        const originInput = document.getElementById('start_station');
        const destinationInput = document.getElementById('end_station');
        const temp = originInput.value;
        originInput.value = destinationInput.value;
        destinationInput.value = temp;
    }
</script>

        这里我将做一个大致的说明,首先页面提供了可以输入出发站和到达站的输入框,然后可以通过交换按钮交换两地(这里参考的就是12306首页的功能按钮,只需要通过JavaScript实现交换两个输入框的值就可以实现)然后我提供了三种不同的车次类型,然后提供了不同的座位类型供客户选择,这里就是比较简单的页面制作,然后就是分页查询的页面了,我将该分页查询的部分放到了表格里面,这里我传输过来的应该是车次的集合,那么就需要去遍历集合里面的元素然后取单个元素里面的单个属性,然后渲染到页面上面,那么利用each去遍历然后取元素就行,这里对于大多数基础开发者还是十分简单的。

        至此大致的页面就是如此。

二、分页查询的实现

        首先讲一下为什么使用传入current_page和page_size去做SQL语句的拼接,因为作者在完成这个系统的时候其实已经用了PageHelper这个方法去帮助我实现分页查询,同时这个方法确实很好同时大大减少了开发时间,但是作者在学习的过程中还是想深入了解一下这种方法,同时也想利用一下大学课堂上学习到的方法去实践完成,而并非小题大做。

        回到正文,当我们在实现这个分页查询的时候,前文已经说到确实时后端在通过和数据库查询相应的车次信息的时候,那么就会返回相应的车次列表,那么读者也可以思考一下,为什么要返回的是集合而不是单个元素,其实很简单,因为无论是无条件查询车次信息,还是查询特定的车次信息,分页查询的逻辑建立在 “返回单个车次信息” 的基础上,读者可以假设,如果需要返回第二页的10条数据,那么后端需要遍历10次去查询10条单个的信息,那么就发出了11次的网路请求,这会极大地增加服务器的负载和数据库的压力,同时也会让前端页面加载变得非常缓慢,用户体验极差等问题,这就是我对分页查询的一个理解。

        总体来说,分页查询也就是通过返回车次集合然后提取单个元素然后渲染页面,作者首先对controller层代码进行了开发,代码如下:

@RequestMapping("/success")
    public String success(HttpServletRequest request, Model model) {
        log.info("登录成功");
        int qry_current_page = Integer.parseInt(request.getParameter("qry_current_page"));
        int qry_page_size = Integer.parseInt(request.getParameter("qry_page_size"));
        List<TrainType> trainList = ticketService.find_train_list(qry_current_page,qry_page_size);
        model.addAttribute("trainList",trainList);
        model.addAttribute("qry_current_page",qry_current_page);
        model.addAttribute("qry_page_size",qry_page_size);
        return "/success";
    }

        这里可以看到作者利用了Model这个接口里面的方法,这个方法就是将后端处理好的数据,“打包” 起来,传递给前端的视图模板。

        这里作者对Model这个核心接口做一个阐述(个人理解),它主要的作用就是作为数据传递的桥梁,它通过Controller中处理好的数据,传递给前端视图也就是本文中的车票查询页面,进行了渲染。其工作原理我认为类似一个键值对(Key-Value)的存储容器,就和Java中的Map一样,然后addAttribute这个方法正是如此,作者可以做一个比较形象的比喻,后端的Controller也就是代码中的success方法,这个就是我们的“打包员”,传递的数据如“tranList”“qry_current_page”就是我们要寄松的包裹,然后我们通过addAttribute将我们要寄出去的东西放入包裹,然后贴上一个标签,也就是我们通过service层传输回来定义的这个trainList,打包然后贴上标签命名为“trainList”,然后在包裹上写上收件地址也就是我们的视图名(“/success”)交给了快递员,然后前端视图就是我们的收件人,然后取出包裹进行一系列操作。

        然后说明一下这个success方法是为了刚进入页面的时候就能看见分页查询后的结果,展示出来的就是所有车次,这里也就可以看见作者定义的这个service方法的名字为find_train_list,然后传入了两个分页查询的参数,接下来我们就来看一下service层的代码:

public List<TrainType> find_train_list(int qryCurrentPage, int qryPageSize) {
        int pos = (qryCurrentPage - 1) * qryPageSize;
        log.info("查询车次列表");
        List<TrainType> trainList = trainTypeMapper.find_train_list(pos, qryPageSize);
        return trainList;
    }

        这里可以看见我对这两个分页的数据做了一个运算,这里讲一下为什么要这么处理。

        这里需要引入一个概念叫做“偏移量”,当我们进行分页查询时,数据库需要知道两个关键信息:LIMIT 每页显示多少条记录;OFFSET:在开始返回记录之前,需要跳过多少条记录。在上述代码中,qryPageSize就是我们的LIMIT,pos就是我们的OFFSET,那为什么要进行一个减一的操作呢?首先我们常常说的“第一页”、“第二页”,页码是从1开始的,数据库在计算偏移量的时候,是从0开始计算的,那么OFFSET 0 表示不跳过任何记录,从第一条开始取。

        这里可以做一个场景,假设我要查询第一页,那么qryCurrentPage = 1,用户也就是想要查第一页到第十页的内容,那么数据库就要跳过0条数据,取10条数据,那么pos = (1 - 1)* 10 = 0,那么生成的SQL也就是SELECT * FROM Train LIMIT 10 OFFSET 0,该语句就是在SQL server中使用到的,如作者使用的MySQL,那么写出来的语句也就是SELECT * FROM Train LIMIT 10,0,这种写法是MySQL和PostgreSQL所支持的,这里不过多阐述。那么如果需要查第二页,那么pos计算后也就是10,那么写出来的SQL语句就是SELECT * FROM Train LIMIT 10,10。读者可以在自己的程序里面自行尝试,这里不过多赘述。

        最后在mapper层里面写出来的语句如下:

@Select("SELECT t.*, s.bussiness_seat, s.first_seat, s.second_seat,s.no_seat " +
            "FROM train t " +
            "LEFT JOIN seat_count s ON t.train_id = s.train_id limit #{pos} , #{qryPageSize}")
    List<TrainType> find_train_list(int pos, int qryPageSize);

        这里作者解释一下为什么是两张表连接查询,因为作者在设计数据库的时候考虑了很久,在猜测12306实际所运用的数据库表结构应该是怎么样,然后为了迎合一下本人的一点点想法,我将座位余量也做了一个可视化的操作,那么我就将车次信息和座位余量分成了两张表,但是座位余量和座位信息是两张不同的表,我也做了一个拆分(因为作者不太清楚具体的业务逻辑也不是很懂数据库应该怎么设计表结构,实际开发中不应该将表设计的太冗杂,这也是作者在开发时候的失误)然后进行了连接查询,这里附上作者设计的数据库表:

        然后作者也将车次的信息表也附上:

        作者就是这样设计的表结构,然后将查询出来的信息返回给前端。至此,整个的分页查询逻辑已经实现了。

三、多条件的分页查询

        前面是实现的简单的分页查询,也就是无条件的查询,现在作者需要实现多条件的查询,符合实际业务中,用户在查询要想乘坐的特定车次,然后返回到页面。

        前端页面不在赘述,同理于前文所附上的代码以及流程。这里我先附上controller层的代码,详细讲述一下我的开发思路:

@RequestMapping("/find_train_list")
    public String find_train_list(HttpServletRequest request, Model model){
        Result result = getResult(request, model);
        List<TrainType> trainList;
        String start_station = request.getParameter("start_station");
        String end_station = request.getParameter("end_station");
        String start_time = request.getParameter("start_time");
        String end_time = request.getParameter("end_time");
        String[] seat_types = request.getParameterValues("seat_type");
        String[] train_types = request.getParameterValues("train_type");
        //可以把这个判断逻辑写进service,这样逻辑就清晰很多
        // 也保证了controller层只负责获取参数,service层只负责处理参数
        // 返回结果,controller层只负责返回结果
        if (start_station == null && end_station == null && seat_types == null && train_types == null
                && start_time == null && end_time == null){
            trainList = ticketService.find_train_list(result.qry_current_page(), result.qry_page_size());
        }else {
            trainList = ticketService.find_train_list_by_condition(result.qry_current_page(), result.qry_page_size(),
                                                                   start_station,end_station,start_time,end_time,
                                                                   seat_types, train_types);
        }
        log.info("当前页码:{},每页显示数量:{}", result.qry_current_page(), result.qry_page_size());
        model.addAttribute("trainList",trainList);

        return "/success";
    }

        这里可以看到作者做了一个参数的接收,依然用到的是HttpServletRequest接口里面的方法,然后将http请求传输来的参数做一个接收,然后这里说明一下为什么接收车次类型和座位席别用数组去接收,因为用户可以选择多个车次,比如城际高铁、复兴号等,可以选择有二等座的车次、有商务座的车次等,这样传输的数据就不只有一个席别或者车次了,同理这样接收的话也就需要遍历数组里面的元素了,这个问题我们后面来说。

        这里对判断做一个解释,为什么我这里加了判断,首先第一点我当时没有做详细的设计,然后当输入框里面没有数据的时候查询出来是无数据的,第二点是当时开发的具体情况我也不太记得了,读者可以尝试将非空判断删去然后测试功能效果。然后就是判断非空的else里面的内容,也就是输入框里面不为空,那么就要对数据进行一个传输然后去根据数据查询相应的车次了。这里我附上service层的代码:

public List<TrainType> find_train_list_by_condition(int qryCurrentPage, int qryPageSize,
                                                        String start_station,String end_station,
                                                        String start_time,String end_time,
                                                         String[] seat_types, String[] train_types) {
        int pos = (qryCurrentPage - 1) * qryPageSize;
        List<TrainType> trainList =trainTypeMapper.find_train_list_by_condition(pos, qryPageSize, start_station,
                                                                                end_station, start_time,
                                                                                end_time,seat_types, train_types);
        return trainList;
    }

        这里其实就是简单的service层方法去调用mapper层的查询,然后返回trainList集合,然后我们来看一下mapper层的代码:

 List<TrainType> find_train_list_by_condition(int pos, int qryPageSize,
                                                 String start_station, String end_station,
                                                 String start_time, String end_time,
                                                 String[] seat_types, String[] train_types);

        这里可以看见我没有加@SELECT的注解,首先该功能查询的时候是一个多条件查询,如果单凭一个长的SQL语句来说的话,很难维护,同时可读性也差,对于我这种基础不牢的开发者小白来说很难接受,所以我这里使用了xml映射,所以我这里附上xml映射的代码:

 <select id="find_train_list_by_condition" resultType="com.itheima.demo12306.Entity.TrainType">
        SELECT t.*,s.bussiness_seat,s.first_seat,s.second_seat,s.no_seat FROM train t left join seat_count s
                                                                         on t.train_id = s.train_id
        <where>
            <if test="start_station != null and start_station != ''">
                AND start_station = #{start_station}
            </if>
            <if test="end_station != null and end_station != ''">
                AND end_station = #{end_station}
            </if>
            <if test="start_time != null and start_time != ''">
                AND DATE(start_time) = DATE(#{start_time})
            </if>
            <if test="train_types != null and train_types.length > 0">
                AND train_type IN
                <foreach collection="train_types" item="type" open="(" close=")" separator=",">
                    #{type}
                </foreach>
            </if>
            <if test="seat_types != null and seat_types.length > 0">
                AND (
                <foreach collection="seat_types" item="seat" separator=" OR ">
                    <if test="seat == '商务座'">s.bussiness_seat > 0</if>
                    <if test="seat == '一等座'">s.first_seat > 0</if>
                    <if test="seat == '二等座'">s.second_seat > 0</if>
                    <if test="seat == '无座'">s.no_seat > 0</if>
                </foreach>
                )
            </if>
<!--            <if test="seat_types != null and seat_types.length > 0">-->
<!--                AND seat_type IN-->
<!--                <foreach collection="seat_types" item="seat" open="(" close=")" separator=",">-->
<!--                    #{seat}-->
<!--                </foreach>-->
<!--            </if>-->
        </where>
        LIMIT #{pos}, #{qryPageSize}
    </select>

        这里可以看到前面传输过来的数组被遍历后,然后提取出来的item有一个type也就是车次,一个seat就是座位类型,这里就是遍历得到的单个元素,前面的就是一个简单的数据判断,如果传来的数据不是空的也不是空字符串,那么就进行一个拼接(后续我也会写一篇文章来讲述一下xml映射)。

        最后我说明一下为什么这里还做了一个Result类,这是自定义的数据传输对象,我这里将接收的那些分页参数做了一个方法的提取,这样方便后面维护也让代码的可读性变高了,这样开发的过程也变得简洁明朗。

        至此多条件的分页查询也告一段落。

四、技术总结

        在使用xml映射的时候,我使用了一个叫mybatis-X的一个IDE插件,这个插件可以帮助开发者快速的找到相应的映射位置,方便查找相关的代码。

        分页查询可以使用PageHelper这个方法去代替原有的SQL语句拼接,这样既能方便维护代码,也利于开发效率的提升。

五、总结

        本文介绍了车票查询页面的开发过程,重点实现了分页查询和多条件查询功能。作者参考12306设计了查询界面,包含出发站、到达站、日期、车次类型和座位类型等查询条件。通过JavaScript实现出发地和目的地的交换功能,利用Thymeleaf模板引擎渲染查询结果。在分页查询方面,详细解释了偏移量计算原理,使用LIMIT和OFFSET实现数据库分页查询。多条件查询通过XML映射文件实现动态SQL拼接,处理车次类型和座位类型等数组参数。希望各方大佬能够指点迷津,给我提出一个宝贵的建议,大二小白很需要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值