Java项目实战笔记--基于SpringBoot3.0开发仿12306高并发售票系统--(二)项目实现-第五篇-核心功能车票预定开发及nacos集成

本文参考自

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

本文是仿12306项目实战第(二)章——项目实现 的第五篇,本篇讲解该项目的核心功能——余票查询、车票预定功能的基础版开发,以及讲解项目与Nacos的集成

本章目录

  • 一、核心功能介绍
  • 二、增加余票信息表,生成代码
  • 三、生成车次时初始化余票信息
  • 四、生成车次时初始化各种座位的余票数量
  • 五、为余票信息页面增加查询条件
  • 六、为会员端增余票查询功能
  • 七、增加订票页面并且实现车次信息传递
    • 1.增加预订按钮,点击预订时,跳转到下单页面,并使用sessionStorage传递参数
    • 2.为余票查询页面缓存查询参数,方便用户使用;将session key写成常量,方便统一维护,可以避免多个功能使用同一个key
    • 3.美化车次信息的显示
    • 4.订单页面显示座位信息
  • 八、订票页面勾选乘客并显示购票列表
    • 1.订票页面,查询我的所有的乘客(可以在新增乘客的时候,增加一个校验:超过50个乘客,就不能再新增了)
    • 2.订票页面,显示我的乘客复选框
    • 3.订票页面,为勾选的乘客构造购票数据
    • 4.订票页面,优化购票列表的展示
    • 5.订票页面,勾选乘客后提交,显示购票列表确认框
  • 九、分解选座购票功能的前后端逻辑
  • 十、订票页面增加选座效果
    • 1.勾选乘客后,提交时,校验余票是否足够(前端校验不一定准,但前端校验可以减轻后端很多压力)
    • 2.根据购票列表,计算出是否支持选座
    • 3.根据购票列表,展示选座按钮
    • 4.余票小于20张时,不允许选座
    • 5.确认提交时,计算出最终每个乘客所选的座位
    • 6.chooseSeatObj先清空,再初始化,保证两排座位是有序的
  • 十、增加确认订单表并生成前后端代码
  • 十一、后端增加确认下单购票接口
  • 十二、确认下单接口数据初始化
  • 十三、预扣减库存并判断余票是否足够
  • 十四、计算多个选座之间的偏移值

一、核心功能介绍

在这里插入图片描述

二、增加余票信息表,生成代码

  • business.sql

    drop table if exists `daily_train_ticket`;
    create table `daily_train_ticket` (`id` bigint not null comment 'id',`date` date not null comment '日期',`train_code` varchar(20) not null comment '车次编号',`start` varchar(20) not null comment '出发站',`start_pinyin` varchar(50) not null comment '出发站拼音',`start_time` time not null comment '出发时间',`start_index` tinyint not null comment '出发站序|本站是整个车次的第几站',`end` varchar(20) not null comment '到达站',`end_pinyin` varchar(50) not null comment '到达站拼音',`end_time` time not null comment '到站时间',`end_index` tinyint not null comment '到站站序|本站是整个车次的第几站',`ydz` int not null comment '一等座余票',`ydz_price` decimal(8, 2) not null comment '一等座票价',`edz` int not null comment '二等座余票',`edz_price` decimal(8, 2) not null comment '二等座票价',`rw` int not null comment '软卧余票',`rw_price` decimal(8, 2) not null comment '软卧票价',`yw` int not null comment '硬卧余票',`yw_price` decimal(8, 2) not null comment '硬卧票价',`create_time` datetime(3) comment '新增时间',`update_time` datetime(3) comment '修改时间',primary key (`id`),unique key `date_train_code_start_end_unique` (`date`, `train_code`, `start`, `end`)
    ) engine=innodb default charset=utf8mb4 comment='余票信息';
    

    实际12306用的是商业缓存软件来做的余票信息缓存,本项目用mysql新建临时表来代替缓存演示业务实现

  • 修改generator-config-business.xml,生成持久层、前后端代码

    <table tableName="daily_train_ticket" domainObjectName="DailyTrainTicket"/>
    

    操作同之前

  • 修改admin/package.json,加规则去除ESLint报错

    "rules": {"vue/multi-word-component-names": 0,"no-undef": 0,"vue/no-unused-vars": 0
    }
    
  • 修改路由、侧边栏

    操作同之前

  • 测试

在这里插入图片描述

数据后续由定时任务填充

三、生成车次时初始化余票信息

  • 给批量方法都加上@Transactional

    虽然由于事务的传递性,外层有事务,内层方法也会是事务,但是规范点还是都加上事务注解

  • DailyTrainTicketService.java

    逻辑:比如车站1——车站2——车站3,站站组合情况就有 1、2;1、3;2、3 三种,需要分别生成每种组合的余票信息

    这里第一版 先解决站站组合逻辑,具体余票信息后面完善

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.domain.DailyTrainTicketExample;
    import com.neilxu.train.business.domain.TrainStation;
    import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
    import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
    import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
    import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
    import java.util.Date;
    import java.util.List;@Service
    public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());dailyTrainTicket.setYdz(0);dailyTrainTicket.setYdzPrice(BigDecimal.ZERO);dailyTrainTicket.setEdz(0);dailyTrainTicket.setEdzPrice(BigDecimal.ZERO);dailyTrainTicket.setRw(0);dailyTrainTicket.setRwPrice(BigDecimal.ZERO);dailyTrainTicket.setYw(0);dailyTrainTicket.setYwPrice(BigDecimal.ZERO);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);}
    }
    
  • DailyTrainService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainExample;
    import com.neilxu.train.business.domain.Train;
    import com.neilxu.train.business.mapper.DailyTrainMapper;
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;import java.util.Date;
    import java.util.List;@Service
    public class DailyTrainService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);@Resourceprivate DailyTrainMapper dailyTrainMapper;@Resourceprivate TrainService trainService;@Resourceprivate DailyTrainStationService dailyTrainStationService;@Resourceprivate DailyTrainCarriageService dailyTrainCarriageService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(DailyTrainSaveReq req) {DateTime now = DateTime.now();DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);if (ObjectUtil.isNull(dailyTrain.getId())) {dailyTrain.setId(SnowUtil.getSnowflakeNextId());dailyTrain.setCreateTime(now);dailyTrain.setUpdateTime(now);dailyTrainMapper.insert(dailyTrain);} else {dailyTrain.setUpdateTime(now);dailyTrainMapper.updateByPrimaryKey(dailyTrain);}}public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {DailyTrainExample dailyTrainExample = new DailyTrainExample();dailyTrainExample.setOrderByClause("date desc, code asc");DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();if (ObjectUtil.isNotNull(req.getDate())) {criteria.andDateEqualTo(req.getDate());}if (ObjectUtil.isNotEmpty(req.getCode())) {criteria.andCodeEqualTo(req.getCode());}LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainMapper.deleteByPrimaryKey(id);}/*** 生成某日所有车次信息,包括车次、车站、车厢、座位* @param date*/public void genDaily(Date date) {List<Train> trainList = trainService.selectAll();if (CollUtil.isEmpty(trainList)) {LOG.info("没有车次基础数据,任务结束");return;}for (Train train : trainList) {genDailyTrain(date, train);}}@Transactionalpublic void genDailyTrain(Date date, Train train) {LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());// 删除该车次已有的数据DailyTrainExample dailyTrainExample = new DailyTrainExample();dailyTrainExample.createCriteria().andDateEqualTo(date).andCodeEqualTo(train.getCode());dailyTrainMapper.deleteByExample(dailyTrainExample);// 生成该车次的数据DateTime now = DateTime.now();DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);dailyTrain.setId(SnowUtil.getSnowflakeNextId());dailyTrain.setCreateTime(now);dailyTrain.setUpdateTime(now);dailyTrain.setDate(date);dailyTrainMapper.insert(dailyTrain);// 生成该车次的车站数据dailyTrainStationService.genDaily(date, train.getCode());// 生成该车次的车厢数据dailyTrainCarriageService.genDaily(date, train.getCode());// 生成该车次的座位数据dailyTrainSeatService.genDaily(date, train.getCode());// 生成该车次的余票数据dailyTrainTicketService.genDaily(date, train.getCode());LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());}
    }
    
  • 测试

