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.hqwc.cn/news/577986.html

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

相关文章

【保姆级】2024年最新Onlyfans使用订阅教程

【必看】开通步骤 订阅OnlyFans的步骤简要总结如下&#xff1a; 开通虚拟信用卡&#xff1a;虚拟信用卡开通使用教程。开卡后&#xff0c;进入首页&#xff0c;就能看到自己的虚拟信用卡信息把虚拟信用卡的信息填写到OnlyFans绑定信用卡界面就OK了 从上面的链接进入开卡费可…

考研数学|《1800》+《660》精华搭配混合用(经验分享)

肯定不行&#xff0c;考研数学哪有这么容易的&#xff01; 先说说这两本习题册&#xff0c;李永乐老师推出的新版660题&#xff0c;相较于18年前的版本&#xff0c;难度略有降低&#xff0c;更加适合初学者。因此&#xff0c;对于处于基础阶段的学习者来说&#xff0c;新版660…

GeoTrust SSL证书有什么优势?

GeoTrust SSL证书具备以下显著优势&#xff1a; 1. 市场占有率比较高&#xff1a;GeoTrust作为知名的SSL证书供应商&#xff0c;拥有广泛的市场接受度和高占有率&#xff0c;表明其产品受到众多企业和网站的信任和采用。 2. 品牌信誉与信任标识&#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…

深度思考:雪花算法snowflake分布式id生成原理详解

雪花算法snowflake是一种优秀的分布式ID生成方案&#xff0c;其优点突出&#xff1a;它能生成全局唯一且递增的ID&#xff0c;确保了数据的一致性和准确性&#xff1b;同时&#xff0c;该算法灵活性强&#xff0c;可自定义各部分bit位&#xff0c;满足不同业务场景的需求&#…

谷粒商城实战(007 压力测试)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第141p-第p150的内容 简介 安装jmeter 安装jmeter 使用中文 这样写就是200个线程循环100次 一共是2万个请求 介绍线程组 添加请求 可以是htt…

【物联网】Qinghub opc-ua 连接协议

基础信息 组件名称 &#xff1a; opcua-connector 组件版本&#xff1a; 1.0.0 组件类型&#xff1a; 系统默认 状 态&#xff1a; 正式发布 组件描述&#xff1a;通过OPCUA连接网关&#xff0c;通过定时任务获取OPCUA相关的数据或通过执行指令控制设备相关参数。 配置文件&a…

软件设计不是CRUD(16):低耦合模块设计理论——行为抽象与设计模式(下)

(接上文《软件设计不是CRUD(15):低耦合模块设计理论——行为抽象与设计模式(中)》) 3.2.4、之前的业务逻辑需要关注后续逻辑的执行成败,并调整自身执行的情况 这个场景在之前场景的基础上增加了新的控制要求,具体来说就是之前已经完成的控制逻辑执行,需要在后续控制…

pdf在浏览器上无法正常加载的问题

一、背景 觉得很有意思给大家分享一下。事情是这样的&#xff0c;开发给我反馈说&#xff0c;线上环境接口请求展示pdf异常&#xff0c;此时碰巧我前不久正好在ingress前加了一层nginx&#xff0c;恰逢此时内心五谷杂陈&#xff0c;思路第一时间便放在了改动项。捣鼓了好久无果…

继承 | Java

继承概念 继承&#xff1a;就是子类继承父类的属性和行为&#xff0c;使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。 定义父类&#xff1a; package com.yh;public class Animal {String color;String brand;int age 5;priva…

农村集中式生活污水分质处理及循环利用技术指南

立项单位&#xff1a;生态环境部土壤与农业农村生态环境监管技术中心、山东文远环保科技股份有限公司、北京易境创联环保有限公司、中国环境科学研究院、广东省环境科学研究院、中铁第五勘察设计院集团有限公司、中华环保联合会水环境治理专业委员会 本文件规定了集中式村镇生活…

实现Flutter应用中的全局导航栏效果

介绍 在移动应用开发中&#xff0c;导航栏是用户与应用交互的重要组成部分之一。它不仅提供了应用程序中不同页面之间的导航功能&#xff0c;还可以展示应用的整体结构和主要功能。因此&#xff0c;设计一个清晰、易用的导航栏对于提升用户体验和应用的可用性至关重要。 在Fl…