(完结)Java项目实战笔记--基于SpringBoot3.0开发仿12306高并发售票系统--(三)项目优化

本文参考自

Springboot3+微服务实战12306高性能售票系统 - 慕课网 (imooc.com)

本文是仿12306项目实战第(三)章——项目优化,本篇将讲解该项目最后的优化部分以及一些压测知识点

本章目录

  • 一、压力测试-高并发优化前后的性能对比
    • 1.压力测试相关概念讲解
    • 2.JMeter压测
    • 3.将mq去除,改用成springboot自带的异步
  • 二、项目功能优化
    • 1.购票页面增加取消排队的功能
    • 2.**余票查询页面增加显示车站信息**
    • 3.购票页面增加发起多人排队功能
    • 4.增加座位销售图
      • 1.增加查询座位销售详情接口
      • 2.增加座位销售图路由及页面,实现页面跳转和参数传递
      • 3.座位销售图页面获得销售信息,同一趟车,不管查哪个区间,查到的销售信息是一样的,由界面再去截取区间的销售信息。功能设计经验:对于复杂的操作,能放到前端的都放到前端,减小后端的压力。
      • 4.显示各车厢各座位的销售详情,使用橙色灰色代码座位可卖与卖出
  • 三、只允许购买两周内的车次

一、压力测试-高并发优化前后的性能对比

1.压力测试相关概念讲解

在这里插入图片描述

我们项目测试的就是下单购票这一个接口,所以tps=qps,然后tps和吞吐量又是一个意思,所以目前三者相等

2.JMeter压测

  • 先将令牌数设置充足

    异步处理后的代码,测试下单购票接口的吞吐量,其实只是和前半部分有关,而前半部分如果令牌数不够,就直接快速失败了,所以防止这种情况导致测试结果不准确,我们直接把令牌数调大。

在这里插入图片描述

  • 开始压测

    这里我们设置500线程永远循环,通过聚合报告看结果

在这里插入图片描述

可以看到结果是900多

在这里插入图片描述

  • 恢复代码到初版

在这里插入图片描述

测试前将座位调多一些然后生成多一些车票,因为是同步的,整个过程会去查询余票数了,没票会快速失败

在这里插入图片描述

在这里插入图片描述

由于如果还是500个线程的话,出现异常太多了,测试结果可能不太准确,我这里就只设置了50个线程来测试

结果:

可以看到吞吐量明显降低,经过我们上一章节的各种优化后(主要是异步),吞吐量提升了大概25倍多

在这里插入图片描述

3.将mq去除,改用成springboot自带的异步