在这里插入图片描述

四、生成车次时初始化各种座位的余票数量

  • DailyTrainSeatService.java

    public int countSeat(Date date, String trainCode, String seatType) {DailyTrainSeatExample example = new DailyTrainSeatExample();example.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode).andSeatTypeEqualTo(seatType);long l = dailyTrainSeatMapper.countByExample(example);if (l == 0L) {return -1;}return (int) l;
    }
    
  • DailyTrainService.java

    // 生成该车次的余票数据
    dailyTrainTicketService.genDaily(dailyTrain, date, train.getCode());
    
  • DailyTrainTicketService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.EnumUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.domain.DailyTrainTicketExample;
    import com.neilxu.train.business.domain.TrainStation;
    import com.neilxu.train.business.enums.SeatTypeEnum;
    import com.neilxu.train.business.enums.TrainTypeEnum;
    import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
    import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
    import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
    import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Date;
    import java.util.List;@Service
    public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);BigDecimal sumKM = BigDecimal.ZERO;for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);sumKM = sumKM.add(trainStationEnd.getKm());DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());// 票价 = 里程之和 * 座位单价 * 车次类型系数String trainType = dailyTrain.getType();// 计算票价系数:TrainTypeEnum.priceRateBigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);dailyTrainTicket.setYdz(ydz);dailyTrainTicket.setYdzPrice(ydzPrice);dailyTrainTicket.setEdz(edz);dailyTrainTicket.setEdzPrice(edzPrice);dailyTrainTicket.setRw(rw);dailyTrainTicket.setRwPrice(rwPrice);dailyTrainTicket.setYw(yw);dailyTrainTicket.setYwPrice(ywPrice);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);}
    }
    
  • 测试

    重新生成车次

在这里插入图片描述

五、为余票信息页面增加查询条件

  • DailyTrainTicketQueryReq.java

    package com.neilxu.train.business.req;import com.neilxu.train.common.req.PageReq;
    import lombok.Data;
    import org.springframework.format.annotation.DateTimeFormat;import java.util.Date;@Data
    public class DailyTrainTicketQueryReq extends PageReq {/*** 日期*/@DateTimeFormat(pattern = "yyyy-MM-dd")private Date date;/*** 车次编号*/private String trainCode;/*** 出发站*/private String start;/*** 到达站*/private String end;}
    
  • DailyTrainTicketService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.EnumUtil;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.domain.DailyTrainTicketExample;
    import com.neilxu.train.business.domain.TrainStation;
    import com.neilxu.train.business.enums.SeatTypeEnum;
    import com.neilxu.train.business.enums.TrainTypeEnum;
    import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
    import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
    import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
    import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Date;
    import java.util.List;@Service
    public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();if (ObjUtil.isNotNull(req.getDate())) {criteria.andDateEqualTo(req.getDate());}if (ObjUtil.isNotEmpty(req.getTrainCode())) {criteria.andTrainCodeEqualTo(req.getTrainCode());}if (ObjUtil.isNotEmpty(req.getStart())) {criteria.andStartEqualTo(req.getStart());}if (ObjUtil.isNotEmpty(req.getEnd())) {criteria.andEndEqualTo(req.getEnd());}LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);BigDecimal sumKM = BigDecimal.ZERO;for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);sumKM = sumKM.add(trainStationEnd.getKm());DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());// 票价 = 里程之和 * 座位单价 * 车次类型系数String trainType = dailyTrain.getType();// 计算票价系数:TrainTypeEnum.priceRateBigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);dailyTrainTicket.setYdz(ydz);dailyTrainTicket.setYdzPrice(ydzPrice);dailyTrainTicket.setEdz(edz);dailyTrainTicket.setEdzPrice(edzPrice);dailyTrainTicket.setRw(rw);dailyTrainTicket.setRwPrice(rwPrice);dailyTrainTicket.setYw(yw);dailyTrainTicket.setYwPrice(ywPrice);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);}
    }
    
  • daily-train-ticket.vue

    <template><p><a-space><train-select-view v-model="params.trainCode" width="200px"></train-select-view><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'"></template></template></a-table>
    </template><script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import TrainSelectView from "@/components/train-select";
    import StationSelectView from "@/components/station-select";export default defineComponent({name: "daily-train-ticket-view",components: {StationSelectView, TrainSelectView},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: 'date',key: 'date',},{title: '车次编号',dataIndex: 'trainCode',key: 'trainCode',},{title: '出发站',dataIndex: 'start',key: 'start',},{title: '出发站拼音',dataIndex: 'startPinyin',key: 'startPinyin',},{title: '出发时间',dataIndex: 'startTime',key: 'startTime',},{title: '出发站序',dataIndex: 'startIndex',key: 'startIndex',},{title: '到达站',dataIndex: 'end',key: 'end',},{title: '到达站拼音',dataIndex: 'endPinyin',key: 'endPinyin',},{title: '到站时间',dataIndex: 'endTime',key: 'endTime',},{title: '到站站序',dataIndex: 'endIndex',key: 'endIndex',},{title: '一等座余票',dataIndex: 'ydz',key: 'ydz',},{title: '一等座票价',dataIndex: 'ydzPrice',key: 'ydzPrice',},{title: '二等座余票',dataIndex: 'edz',key: 'edz',},{title: '二等座票价',dataIndex: 'edzPrice',key: 'edzPrice',},{title: '软卧余票',dataIndex: 'rw',key: 'rw',},{title: '软卧票价',dataIndex: 'rwPrice',key: 'rwPrice',},{title: '硬卧余票',dataIndex: 'yw',key: 'yw',},{title: '硬卧票价',dataIndex: 'ywPrice',key: 'ywPrice',},];const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/business/admin/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});};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params};},
    });
    </script>
    
  • 测试

在这里插入图片描述

