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

本文参考自

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

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

本章目录

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

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

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

在这里插入图片描述

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

2.JMeter压测

  • 先将令牌数设置充足

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

在这里插入图片描述

  • 开始压测

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

在这里插入图片描述

可以看到结果是900多

在这里插入图片描述

  • 恢复代码到初版

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

结果:

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

在这里插入图片描述

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

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

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

  • 换成springboot自带的异步

    • BusinessApplication.java

      @EnableAsync
      public class BusinessApplication {
      
    • BeforeConfirmOrderService

      package com.neilxu.train.business.service;import cn.hutool.core.date.DateTime;
      import com.alibaba.csp.sentinel.annotation.SentinelResource;
      import com.alibaba.csp.sentinel.slots.block.BlockException;
      import com.alibaba.fastjson.JSON;
      import com.neilxu.train.business.domain.ConfirmOrder;
      import com.neilxu.train.business.dto.ConfirmOrderMQDto;
      import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
      import com.neilxu.train.business.mapper.ConfirmOrderMapper;
      import com.neilxu.train.business.req.ConfirmOrderDoReq;
      import com.neilxu.train.business.req.ConfirmOrderTicketReq;
      import com.neilxu.train.common.context.LoginMemberContext;
      import com.neilxu.train.common.exception.BusinessException;
      import com.neilxu.train.common.exception.BusinessExceptionEnum;
      import com.neilxu.train.common.util.SnowUtil;
      import jakarta.annotation.Resource;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.slf4j.MDC;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;import java.util.Date;
      import java.util.List;@Service
      public class BeforeConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(BeforeConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Autowiredprivate SkTokenService skTokenService;//    @Resource
      //    public RocketMQTemplate rocketMQTemplate;@Resourceprivate 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,要选的是二等座,就筛选出ABCDFconst 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 {@Autowiredprivate 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,要选的是二等座,就筛选出ABCDFconst 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 {@Autowiredprivate 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:00let 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.rhkb.cn/news/291815.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux部署Sonarqube+Gogs+Jenkins(一)

Linux部署SonarqubeGogsJenkins 一、1.Linux安装JDK11环境1. 本地进行上传2. 进入到/usr/java目录&#xff0c;并且进行解压3. 配置文件/etc/profile&#xff0c;配置环境变量4.让对应的配置文件生效5. 验证 二、Linux安装Python环境三、Linux安装Jenkins环境1、/usr目录下创建…

【送书福利第六期】:《AI绘画教程:Midjourney使用方法与技巧从入门到精通》

文章目录 一、《AI绘画教程&#xff1a;Midjourney使用方法与技巧从入门到精通》二、内容介绍三、作者介绍&#x1f324;️粉丝福利 一、《AI绘画教程&#xff1a;Midjourney使用方法与技巧从入门到精通》 一本书读懂Midjourney绘画&#xff0c;让创意更简单&#xff0c;让设计…

Mysql连接报错:1130-host ... is not allowed to connect to this MySql server如何处理

我用navicat连接我的阿里云服务器的mysql服务器的时候,出现了1130的报错。&#xff08;mysql Server version: 5.7.42-0ubuntu0.18.04.1 (Ubuntu)&#xff09; 我来记录一下这个原因&#xff0c;以及修改过程&#xff01; 1.首先进入mysql -u root -p&#xff0c; mysql客户端…

车辆充电桩管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目持续更新中..... 2024年计算机毕业论文&#xff08;设计&#xff09;学生选题参考合集推荐收藏&#xff08;包含Springboot、jsp、ssmvue等技术项目合集&#xff09; 1. 前台功能…

命名空间【C++】(超详细)

文章目录 命名空间的概念命名空间的定义命名空间定义的位置作用域每一个命名空间都是一个独立的域作用域符&#xff1a;&#xff1a; 编译器找一个变量/函数等的定义&#xff0c;寻找域的顺序为什么要有命名空间&#xff1f;1.解决库与程序员定义的同名的重定义问题2.解决程序员…

变分信息瓶颈

变分信息瓶颈和互信息的定义 1 变分信息瓶颈 定义&#xff1a;变分信息瓶颈&#xff08;Variational Information Bottleneck&#xff09;是一种用于学习数据表示的方法&#xff0c;它旨在通过最小化输入和表示之间的互信息来实现数据的压缩和表示学习。这种方法通常用于无监…

pnpm、monorepo分包管理、多包管理、npm、vite、前端工程化、保姆级教程

浅尝pnpm monorepo 多包管理方案 &#x1f4a1;tips: 创建pnpm monorope多包管理框架流程 初始化 mkdir taurus & cd taurus pnpm init创建基础文件 创建文件pnpm-workspace.yaml packages:- packages/**创建文件夹packages/ -packages/ -package.json -pnpm-workspace…

Java类与对象:从概念到实践的全景解析!

​ 个人主页&#xff1a;秋风起&#xff0c;再归来~ 文章专栏&#xff1a;javaSE的修炼之路 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 1、类的定义格式 在java中定义类时需要用到…

DSVPN实验报告

一、分析要求 1. 配置R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;所有地址均配为公有IP地址。 - 在R5上&#xff0c;将接口配置为公有IP地址&#xff0c;并确保只进行了IP地址配置。 2. R1和R5之间使用PPP的PAP认证&#xff0c;R5为主认证方&#xff1b;R2于R5之间…

【MySQL】DQL-基础查询-语句&演示(查询多个字段 / 所有字段/并设置别名/去重)

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

一分钟了解三极管到底放大了什么?

目录 三极管放大原理 三极管的工作状态 截止区 放大区 饱和区 三极管的放大作用 电流放大 电压放大 功率放大 在电子学中&#xff0c;三极管通常用于实现信号放大、开关控制等多种功能&#xff0c;三极管主要功能是放大电流信号。下面&#xff0c;将详细解释三极管放大的原理和…

海格里斯助推实体制造业转型升级 “算法定义硬件”解题AIoT市场

随着自动化的发展&#xff0c;电子商务和智能制造推动了自动化立体仓库的快速发展与创新&#xff0c;产生了“密集仓储”的概念。对于一个实体企业来讲&#xff0c;其数智物流转型正在趋向于“去伪存真”&#xff0c;企业追求高ROI与真实经济价值&#xff0c;具有降本增效的业务…

java(1)之环境部署

1、下载安装包 直接百度java 点击这个就可以&#xff0c;进去之后下载&#xff0c;根据自身情况&#xff0c;window就下Windows版本的记得下那个jdk别下别的&#xff08;用不了&#xff09;&#xff0c;然后下一个编译器可以是idea可以是eclipse都可以 2、环境搭建 分为两步…

使用pdf表单域填充pdf内容

需要引用如下包 <dependency><groupId>com.itextpdf</groupId><artifactId>itext-core</artifactId><version>8.0.3</version><type>pom</type></dependency>1、预先准备一个pdf模板&#xff0c;并在指定位置添加…

IDEA的Scala环境搭建

目录 前言 Scala的概述 Scala环境的搭建 一、配置Windows的JAVA环境 二、配置Windows的Scala环境 编写一个Scala程序 前言 学习Scala最好先掌握Java基础及高级部分知识&#xff0c;文章正文中会提到Scala与Java的联系&#xff0c;简单来讲Scala好比是Java的加强版&#x…

云原生(六)、CICD - Jenkins快速入门

Jenkuns快速入门 一、CICD概述 CICD是持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;的缩写。它是软件开发中的一种流程和方法论&#xff0c;旨在通过自动化的方式频繁地将代码集成到共享存储库中&#xf…

Jira 软件缺陷管理 (软件测试)

内容来源&#xff1a;总结黑马课程 1.软件缺陷信息 2.创建缺陷问题 2.1 缺陷模板 2.2 创建缺陷问题模板

vite+vue3使用模块化批量发布Mockjs接口

在Vue3项目中使用Mock.js可以模拟后端接口数据&#xff0c;方便前端开发和调试。下面是使用vitevue3使用模块化批量发布Mockjs接口的步骤&#xff1a; 1. 安装Mock.js 在Vue3项目的根目录下&#xff0c;使用以下命令安装Mock.js&#xff1a; npm install mockjs --save-dev …

flutter Got socket error trying to find package nested at

flutter Got socket error trying to find package nested at xxx 报错信息&#xff1a;“Got socket error trying to find package nested at” 通常出现在Flutter尝试从pub.dev获取依赖包时&#xff0c;由于网络问题导致无法连接到pub.dev或者无法正确解析包的路径。 例如&…

element-ui inputNumber 组件源码分享

今日简单分享 inputNumber 组件的实现原理&#xff0c;主要从以下四个方面来分享&#xff1a; 1、inputNumber 组件的页面结构 2、inputNumber 组件的属性 3、inputNumber 组件的事件 4、inputNumber 组件的方法 一、inputNumber 组件的页面结构。 二、inputNumber 组件的…