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

news2024/11/26 4:45:36

本文参考自

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>
    
  • 效果

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

系统需求分析报告(原件获取)

第1章 序言 第2章 引言 2.1 项目概述 2.2 编写目的 2.3 文档约定 2.4 预期读者及阅读建议 第3章 技术要求 3.1 软件开发要求 第4章 项目建设内容 第5章 系统安全需求 5.1 物理设计安全 5.2 系统安全设计 5.3 网络安全设计 5.4 应用安全设计 5.5 对用户安全管理 …

Android 自定义EditText

文章目录 Android 自定义EditText概述源码可清空内容的EditText可显示密码的EditText 使用源码下载 Android 自定义EditText 概述 定义一款可清空内容的 ClearEditText 和可显示密码的 PasswordEditText&#xff0c;支持修改提示图标和大小、背景图片等。 源码 基类&#xf…

相机标定学习记录

相机标定是计算机视觉和机器视觉领域中的一项基本技术&#xff0c;它的主要目的是通过获取相机的内部参数&#xff08;内参&#xff09;和外部参数&#xff08;外参&#xff09;&#xff0c;以及镜头畸变参数&#xff0c;建立起现实世界中的点与相机成像平面上对应像素点之间准…

枚举--enum和动态内存管理(malloc和free)

枚举---enum&#xff1a;它的本意就是列举事物&#xff0c;比如&#xff0c;颜色和性别&#xff0c;则代码为&#xff1a; #include<stdio.h> //枚举的示例&#xff1a;性别&#xff0c;颜色 enum sex//性别 {MALE,FEMALE,SECRTY }; enum clore//颜色 {ROW,BLUS,GREEN …

查找某数据在单链表中出现的次数

#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> typedef int ElemType; typedef struct LinkNode {ElemType data;LinkNode* next; }LinkNode, * LinkList; //尾插法建立单链表 void creatLinkList(LinkList& L) {L (LinkNode*)mallo…

Vue系列——数据对象

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

本地项目上传到GitHub

本文档因使用实际项目提交做为案例&#xff0c;故使用xxx等字符进行脱敏&#xff0c;同时隐藏了部分输出&#xff0c;已实际项目和命令行输出为准 0、 Git 安装与GitHub注册 1&#xff09; 在下述地址下载Git&#xff0c;安装一路默认下一步即可。安装完成后&#xff0c;随便…

【面试经典150 | 动态规划】零钱兑换

文章目录 Tag题目来源解题思路方法一&#xff1a;动态规划 写在最后 Tag 【动态规划】【数组】 题目来源 322. 零钱兑换 解题思路 方法一&#xff1a;动态规划 定义状态 dp[i] 表示凑成总金额的最少硬币个数。 状态转移 从小到大枚举要凑成的金额 i&#xff0c;如果当前…

电动车“锂”改“铅”屡被查?新国标或疏于考虑用户真实需求

最近几个月&#xff0c;电动自行车又走到了舆论中心。 “315期间”&#xff0c;不少媒体集中报道了超标电动自行车改装上牌事件。3月18日至20日&#xff0c;新华社推出电动自行车安全隐患系列调查&#xff0c;聚焦点之一就是改装超标问题。而在近段时间&#xff0c;综合媒体报…

MarTech调研总结整理

整体介绍 概念解释&#xff1a; Martech是一种智慧营销的概念&#xff0c;将割裂的营销&#xff08;Marketing&#xff09;、技术&#xff08;Technology&#xff09;与管理&#xff08;Management&#xff09;联系在一起&#xff0c;Martech将技术溶于全营销流程中&#xff0…

基于Java实验室预约管理系统设计与实现(源码+部署文档)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

《操作系统导论》第14章读书笔记:插叙:内存操作API

《操作系统导论》第14章读书笔记&#xff1a;插叙&#xff1a;内存操作API —— 杭州 2024-03-30 夜 文章目录 《操作系统导论》第14章读书笔记&#xff1a;插叙&#xff1a;内存操作API1.内存类型1.1.栈内存&#xff1a;它的申请和释放操作是编译器来隐式管理的&#xff0c;所…

FebHost:意大利.IT域名一张意大利网络名片

.IT域名是意大利的国家顶级域名&#xff0c;对于意大利企业和个人而言,拥有一个属于自己的”.IT”域名无疑是件令人自豪的事。这个被誉为意大利互联网标志性代表的域名,不仅隐含着浓厚的意大利文化特色,还为使用者在当地市场的推广铺平了道路。 对于那些希望在意大利市场建立强…

2核4G服务器可以承载多少用户?卡不卡?

腾讯云轻量应用服务器2核4G5M配置性能测评&#xff0c;腾讯云轻量2核4G5M带宽服务器支持多少人在线访问&#xff1f;并发数10&#xff0c;支持每天5000IP人数访问&#xff0c;腾讯云百科txybk.com整理2核4G服务器支持多少人同时在线&#xff1f;并发数测试、CPU性能、内存性能、…

手把手在K210上部署自己在线训练的YOLO模型

小白花了两天时间学习了一下K210&#xff0c;将在线训练的模型部署在K210&#xff08;代码后面给出&#xff09;上&#xff0c;能够识别卡皮巴拉水杯&#xff08;没错&#xff0c;卡皮巴拉&#xff0c;情绪稳定&#xff0c;真的可爱&#xff01;&#xff09;。数据集是用K210拍…

C语言例1-11:语句 while(!a); 中的表达式 !a 可以替换为

A. a!1 B. a!0 C. a0 D. a1 答案&#xff1a;C while()成真才执行&#xff0c;所以!a1 &#xff0c;也就是 a0 原代码如下&#xff1a; #include<stdio.h> int main(void) {int a0;while(!a){a;printf("a\n");} return 0; } 结果如…

数字化转型导师坚鹏:新质生产力发展解读、方法与案例

新质生产力发展解读、方法与案例 课程背景&#xff1a; 很多学员存在以下问题&#xff1a; 不知道如何理解新质生产力&#xff1f; 不清楚如何发展新质生产力&#xff1f; 不知道新质生产力发展案例&#xff1f; 课程特色&#xff1a; 有实战案例 有原创观点 有…

Linux课程____selinux模式

一、是什么 它叫做“安全增强型 Linux&#xff08;Security-Enhanced Linux&#xff09;”&#xff0c;简称 SELinux&#xff0c;它是 Linux 的一个安全子系统 二、有啥用 就是最大限度地减小系统中服务进程可访问的资源&#xff08;根据的是最小权限原则&#xff09;。避免…

leetcode:392. 判断子序列

题目&#xff1a; class Solution { public:bool isSubsequence(string s, string t) {} }; 题解&#xff1a; 很巧妙的题解&#xff1a;循环遍历两个字符串&#xff0c;两个字符串都没遍完就继续遍历&#xff0c;字符串s先遍历完结果为true&#xff0c;字符串t先遍历完结果为…

2024第17届计算机设计大赛开始啦(保研竞赛)

中国大学生计算机设计大赛是面向高校本科生的竞赛&#xff0c;旨在培养创新型、复合型、应用型人才。2024年大赛的主题包括软件应用、微课与教学辅助等11个大类。参赛队由1&#xff5e;3名本科生组成&#xff0c;指导教师不多于2人。在组队和选题方面&#xff0c;强调团结协作和…