问题:当前页面显示信息太乱,且有多余的列,需要优化

  • 优化余票信息页面

    • daily-train-ticket.vue

      <template><p><a-space><train-select-view v-model="params.trainCode" width="200px"></train-select-view><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'"></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>
      </template><script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      import TrainSelectView from "@/components/train-select";
      import StationSelectView from "@/components/station-select";
      import dayjs from "dayjs";export default defineComponent({name: "daily-train-ticket-view",components: {StationSelectView, TrainSelectView},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: 'date',key: 'date',},{title: '车次编号',dataIndex: 'trainCode',key: 'trainCode',},{title: '车站',dataIndex: 'station',},{title: '时间',dataIndex: 'time',},{title: '历时',dataIndex: 'duration',},// {//   title: '出发站',//   dataIndex: 'start',//   key: 'start',// },// {//   title: '出发站拼音',//   dataIndex: 'startPinyin',//   key: 'startPinyin',// },// {//   title: '出发时间',//   dataIndex: 'startTime',//   key: 'startTime',// },// {//   title: '出发站序',//   dataIndex: 'startIndex',//   key: 'startIndex',// },// {//   title: '到达站',//   dataIndex: 'end',//   key: 'end',// },// {//   title: '到达站拼音',//   dataIndex: 'endPinyin',//   key: 'endPinyin',// },// {//   title: '到站时间',//   dataIndex: 'endTime',//   key: 'endTime',// },// {//   title: '到站站序',//   dataIndex: 'endIndex',//   key: 'endIndex',// },{title: '一等座',dataIndex: 'ydz',key: 'ydz',},// {//   title: '一等座票价',//   dataIndex: 'ydzPrice',//   key: 'ydzPrice',// },{title: '二等座',dataIndex: 'edz',key: 'edz',},// {//   title: '二等座票价',//   dataIndex: 'edzPrice',//   key: 'edzPrice',// },{title: '软卧',dataIndex: 'rw',key: 'rw',},// {//   title: '软卧票价',//   dataIndex: 'rwPrice',//   key: 'rwPrice',// },{title: '硬卧',dataIndex: 'yw',key: 'yw',},// {//   title: '硬卧票价',//   dataIndex: 'ywPrice',//   key: 'ywPrice',// },];const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/business/admin/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');};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params,calDuration};},
      });
      </script>
      
    • 测试

在这里插入图片描述