实际项目中看情况增加中间件,并不是中间件越多越好,像这里我们用springboot的异步,也能达到同样的效果,吞吐量也擦不多

  • 注释掉所有和mq相关的代码、依赖、配置

  • 换成springboot自带的异步

    • BusinessApplication.java

      @EnableAsync
      public class BusinessApplication {
      
    • BeforeConfirmOrderService

      package com.neilxu.train.business.service;
      
      import cn.hutool.core.date.DateTime;
      import com.alibaba.csp.sentinel.annotation.SentinelResource;
      import com.alibaba.csp.sentinel.slots.block.BlockException;
      import com.alibaba.fastjson.JSON;
      import com.neilxu.train.business.domain.ConfirmOrder;
      import com.neilxu.train.business.dto.ConfirmOrderMQDto;
      import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
      import com.neilxu.train.business.mapper.ConfirmOrderMapper;
      import com.neilxu.train.business.req.ConfirmOrderDoReq;
      import com.neilxu.train.business.req.ConfirmOrderTicketReq;
      import com.neilxu.train.common.context.LoginMemberContext;
      import com.neilxu.train.common.exception.BusinessException;
      import com.neilxu.train.common.exception.BusinessExceptionEnum;
      import com.neilxu.train.common.util.SnowUtil;
      import jakarta.annotation.Resource;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.slf4j.MDC;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      
      import java.util.Date;
      import java.util.List;
      
      @Service
      public class BeforeConfirmOrderService {
      
          private static final Logger LOG = LoggerFactory.getLogger(BeforeConfirmOrderService.class);
      
          @Resource
          private ConfirmOrderMapper confirmOrderMapper;
      
          @Autowired
          private SkTokenService skTokenService;
      
      //    @Resource
      //    public RocketMQTemplate rocketMQTemplate;
          @Resource
          private ConfirmOrderService confirmOrderService;
      
          @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
          public Long beforeDoConfirm(ConfirmOrderDoReq req) {
              req.setMemberId(LoginMemberContext.getId());
              // 校验令牌余量
              boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
              if (validSkToken) {
                  LOG.info("令牌校验通过");
              } else {
                  LOG.info("令牌校验不通过");
                  throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
              }
      
              Date date = req.getDate();
              String trainCode = req.getTrainCode();
              String start = req.getStart();
              String end = req.getEnd();
              List<ConfirmOrderTicketReq> tickets = req.getTickets();
      
              // 保存确认订单表,状态初始
              DateTime now = DateTime.now();
              ConfirmOrder confirmOrder = new ConfirmOrder();
              confirmOrder.setId(SnowUtil.getSnowflakeNextId());
              confirmOrder.setCreateTime(now);
              confirmOrder.setUpdateTime(now);
              confirmOrder.setMemberId(req.getMemberId());
              confirmOrder.setDate(date);
              confirmOrder.setTrainCode(trainCode);
              confirmOrder.setStart(start);
              confirmOrder.setEnd(end);
              confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
              confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
              confirmOrder.setTickets(JSON.toJSONString(tickets));
              confirmOrderMapper.insert(confirmOrder);
      
              // 发送MQ排队购票
              ConfirmOrderMQDto confirmOrderMQDto = new ConfirmOrderMQDto();
              confirmOrderMQDto.setDate(req.getDate());
              confirmOrderMQDto.setTrainCode(req.getTrainCode());
              confirmOrderMQDto.setLogId(MDC.get("LOG_ID"));
              String reqJson = JSON.toJSONString(confirmOrderMQDto);
      //        LOG.info("排队购票,发送mq开始,消息:{}", reqJson);
      //        rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
      //        LOG.info("排队购票,发送mq结束");
      
              confirmOrderService.doConfirm(confirmOrderMQDto);
      
              return confirmOrder.getId();
      
          }
      
          /**
           * 降级方法,需包含限流方法的所有参数和BlockException参数
           * @param req
           * @param e
           */
          public void beforeDoConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
              LOG.info("购票请求被限流:{}", req);
              throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
          }
      }
      
    • ConfirmOrderService.java

      @Async
      @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock")
      public void doConfirm(ConfirmOrderMQDto dto) {
          MDC.put("LOG_ID", dto.getLogId());
          LOG.info("异步出票开始:{}", dto);
      
  • 测试吞吐量

    结果和mq的相差不大

在这里插入图片描述

二、项目功能优化

在这里插入图片描述

1.购票页面增加取消排队的功能

逻辑就是主动将订单状态改为 取消

  • ConfirmOrderService.java

    /**
     * 取消排队,只有I状态才能取消排队,所以按状态更新
     * @param id
     */
    public Integer cancel(Long id) {
        ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
        ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
        criteria.andIdEqualTo(id).andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
        ConfirmOrder confirmOrder = new ConfirmOrder();
        confirmOrder.setStatus(ConfirmOrderStatusEnum.CANCEL.getCode());
        return confirmOrderMapper.updateByExampleSelective(confirmOrder, confirmOrderExample);
    }
    
  • ConfirmOrderController.java

    @GetMapping("/cancel/{id}")
    public CommonResp<Integer> cancel(@PathVariable Long id) {
        Integer count = confirmOrderService.cancel(id);
        return new CommonResp<>(count);
    }
    
  • order.vue

    <template>
      <div class="order-train">
        <span class="order-train-main">{{dailyTrainTicket.date}}</span>&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.start}}</span>站
        <span class="order-train-main">({{dailyTrainTicket.startTime}})</span>&nbsp;
        <span class="order-train-main">——</span>&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.end}}</span>站
        <span class="order-train-main">({{dailyTrainTicket.endTime}})</span>&nbsp;
    
        <div class="order-train-ticket">
          <span v-for="item in seatTypes" :key="item.type">
            <span>{{item.desc}}</span>:
            <span class="order-train-ticket-main">{{item.price}}¥</span>&nbsp;
            <span class="order-train-ticket-main">{{item.count}}</span>&nbsp;张票&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
          </span>
        </div>
      </div>
      <a-divider></a-divider>
      <b>勾选要购票的乘客:</b>&nbsp;
      <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
    
      <div class="order-tickets">
        <a-row class="order-tickets-header" v-if="tickets.length > 0">
          <a-col :span="2">乘客</a-col>
          <a-col :span="6">身份证</a-col>
          <a-col :span="4">票种</a-col>
          <a-col :span="4">座位类型</a-col>
        </a-row>
        <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
          <a-col :span="2">{{ticket.passengerName}}</a-col>
          <a-col :span="6">{{ticket.passengerIdCard}}</a-col>
          <a-col :span="4">
            <a-select v-model:value="ticket.passengerType" style="width: 100%">
              <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-col>
          <a-col :span="4">
            <a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
              <a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-col>
        </a-row>
      </div>
      <div v-if="tickets.length > 0">
        <a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
      </div>
    
      <a-modal v-model:visible="visible" title="请核对以下信息"
               style="top: 50px; width: 800px"
               ok-text="确认" cancel-text="取消"
               @ok="showFirstImageCodeModal">
        <div class="order-tickets">
          <a-row class="order-tickets-header" v-if="tickets.length > 0">
            <a-col :span="3">乘客</a-col>
            <a-col :span="15">身份证</a-col>
            <a-col :span="3">票种</a-col>
            <a-col :span="3">座位类型</a-col>
          </a-row>
          <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
            <a-col :span="3">{{ticket.passengerName}}</a-col>
            <a-col :span="15">{{ticket.passengerIdCard}}</a-col>
            <a-col :span="3">
              <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
                <span v-if="item.code === ticket.passengerType">
                  {{item.desc}}
                </span>
              </span>
            </a-col>
            <a-col :span="3">
              <span v-for="item in seatTypes" :key="item.code">
                <span v-if="item.code === ticket.seatTypeCode">
                  {{item.desc}}
                </span>
              </span>
            </a-col>
          </a-row>
          <br/>
          <div v-if="chooseSeatType === 0" style="color: red;">
            您购买的车票不支持选座
            <div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
            <div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
          </div>
          <div v-else style="text-align: center">
            <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
                      v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
            <div v-if="tickets.length > 1">
              <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
                        v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
            </div>
            <div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
          </div>
          <!--<br/>-->
          <!--最终购票:{{tickets}}-->
          <!--最终选座:{{chooseSeatObj}}-->
        </div>
      </a-modal>
    
      <!-- 第二层验证码 后端 -->
      <a-modal v-model:visible="imageCodeModalVisible" :title="null" :footer="null" :closable="false"
               style="top: 50px; width: 400px">
        <p style="text-align: center; font-weight: bold; font-size: 18px">
          使用服务端验证码削弱瞬时高峰<br/>
          防止机器人刷票
        </p>
        <p>
          <a-input v-model:value="imageCode" placeholder="图片验证码">
            <template #suffix>
              <img v-show="!!imageCodeSrc" :src="imageCodeSrc" alt="验证码" v-on:click="loadImageCode()"/>
            </template>
          </a-input>
        </p>
        <a-button type="danger" block @click="handleOk">输入验证码后开始购票</a-button>
      </a-modal>
    
      <!-- 第一层验证码 纯前端 -->
      <a-modal v-model:visible="firstImageCodeModalVisible" :title="null" :footer="null" :closable="false"
               style="top: 50px; width: 400px">
        <p style="text-align: center; font-weight: bold; font-size: 18px">
          使用纯前端验证码削弱瞬时高峰<br/>
          减小后端验证码接口的压力
        </p>
        <p>
          <a-input v-model:value="firstImageCodeTarget" placeholder="验证码">
            <template #suffix>
              {{firstImageCodeSourceA}} + {{firstImageCodeSourceB}}
            </template>
          </a-input>
        </p>
        <a-button type="danger" block @click="validFirstImageCode">提交验证码</a-button>
      </a-modal>
    
      <a-modal v-model:visible="lineModalVisible" title="排队购票" :footer="null" :maskClosable="false" :closable="false"
               style="top: 50px; width: 400px">
        <div class="book-line">
          <div v-show="confirmOrderLineCount < 0">
            <loading-outlined /> 系统正在处理中...
          </div>
          <div v-show="confirmOrderLineCount >= 0">
            <loading-outlined /> 您前面还有{{confirmOrderLineCount}}位用户在购票,排队中,请稍候
          </div>
        </div>
        <br/>
        <a-button type="danger" @click="onCancelOrder">取消购票</a-button>
      </a-modal>
    </template>
    
    <script>
    
    import {defineComponent, ref, onMounted, watch, computed} from 'vue';
    import axios from "axios";
    import {notification} from "ant-design-vue";
    
    export default defineComponent({
      name: "order-view",
      setup() {
        const passengers = ref([]);
        const passengerOptions = ref([]);
        const passengerChecks = ref([]);
        const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
        console.log("下单的车次信息", dailyTrainTicket);
    
        const SEAT_TYPE = window.SEAT_TYPE;
        console.log(SEAT_TYPE)
        // 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
        // {
        //   type: "YDZ",
        //   code: "1",
        //   desc: "一等座",
        //   count: "100",
        //   price: "50",
        // }
        // 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
        const seatTypes = [];
        for (let KEY in SEAT_TYPE) {
          let key = KEY.toLowerCase();
          if (dailyTrainTicket[key] >= 0) {
            seatTypes.push({
              type: KEY,
              code: SEAT_TYPE[KEY]["code"],
              desc: SEAT_TYPE[KEY]["desc"],
              count: dailyTrainTicket[key],
              price: dailyTrainTicket[key + 'Price'],
            })
          }
        }
        console.log("本车次提供的座位:", seatTypes)
        // 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
        // {
        //   passengerId: 123,
        //   passengerType: "1",
        //   passengerName: "张三",
        //   passengerIdCard: "12323132132",
        //   seatTypeCode: "1",
        //   seat: "C1"
        // }
        const tickets = ref([]);
        const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
        const visible = ref(false);
        const lineModalVisible = ref(false);
        const confirmOrderId = ref();
        const confirmOrderLineCount = ref(-1);
    
        // 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
        watch(() => passengerChecks.value, (newVal, oldVal)=>{
          console.log("勾选乘客发生变化", newVal, oldVal)
          // 每次有变化时,把购票列表清空,重新构造列表
          tickets.value = [];
          passengerChecks.value.forEach((item) => tickets.value.push({
            passengerId: item.id,
            passengerType: item.type,
            seatTypeCode: seatTypes[0].code,
            passengerName: item.name,
            passengerIdCard: item.idCard
          }))
        }, {immediate: true});
    
        // 0:不支持选座;1:选一等座;2:选二等座
        const chooseSeatType = ref(0);
        // 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          chooseSeatObj.value = {};
          for (let i = 1; i <= 2; i++) {
            SEAT_COL_ARRAY.value.forEach((item) => {
              chooseSeatObj.value[item.code + i] = false;
            })
          }
          console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
        }, {immediate: true});
    
        const handleQueryPassenger = () => {
          axios.get("/member/passenger/query-mine").then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content;
              passengers.value.forEach((item) => passengerOptions.value.push({
                label: item.name,
                value: item
              }))
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const finishCheckPassenger = () => {
          console.log("购票列表:", tickets.value);
    
          if (tickets.value.length > 5) {
            notification.error({description: '最多只能购买5张车票'});
            return;
          }
    
          // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
          // 前端校验不一定准,但前端校验可以减轻后端很多压力
          // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
          let seatTypesTemp = Tool.copy(seatTypes);
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            for (let j = 0; j < seatTypesTemp.length; j++) {
              let seatType = seatTypesTemp[j];
              // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
              if (ticket.seatTypeCode === seatType.code) {
                seatType.count--;
                if (seatType.count < 0) {
                  notification.error({description: seatType.desc + '余票不足'});
                  return;
                }
              }
            }
          }
          console.log("前端余票校验通过");
    
          // 判断是否支持选座,只有纯一等座和纯二等座支持选座
          // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
          let ticketSeatTypeCodes = [];
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            ticketSeatTypeCodes.push(ticket.seatTypeCode);
          }
          // 为购票列表中的所有座位类型去重:[1, 2]
          const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
          console.log("选好的座位类型:", ticketSeatTypeCodesSet);
          if (ticketSeatTypeCodesSet.length !== 1) {
            console.log("选了多种座位,不支持选座");
            chooseSeatType.value = 0;
          } else {
            // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
            if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
              console.log("一等座选座");
              chooseSeatType.value = SEAT_TYPE.YDZ.code;
            } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
              console.log("二等座选座");
              chooseSeatType.value = SEAT_TYPE.EDZ.code;
            } else {
              console.log("不是一等座或二等座,不支持选座");
              chooseSeatType.value = 0;
            }
    
            // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
            if (chooseSeatType.value !== 0) {
              for (let i = 0; i < seatTypes.length; i++) {
                let seatType = seatTypes[i];
                // 找到同类型座位
                if (ticketSeatTypeCodesSet[0] === seatType.code) {
                  // 判断余票,小于20张就不支持选座
                  if (seatType.count < 20) {
                    console.log("余票小于20张就不支持选座")
                    chooseSeatType.value = 0;
                    break;
                  }
                }
              }
            }
          }
    
          // 弹出确认界面
          visible.value = true;
    
        };
    
        const handleOk = () => {
          if (Tool.isEmpty(imageCode.value)) {
            notification.error({description: '验证码不能为空'});
            return;
          }
    
          console.log("选好的座位:", chooseSeatObj.value);
    
          // 设置每张票的座位
          // 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
          for (let i = 0; i < tickets.value.length; i++) {
            tickets.value[i].seat = null;
          }
          let i = -1;
          // 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
          for (let key in chooseSeatObj.value) {
            if (chooseSeatObj.value[key]) {
              i++;
              if (i > tickets.value.length - 1) {
                notification.error({description: '所选座位数大于购票数'});
                return;
              }
              tickets.value[i].seat = key;
            }
          }
          if (i > -1 && i < (tickets.value.length - 1)) {
            notification.error({description: '所选座位数小于购票数'});
            return;
          }
    
          console.log("最终购票:", tickets.value);
    
          axios.post("/business/confirm-order/do", {
            dailyTrainTicketId: dailyTrainTicket.id,
            date: dailyTrainTicket.date,
            trainCode: dailyTrainTicket.trainCode,
            start: dailyTrainTicket.start,
            end: dailyTrainTicket.end,
            tickets: tickets.value,
            imageCodeToken: imageCodeToken.value,
            imageCode: imageCode.value,
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              // notification.success({description: "下单成功!"});
              visible.value = false;
              imageCodeModalVisible.value = false;
              lineModalVisible.value = true;
              confirmOrderId.value = data.content;
              queryLineCount();
            } else {
              notification.error({description: data.message});
            }
          });
        }
    
        /* ------------------- 定时查询订单状态 --------------------- */
        // 确认订单后定时查询
        let queryLineCountInterval;
    
        // 定时查询订单结果/排队数量
        const queryLineCount = () => {
          confirmOrderLineCount.value = -1;
          queryLineCountInterval = setInterval(function () {
            axios.get("/business/confirm-order/query-line-count/" + confirmOrderId.value).then((response) => {
              let data = response.data;
              if (data.success) {
                let result = data.content;
                switch (result) {
                  case -1 :
                    notification.success({description: "购票成功!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  case -2:
                    notification.error({description: "购票失败!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  case -3:
                    notification.error({description: "抱歉,没票了!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  default:
                    confirmOrderLineCount.value = result;
                }
              } else {
                notification.error({description: data.message});
              }
            });
          }, 500);
        };
    
        /* ------------------- 第二层验证码 --------------------- */
        const imageCodeModalVisible = ref();
        const imageCodeToken = ref();
        const imageCodeSrc = ref();
        const imageCode = ref();
        /**
         * 加载图形验证码
         */
        const loadImageCode = () => {
          imageCodeToken.value = Tool.uuid(8);
          imageCodeSrc.value = process.env.VUE_APP_SERVER + '/business/kaptcha/image-code/' + imageCodeToken.value;
        };
    
        const showImageCodeModal = () => {
          loadImageCode();
          imageCodeModalVisible.value = true;
        };
    
        /* ------------------- 第一层验证码 --------------------- */
        const firstImageCodeSourceA = ref();
        const firstImageCodeSourceB = ref();
        const firstImageCodeTarget = ref();
        const firstImageCodeModalVisible = ref();
    
        /**
         * 加载第一层验证码
         */
        const loadFirstImageCode = () => {
          // 获取1~10的数:Math.floor(Math.random()*10 + 1)
          firstImageCodeSourceA.value = Math.floor(Math.random()*10 + 1) + 10;
          firstImageCodeSourceB.value = Math.floor(Math.random()*10 + 1) + 20;
        };
    
        /**
         * 显示第一层验证码弹出框
         */
        const showFirstImageCodeModal = () => {
          loadFirstImageCode();
          firstImageCodeModalVisible.value = true;
        };
    
        /**
         * 校验第一层验证码
         */
        const validFirstImageCode = () => {
          if (parseInt(firstImageCodeTarget.value) === parseInt(firstImageCodeSourceA.value + firstImageCodeSourceB.value)) {
            // 第一层验证通过
            firstImageCodeModalVisible.value = false;
            showImageCodeModal();
          } else {
            notification.error({description: '验证码错误'});
          }
        };
    
        /**
         * 取消排队
         */
        const onCancelOrder = () => {
          axios.get("/business/confirm-order/cancel/" + confirmOrderId.value).then((response) => {
            let data = response.data;
            if (data.success) {
              let result = data.content;
              if (result === 1) {
                notification.success({description: "取消成功!"});
                // 取消成功时,不用再轮询排队结果
                clearInterval(queryLineCountInterval);
                lineModalVisible.value = false;
              } else {
                notification.error({description: "取消失败!"});
              }
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          handleQueryPassenger();
        });
    
        return {
          passengers,
          dailyTrainTicket,
          seatTypes,
          passengerOptions,
          passengerChecks,
          tickets,
          PASSENGER_TYPE_ARRAY,
          visible,
          finishCheckPassenger,
          chooseSeatType,
          chooseSeatObj,
          SEAT_COL_ARRAY,
          handleOk,
          imageCodeToken,
          imageCodeSrc,
          imageCode,
          showImageCodeModal,
          imageCodeModalVisible,
          loadImageCode,
          firstImageCodeSourceA,
          firstImageCodeSourceB,
          firstImageCodeTarget,
          firstImageCodeModalVisible,
          showFirstImageCodeModal,
          validFirstImageCode,
          lineModalVisible,
          confirmOrderId,
          confirmOrderLineCount,
          onCancelOrder
        };
      },
    });
    </script>
    
    <style>
    .order-train .order-train-main {
      font-size: 18px;
      font-weight: bold;
    }
    .order-train .order-train-ticket {
      margin-top: 15px;
    }
    .order-train .order-train-ticket .order-train-ticket-main {
      color: red;
      font-size: 18px;
    }
    
    .order-tickets {
      margin: 10px 0;
    }
    .order-tickets .ant-col {
      padding: 5px 10px;
    }
    .order-tickets .order-tickets-header {
      background-color: cornflowerblue;
      border: solid 1px cornflowerblue;
      color: white;
      font-size: 16px;
      padding: 5px 0;
    }
    .order-tickets .order-tickets-row {
      border: solid 1px cornflowerblue;
      border-top: none;
      vertical-align: middle;
      line-height: 30px;
    }
    
    .order-tickets .choose-seat-item {
      margin: 5px 5px;
    }
    </style>
    
  • 效果

在这里插入图片描述

2.余票查询页面增加显示车站信息

完善余票查询的功能体验,可以看到某个车次的所有途径车站和到站出站时间信息

  • DailyTrainStationQueryAllReq.java

    package com.neilxu.train.business.req;
    
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import lombok.Data;
    import org.springframework.format.annotation.DateTimeFormat;
    
    import java.util.Date;
    
    @Data
    public class DailyTrainStationQueryAllReq {
    
        /**
         * 日期
         */
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        @NotNull(message = "【日期】不能为空")
        private Date date;
    
        /**
         * 车次编号
         */
        @NotBlank(message = "【车次编号】不能为空")
        private String trainCode;
        
    }
    
  • DailyTrainStationService.java

    /**
     * 按车次日期查询车站列表,用于界面显示一列车经过的车站
     */
    public List<DailyTrainStationQueryResp> queryByTrain(Date date, String trainCode) {
        DailyTrainStationExample dailyTrainStationExample = new DailyTrainStationExample();
        dailyTrainStationExample.setOrderByClause("`index` asc");
        dailyTrainStationExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);
        List<DailyTrainStation> list = dailyTrainStationMapper.selectByExample(dailyTrainStationExample);
        return BeanUtil.copyToList(list, DailyTrainStationQueryResp.class);
    }
    
  • DailyTrainStationController.java

    package com.neilxu.train.business.controller;
    
    import com.neilxu.train.business.req.DailyTrainStationQueryAllReq;
    import com.neilxu.train.business.resp.DailyTrainStationQueryResp;
    import com.neilxu.train.business.service.DailyTrainStationService;
    import com.neilxu.train.common.resp.CommonResp;
    import jakarta.validation.Valid;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/daily-train-station")
    public class DailyTrainStationController {
    
        @Autowired
        private DailyTrainStationService dailyTrainStationService;
    
        @GetMapping("/query-by-train-code")
        public CommonResp<List<DailyTrainStationQueryResp>> queryByTrain(@Valid DailyTrainStationQueryAllReq req) {
            List<DailyTrainStationQueryResp> list = dailyTrainStationService.queryByTrain(req.getDate(), req.getTrainCode());
            return new CommonResp<>(list);
        }
    
    }
    
  • ticket.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
          <station-select-view v-model="params.start" width="200px"></station-select-view>
          <station-select-view v-model="params.end" width="200px"></station-select-view>
          <a-button type="primary" @click="handleQuery()">查找</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrainTickets"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-button type="primary" @click="toOrder(record)">预订</a-button>
              <a-button type="primary" @click="showStation(record)">途经车站</a-button>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'station'">
            {{record.start}}<br/>
            {{record.end}}
          </template>
          <template v-else-if="column.dataIndex === 'time'">
            {{record.startTime}}<br/>
            {{record.endTime}}
          </template>
          <template v-else-if="column.dataIndex === 'duration'">
            {{calDuration(record.startTime, record.endTime)}}<br/>
            <div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
              次日到达
            </div>
            <div v-else>
              当日到达
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'ydz'">
            <div v-if="record.ydz >= 0">
              {{record.ydz}}<br/>
              {{record.ydzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'edz'">
            <div v-if="record.edz >= 0">
              {{record.edz}}<br/>
              {{record.edzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'rw'">
            <div v-if="record.rw >= 0">
              {{record.rw}}<br/>
              {{record.rwPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'yw'">
            <div v-if="record.yw >= 0">
              {{record.yw}}<br/>
              {{record.ywPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
        </template>
      </a-table>
    
      <!-- 途经车站 -->
      <a-modal style="top: 30px" v-model:visible="visible" :title="null" :footer="null" :closable="false">
        <a-table :data-source="stations" :pagination="false">
          <a-table-column key="index" title="站序" data-index="index" />
          <a-table-column key="name" title="站名" data-index="name" />
          <a-table-column key="inTime" title="进站时间" data-index="inTime">
            <template #default="{ record }">
              {{record.index === 0 ? '-' : record.inTime}}
            </template>
          </a-table-column>
          <a-table-column key="outTime" title="出站时间" data-index="outTime">
            <template #default="{ record }">
              {{record.index === (stations.length - 1) ? '-' : record.outTime}}
            </template>
          </a-table-column>
          <a-table-column key="stopTime" title="停站时长" data-index="stopTime">
            <template #default="{ record }">
              {{record.index === 0 || record.index === (stations.length - 1) ? '-' : record.stopTime}}
            </template>
          </a-table-column>
        </a-table>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import StationSelectView from "@/components/station-select";
    import dayjs from "dayjs";
    import router from "@/router";
    
    export default defineComponent({
      name: "ticket-view",
      components: {StationSelectView},
      setup() {
        const visible = ref(false);
        let dailyTrainTicket = ref({
          id: undefined,
          date: undefined,
          trainCode: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          startIndex: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          endIndex: undefined,
          ydz: undefined,
          ydzPrice: undefined,
          edz: undefined,
          edzPrice: undefined,
          rw: undefined,
          rwPrice: undefined,
          yw: undefined,
          ywPrice: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrainTickets = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const params = ref({});
        const columns = [
          {
            title: '车次编号',
            dataIndex: 'trainCode',
            key: 'trainCode',
          },
          {
            title: '车站',
            dataIndex: 'station',
          },
          {
            title: '时间',
            dataIndex: 'time',
          },
          {
            title: '历时',
            dataIndex: 'duration',
          },
          {
            title: '一等座',
            dataIndex: 'ydz',
            key: 'ydz',
          },
          {
            title: '二等座',
            dataIndex: 'edz',
            key: 'edz',
          },
          {
            title: '软卧',
            dataIndex: 'rw',
            key: 'rw',
          },
          {
            title: '硬卧',
            dataIndex: 'yw',
            key: 'yw',
          },
          {
            title: '操作',
            dataIndex: 'operation',
          },
        ];
    
    
        const handleQuery = (param) => {
          if (Tool.isEmpty(params.value.date)) {
            notification.error({description: "请输入日期"});
            return;
          }
          if (Tool.isEmpty(params.value.start)) {
            notification.error({description: "请输入出发地"});
            return;
          }
          if (Tool.isEmpty(params.value.end)) {
            notification.error({description: "请输入目的地"});
            return;
          }
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
    
          // 保存查询参数
          SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
    
          loading.value = true;
          axios.get("/business/daily-train-ticket/query-list", {
            params: {
              page: param.page,
              size: param.size,
              trainCode: params.value.trainCode,
              date: params.value.date,
              start: params.value.start,
              end: params.value.end
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrainTickets.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (page) => {
          // console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
          pagination.value.pageSize = page.pageSize;
          handleQuery({
            page: page.current,
            size: page.pageSize
          });
        };
    
        const calDuration = (startTime, endTime) => {
          let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
          return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
        };
    
        const toOrder = (record) => {
          dailyTrainTicket.value = Tool.copy(record);
          SessionStorage.set(SESSION_ORDER, dailyTrainTicket.value);
          router.push("/order")
        };
    
        // ---------------------- 途经车站 ----------------------
        const stations = ref([]);
        const showStation = record => {
          visible.value = true;
          axios.get("/business/daily-train-station/query-by-train-code", {
            params: {
              date: record.date,
              trainCode: record.trainCode
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              stations.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          //  "|| {}"是常用技巧,可以避免空指针异常
          params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
          if (Tool.isNotEmpty(params.value)) {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          }
        });
    
        return {
          dailyTrainTicket,
          visible,
          dailyTrainTickets,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          params,
          calDuration,
          toOrder,
          showStation,
          stations
        };
      },
    });
    </script>
    
  • 效果

在这里插入图片描述

3.购票页面增加发起多人排队功能

本质就是一次下多条订单,最后返给前端的是最后一条订单的id,给前端的效果就是我是排队在最后面的那个订单

  • ConfirmOrderDoReq.java

    /**
     * 加入排队人数,用于体验排队功能
     */
    private int lineNumber;
    
    @Override
    public String toString() {
        return "ConfirmOrderDoReq{" +
                "memberId=" + memberId +
                ", date=" + date +
                ", trainCode='" + trainCode + '\'' +
                ", start='" + start + '\'' +
                ", end='" + end + '\'' +
                ", dailyTrainTicketId=" + dailyTrainTicketId +
                ", tickets=" + tickets +
                ", imageCode='" + imageCode + '\'' +
                ", imageCodeToken='" + imageCodeToken + '\'' +
                ", logId='" + logId + '\'' +
                ", lineNumber=" + lineNumber +
                '}';
    }
    
  • BeforeConfirmOrderService.java

    @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
    public Long beforeDoConfirm(ConfirmOrderDoReq req) {
        Long id = null;
        // 根据前端传值,加入排队人数
        for (int i = 0; i < req.getLineNumber() + 1; i++) {
            req.setMemberId(LoginMemberContext.getId());
            // 校验令牌余量
            boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
            if (validSkToken) {
                LOG.info("令牌校验通过");
            } else {
                LOG.info("令牌校验不通过");
                throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
            }
    
            Date date = req.getDate();
            String trainCode = req.getTrainCode();
            String start = req.getStart();
            String end = req.getEnd();
            List<ConfirmOrderTicketReq> tickets = req.getTickets();
    
            // 保存确认订单表,状态初始
            DateTime now = DateTime.now();
            ConfirmOrder confirmOrder = new ConfirmOrder();
            confirmOrder.setId(SnowUtil.getSnowflakeNextId());
            confirmOrder.setCreateTime(now);
            confirmOrder.setUpdateTime(now);
            confirmOrder.setMemberId(req.getMemberId());
            confirmOrder.setDate(date);
            confirmOrder.setTrainCode(trainCode);
            confirmOrder.setStart(start);
            confirmOrder.setEnd(end);
            confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
            confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
            confirmOrder.setTickets(JSON.toJSONString(tickets));
            confirmOrderMapper.insert(confirmOrder);
    
            // 发送MQ排队购票
            ConfirmOrderMQDto confirmOrderMQDto = new ConfirmOrderMQDto();
            confirmOrderMQDto.setDate(req.getDate());
            confirmOrderMQDto.setTrainCode(req.getTrainCode());
            confirmOrderMQDto.setLogId(MDC.get("LOG_ID"));
            String reqJson = JSON.toJSONString(confirmOrderMQDto);
            // LOG.info("排队购票,发送mq开始,消息:{}", reqJson);
            // rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
            // LOG.info("排队购票,发送mq结束");
            confirmOrderService.doConfirm(confirmOrderMQDto);
            id = confirmOrder.getId();
        }
        return id;
    }
    
  • order.vue

    <template>
      <div class="order-train">
        <span class="order-train-main">{{dailyTrainTicket.date}}</span>&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.start}}</span>站
        <span class="order-train-main">({{dailyTrainTicket.startTime}})</span>&nbsp;
        <span class="order-train-main">——</span>&nbsp;
        <span class="order-train-main">{{dailyTrainTicket.end}}</span>站
        <span class="order-train-main">({{dailyTrainTicket.endTime}})</span>&nbsp;
    
        <div class="order-train-ticket">
          <span v-for="item in seatTypes" :key="item.type">
            <span>{{item.desc}}</span>:
            <span class="order-train-ticket-main">{{item.price}}¥</span>&nbsp;
            <span class="order-train-ticket-main">{{item.count}}</span>&nbsp;张票&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
          </span>
        </div>
      </div>
      <a-divider></a-divider>
      <b>勾选要购票的乘客:</b>&nbsp;
      <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
    
      <div class="order-tickets">
        <a-row class="order-tickets-header" v-if="tickets.length > 0">
          <a-col :span="2">乘客</a-col>
          <a-col :span="6">身份证</a-col>
          <a-col :span="4">票种</a-col>
          <a-col :span="4">座位类型</a-col>
        </a-row>
        <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
          <a-col :span="2">{{ticket.passengerName}}</a-col>
          <a-col :span="6">{{ticket.passengerIdCard}}</a-col>
          <a-col :span="4">
            <a-select v-model:value="ticket.passengerType" style="width: 100%">
              <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-col>
          <a-col :span="4">
            <a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
              <a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-col>
        </a-row>
      </div>
      <div v-if="tickets.length > 0">
        <a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
      </div>
    
      <a-modal v-model:visible="visible" title="请核对以下信息"
               style="top: 50px; width: 800px"
               ok-text="确认" cancel-text="取消"
               @ok="showFirstImageCodeModal">
        <div class="order-tickets">
          <a-row class="order-tickets-header" v-if="tickets.length > 0">
            <a-col :span="3">乘客</a-col>
            <a-col :span="15">身份证</a-col>
            <a-col :span="3">票种</a-col>
            <a-col :span="3">座位类型</a-col>
          </a-row>
          <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
            <a-col :span="3">{{ticket.passengerName}}</a-col>
            <a-col :span="15">{{ticket.passengerIdCard}}</a-col>
            <a-col :span="3">
              <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
                <span v-if="item.code === ticket.passengerType">
                  {{item.desc}}
                </span>
              </span>
            </a-col>
            <a-col :span="3">
              <span v-for="item in seatTypes" :key="item.code">
                <span v-if="item.code === ticket.seatTypeCode">
                  {{item.desc}}
                </span>
              </span>
            </a-col>
          </a-row>
          <br/>
          <div v-if="chooseSeatType === 0" style="color: red;">
            您购买的车票不支持选座
            <div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
            <div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
          </div>
          <div v-else style="text-align: center">
            <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
                      v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
            <div v-if="tickets.length > 1">
              <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
                        v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
            </div>
            <div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
          </div>
          <br>
          <div style="color: red">
            体验排队购票,加入多人一起排队购票:
            <a-input-number v-model:value="lineNumber" :min="0" :max="20" />
          </div>
          <!--<br/>-->
          <!--最终购票:{{tickets}}-->
          <!--最终选座:{{chooseSeatObj}}-->
        </div>
      </a-modal>
    
      <!-- 第二层验证码 后端 -->
      <a-modal v-model:visible="imageCodeModalVisible" :title="null" :footer="null" :closable="false"
               style="top: 50px; width: 400px">
        <p style="text-align: center; font-weight: bold; font-size: 18px">
          使用服务端验证码削弱瞬时高峰<br/>
          防止机器人刷票
        </p>
        <p>
          <a-input v-model:value="imageCode" placeholder="图片验证码">
            <template #suffix>
              <img v-show="!!imageCodeSrc" :src="imageCodeSrc" alt="验证码" v-on:click="loadImageCode()"/>
            </template>
          </a-input>
        </p>
        <a-button type="danger" block @click="handleOk">输入验证码后开始购票</a-button>
      </a-modal>
    
      <!-- 第一层验证码 纯前端 -->
      <a-modal v-model:visible="firstImageCodeModalVisible" :title="null" :footer="null" :closable="false"
               style="top: 50px; width: 400px">
        <p style="text-align: center; font-weight: bold; font-size: 18px">
          使用纯前端验证码削弱瞬时高峰<br/>
          减小后端验证码接口的压力
        </p>
        <p>
          <a-input v-model:value="firstImageCodeTarget" placeholder="验证码">
            <template #suffix>
              {{firstImageCodeSourceA}} + {{firstImageCodeSourceB}}
            </template>
          </a-input>
        </p>
        <a-button type="danger" block @click="validFirstImageCode">提交验证码</a-button>
      </a-modal>
    
      <a-modal v-model:visible="lineModalVisible" title="排队购票" :footer="null" :maskClosable="false" :closable="false"
               style="top: 50px; width: 400px">
        <div class="book-line">
          <div v-show="confirmOrderLineCount < 0">
            <loading-outlined /> 系统正在处理中...
          </div>
          <div v-show="confirmOrderLineCount >= 0">
            <loading-outlined /> 您前面还有{{confirmOrderLineCount}}位用户在购票,排队中,请稍候
          </div>
        </div>
        <br/>
        <a-button type="danger" @click="onCancelOrder">取消购票</a-button>
      </a-modal>
    </template>
    
    <script>
    
    import {defineComponent, ref, onMounted, watch, computed} from 'vue';
    import axios from "axios";
    import {notification} from "ant-design-vue";
    
    export default defineComponent({
      name: "order-view",
      setup() {
        const passengers = ref([]);
        const passengerOptions = ref([]);
        const passengerChecks = ref([]);
        const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
        console.log("下单的车次信息", dailyTrainTicket);
    
        const SEAT_TYPE = window.SEAT_TYPE;
        console.log(SEAT_TYPE)
        // 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
        // {
        //   type: "YDZ",
        //   code: "1",
        //   desc: "一等座",
        //   count: "100",
        //   price: "50",
        // }
        // 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
        const seatTypes = [];
        for (let KEY in SEAT_TYPE) {
          let key = KEY.toLowerCase();
          if (dailyTrainTicket[key] >= 0) {
            seatTypes.push({
              type: KEY,
              code: SEAT_TYPE[KEY]["code"],
              desc: SEAT_TYPE[KEY]["desc"],
              count: dailyTrainTicket[key],
              price: dailyTrainTicket[key + 'Price'],
            })
          }
        }
        console.log("本车次提供的座位:", seatTypes)
        // 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
        // {
        //   passengerId: 123,
        //   passengerType: "1",
        //   passengerName: "张三",
        //   passengerIdCard: "12323132132",
        //   seatTypeCode: "1",
        //   seat: "C1"
        // }
        const tickets = ref([]);
        const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
        const visible = ref(false);
        const lineModalVisible = ref(false);
        const confirmOrderId = ref();
        const confirmOrderLineCount = ref(-1);
        const lineNumber = ref(5);
    
        // 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
        watch(() => passengerChecks.value, (newVal, oldVal)=>{
          console.log("勾选乘客发生变化", newVal, oldVal)
          // 每次有变化时,把购票列表清空,重新构造列表
          tickets.value = [];
          passengerChecks.value.forEach((item) => tickets.value.push({
            passengerId: item.id,
            passengerType: item.type,
            seatTypeCode: seatTypes[0].code,
            passengerName: item.name,
            passengerIdCard: item.idCard
          }))
        }, {immediate: true});
    
        // 0:不支持选座;1:选一等座;2:选二等座
        const chooseSeatType = ref(0);
        // 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          chooseSeatObj.value = {};
          for (let i = 1; i <= 2; i++) {
            SEAT_COL_ARRAY.value.forEach((item) => {
              chooseSeatObj.value[item.code + i] = false;
            })
          }
          console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
        }, {immediate: true});
    
        const handleQueryPassenger = () => {
          axios.get("/member/passenger/query-mine").then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content;
              passengers.value.forEach((item) => passengerOptions.value.push({
                label: item.name,
                value: item
              }))
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const finishCheckPassenger = () => {
          console.log("购票列表:", tickets.value);
    
          if (tickets.value.length > 5) {
            notification.error({description: '最多只能购买5张车票'});
            return;
          }
    
          // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
          // 前端校验不一定准,但前端校验可以减轻后端很多压力
          // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
          let seatTypesTemp = Tool.copy(seatTypes);
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            for (let j = 0; j < seatTypesTemp.length; j++) {
              let seatType = seatTypesTemp[j];
              // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
              if (ticket.seatTypeCode === seatType.code) {
                seatType.count--;
                if (seatType.count < 0) {
                  notification.error({description: seatType.desc + '余票不足'});
                  return;
                }
              }
            }
          }
          console.log("前端余票校验通过");
    
          // 判断是否支持选座,只有纯一等座和纯二等座支持选座
          // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
          let ticketSeatTypeCodes = [];
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            ticketSeatTypeCodes.push(ticket.seatTypeCode);
          }
          // 为购票列表中的所有座位类型去重:[1, 2]
          const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
          console.log("选好的座位类型:", ticketSeatTypeCodesSet);
          if (ticketSeatTypeCodesSet.length !== 1) {
            console.log("选了多种座位,不支持选座");
            chooseSeatType.value = 0;
          } else {
            // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
            if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
              console.log("一等座选座");
              chooseSeatType.value = SEAT_TYPE.YDZ.code;
            } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
              console.log("二等座选座");
              chooseSeatType.value = SEAT_TYPE.EDZ.code;
            } else {
              console.log("不是一等座或二等座,不支持选座");
              chooseSeatType.value = 0;
            }
    
            // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
            if (chooseSeatType.value !== 0) {
              for (let i = 0; i < seatTypes.length; i++) {
                let seatType = seatTypes[i];
                // 找到同类型座位
                if (ticketSeatTypeCodesSet[0] === seatType.code) {
                  // 判断余票,小于20张就不支持选座
                  if (seatType.count < 20) {
                    console.log("余票小于20张就不支持选座")
                    chooseSeatType.value = 0;
                    break;
                  }
                }
              }
            }
          }
    
          // 弹出确认界面
          visible.value = true;
    
        };
    
        const handleOk = () => {
          if (Tool.isEmpty(imageCode.value)) {
            notification.error({description: '验证码不能为空'});
            return;
          }
    
          console.log("选好的座位:", chooseSeatObj.value);
    
          // 设置每张票的座位
          // 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
          for (let i = 0; i < tickets.value.length; i++) {
            tickets.value[i].seat = null;
          }
          let i = -1;
          // 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
          for (let key in chooseSeatObj.value) {
            if (chooseSeatObj.value[key]) {
              i++;
              if (i > tickets.value.length - 1) {
                notification.error({description: '所选座位数大于购票数'});
                return;
              }
              tickets.value[i].seat = key;
            }
          }
          if (i > -1 && i < (tickets.value.length - 1)) {
            notification.error({description: '所选座位数小于购票数'});
            return;
          }
    
          console.log("最终购票:", tickets.value);
    
          axios.post("/business/confirm-order/do", {
            dailyTrainTicketId: dailyTrainTicket.id,
            date: dailyTrainTicket.date,
            trainCode: dailyTrainTicket.trainCode,
            start: dailyTrainTicket.start,
            end: dailyTrainTicket.end,
            tickets: tickets.value,
            imageCodeToken: imageCodeToken.value,
            imageCode: imageCode.value,
            lineNumber: lineNumber.value
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              // notification.success({description: "下单成功!"});
              visible.value = false;
              imageCodeModalVisible.value = false;
              lineModalVisible.value = true;
              confirmOrderId.value = data.content;
              queryLineCount();
            } else {
              notification.error({description: data.message});
            }
          });
        }
    
        /* ------------------- 定时查询订单状态 --------------------- */
        // 确认订单后定时查询
        let queryLineCountInterval;
    
        // 定时查询订单结果/排队数量
        const queryLineCount = () => {
          confirmOrderLineCount.value = -1;
          queryLineCountInterval = setInterval(function () {
            axios.get("/business/confirm-order/query-line-count/" + confirmOrderId.value).then((response) => {
              let data = response.data;
              if (data.success) {
                let result = data.content;
                switch (result) {
                  case -1 :
                    notification.success({description: "购票成功!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  case -2:
                    notification.error({description: "购票失败!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  case -3:
                    notification.error({description: "抱歉,没票了!"});
                    lineModalVisible.value = false;
                    clearInterval(queryLineCountInterval);
                    break;
                  default:
                    confirmOrderLineCount.value = result;
                }
              } else {
                notification.error({description: data.message});
              }
            });
          }, 500);
        };
    
        /* ------------------- 第二层验证码 --------------------- */
        const imageCodeModalVisible = ref();
        const imageCodeToken = ref();
        const imageCodeSrc = ref();
        const imageCode = ref();
        /**
         * 加载图形验证码
         */
        const loadImageCode = () => {
          imageCodeToken.value = Tool.uuid(8);
          imageCodeSrc.value = process.env.VUE_APP_SERVER + '/business/kaptcha/image-code/' + imageCodeToken.value;
        };
    
        const showImageCodeModal = () => {
          loadImageCode();
          imageCodeModalVisible.value = true;
        };
    
        /* ------------------- 第一层验证码 --------------------- */
        const firstImageCodeSourceA = ref();
        const firstImageCodeSourceB = ref();
        const firstImageCodeTarget = ref();
        const firstImageCodeModalVisible = ref();
    
        /**
         * 加载第一层验证码
         */
        const loadFirstImageCode = () => {
          // 获取1~10的数:Math.floor(Math.random()*10 + 1)
          firstImageCodeSourceA.value = Math.floor(Math.random()*10 + 1) + 10;
          firstImageCodeSourceB.value = Math.floor(Math.random()*10 + 1) + 20;
        };
    
        /**
         * 显示第一层验证码弹出框
         */
        const showFirstImageCodeModal = () => {
          loadFirstImageCode();
          firstImageCodeModalVisible.value = true;
        };
    
        /**
         * 校验第一层验证码
         */
        const validFirstImageCode = () => {
          if (parseInt(firstImageCodeTarget.value) === parseInt(firstImageCodeSourceA.value + firstImageCodeSourceB.value)) {
            // 第一层验证通过
            firstImageCodeModalVisible.value = false;
            showImageCodeModal();
          } else {
            notification.error({description: '验证码错误'});
          }
        };
    
        /**
         * 取消排队
         */
        const onCancelOrder = () => {
          axios.get("/business/confirm-order/cancel/" + confirmOrderId.value).then((response) => {
            let data = response.data;
            if (data.success) {
              let result = data.content;
              if (result === 1) {
                notification.success({description: "取消成功!"});
                // 取消成功时,不用再轮询排队结果
                clearInterval(queryLineCountInterval);
                lineModalVisible.value = false;
              } else {
                notification.error({description: "取消失败!"});
              }
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          handleQueryPassenger();
        });
    
        return {
          passengers,
          dailyTrainTicket,
          seatTypes,
          passengerOptions,
          passengerChecks,
          tickets,
          PASSENGER_TYPE_ARRAY,
          visible,
          finishCheckPassenger,
          chooseSeatType,
          chooseSeatObj,
          SEAT_COL_ARRAY,
          handleOk,
          imageCodeToken,
          imageCodeSrc,
          imageCode,
          showImageCodeModal,
          imageCodeModalVisible,
          loadImageCode,
          firstImageCodeSourceA,
          firstImageCodeSourceB,
          firstImageCodeTarget,
          firstImageCodeModalVisible,
          showFirstImageCodeModal,
          validFirstImageCode,
          lineModalVisible,
          confirmOrderId,
          confirmOrderLineCount,
          onCancelOrder,
          lineNumber
        };
      },
    });
    </script>
    
    <style>
    .order-train .order-train-main {
      font-size: 18px;
      font-weight: bold;
    }
    .order-train .order-train-ticket {
      margin-top: 15px;
    }
    .order-train .order-train-ticket .order-train-ticket-main {
      color: red;
      font-size: 18px;
    }
    
    .order-tickets {
      margin: 10px 0;
    }
    .order-tickets .ant-col {
      padding: 5px 10px;
    }
    .order-tickets .order-tickets-header {
      background-color: cornflowerblue;
      border: solid 1px cornflowerblue;
      color: white;
      font-size: 16px;
      padding: 5px 0;
    }
    .order-tickets .order-tickets-row {
      border: solid 1px cornflowerblue;
      border-top: none;
      vertical-align: middle;
      line-height: 30px;
    }
    
    .order-tickets .choose-seat-item {
      margin: 5px 5px;
    }
    </style>
    
  • 效果

在这里插入图片描述

4.增加座位销售图

额外的功能,最终展现类似电影院座位销售图的效果

1.增加查询座位销售详情接口

  • com.neilxu.train.business.req.SeatSellReq

    package com.neilxu.train.business.req;
    
    import jakarta.validation.constraints.NotNull;
    import lombok.Data;
    import org.springframework.format.annotation.DateTimeFormat;
    
    import java.util.Date;
    
    @Data
    public class SeatSellReq {
    
        /**
         * 日期
         */
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        @NotNull(message = "【日期】不能为空")
        private Date date;
    
        /**
         * 车次编号
         */
        @NotNull(message = "【车次编号】不能为空")
        private String trainCode;
    
    }
    
  • com.neilxu.train.business.resp.SeatSellResp

    package com.neilxu.train.business.resp;
    
    import lombok.Data;
    
    @Data
    public class SeatSellResp {
    
        /**
         * 箱序
         */
        private Integer carriageIndex;
    
        /**
         * 排号|01, 02
         */
        private String row;
    
        /**
         * 列号|枚举[SeatColEnum]
         */
        private String col;
    
        /**
         * 座位类型|枚举[SeatTypeEnum]
         */
        private String seatType;
    
        /**
         * 售卖情况|将经过的车站用01拼接,0表示可卖,1表示已卖
         */
        private String sell;
    
    }
    
  • com.neilxu.train.business.service.DailyTrainSeatService

    /**
     * 查询某日某车次的所有座位
     */
    public List<SeatSellResp> querySeatSell(SeatSellReq req) {
        Date date = req.getDate();
        String trainCode = req.getTrainCode();
        LOG.info("查询日期【{}】车次【{}】的座位销售信息", DateUtil.formatDate(date), trainCode);
        DailyTrainSeatExample dailyTrainSeatExample = new DailyTrainSeatExample();
        dailyTrainSeatExample.setOrderByClause("`carriage_index` asc, carriage_seat_index asc");
        dailyTrainSeatExample.createCriteria()
                .andDateEqualTo(date)
                .andTrainCodeEqualTo(trainCode);
        return BeanUtil.copyToList(dailyTrainSeatMapper.selectByExample(dailyTrainSeatExample), SeatSellResp.class);
    }
    
  • com.neilxu.train.business.controller.SeatSellController

    package com.neilxu.train.business.controller;
    
    import com.neilxu.train.business.req.SeatSellReq;
    import com.neilxu.train.business.resp.SeatSellResp;
    import com.neilxu.train.business.service.DailyTrainSeatService;
    import com.neilxu.train.common.resp.CommonResp;
    import jakarta.validation.Valid;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    ;
    
    @RestController
    @RequestMapping("/seat-sell")
    public class SeatSellController {
    
        @Autowired
        private DailyTrainSeatService dailyTrainSeatService;
    
        @GetMapping("/query")
        public CommonResp<List<SeatSellResp>> query(@Valid SeatSellReq req) {
            List<SeatSellResp> seatList = dailyTrainSeatService.querySeatSell(req);
            return new CommonResp<>(seatList);
        }
    
    }
    
  • 测试

    http/business-seat.http

    GET http://localhost:8000/business/seat-sell/query?date=2024-04-10&trainCode=D2
    Accept: application/json
    token: {{token}}
    
    ###
    

在这里插入图片描述

2.增加座位销售图路由及页面,实现页面跳转和参数传递

  • web/src/views/main/seat.vue

    <template>
      <div v-if="!param.date">
        请到余票查询里选择一趟列车,
        <router-link to="/ticket">
          跳转到余票查询
        </router-link>
      </div>
      <div v-else>
        <p>
          日期:{{param.date}},车次:{{param.trainCode}},出发站:{{param.start}},到达站:{{param.end}}
        </p>
      </div>
    </template>
    
    <script>
    
    import { defineComponent, ref } from 'vue';
    import {useRoute} from "vue-router";
    
    export default defineComponent({
      name: "welcome-view",
      setup() {
        const route = useRoute();
        const param = ref({});
        param.value = route.query;
    
        return {
          param
        };
      },
    });
    </script>
    
  • 增加路由、侧边栏、顶部菜单栏

    操作同之前

  • web/src/views/main/ticket.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
          <station-select-view v-model="params.start" width="200px"></station-select-view>
          <station-select-view v-model="params.end" width="200px"></station-select-view>
          <a-button type="primary" @click="handleQuery()">查找</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrainTickets"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-button type="primary" @click="toOrder(record)">预订</a-button>
              <router-link :to="{
                path: '/seat',
                query: {
                  date: record.date,
                  trainCode: record.trainCode,
                  start: record.start,
                  startIndex: record.startIndex,
                  end: record.end,
                  endIndex: record.endIndex
                }
              }">
                <a-button type="primary">座位销售图</a-button>
              </router-link>
              <a-button type="primary" @click="showStation(record)">途经车站</a-button>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'station'">
            {{record.start}}<br/>
            {{record.end}}
          </template>
          <template v-else-if="column.dataIndex === 'time'">
            {{record.startTime}}<br/>
            {{record.endTime}}
          </template>
          <template v-else-if="column.dataIndex === 'duration'">
            {{calDuration(record.startTime, record.endTime)}}<br/>
            <div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
              次日到达
            </div>
            <div v-else>
              当日到达
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'ydz'">
            <div v-if="record.ydz >= 0">
              {{record.ydz}}<br/>
              {{record.ydzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'edz'">
            <div v-if="record.edz >= 0">
              {{record.edz}}<br/>
              {{record.edzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'rw'">
            <div v-if="record.rw >= 0">
              {{record.rw}}<br/>
              {{record.rwPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'yw'">
            <div v-if="record.yw >= 0">
              {{record.yw}}<br/>
              {{record.ywPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
        </template>
      </a-table>
    
      <!-- 途经车站 -->
      <a-modal style="top: 30px" v-model:visible="visible" :title="null" :footer="null" :closable="false">
        <a-table :data-source="stations" :pagination="false">
          <a-table-column key="index" title="站序" data-index="index" />
          <a-table-column key="name" title="站名" data-index="name" />
          <a-table-column key="inTime" title="进站时间" data-index="inTime">
            <template #default="{ record }">
              {{record.index === 0 ? '-' : record.inTime}}
            </template>
          </a-table-column>
          <a-table-column key="outTime" title="出站时间" data-index="outTime">
            <template #default="{ record }">
              {{record.index === (stations.length - 1) ? '-' : record.outTime}}
            </template>
          </a-table-column>
          <a-table-column key="stopTime" title="停站时长" data-index="stopTime">
            <template #default="{ record }">
              {{record.index === 0 || record.index === (stations.length - 1) ? '-' : record.stopTime}}
            </template>
          </a-table-column>
        </a-table>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import StationSelectView from "@/components/station-select";
    import dayjs from "dayjs";
    import router from "@/router";
    
    export default defineComponent({
      name: "ticket-view",
      components: {StationSelectView},
      setup() {
        const visible = ref(false);
        let dailyTrainTicket = ref({
          id: undefined,
          date: undefined,
          trainCode: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          startIndex: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          endIndex: undefined,
          ydz: undefined,
          ydzPrice: undefined,
          edz: undefined,
          edzPrice: undefined,
          rw: undefined,
          rwPrice: undefined,
          yw: undefined,
          ywPrice: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrainTickets = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const params = ref({});
        const columns = [
          {
            title: '车次编号',
            dataIndex: 'trainCode',
            key: 'trainCode',
          },
          {
            title: '车站',
            dataIndex: 'station',
          },
          {
            title: '时间',
            dataIndex: 'time',
          },
          {
            title: '历时',
            dataIndex: 'duration',
          },
          {
            title: '一等座',
            dataIndex: 'ydz',
            key: 'ydz',
          },
          {
            title: '二等座',
            dataIndex: 'edz',
            key: 'edz',
          },
          {
            title: '软卧',
            dataIndex: 'rw',
            key: 'rw',
          },
          {
            title: '硬卧',
            dataIndex: 'yw',
            key: 'yw',
          },
          {
            title: '操作',
            dataIndex: 'operation',
          },
        ];
    
    
        const handleQuery = (param) => {
          if (Tool.isEmpty(params.value.date)) {
            notification.error({description: "请输入日期"});
            return;
          }
          if (Tool.isEmpty(params.value.start)) {
            notification.error({description: "请输入出发地"});
            return;
          }
          if (Tool.isEmpty(params.value.end)) {
            notification.error({description: "请输入目的地"});
            return;
          }
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
    
          // 保存查询参数
          SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
    
          loading.value = true;
          axios.get("/business/daily-train-ticket/query-list", {
            params: {
              page: param.page,
              size: param.size,
              trainCode: params.value.trainCode,
              date: params.value.date,
              start: params.value.start,
              end: params.value.end
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrainTickets.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (page) => {
          // console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
          pagination.value.pageSize = page.pageSize;
          handleQuery({
            page: page.current,
            size: page.pageSize
          });
        };
    
        const calDuration = (startTime, endTime) => {
          let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
          return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
        };
    
        const toOrder = (record) => {
          dailyTrainTicket.value = Tool.copy(record);
          SessionStorage.set(SESSION_ORDER, dailyTrainTicket.value);
          router.push("/order")
        };
    
        // ---------------------- 途经车站 ----------------------
        const stations = ref([]);
        const showStation = record => {
          visible.value = true;
          axios.get("/business/daily-train-station/query-by-train-code", {
            params: {
              date: record.date,
              trainCode: record.trainCode
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              stations.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          //  "|| {}"是常用技巧,可以避免空指针异常
          params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
          if (Tool.isNotEmpty(params.value)) {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          }
        });
    
        return {
          dailyTrainTicket,
          visible,
          dailyTrainTickets,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          params,
          calDuration,
          toOrder,
          showStation,
          stations
        };
      },
    });
    </script>
    

3.座位销售图页面获得销售信息,同一趟车,不管查哪个区间,查到的销售信息是一样的,由界面再去截取区间的销售信息。功能设计经验:对于复杂的操作,能放到前端的都放到前端,减小后端的压力。

  • web/src/views/main/seat.vue

    <template>
      <div v-if="!param.date">
        请到余票查询里选择一趟列车,
        <router-link to="/ticket">
          跳转到余票查询
        </router-link>
      </div>
      <div v-else>
        <p>
          日期:{{param.date}},车次:{{param.trainCode}},出发站:{{param.start}},到达站:{{param.end}}
        </p>
        <p>
          {{list}}
        </p>
      </div>
    </template>
    
    <script>
    
    import { defineComponent, ref, onMounted } from 'vue';
    import axios from "axios";
    import {notification} from "ant-design-vue";
    import {useRoute} from "vue-router";
    
    export default defineComponent({
      name: "welcome-view",
      setup() {
        const route = useRoute();
        const param = ref({});
        param.value = route.query;
        const list = ref();
    
        // 查询一列火车的所有销售信息
        const querySeat = () => {
          axios.get("/business/seat-sell/query", {
            params: {
              date: param.value.date,
              trainCode: param.value.trainCode,
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              list.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          if (param.value.date) {
            querySeat();
          }
        });
    
        return {
          param,
          querySeat,
          list
        };
      },
    });
    </script>
    

4.显示各车厢各座位的销售详情,使用橙色灰色代码座位可卖与卖出

  • train-station.vue

    <a-form-item label="站序">
      <a-input v-model:value="trainStation.index" />
      <span style="color: red">重要:第1站是0,对显示销售图有影响</span>
    </a-form-item>
    
  • seat.vue

    <template>
      <div v-if="!param.date">
        请到余票查询里选择一趟列车,
        <router-link to="/ticket">
          跳转到余票查询
        </router-link>
      </div>
      <div v-else>
        <p style="font-weight: bold;">
          日期:{{param.date}},车次:{{param.trainCode}},出发站:{{param.start}},到达站:{{param.end}}
        </p>
    
        <table>
          <tr>
            <td style="width: 25px; background: #FF9900;"></td>
            <td>:已被购买</td>
            <td style="width: 20px;"></td>
            <td style="width: 25px; background: #999999;"></td>
            <td>:未被购买</td>
          </tr>
        </table>
        <br>
        <div v-for="(seatObj, carriage) in train" :key="carriage"
             style="border: 3px solid #99CCFF;
                     margin-bottom: 30px;
                     padding: 5px;
                     border-radius: 4px">
          <div style="display:block;
                      width:50px;
                      height:10px;
                      position:relative;
                      top:-15px;
                      text-align: center;
                      background: white;">
            {{carriage}}
          </div>
          <table>
            <tr>
              <td v-for="(sell, index) in Object.values(seatObj)[0]" :key="index"
                  style="text-align: center">
                {{index + 1}}
              </td>
            </tr>
            <tr v-for="(sellList, col) in seatObj" :key="col">
              <td v-for="(sell, index) in sellList" :key="index"
                  style="text-align: center;
                          border: 2px solid white;
                          background: grey;
                          padding: 0 4px;
                          color: white;
                          "
                  :style="{background: (sell > 0 ? '#FF9900' : '#999999')}">{{col}}</td>
            </tr>
          </table>
        </div>
      </div>
    </template>
    
    <script>
    
    import {defineComponent, onMounted, ref} from 'vue';
    import axios from "axios";
    import {notification} from "ant-design-vue";
    import {useRoute} from "vue-router";
    
    export default defineComponent({
      name: "seat-view",
      setup() {
        const route = useRoute();
        const param = ref({});
        param.value = route.query;
        const list = ref();
        // 使用对象更便于组装数组,三维数组只能存储最终的01,不能存储“车箱1”,“A”这些数据
        // {
        //   "车箱1": {
        //      "A" : ["000", "001", "001", "001"],
        //      "B" : ["000", "001", "001", "001"],
        //      "C" : ["000", "001", "001", "001"],
        //      "D" : ["000", "001", "001", "001"]
        //    }, "车箱2": {
        //      "A" : ["000", "001", "001", "001"],
        //      "B" : ["000", "001", "001", "001"],
        //      "C" : ["000", "001", "001", "001"],
        //      "D" : ["000", "001", "001", "001"],
        //      "D" : ["000", "001", "001", "001"]
        //    }
        // }
        let train = ref({});
    
        // 查询一列火车的所有车站
        const querySeat = () => {
          axios.get("/business/seat-sell/query", {
            params: {
              date: param.value.date,
              trainCode: param.value.trainCode,
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              list.value = data.content;
              format();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 截取出当前区间的销售信息,并判断是否有票
         */
        const format = () => {
          let _train = {};
    
          for (let i = 0; i < list.value.length; i++) {
            let item = list.value[i];
    
            // 计算当前区间是否还有票,约定:站序是从0开始
            let sellDB = item.sell;
    
            // 假设6站:start = 1, end = 3, sellDB = 11111,最终得到:sell = 01110,转int 1100,不可买
            // 假设6站:start = 1, end = 3, sellDB = 11011,最终得到:sell = 01010,转int 1000,不可买
            // 假设6站:start = 1, end = 3, sellDB = 10001,最终得到:sell = 00000,转int 0,可买
            // 验证代码:
            // let sellDB = "123456789";
            // let start = 1;
            // let end = 3;
            // let sell = sellDB.substr(start, end - start)
            // console.log(sell)
            let sell = sellDB.substr(param.value.startIndex, param.value.endIndex - param.value.startIndex);
            // console.log("完整的销卖信息:", sellDB, "区间内的销卖信息", sell);
    
            // 将sell放入火车数据中
            if (!_train["车箱" + item.carriageIndex]) {
              _train["车箱" + item.carriageIndex] = {};
            }
            if (!_train["车箱" + item.carriageIndex][item.col]) {
              _train["车箱" + item.carriageIndex][item.col] = [];
            }
            _train["车箱" + item.carriageIndex][item.col].push(parseInt(sell));
          }
    
          train.value = _train;
        }
    
        onMounted(() => {
          if (param.value.date) {
            querySeat();
          }
        });
    
        return {
          param,
          train
        };
      },
    });
    </script>
    
  • 测试效果

在这里插入图片描述

三、只允许购买两周内的车次

  • ticket.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" :disabled-date="disabledDate" placeholder="请选择日期"></a-date-picker>
          <station-select-view v-model="params.start" width="200px"></station-select-view>
          <station-select-view v-model="params.end" width="200px"></station-select-view>
          <a-button type="primary" @click="handleQuery()">查找</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrainTickets"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-button type="primary" @click="toOrder(record)" :disabled="isExpire(record)">{{isExpire(record) ? "过期" : "预订"}}</a-button>
              <router-link :to="{
                path: '/seat',
                query: {
                  date: record.date,
                  trainCode: record.trainCode,
                  start: record.start,
                  startIndex: record.startIndex,
                  end: record.end,
                  endIndex: record.endIndex
                }
              }">
                <a-button type="primary">座位销售图</a-button>
              </router-link>
              <a-button type="primary" @click="showStation(record)">途经车站</a-button>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'station'">
            {{record.start}}<br/>
            {{record.end}}
          </template>
          <template v-else-if="column.dataIndex === 'time'">
            {{record.startTime}}<br/>
            {{record.endTime}}
          </template>
          <template v-else-if="column.dataIndex === 'duration'">
            {{calDuration(record.startTime, record.endTime)}}<br/>
            <div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
              次日到达
            </div>
            <div v-else>
              当日到达
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'ydz'">
            <div v-if="record.ydz >= 0">
              {{record.ydz}}<br/>
              {{record.ydzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'edz'">
            <div v-if="record.edz >= 0">
              {{record.edz}}<br/>
              {{record.edzPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'rw'">
            <div v-if="record.rw >= 0">
              {{record.rw}}<br/>
              {{record.rwPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
          <template v-else-if="column.dataIndex === 'yw'">
            <div v-if="record.yw >= 0">
              {{record.yw}}<br/>
              {{record.ywPrice}}¥
            </div>
            <div v-else>
              --
            </div>
          </template>
        </template>
      </a-table>
    
      <!-- 途经车站 -->
      <a-modal style="top: 30px" v-model:visible="visible" :title="null" :footer="null" :closable="false">
        <a-table :data-source="stations" :pagination="false">
          <a-table-column key="index" title="站序" data-index="index" />
          <a-table-column key="name" title="站名" data-index="name" />
          <a-table-column key="inTime" title="进站时间" data-index="inTime">
            <template #default="{ record }">
              {{record.index === 0 ? '-' : record.inTime}}
            </template>
          </a-table-column>
          <a-table-column key="outTime" title="出站时间" data-index="outTime">
            <template #default="{ record }">
              {{record.index === (stations.length - 1) ? '-' : record.outTime}}
            </template>
          </a-table-column>
          <a-table-column key="stopTime" title="停站时长" data-index="stopTime">
            <template #default="{ record }">
              {{record.index === 0 || record.index === (stations.length - 1) ? '-' : record.stopTime}}
            </template>
          </a-table-column>
        </a-table>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import StationSelectView from "@/components/station-select";
    import dayjs from "dayjs";
    import router from "@/router";
    
    export default defineComponent({
      name: "ticket-view",
      components: {StationSelectView},
      setup() {
        const visible = ref(false);
        let dailyTrainTicket = ref({
          id: undefined,
          date: undefined,
          trainCode: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          startIndex: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          endIndex: undefined,
          ydz: undefined,
          ydzPrice: undefined,
          edz: undefined,
          edzPrice: undefined,
          rw: undefined,
          rwPrice: undefined,
          yw: undefined,
          ywPrice: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrainTickets = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const params = ref({});
        const columns = [
          {
            title: '车次编号',
            dataIndex: 'trainCode',
            key: 'trainCode',
          },
          {
            title: '车站',
            dataIndex: 'station',
          },
          {
            title: '时间',
            dataIndex: 'time',
          },
          {
            title: '历时',
            dataIndex: 'duration',
          },
          {
            title: '一等座',
            dataIndex: 'ydz',
            key: 'ydz',
          },
          {
            title: '二等座',
            dataIndex: 'edz',
            key: 'edz',
          },
          {
            title: '软卧',
            dataIndex: 'rw',
            key: 'rw',
          },
          {
            title: '硬卧',
            dataIndex: 'yw',
            key: 'yw',
          },
          {
            title: '操作',
            dataIndex: 'operation',
          },
        ];
    
    
        const handleQuery = (param) => {
          if (Tool.isEmpty(params.value.date)) {
            notification.error({description: "请输入日期"});
            return;
          }
          if (Tool.isEmpty(params.value.start)) {
            notification.error({description: "请输入出发地"});
            return;
          }
          if (Tool.isEmpty(params.value.end)) {
            notification.error({description: "请输入目的地"});
            return;
          }
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
    
          // 保存查询参数
          SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
    
          loading.value = true;
          axios.get("/business/daily-train-ticket/query-list", {
            params: {
              page: param.page,
              size: param.size,
              trainCode: params.value.trainCode,
              date: params.value.date,
              start: params.value.start,
              end: params.value.end
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrainTickets.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (page) => {
          // console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
          pagination.value.pageSize = page.pageSize;
          handleQuery({
            page: page.current,
            size: page.pageSize
          });
        };
    
        const calDuration = (startTime, endTime) => {
          let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
          return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
        };
    
        const toOrder = (record) => {
          dailyTrainTicket.value = Tool.copy(record);
          SessionStorage.set(SESSION_ORDER, dailyTrainTicket.value);
          router.push("/order")
        };
    
        // ---------------------- 途经车站 ----------------------
        const stations = ref([]);
        const showStation = record => {
          visible.value = true;
          axios.get("/business/daily-train-station/query-by-train-code", {
            params: {
              date: record.date,
              trainCode: record.trainCode
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              stations.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        // 不能选择今天以前及两周以后的日期
        const disabledDate = current => {
          return current && (current <= dayjs().add(-1, 'day') || current > dayjs().add(14, 'day'));
        };
    
        // 判断是否过期
        const isExpire = (record) => {
          // 标准时间:2000/01/01 00:00:00
          let startDateTimeString = record.date.replace(/-/g, "/") + " " + record.startTime;
          let startDateTime = new Date(startDateTimeString);
    
          //当前时间
          let now = new Date();
    
          console.log(startDateTime)
          return now.valueOf() >= startDateTime.valueOf();
        };
    
        onMounted(() => {
          //  "|| {}"是常用技巧,可以避免空指针异常
          params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
          if (Tool.isNotEmpty(params.value)) {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          }
        });
    
        return {
          dailyTrainTicket,
          visible,
          dailyTrainTickets,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          params,
          calDuration,
          toOrder,
          showStation,
          stations,
          disabledDate,
          isExpire
        };
      },
    });
    </script>
    
  • 效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/500484.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Modelsim手动仿真实例

目录 1. 软件链接 2. 为什么要使用Modelsim 3. Modelsim仿真工程由几部分组成&#xff1f; 4. 上手实例 4.1. 新建文件夹 4.2. 指定目录 4.3. 新建工程 4.4. 新建设计文件&#xff08;Design Files&#xff09; 4.5. 新建测试平台文件&#xff08;Testbench Files&…

H7310 线性恒流调光芯片 支持24V30V48V60V100V转3.3V5V12V1.5A 外围简单 性价比高

线性恒流调光芯片是一种能够将输入电压稳定转换为恒定电流输出的电子设备&#xff0c;同时支持调光功能。这种芯片通常具有较高的效率和稳定性&#xff0c;适用于LED照明、显示屏等领域。 针对您提到的支持24V、30V、48V、60V、100V转3.3V、5V、12V&#xff0c;并且能够提供1.…

二十九 超级数据查看器 讲解稿 查询复用

二十九 超级数据查看器 讲解稿 查询复用 ​点击此处 以新页面 打开B站 播放当前教学视频 点击访问app下载页面 百度手机助手 下载地址 大家好&#xff0c;今天我们讲一下超级数据查看器的查询复用功能&#xff0c;这是新版本要增加的功能&#xff0c;这讲是预告。 先介绍…

数据可视化Grafana Windows 安装使用教程(中文版)

1.跳转连接 天梦星服务平台 (tmxkj.top)https://tmxkj.top/#/site?url 2.下载应用程序 官网地址&#xff1a;Grafana get started | Cloud, Self-managed, Enterprisehttps://grafana.com/get/ 3.修改配置文件 grafana\conf\defaults 4.启动\bin\目录下serve应用程序 浏…

机器学习——降维算法-主成分分析(PCA)

机器学习——降维算法-主成分分析&#xff08;PCA&#xff09; 在机器学习领域&#xff0c;主成分分析&#xff08;Principal Component Analysis&#xff0c;简称PCA&#xff09;是一种常用的降维技术&#xff0c;用于减少数据集中特征的数量&#xff0c;同时保留数据集的主要…

尾矿库在线安全监测:提升矿山安全水平

在矿山安全领域&#xff0c;尾矿库的安全管理尤为关键。尾矿库作为矿山生产链条的重要环节&#xff0c;其稳定性不仅关系到生产活动的持续进行&#xff0c;更直接影响着周边环境和人民群众的生命财产安全。因此&#xff0c;尾矿库的安全监测显得尤为重要。近年来&#xff0c;随…

YOLOv9改进策略 : C2f改进 | 引入YOLOv8 C2f结构

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文改进内容&#xff1a;应订阅者需求&#xff0c;如何将YOLOv8 C2f结构引入到YOLOv9 &#x1f4a1;&#x1f4a1;&#x1f4a1;C2f层是一种特殊的卷积层&#xff0c;用于将不同尺度的特征图融合在一起&#xff0c;以提高目标检测的准…

XXE漏洞知识及ctfshow例题

XXE漏洞相关知识 XXE全称为XML Enternal Entity Injection 中文叫xml外部实体注入 什么是xml 简单了解XML&#xff1a; &#xff08;xml和html的区别可以简易的理解成&#xff1a;xml是用来储存数据和传输数据的而html是用来将数据展现出来&#xff09; XML 指可扩展标记语…

UE5数字孪生系列笔记(三)

C创建Pawn类玩家 创建一个GameMode蓝图用来加载我们自定义的游戏Mode新建一个Pawn的C&#xff0c;MyCharacter类作为玩家&#xff0c;新建一个相机组件与相机臂组件&#xff0c;box组件作为根组件 // Fill out your copyright notice in the Description page of Project Set…

【力扣】300. 最长递增子序列(DFS+DP两种方法实现)

目录 题目传送最长递增子序列[DFS 方法]DFS方法思路图思路简述代码大家可以自行考虑有没有优化的方法 最长递增子序列[DP]方法DP方法思路图思路简述代码方案 题目传送 原题目链接 最长递增子序列[DFS 方法] DFS方法思路图 思路简述 对于序列中的每一个数字只有选择和不选择两…

C语言查找-----------BF算法KMP算法

1.问题引入 有一个主字符串&#xff0c;有一个子字符串&#xff0c;要求我们寻找子字符串在主字符串里面开始出现的位置&#xff1b; 2.BF算法 BF算法就是暴力算法&#xff0c;这个做法虽然效率不高&#xff0c;但是按照我们传统的思路依然能够得到结果&#xff0c;接下来我们…

LeetCode 523. 连续的子数组和

解题思路 相关代码 class Solution {public boolean checkSubarraySum(int[] nums, int k) {int s[] new int[nums.length1];for(int i1;i<nums.length;i) s[i]s[i-1]nums[i-1];Set<Integer> set new HashSet<>(); for(int i2;i<nums.length;i){set.ad…

filebox在线文件管理工具V1.11.1.1查分吧修改自用版免费分享[PHP]

* 基于:https://down.chinaz.com/soft/35899.htm * 查分吧 修改自用版今日对外分享(自2016年1.10版本以来一直用他云开发:Web环境即时看效果) * 也可以用于本人很多txt/csv通用查询系统的在线管理后台管理数据 * 默认登陆账号filebox密码nidemima * 修改账号密码:21-22行;获取…

Java八股文(K8S)

Java八股文のK8S K8S K8S 请解释什么是Kubernetes&#xff1f; Kubernetes是一个开源的容器编排和管理工具&#xff0c;用于自动化部署、扩展和管理容器化应用程序。 请解释Kubernetes中的Pod、Deployment和Service之间的关系。 ● Pod是Kubernetes的最小部署单元&#xff0c;…

练习 13 Web [极客大挑战 2019]Secret File

php伪协议请求&#xff0c;php代码审计 参考&#xff1a;BUUCTF__[极客大挑战 2019]Secret File_题解 没有任何上传和登录页面 查看前端源码 发现 <a id"master" href"./Archive_room.php" style"background-color:#000000;height:70px;width:20…

【java9】java9新特性值之集合不可变实例工厂方法

Java9为集合接口List、Set、Map提供了创建不可变实例的工厂方法。这些工厂方法为便利而生&#xff0c;以简单的方式创建这些集合的不可变实例。 Java9之前创建不可变集合 在Java9之前&#xff0c;创建不可变集合通常需要通过其他方式&#xff0c;比如使用Collections.unmodif…

day4 linux上部署第一个nest项目(java转ts全栈/3R教室)

背景&#xff1a;上一篇吧nest-vben-admin项目&#xff0c;再开发环境上跑通了&#xff0c;并且build出来了dist文件&#xff0c;接下来再部署到linux试试吧 dist文件夹是干嘛的&#xff1f; 一个pnpn install 直接生成了两个dist文件夹&#xff0c;前端admin项目一个&#xf…

用Kimichat快速识别出图片中的表格保存到Excel

如果有一张图片格式的表格&#xff0c;想要快速复制到Excel表格中&#xff0c;那么一般要借助于OCR工具。之前试过不少在线OCR工具&#xff0c;识别效果差强人意。其实&#xff0c;kimichat就可以非常好的完成这个任务。 下面是一张研报中的表格&#xff0c;只能以图片形式保存…

153 Linux C++ 通讯架构实战8 ,日志打印实战,设置时区,main函数中顺序调整

日志打印实战 //日志的重要性&#xff1a;供日后运行维护人员去查看、定位和解决问题&#xff1b; //新文件&#xff1a;ngx_printf.cxx以及ngx_log.cxx。 //ngx_printf.cxx&#xff1a;放和打印格式相关的函数&#xff1b; //ngx_log.cxx&#xff1a;放和日志相关…

【计算机考研】数学难,到底难在哪里?看这一篇深度分析

数一和数二的难度系数都不在一个重量级&#xff01; 数一这货&#xff0c;容量真不是数二能比的&#xff01;除了高数、线代这些常规操作&#xff0c;还要啃概率论与数理统计这本大厚书&#xff0c;简直是让人头大&#xff01; 考研数学嘛&#xff0c;大家都知道&#xff0c;…