Bootstrap

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

本文参考自

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

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

本章目录

一、核心功能介绍

在这里插入图片描述

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

  • 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);
    
        @Resource
        private DailyTrainTicketMapper dailyTrainTicketMapper;
    
        @Resource
        private 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);
        }
    
        @Transactional
        public 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);
    
        @Resource
        private DailyTrainMapper dailyTrainMapper;
    
        @Resource
        private TrainService trainService;
    
        @Resource
        private DailyTrainStationService dailyTrainStationService;
    
        @Resource
        private DailyTrainCarriageService dailyTrainCarriageService;
    
        @Resource
        private DailyTrainSeatService dailyTrainSeatService;
    
        @Resource
        private 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);
            }
        }
    
        @Transactional
        public 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);
    
        @Resource
        private DailyTrainTicketMapper dailyTrainTicketMapper;
    
        @Resource
        private TrainStationService trainStationService;
    
        @Resource
        private 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);
        }
    
        @Transactional
        public 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.priceRate
                    BigDecimal 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);
    
        @Resource
        private DailyTrainTicketMapper dailyTrainTicketMapper;
    
        @Resource
        private TrainStationService trainStationService;
    
        @Resource
        private 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);
        }
    
        @Transactional
        public 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.priceRate
                    BigDecimal 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-popconfirm
                    title="删除后不可恢复,确认删除?"
                    @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 {
      
          @Resource
          private 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 {
      
          @Resource
          private 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 {
      
          @Resource
          private 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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          chooseSeatObj.value = {};
          for (let i = 1; i <= 2; i++) {
            SEAT_COL_ARRAY.value.forEach((item) => {
              chooseSeatObj.value[item.code + i] = false;
            })
          }
          console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
        }, {immediate: true});
    
        const handleQueryPassenger = () => {
          axios.get("/member/passenger/query-mine").then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content;
              passengers.value.forEach((item) => passengerOptions.value.push({
                label: item.name,
                value: item
              }))
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const finishCheckPassenger = () => {
          console.log("购票列表:", tickets.value);
    
          if (tickets.value.length > 5) {
            notification.error({description: '最多只能购买5张车票'});
            return;
          }
    
          // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
          // 前端校验不一定准,但前端校验可以减轻后端很多压力
          // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
          let seatTypesTemp = Tool.copy(seatTypes);
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            for (let j = 0; j < seatTypesTemp.length; j++) {
              let seatType = seatTypesTemp[j];
              // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
              if (ticket.seatTypeCode === seatType.code) {
                seatType.count--;
                if (seatType.count < 0) {
                  notification.error({description: seatType.desc + '余票不足'});
                  return;
                }
              }
            }
          }
          console.log("前端余票校验通过");
    
          // 判断是否支持选座,只有纯一等座和纯二等座支持选座
          // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
          let ticketSeatTypeCodes = [];
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            ticketSeatTypeCodes.push(ticket.seatTypeCode);
          }
          // 为购票列表中的所有座位类型去重:[1, 2]
          const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
          console.log("选好的座位类型:", ticketSeatTypeCodesSet);
          if (ticketSeatTypeCodesSet.length !== 1) {
            console.log("选了多种座位,不支持选座");
            chooseSeatType.value = 0;
          } else {
            // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
            if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
              console.log("一等座选座");
              chooseSeatType.value = SEAT_TYPE.YDZ.code;
            } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
              console.log("二等座选座");
              chooseSeatType.value = SEAT_TYPE.EDZ.code;
            } else {
              console.log("不是一等座或二等座,不支持选座");
              chooseSeatType.value = 0;
            }
    
            // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
            if (chooseSeatType.value !== 0) {
              for (let i = 0; i < seatTypes.length; i++) {
                let seatType = seatTypes[i];
                // 找到同类型座位
                if (ticketSeatTypeCodesSet[0] === seatType.code) {
                  // 判断余票,小于20张就不支持选座
                  if (seatType.count < 20) {
                    console.log("余票小于20张就不支持选座")
                    chooseSeatType.value = 0;
                    break;
                  }
                }
              }
            }
          }
    
          // 弹出确认界面
          visible.value = true;
    
        };
    
        const handleOk = () => {
          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;
    
        @Override
        public 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 {
    
        @Resource
        private 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);
    
        @Resource
        private 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 {
    
        @Resource
        private 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,要选的是二等座,就筛选出ABCDF
        const SEAT_COL_ARRAY = computed(() => {
          return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
        });
        // 选择的座位
        // {
        //   A1: false, C1: true,D1: false, F1: false,
        //   A2: false, C2: false,D2: true, F2: false
        // }
        const chooseSeatObj = ref({});
        watch(() => SEAT_COL_ARRAY.value, () => {
          chooseSeatObj.value = {};
          for (let i = 1; i <= 2; i++) {
            SEAT_COL_ARRAY.value.forEach((item) => {
              chooseSeatObj.value[item.code + i] = false;
            })
          }
          console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
        }, {immediate: true});
    
        const handleQueryPassenger = () => {
          axios.get("/member/passenger/query-mine").then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content;
              passengers.value.forEach((item) => passengerOptions.value.push({
                label: item.name,
                value: item
              }))
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const finishCheckPassenger = () => {
          console.log("购票列表:", tickets.value);
    
          if (tickets.value.length > 5) {
            notification.error({description: '最多只能购买5张车票'});
            return;
          }
    
          // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
          // 前端校验不一定准,但前端校验可以减轻后端很多压力
          // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
          let seatTypesTemp = Tool.copy(seatTypes);
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            for (let j = 0; j < seatTypesTemp.length; j++) {
              let seatType = seatTypesTemp[j];
              // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
              if (ticket.seatTypeCode === seatType.code) {
                seatType.count--;
                if (seatType.count < 0) {
                  notification.error({description: seatType.desc + '余票不足'});
                  return;
                }
              }
            }
          }
          console.log("前端余票校验通过");
    
          // 判断是否支持选座,只有纯一等座和纯二等座支持选座
          // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
          let ticketSeatTypeCodes = [];
          for (let i = 0; i < tickets.value.length; i++) {
            let ticket = tickets.value[i];
            ticketSeatTypeCodes.push(ticket.seatTypeCode);
          }
          // 为购票列表中的所有座位类型去重:[1, 2]
          const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
          console.log("选好的座位类型:", ticketSeatTypeCodesSet);
          if (ticketSeatTypeCodesSet.length !== 1) {
            console.log("选了多种座位,不支持选座");
            chooseSeatType.value = 0;
          } else {
            // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
            if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
              console.log("一等座选座");
              chooseSeatType.value = SEAT_TYPE.YDZ.code;
            } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
              console.log("二等座选座");
              chooseSeatType.value = SEAT_TYPE.EDZ.code;
            } else {
              console.log("不是一等座或二等座,不支持选座");
              chooseSeatType.value = 0;
            }
    
            // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
            if (chooseSeatType.value !== 0) {
              for (let i = 0; i < seatTypes.length; i++) {
                let seatType = seatTypes[i];
                // 找到同类型座位
                if (ticketSeatTypeCodesSet[0] === seatType.code) {
                  // 判断余票,小于20张就不支持选座
                  if (seatType.count < 20) {
                    console.log("余票小于20张就不支持选座")
                    chooseSeatType.value = 0;
                    break;
                  }
                }
              }
            }
          }
    
          // 弹出确认界面
          visible.value = true;
    
        };
    
        const handleOk = () => {
          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);
    
        @Resource
        private ConfirmOrderMapper confirmOrderMapper;
    
        @Resource
        private 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);
    
        @Resource
        private ConfirmOrderMapper confirmOrderMapper;
    
        @Resource
        private 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);
    
        @Resource
        private ConfirmOrderMapper confirmOrderMapper;
    
        @Resource
        private 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);
                    }
                }
            }
        }
    }
    
  • 测试

在这里插入图片描述

在这里插入图片描述

;