六、为会员端增余票查询功能

  • 修改乘客管理页面,正常显示页面;修改前端模块的启动指令名称

    • passenger.vue

      web/src/views/main/passenger.vue

      <template><p><a-space><a-button type="primary" @click="handleQuery()">刷新</a-button><a-button type="primary" @click="onAdd">新增</a-button></a-space></p><a-table :dataSource="passengers":columns="columns":pagination="pagination"@change="handleTableChange":loading="loading"><template #bodyCell="{ column, record }"><template v-if="column.dataIndex === 'operation'"><a-space><a-popconfirmtitle="删除后不可恢复,确认删除?"@confirm="onDelete(record)"ok-text="确认" cancel-text="取消"><a style="color: red">删除</a></a-popconfirm><a @click="onEdit(record)">编辑</a></a-space></template><template v-else-if="column.dataIndex === 'type'"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === record.type">{{item.desc}}</span></span></template></template></a-table><a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"ok-text="确认" cancel-text="取消"><a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }"><a-form-item label="姓名"><a-input v-model:value="passenger.name" /></a-form-item><a-form-item label="身份证"><a-input v-model:value="passenger.idCard" /></a-form-item><a-form-item label="旅客类型"><a-select v-model:value="passenger.type"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-form-item></a-form></a-modal>
      </template><script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";export default defineComponent({name: "passenger-view",setup() {const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);let passenger = ref({id: undefined,memberId: undefined,name: undefined,idCard: undefined,type: undefined,createTime: undefined,updateTime: undefined,});const passengers = ref([]);// 分页的三个属性名是固定的const pagination = ref({total: 0,current: 1,pageSize: 10,});let loading = ref(false);const columns = [{title: '会员id',dataIndex: 'memberId',key: 'memberId',},{title: '姓名',dataIndex: 'name',key: 'name',},{title: '身份证',dataIndex: 'idCard',key: 'idCard',},{title: '旅客类型',dataIndex: 'type',key: 'type',},{title: '操作',dataIndex: 'operation'}];const onAdd = () => {passenger.value = {};visible.value = true;};const onEdit = (record) => {passenger.value = window.Tool.copy(record);visible.value = true;};const onDelete = (record) => {axios.delete("/member/passenger/delete/" + record.id).then((response) => {const data = response.data;if (data.success) {notification.success({description: "删除成功!"});handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {notification.error({description: data.message});}});};const handleOk = () => {axios.post("/member/passenger/save", passenger.value).then((response) => {let data = response.data;if (data.success) {notification.success({description: "保存成功!"});visible.value = false;handleQuery({page: pagination.value.current,size: pagination.value.pageSize});} else {notification.error({description: data.message});}});};const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/member/passenger/query-list", {params: {page: param.page,size: param.size}}).then((response) => {loading.value = false;let data = response.data;if (data.success) {passengers.value = data.content.list;// 设置分页控件的值pagination.value.current = param.page;pagination.value.total = data.content.total;} else {notification.error({description: data.message});}});};const handleTableChange = (pagination) => {// console.log("看看自带的分页参数都有啥:" + pagination);handleQuery({page: pagination.current,size: pagination.pageSize});};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {PASSENGER_TYPE_ARRAY,passenger,visible,passengers,pagination,columns,handleTableChange,handleQuery,loading,onAdd,handleOk,onEdit,onDelete};},
      });
      </script>
      
    • admin/package.json

      "scripts": {"admin-dev": "vue-cli-service serve --mode dev --port 9001","admin-prod": "vue-cli-service serve --mode prod --port 9001","build": "vue-cli-service build","lint": "vue-cli-service lint"
      },
      
    • web/package.json

      "private": true,
      "scripts": {"web-dev": "vue-cli-service serve --mode dev --port 9000","web-prod": "vue-cli-service serve --mode prod --port 9000","build": "vue-cli-service build","lint": "vue-cli-service lint"
      },
      
    • 测试效果

在这里插入图片描述

  • 会员端增加余票查询页面,功能和控台端完全一致

    操作:将前面控台端的代码复制到用户端

    • DailyTrainTicketController.java

      package com.neilxu.train.business.controller;import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
      import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
      import com.neilxu.train.business.service.DailyTrainTicketService;
      import com.neilxu.train.common.resp.CommonResp;
      import com.neilxu.train.common.resp.PageResp;
      import jakarta.annotation.Resource;
      import jakarta.validation.Valid;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;@RestController
      @RequestMapping("/daily-train-ticket")
      public class DailyTrainTicketController {@Resourceprivate DailyTrainTicketService dailyTrainTicketService;@GetMapping("/query-list")public CommonResp<PageResp<DailyTrainTicketQueryResp>> queryList(@Valid DailyTrainTicketQueryReq req) {PageResp<DailyTrainTicketQueryResp> list = dailyTrainTicketService.queryList(req);return new CommonResp<>(list);}}
      
    • StationController.java

      package com.neilxu.train.business.controller;import com.neilxu.train.business.resp.StationQueryResp;
      import com.neilxu.train.business.service.StationService;
      import com.neilxu.train.common.resp.CommonResp;
      import jakarta.annotation.Resource;
      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("/station")
      public class StationController {@Resourceprivate StationService stationService;@GetMapping("/query-all")public CommonResp<List<StationQueryResp>> queryList() {List<StationQueryResp> list = stationService.queryAll();return new CommonResp<>(list);}}
      
    • TrainController.java

      package com.neilxu.train.business.controller;import com.neilxu.train.business.resp.TrainQueryResp;
      import com.neilxu.train.business.service.TrainService;
      import com.neilxu.train.common.resp.CommonResp;
      import jakarta.annotation.Resource;
      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("/train")
      public class TrainController {@Resourceprivate TrainService trainService;@GetMapping("/query-all")public CommonResp<List<TrainQueryResp>> queryList() {List<TrainQueryResp> list = trainService.queryAll();return new CommonResp<>(list);}}
      
    • station-select.vue

      <template><a-select v-model:value="name" show-search allowClear:filterOption="filterNameOption"@change="onChange" placeholder="请选择车站":style="'width: ' + localWidth"><a-select-option v-for="item in stations" :key="item.name" :value="item.name" :label="item.name + item.namePinyin + item.namePy">{{item.name}} {{item.namePinyin}} ~ {{item.namePy}}</a-select-option></a-select>
      </template><script>import {defineComponent, onMounted, ref, watch} from 'vue';
      import axios from "axios";
      import {notification} from "ant-design-vue";export default defineComponent({name: "station-select-view",props: ["modelValue", "width"],emits: ['update:modelValue', 'change'],setup(props, {emit}) {const name = ref();const stations = ref([]);const localWidth = ref(props.width);if (Tool.isEmpty(props.width)) {localWidth.value = "100%";}// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效watch(() => props.modelValue, ()=>{console.log("props.modelValue", props.modelValue);name.value = props.modelValue;}, {immediate: true});/*** 查询所有的车站,用于车站下拉框*/const queryAllStation = () => {axios.get("/business/station/query-all").then((response) => {let data = response.data;if (data.success) {stations.value = data.content;} else {notification.error({description: data.message});}});};/*** 车站下拉框筛选*/const filterNameOption = (input, option) => {console.log(input, option);return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;};/*** 将当前组件的值响应给父组件* @param value*/const onChange = (value) => {emit('update:modelValue', value);let station = stations.value.filter(item => item.code === value)[0];if (Tool.isEmpty(station)) {station = {};}emit('change', station);};onMounted(() => {queryAllStation();});return {name,stations,filterNameOption,onChange,localWidth};},
      });
      </script>
      
    • train-select.vue

      <template><a-select v-model:value="trainCode" show-search allowClear:filterOption="filterTrainCodeOption"@change="onChange" placeholder="请选择车次":style="'width: ' + localWidth"><a-select-option v-for="item in trains" :key="item.code" :value="item.code" :label="item.code + item.start + item.end">{{item.code}} {{item.start}} ~ {{item.end}}</a-select-option></a-select>
      </template><script>import {defineComponent, onMounted, ref, watch} from 'vue';
      import axios from "axios";
      import {notification} from "ant-design-vue";export default defineComponent({name: "train-select-view",props: ["modelValue", "width"],emits: ['update:modelValue', 'change'],setup(props, {emit}) {const trainCode = ref();const trains = ref([]);const localWidth = ref(props.width);if (Tool.isEmpty(props.width)) {localWidth.value = "100%";}// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效watch(() => props.modelValue, ()=>{console.log("props.modelValue", props.modelValue);trainCode.value = props.modelValue;}, {immediate: true});/*** 查询所有的车次,用于车次下拉框*/const queryAllTrain = () => {axios.get("/business/train/query-all").then((response) => {let data = response.data;if (data.success) {trains.value = data.content;} else {notification.error({description: data.message});}});};/*** 车次下拉框筛选*/const filterTrainCodeOption = (input, option) => {console.log(input, option);return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;};/*** 将当前组件的值响应给父组件* @param value*/const onChange = (value) => {emit('update:modelValue', value);let train = trains.value.filter(item => item.code === value)[0];if (Tool.isEmpty(train)) {train = {};}emit('change', train);};onMounted(() => {queryAllTrain();});return {trainCode,trains,filterTrainCodeOption,onChange,localWidth};},
      });
      </script>
      
    • the-header.vue和the-sider.vue

      <a-menu-item key="/ticket"><router-link to="/ticket"><user-outlined /> &nbsp; 余票查询</router-link>
      </a-menu-item>
      
    • web/src/router/index.js

      import { createRouter, createWebHistory } from 'vue-router'
      import store from "@/store";
      import {notification} from "ant-design-vue";const routes = [{path: '/login',component: () => import('../views/login.vue')
      }, {path: '/',component: () => import('../views/main.vue'),meta: {loginRequire: true},children: [{path: 'welcome',component: () => import('../views/main/welcome.vue'),}, {path: 'passenger',component: () => import('../views/main/passenger.vue'),}, {path: 'ticket',component: () => import('../views/main/ticket.vue'),}]
      }, {path: '',redirect: '/welcome'
      }];const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes
      })// 路由登录拦截
      router.beforeEach((to, from, next) => {// 要不要对meta.loginRequire属性做监控拦截if (to.matched.some(function (item) {console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);return item.meta.loginRequire})) {const _member = store.state.member;console.log("页面登录校验开始:", _member);if (!_member.token) {console.log("用户未登录或登录超时!");notification.error({ description: "未登录或登录超时" });next('/login');} else {next();}} else {next();}
      });export default router
      
    • ticket.vue

      <template><p><a-space><train-select-view v-model="params.trainCode" width="200px"></train-select-view><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'"></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>
      </template><script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      import TrainSelectView from "@/components/train-select";
      import StationSelectView from "@/components/station-select";
      import dayjs from "dayjs";export default defineComponent({name: "ticket-view",components: {StationSelectView, TrainSelectView},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: 'date',key: 'date',},{title: '车次编号',dataIndex: 'trainCode',key: 'trainCode',},{title: '车站',dataIndex: 'station',},{title: '时间',dataIndex: 'time',},{title: '历时',dataIndex: 'duration',},// {//   title: '出发站',//   dataIndex: 'start',//   key: 'start',// },// {//   title: '出发站拼音',//   dataIndex: 'startPinyin',//   key: 'startPinyin',// },// {//   title: '出发时间',//   dataIndex: 'startTime',//   key: 'startTime',// },// {//   title: '出发站序',//   dataIndex: 'startIndex',//   key: 'startIndex',// },// {//   title: '到达站',//   dataIndex: 'end',//   key: 'end',// },// {//   title: '到达站拼音',//   dataIndex: 'endPinyin',//   key: 'endPinyin',// },// {//   title: '到站时间',//   dataIndex: 'endTime',//   key: 'endTime',// },// {//   title: '到站站序',//   dataIndex: 'endIndex',//   key: 'endIndex',// },{title: '一等座',dataIndex: 'ydz',key: 'ydz',},// {//   title: '一等座票价',//   dataIndex: 'ydzPrice',//   key: 'ydzPrice',// },{title: '二等座',dataIndex: 'edz',key: 'edz',},// {//   title: '二等座票价',//   dataIndex: 'edzPrice',//   key: 'edzPrice',// },{title: '软卧',dataIndex: 'rw',key: 'rw',},// {//   title: '软卧票价',//   dataIndex: 'rwPrice',//   key: 'rwPrice',// },{title: '硬卧',dataIndex: 'yw',key: 'yw',},// {//   title: '硬卧票价',//   dataIndex: 'ywPrice',//   key: 'ywPrice',// },];const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}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');};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params,calDuration};},
      });
      </script>
      
    • 测试

在这里插入图片描述

问题:用户端不需要根据车次查询,另外三个参数则必须输入
  • 修改余票查询页面,查询的三个参数必输,去掉车次查询条件

    • 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'"></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>
      </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";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',},];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};}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');};onMounted(() => {// handleQuery({//   page: 1,//   size: pagination.value.pageSize// });});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params,calDuration};},
      });
      </script>
      
    • 效果

在这里插入图片描述

七、增加订票页面并且实现车次信息传递

1.增加预订按钮,点击预订时,跳转到下单页面,并使用sessionStorage传递参数

  • order.vue

    下单页面

    <template><div>{{ dailyTrainTicket }}</div>
    </template><script>import {defineComponent} from 'vue';export default defineComponent({name: "order-view",setup() {const dailyTrainTicket = SessionStorage.get("dailyTrainTicket") || {};console.log("下单的车次信息", dailyTrainTicket);return {dailyTrainTicket};},
    });
    </script>
    
  • 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-button type="primary" @click="toOrder(record)">预订</a-button></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>
    </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};}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("dailyTrainTicket", dailyTrainTicket.value);router.push("/order")};onMounted(() => {// handleQuery({//   page: 1,//   size: pagination.value.pageSize// });});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params,calDuration,toOrder};},
    });
    </script>
    
  • 路由

    import { createRouter, createWebHistory } from 'vue-router'
    import store from "@/store";
    import {notification} from "ant-design-vue";const routes = [{path: '/login',component: () => import('../views/login.vue')
    }, {path: '/',component: () => import('../views/main.vue'),meta: {loginRequire: true},children: [{path: 'welcome',component: () => import('../views/main/welcome.vue'),}, {path: 'passenger',component: () => import('../views/main/passenger.vue'),}, {path: 'ticket',component: () => import('../views/main/ticket.vue'),}, {path: 'order',component: () => import('../views/main/order.vue'),}]
    }, {path: '',redirect: '/welcome'
    }];const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes
    })// 路由登录拦截
    router.beforeEach((to, from, next) => {// 要不要对meta.loginRequire属性做监控拦截if (to.matched.some(function (item) {console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);return item.meta.loginRequire})) {const _member = store.state.member;console.log("页面登录校验开始:", _member);if (!_member.token) {console.log("用户未登录或登录超时!");notification.error({ description: "未登录或登录超时" });next('/login');} else {next();}} else {next();}
    });export default router
    
  • 效果

    点击预定

在这里插入图片描述

2.为余票查询页面缓存查询参数,方便用户使用;将session key写成常量,方便统一维护,可以避免多个功能使用同一个key

  • web/public/js/session-storage.js

    // 所有的session key都在这里统一定义,可以避免多个功能使用同一个key
    SESSION_ORDER = "SESSION_ORDER";
    SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";
    
  • web/src/views/main/order.vue

    export default defineComponent({name: "order-view",setup() {const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);return {dailyTrainTicket};},
    });
    
  • 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-button type="primary" @click="toOrder(record)">预订</a-button></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>
    </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")};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};},
    });
    </script>
    
  • 效果

    只要不关闭页面,可以缓存查询条件

在这里插入图片描述

3.美化车次信息的显示

  • web/src/views/main/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>
    </template><script>import {defineComponent} from 'vue';export default defineComponent({name: "order-view",setup() {const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);return {dailyTrainTicket};},
    });
    </script><style>
    .order-train .order-train-main {font-size: 18px;font-weight: bold;
    }
    </style>
    
  • 效果

在这里插入图片描述

4.订单页面显示座位信息

  • 重新生成下枚举文件

    修改EnumGenerator.java,生成web/src/assets/js/enums.js

  • web/src/views/main/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>
    </template><script>import {defineComponent} from 'vue';export default defineComponent({name: "order-view",setup() {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)return {dailyTrainTicket,seatTypes};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

八、订票页面勾选乘客并显示购票列表

1.订票页面,查询我的所有的乘客(可以在新增乘客的时候,增加一个校验:超过50个乘客,就不能再新增了)

  • PassengerService.java

    /*** 查询我的所有乘客*/
    public List<PassengerQueryResp> queryMine() {PassengerExample passengerExample = new PassengerExample();passengerExample.setOrderByClause("name asc");PassengerExample.Criteria criteria = passengerExample.createCriteria();criteria.andMemberIdEqualTo(LoginMemberContext.getId());List<Passenger> list = passengerMapper.selectByExample(passengerExample);return BeanUtil.copyToList(list, PassengerQueryResp.class);
    }
    
  • PassengerController.java

    @GetMapping("/query-mine")
    public CommonResp<List<PassengerQueryResp>> queryMine() {List<PassengerQueryResp> list = passengerService.queryMine();return new CommonResp<>(list);
    }
    
  • web/src/views/main/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>{{passengers}}
    </template><script>import {defineComponent, ref, onMounted} from 'vue';
    import axios from "axios";
    import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = 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)const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;} else {notification.error({description: data.message});}});};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

2.订票页面,显示我的乘客复选框

  • web/src/views/main/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" /><br/>选中的乘客:{{passengerChecks}}
    </template><script>import {defineComponent, ref, onMounted} 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)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.id}))} else {notification.error({description: data.message});}});};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

3.订票页面,为勾选的乘客构造购票数据

  • web/src/views/main/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" /><br/>选中的乘客:{{passengerChecks}}<br/>购票列表:{{tickets}}
    </template><script>import {defineComponent, ref, onMounted, watch} 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"// }const tickets = ref([]);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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});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});}});};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

4.订票页面,优化购票列表的展示

  • web/src/views/main/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" /><br/>选中的乘客:{{passengerChecks}}<br/>购票列表:{{tickets}}<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>
    </template><script>import {defineComponent, ref, onMounted, watch} 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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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});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});}});};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

5.订票页面,勾选乘客后提交,显示购票列表确认框

  • web/src/views/main/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="取消"><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></div></a-modal>
    </template><script>import {defineComponent, ref, onMounted, watch} 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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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});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;}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

在这里插入图片描述

九、分解选座购票功能的前后端逻辑

12306规则:

  • 只有全部是一等座或全部是二等座才支持选座
  • 余票小于一定数量时,不允许选座(本项目以20为例)

在这里插入图片描述

  • 构造两个重要的响应式变量:

    // 0:不支持选座;1:选一等座;2:选二等座
    const chooseSeatType = ref(0);// 选择的座位
    // {
    //   A1: false, C1: true,D1: false, F1: false,
    //   A2: false, C2: false,D2: true, F2: false
    // }
    const chooseSeatObj = ref({});
    
  • 最终购票tickets:

    // seat可选,当无选座时,seat为空
    [{passengerId: 123,passengerType: "1",seatTypeCode: "1",passengerName: "张三",passengerIdCard: "12323132132",seat: "C1"
    }, {passengerId: 123,passengerType: "1",seatTypeCode: "1",passengerName: "李四",passengerIdCard: "12323132132",seat: "D2"
    }]
    
  • 座位售卖详情,比如有ABCDE五个站,sell=0110,则AB未被购买,AC已被购买

    在这里插入图片描述

  • 后端购票逻辑,分成选座和不选座
    不选座,以购买一等座为例:遍历一等座车厢,每个车厢从1号座位开始找,未被购买的,就选中它
    选座,以购买两张一等座AB为例:遍历一等座车厢,每个车厢从1号座位开始找A列座位,未被购买的,就预选中它;再挑它旁边的B,如果也未被购买,则最终选中这两个座位,如果B已被购买,则回到第一步,继续找未被购买的A座。

    从第二个座位开始,需要计算和第一个座位的偏移值,可以减少循环,提高选座效率

    举例:当选择A1和C2座位,遍历找到A1座位,索引为0,则C2不需要再遍历,直接计算出偏移值是5,即索引是5,看可以不可选就行了

十、订票页面增加选座效果

这节是前端的选座逻辑的实现

1.勾选乘客后,提交时,校验余票是否足够(前端校验不一定准,但前端校验可以减轻后端很多压力)

  • order.vue

        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("前端余票校验通过");// 弹出确认界面visible.value = true;};
    

2.根据购票列表,计算出是否支持选座

只有都选择一等座或都选二等座,才可以支持选座

获取到选座类型后,如果是一等座得到一等座的选座对象,二等座得到二等座的选座对象,如果是不可选,选座对象为空

  • 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="取消"><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/>选座类型chooseSeatType:{{chooseSeatType}}<br/>选座对象chooseSeatType:{{chooseSeatObj}}<br/>座位类型SEAT_COL_ARRAY:{{SEAT_COL_ARRAY}}</div></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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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, () => {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;}}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};},
    });
    </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;
    }
    </style>
    
  • 效果

在这里插入图片描述

在这里插入图片描述

3.根据购票列表,展示选座按钮

  • 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="取消"><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/>选座对象chooseSeatType:{{chooseSeatObj}}<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></div></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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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, () => {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;}}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};},
    });
    </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.余票小于20张时,不允许选座

  • 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="取消"><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></div></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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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, () => {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;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};},
    });
    </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>
    

5.确认提交时,计算出最终每个乘客所选的座位

  • 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="handleOk"><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}}</div></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"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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, () => {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 = () => {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);}onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,handleOk,};},
    });
    </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>
    
  • 效果

在这里插入图片描述

6.chooseSeatObj先清空,再初始化,保证两排座位是有序的

  • 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="handleOk"><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>
    </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);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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 = () => {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);}onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,handleOk,};},
    });
    </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>
    
  • 效果

在这里插入图片描述

十、增加确认订单表并生成前后端代码

  • 新增表

    sql/business.sql

    这里的车票使用json类型,实际上也可以使用子表来做

    drop table if exists `confirm_order`;
    create table `confirm_order` (`id` bigint not null comment 'id',`member_id` bigint not null comment '会员id',`date` date not null comment '日期',`train_code` varchar(20) not null comment '车次编号',`start` varchar(20) not null comment '出发站',`end` varchar(20) not null comment '到达站',`daily_train_ticket_id` bigint not null comment '余票ID',`tickets` json not null comment '车票',`status` char(1) not null comment '订单状态|枚举[ConfirmOrderStatusEnum]',`create_time` datetime(3) comment '新增时间',`update_time` datetime(3) comment '修改时间',primary key (`id`),index `date_train_code_index` (`date`, `train_code`)
    ) engine=innodb default charset=utf8mb4 comment='确认订单';
    
  • ConfirmOrderStatusEnum

    enum也是可以用lombok注解的

    package com.neilxu.train.business.enums;public enum ConfirmOrderStatusEnum {INIT("I", "初始"),PENDING("P", "处理中"),SUCCESS("S", "成功"),FAILURE("F", "失败"),EMPTY("E", "无票"),CANCEL("C", "取消");private String code;private String desc;ConfirmOrderStatusEnum(String code, String desc) {this.code = code;this.desc = desc;}@Override    public String toString() {return "ConfirmOrderStatusEnum{" +"code='" + code + '\'' +", desc='" + desc + '\'' +"} " + super.toString();}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public void setDesc(String desc) {this.desc = desc;}public String getDesc() {return desc;}}
    
  • 修改generator-config-business.xml、ServerGenerator.java、EnumGenerator.java生成代码

    操作同之前,注意是admin

  • 修改路由、侧边栏

    操作同之前,注意是admin

  • 效果

在这里插入图片描述

十一、后端增加确认下单购票接口

这节整理下后端确认下单购票接口的逻辑

  • com.neilxu.train.business.req.ConfirmOrderTicketReq

    package com.neilxu.train.business.req;import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import lombok.Data;@Data
    public class ConfirmOrderTicketReq {/*** 乘客ID*/@NotNull(message = "【乘客ID】不能为空")private Long passengerId;/*** 乘客票种*/@NotBlank(message = "【乘客票种】不能为空")private String passengerType;/*** 乘客名称*/@NotBlank(message = "【乘客名称】不能为空")private String passengerName;/*** 乘客身份证*/@NotBlank(message = "【乘客身份证】不能为空")private String passengerIdCard;/*** 座位类型code*/@NotBlank(message = "【座位类型code】不能为空")private String seatTypeCode;/*** 选座,可空,值示例:A1*/private String seat;}
    
  • ConfirmOrderSaveReq.java ——> ConfirmOrderDoReq.java

    package com.neilxu.train.business.req;import com.fasterxml.jackson.annotation.JsonFormat;
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import lombok.Data;import java.util.Date;
    import java.util.List;@Data
    public class ConfirmOrderDoReq {/*** 会员id*/@NotNull(message = "【会员id】不能为空")private Long memberId;/*** 日期*/@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")@NotNull(message = "【日期】不能为空")private Date date;/*** 车次编号*/@NotBlank(message = "【车次编号】不能为空")private String trainCode;/*** 出发站*/@NotBlank(message = "【出发站】不能为空")private String start;/*** 到达站*/@NotBlank(message = "【到达站】不能为空")private String end;/*** 余票ID*/@NotNull(message = "【余票ID】不能为空")private Long dailyTrainTicketId;/*** 车票*/@NotBlank(message = "【车票】不能为空")private List<ConfirmOrderTicketReq> tickets;@Overridepublic String toString() {return "ConfirmOrderDoReq{" +"memberId=" + memberId +", date=" + date +", trainCode='" + trainCode + '\'' +", start='" + start + '\'' +", end='" + end + '\'' +", dailyTrainTicketId=" + dailyTrainTicketId +", tickets=" + tickets +'}';}
    }
    
  • ConfirmOrderAdminController.java

    package com.neilxu.train.business.controller.admin;import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.service.ConfirmOrderService;
    import com.neilxu.train.common.resp.CommonResp;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;;@RestController
    @RequestMapping("/admin/confirm-order")
    public class ConfirmOrderAdminController {@Resourceprivate ConfirmOrderService confirmOrderService;@PostMapping("/save")public CommonResp<Object> save(@Valid @RequestBody ConfirmOrderDoReq req) {confirmOrderService.save(req);return new CommonResp<>();}
    }
    
  • ConfirmOrderService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.business.domain.ConfirmOrder;
    import com.neilxu.train.business.domain.ConfirmOrderExample;
    import com.neilxu.train.business.mapper.ConfirmOrderMapper;
    import com.neilxu.train.business.req.ConfirmOrderQueryReq;
    import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;import java.util.List;@Service
    public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {//省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,ticket条数>0,同乘客同车次是否已经买过//保存确认订单表,状态初始//查出余票记录,需要得到真实的库存//扣减余票数量,并判断余票是否足够//选座//一个车厢一个车厢的获取座位数据//挑选符合条件的座位,如果这个车厢不满足,则进入下一个车厢(多个选座应该在同一车厢)//选中座位后事务处理//座位表修改售卖情况sell//余票详情表修改余票//为会员增加购票记录//更新确认订单为成功}
    }
    
  • ConfirmOrderController.java

    package com.neilxu.train.business.controller;import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.service.ConfirmOrderService;
    import com.neilxu.train.common.resp.CommonResp;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;@RestController
    @RequestMapping("/confirm-order")
    public class ConfirmOrderController {@Resourceprivate ConfirmOrderService confirmOrderService;@PostMapping("/do")public CommonResp<Object> doConfirm(@Valid @RequestBody ConfirmOrderDoReq req) {confirmOrderService.doConfirm(req);return new CommonResp<>();}}
    
  • 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="handleOk"><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>
    </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);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表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 = () => {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}).then((response) => {let data = response.data;if (data.success) {notification.success({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,};},
    });
    </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>
    

十二、确认下单接口数据初始化

小tips:

ctrl + alt + v 可以快速提取变量

  • DailyTrainTicketService.java

    public DailyTrainTicket selectByUnique(Date date, String trainCode, String start, String end) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode).andStartEqualTo(start).andEndEqualTo(end);List<DailyTrainTicket> list = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);if (CollUtil.isNotEmpty(list)) {return list.get(0);} else {return null;}
    }
    
  • ConfirmOrderService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import com.alibaba.fastjson.JSON;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
    import com.neilxu.train.common.context.LoginMemberContext;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.business.domain.ConfirmOrder;
    import com.neilxu.train.business.domain.ConfirmOrderExample;
    import com.neilxu.train.business.mapper.ConfirmOrderMapper;
    import com.neilxu.train.business.req.ConfirmOrderQueryReq;
    import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;import java.util.Date;
    import java.util.List;@Service
    public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过Date date = req.getDate();String trainCode = req.getTrainCode();String start = req.getStart();String end = req.getEnd();// 保存确认订单表,状态初始DateTime now = DateTime.now();ConfirmOrder confirmOrder = new ConfirmOrder();confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrder.setMemberId(LoginMemberContext.getId());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(req.getTickets()));confirmOrderMapper.insert(confirmOrder);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 扣减余票数量,并判断余票是否足够// 选座// 一个车厢一个车厢的获取座位数据// 挑选符合条件的座位,如果这个车厢不满足,则进入下个车厢(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功}
    }
    

十三、预扣减库存并判断余票是否足够

  • BusinessExceptionEnum.java

    CONFIRM_ORDER_TICKET_COUNT_ERROR("余票不足"),
    
  • ConfirmOrderService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.EnumUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.alibaba.fastjson.JSON;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.ConfirmOrder;
    import com.neilxu.train.business.domain.ConfirmOrderExample;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
    import com.neilxu.train.business.enums.SeatTypeEnum;
    import com.neilxu.train.business.mapper.ConfirmOrderMapper;
    import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.req.ConfirmOrderQueryReq;
    import com.neilxu.train.business.req.ConfirmOrderTicketReq;
    import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
    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.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;import java.util.Date;
    import java.util.List;@Service
    public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过Date date = req.getDate();String trainCode = req.getTrainCode();String start = req.getStart();String end = req.getEnd();// 保存确认订单表,状态初始DateTime now = DateTime.now();ConfirmOrder confirmOrder = new ConfirmOrder();confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrder.setMemberId(LoginMemberContext.getId());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(req.getTickets()));confirmOrderMapper.insert(confirmOrder);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 预扣减余票数量,并判断余票是否足够reduceTickets(req, dailyTrainTicket);// 选座// 一个车箱一个车箱的获取座位数据// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功}private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {String seatTypeCode = ticketReq.getSeatTypeCode();SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);switch (seatTypeEnum) {case YDZ -> {int countLeft = dailyTrainTicket.getYdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYdz(countLeft);}case EDZ -> {int countLeft = dailyTrainTicket.getEdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setEdz(countLeft);}case RW -> {int countLeft = dailyTrainTicket.getRw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setRw(countLeft);}case YW -> {int countLeft = dailyTrainTicket.getYw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYw(countLeft);}}}}
    }
    

十四、计算多个选座之间的偏移值

  • ConfirmOrderService.java

    package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.EnumUtil;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.core.util.StrUtil;
    import com.alibaba.fastjson.JSON;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.ConfirmOrder;
    import com.neilxu.train.business.domain.ConfirmOrderExample;
    import com.neilxu.train.business.domain.DailyTrainTicket;
    import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
    import com.neilxu.train.business.enums.SeatColEnum;
    import com.neilxu.train.business.enums.SeatTypeEnum;
    import com.neilxu.train.business.mapper.ConfirmOrderMapper;
    import com.neilxu.train.business.req.ConfirmOrderDoReq;
    import com.neilxu.train.business.req.ConfirmOrderQueryReq;
    import com.neilxu.train.business.req.ConfirmOrderTicketReq;
    import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
    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.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;@Service
    public class ConfirmOrderService {public static final ArrayList<Object> OBJECT = new ArrayList<>();private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过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(LoginMemberContext.getId());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);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 预扣减余票数量,并判断余票是否足够reduceTickets(req, dailyTrainTicket);// 计算相对第一个座位的偏移值// 比如选择的是C1,D2,则偏移值是:[0,5]// 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]ConfirmOrderTicketReq ticketReq0 = tickets.get(0);if(StrUtil.isNotBlank(ticketReq0.getSeat())) {LOG.info("本次购票有选座");// 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());LOG.info("本次选座的座位类型包含的列:{}", colEnumList);// 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}List<String> referSeatList = new ArrayList<>();for (int i = 1; i <= 2; i++) {for (SeatColEnum seatColEnum : colEnumList) {referSeatList.add(seatColEnum.getCode() + i);}}LOG.info("用于作参照的两排座位:{}", referSeatList);List<Integer> offsetList = new ArrayList<>();// 绝对偏移值,即:在参照座位列表中的位置List<Integer> aboluteOffsetList = new ArrayList<>();for (ConfirmOrderTicketReq ticketReq : tickets) {int index = referSeatList.indexOf(ticketReq.getSeat());aboluteOffsetList.add(index);}LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);for (Integer index : aboluteOffsetList) {int offset = index - aboluteOffsetList.get(0);offsetList.add(offset);}LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);} else {LOG.info("本次购票没有选座");}// 选座// 一个车箱一个车箱的获取座位数据// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功}private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {String seatTypeCode = ticketReq.getSeatTypeCode();SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);switch (seatTypeEnum) {case YDZ -> {int countLeft = dailyTrainTicket.getYdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYdz(countLeft);}case EDZ -> {int countLeft = dailyTrainTicket.getEdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setEdz(countLeft);}case RW -> {int countLeft = dailyTrainTicket.getRw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setRw(countLeft);}case YW -> {int countLeft = dailyTrainTicket.getYw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYw(countLeft);}}}}
    }
    
  • 测试

在这里插入图片描述

在这里插入图片描述

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

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

相关文章

【Canvas与艺术】双“方齿齿轮”啮合示意图

【关键点】 齿轮数组的建立、旋转角度的调整。 【图例】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>双“方齿齿轮”啮合…

WIFI驱动移植实验:连接WIFI热点

一. 简介 前一篇文章做了WIFI联网前的工作&#xff0c;文章如下&#xff1a; WIFI驱动移植实验&#xff1a;WIFI 联网前的工作-CSDN博客 本文在上面一篇文章工作实现的基础上&#xff0c;实现 WIFI的连接。 二. WIFI驱动移植实验&#xff1a;WIFI 联网测试 这里WIFI联网的…

什么是量子计算?

什么是量子计算&#xff1f; 量子计算机仍处于起步阶段&#xff0c;正在影响已经在经典计算机上运行的新一代模拟&#xff0c;现在使用 NVIDIA cuQuantum SDK 进行加速。 在史蒂夫乔布斯 (Steve Jobs) 推出可以放入口袋的计算机之前 27 年&#xff0c;物理学家保罗贝尼奥夫 (P…

深入理解数据结构第一弹——二叉树(1)——堆

前言&#xff1a; 在前面我们已经学习了数据结构的基础操作&#xff1a;顺序表和链表及其相关内容&#xff0c;今天我们来学一点有些难度的知识——数据结构中的二叉树&#xff0c;今天我们先来学习二叉树中堆的知识&#xff0c;这部分内容还是非常有意思的&#xff0c;下面我们…

3D密集面部对齐项目 | 基于Pytorch实现的快速+准确+稳定的3D面部对齐算法

项目应用场景 可以应用于人脸面部三维特征点的提取 人脸面部的三维重建&#xff0c;项目的特点是基于 Pytorch 实现、快速、准确、稳定 项目效果&#xff1a; 项目流程 > 具体参见项目内README.md (1) 构建 sh ./build.sh (2) 执行示例 # 1. running on still i…

持续交付与持续部署相关概念(CD)

目录 一、概述 二、持续交付基本概念 2.1 持续交付的含义 2.1.1 项目管理的视角 2.1.2 产品研发的视角 2.1.3 总结 2.2 持续交付涉及的运作环境 2.2.1 开发环境 2.2.2 测试环境 2.2.3 UAT环境 2.2.4 准生产环境 2.2.5 生产环境 2.3 总结 三、持续部署基本概念 3.…

redis对象list

Redis List是一组连接起来的字符串集合。 写操作&#xff1a; LPUSH 语法:LPUSH key value [value …] 功能:从头部增加元素,返回值为List中元素的总数。 RPUSH 语法:RPUSH key value [value …] 功能:从尾部增加元素,返回值为List中元素的总数。 LPOP 语法:LPOP key 功能…

用Python实现办公自动化(自动化处理Excel工作簿)

自动化处理Excel工作簿 &#xff08;一&#xff09;批量生产产品出货清单 以“出货统计表”为例&#xff0c; 需求&#xff1a;将出货记录按照出货日期分类整理成多张出货清单 “出货统计表数据案例” “产品出货清单模板” 1.提取出货统计表的数据 “Python程序代码” # 使用…

安全SCDN的威胁情报库对DDOS防护有什么好处

目前网络攻击事件频频发生&#xff0c;DDoS&#xff08;分布式拒绝服务&#xff09;攻击已成为各种企业&#xff08;小到区域性小公司大到各种跨国公司&#xff09;的主要威胁&#xff0c;DDoS 攻击可能会对企业造成重大损害和破坏&#xff0c;比如对目标公司的业务造成产生不利…

C#使用SQLite(含加密)保姆级教程

C#使用SQLite 文章目录 C#使用SQLite涉及框架及库复制runtimes创建加密SQLite文件生成连接字串执行SQL生成表SQLiteConnectionFactory.cs 代码结构最后 涉及框架及库 自己在NuGet管理器里面安装即可 Chloe.SQLite&#xff1a;ORM框架Microsoft.Data.Sqlite.Core&#xff1a;驱…

React Native框架开发APP,安装免费的图标库(react-native-vector-icons)并使用详解

一、安装图标库 要使用免费的图标库&#xff0c;你可以使用 React Native Vector Icons 库。 首先&#xff0c;确保你已经安装了 react-native-vector-icons&#xff1a; npm install --save react-native-vector-iconsnpm install --save-dev types/react-native-vector-ic…

并发编程之CountDownLatch和CyclicBarrier的详细解析(带小案例)

CountDownLatch 倒计时锁存器 用来解决线程执行次序的问题 CountDownLatch主要有两个方法&#xff0c;当一个或多个线程调用await方法时&#xff0c;这些线程会阻塞。 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)&#xff0c; 当计数器的值变为…

云服务器8核32G配置租用优惠价格94元/月、1362元一年

8核32G云服务器京东云轻量云主机价格94元1个月、282元3个月、673元6个月、1362元一年&#xff0c;配置8C32G-100G SSD系统盘-10M带宽-2000G月流量 华北-北京&#xff0c;京东云优惠活动 yunfuwuqiba.com/go/jd 活动链接打开如下图&#xff1a; 8核32G云服务器京东云轻量云主机价…

CSS(三)---【盒子模型、边框、外边距合并】

零.前言 本篇主要介绍CSS中最重要的一种概念模型&#xff1a;“盒子模型”。 关于CSS的更多内容&#xff0c;可以查看作者之前的文章&#xff1a; CSS(一)---【CSS简介、导入方式、八种选择器、优先级】-CSDN博客 CSS(二)---【常见属性、复合属性使用】-CSDN博客 一.盒子模…

WebCopilot:一款功能强大的子域名枚举和安全漏洞扫描工具

关于WebCopilot WebCopilot是一款功能强大的子域名枚举和安全漏洞扫描工具&#xff0c;该工具能够枚举目标域名下的子域名&#xff0c;并使用不同的开源工具检测目标存在的安全漏洞。 工具运行机制 WebCopilot首先会使用assetsfinder、submaster、subfinder、accumt、finddom…

Jamba:AI21 实验室发布首个应用级的 Mamba 架构 AI 模型

AI21 实验室发布了 Jamba&#xff0c;这是全球首个基于 Mamba 架构的、可用于实际应用的 AI 模型。目前大多数模型&#xff08;例如 GPT、Gemini 和 Llama&#xff09;都基于 Transformer 架构。Jamba 结合了 Mamba 结构化状态空间模型 (SSM) 和传统 Transformer 架构的优点&am…

属性选择器

1.[title]{background:yellow;}&#xff1a;所有带title标签设置成黄色 2.div[class]{background:yellow;}&#xff1a;所有div中带class标签设置成黄色 3.div[classbox1]{border:1px solid blue; }&#xff1a;div中包含class并且classbox1的设置成蓝边框 4. class…

Postman Tests设置Global读取不是最新值,跟Tests执行顺序有关(踩坑笔记)

前言 在执行Run Collection的时候&#xff0c;发现设置的全局变量每次读取都是旧值&#xff0c;没有读取到最新的值。 背景 有2个地方需要动态参数&#xff0c;一个URL&#xff0c;一个Body&#xff0c;因此需要设置Tests脚本。 url动态参数 url&#xff1a;动态参数projec…

【Java】打包:JAR、EAR、WAR

打包&#xff1a;JAR、EAR、WAR war 是一个 Web 模块&#xff0c;其中需要包括 WEB-INF&#xff0c;是可以直接运行的 WEB 模块。而 jar 一般只是包括一些 class 文件&#xff0c;在声明了 main_class 之后是可以用 java 命令运行的。 它们都是压缩的包&#xff0c;拿 Tomcat …

【基于springboot分析Quartz(v2.3.2)的启动流程】

基于springboot分析Quartz&#xff08;v2.3.2&#xff09;的启动流程 最近公司的定时任务使用了Quartz框架&#xff0c;在开发中经常出现定任务不执行了的问题&#xff0c;但是我又找不到原因所在&#xff0c;可把我愁坏了。于是我决定看看Quartz框架是怎么调度任务的。&#x…