本文参考自
Springboot3+微服务实战12306高性能售票系统 - 慕课网 (imooc.com)
本文是仿12306项目实战第(二)章——项目实现 的第五篇,本篇讲解该项目的核心功能——余票查询、车票预定功能的基础版开发,以及讲解项目与Nacos的集成
本章目录
- 一、核心功能介绍
- 二、增加余票信息表,生成代码
- 三、生成车次时初始化余票信息
- 四、生成车次时初始化各种座位的余票数量
- 五、为余票信息页面增加查询条件
- 六、为会员端增余票查询功能
- 七、增加订票页面并且实现车次信息传递
- 1.增加预订按钮,点击预订时,跳转到下单页面,并使用sessionStorage传递参数
- 2.为余票查询页面缓存查询参数,方便用户使用;将session key写成常量,方便统一维护,可以避免多个功能使用同一个key
- 3.美化车次信息的显示
- 4.订单页面显示座位信息
- 八、订票页面勾选乘客并显示购票列表
- 1.订票页面,查询我的所有的乘客(可以在新增乘客的时候,增加一个校验:超过50个乘客,就不能再新增了)
- 2.订票页面,显示我的乘客复选框
- 3.订票页面,为勾选的乘客构造购票数据
- 4.订票页面,优化购票列表的展示
- 5.订票页面,勾选乘客后提交,显示购票列表确认框
- 九、分解选座购票功能的前后端逻辑
- 十、订票页面增加选座效果
- 1.勾选乘客后,提交时,校验余票是否足够(前端校验不一定准,但前端校验可以减轻后端很多压力)
- 2.根据购票列表,计算出是否支持选座
- 3.根据购票列表,展示选座按钮
- 4.余票小于20张时,不允许选座
- 5.确认提交时,计算出最终每个乘客所选的座位
- 6.chooseSeatObj先清空,再初始化,保证两排座位是有序的
- 十、增加确认订单表并生成前后端代码
- 十一、后端增加确认下单购票接口
- 十二、确认下单接口数据初始化
- 十三、预扣减库存并判断余票是否足够
- 十四、计算多个选座之间的偏移值
一、核心功能介绍
二、增加余票信息表,生成代码
-
business.sql
drop table if exists `daily_train_ticket`; create table `daily_train_ticket` (`id` bigint not null comment 'id',`date` date not null comment '日期',`train_code` varchar(20) not null comment '车次编号',`start` varchar(20) not null comment '出发站',`start_pinyin` varchar(50) not null comment '出发站拼音',`start_time` time not null comment '出发时间',`start_index` tinyint not null comment '出发站序|本站是整个车次的第几站',`end` varchar(20) not null comment '到达站',`end_pinyin` varchar(50) not null comment '到达站拼音',`end_time` time not null comment '到站时间',`end_index` tinyint not null comment '到站站序|本站是整个车次的第几站',`ydz` int not null comment '一等座余票',`ydz_price` decimal(8, 2) not null comment '一等座票价',`edz` int not null comment '二等座余票',`edz_price` decimal(8, 2) not null comment '二等座票价',`rw` int not null comment '软卧余票',`rw_price` decimal(8, 2) not null comment '软卧票价',`yw` int not null comment '硬卧余票',`yw_price` decimal(8, 2) not null comment '硬卧票价',`create_time` datetime(3) comment '新增时间',`update_time` datetime(3) comment '修改时间',primary key (`id`),unique key `date_train_code_start_end_unique` (`date`, `train_code`, `start`, `end`) ) engine=innodb default charset=utf8mb4 comment='余票信息';
实际12306用的是商业缓存软件来做的余票信息缓存,本项目用mysql新建临时表来代替缓存演示业务实现
-
修改generator-config-business.xml,生成持久层、前后端代码
<table tableName="daily_train_ticket" domainObjectName="DailyTrainTicket"/>
操作同之前
-
修改admin/package.json,加规则去除ESLint报错
"rules": {"vue/multi-word-component-names": 0,"no-undef": 0,"vue/no-unused-vars": 0 }
-
修改路由、侧边栏
操作同之前
-
测试
数据后续由定时任务填充
三、生成车次时初始化余票信息
-
给批量方法都加上@Transactional
虽然由于事务的传递性,外层有事务,内层方法也会是事务,但是规范点还是都加上事务注解
-
DailyTrainTicketService.java
逻辑:比如车站1——车站2——车站3,站站组合情况就有 1、2;1、3;2、3 三种,需要分别生成每种组合的余票信息
这里第一版 先解决站站组合逻辑,具体余票信息后面完善
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.domain.DailyTrainTicketExample; import com.neilxu.train.business.domain.TrainStation; import com.neilxu.train.business.mapper.DailyTrainTicketMapper; import com.neilxu.train.business.req.DailyTrainTicketQueryReq; import com.neilxu.train.business.req.DailyTrainTicketSaveReq; import com.neilxu.train.business.resp.DailyTrainTicketQueryResp; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal; import java.util.Date; import java.util.List;@Service public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());dailyTrainTicket.setYdz(0);dailyTrainTicket.setYdzPrice(BigDecimal.ZERO);dailyTrainTicket.setEdz(0);dailyTrainTicket.setEdzPrice(BigDecimal.ZERO);dailyTrainTicket.setRw(0);dailyTrainTicket.setRwPrice(BigDecimal.ZERO);dailyTrainTicket.setYw(0);dailyTrainTicket.setYwPrice(BigDecimal.ZERO);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);} }
-
DailyTrainService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.DailyTrain; import com.neilxu.train.business.domain.DailyTrainExample; import com.neilxu.train.business.domain.Train; import com.neilxu.train.business.mapper.DailyTrainMapper; import com.neilxu.train.business.req.DailyTrainQueryReq; import com.neilxu.train.business.req.DailyTrainSaveReq; import com.neilxu.train.business.resp.DailyTrainQueryResp; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.util.Date; import java.util.List;@Service public class DailyTrainService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);@Resourceprivate DailyTrainMapper dailyTrainMapper;@Resourceprivate TrainService trainService;@Resourceprivate DailyTrainStationService dailyTrainStationService;@Resourceprivate DailyTrainCarriageService dailyTrainCarriageService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(DailyTrainSaveReq req) {DateTime now = DateTime.now();DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);if (ObjectUtil.isNull(dailyTrain.getId())) {dailyTrain.setId(SnowUtil.getSnowflakeNextId());dailyTrain.setCreateTime(now);dailyTrain.setUpdateTime(now);dailyTrainMapper.insert(dailyTrain);} else {dailyTrain.setUpdateTime(now);dailyTrainMapper.updateByPrimaryKey(dailyTrain);}}public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {DailyTrainExample dailyTrainExample = new DailyTrainExample();dailyTrainExample.setOrderByClause("date desc, code asc");DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();if (ObjectUtil.isNotNull(req.getDate())) {criteria.andDateEqualTo(req.getDate());}if (ObjectUtil.isNotEmpty(req.getCode())) {criteria.andCodeEqualTo(req.getCode());}LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainMapper.deleteByPrimaryKey(id);}/*** 生成某日所有车次信息,包括车次、车站、车厢、座位* @param date*/public void genDaily(Date date) {List<Train> trainList = trainService.selectAll();if (CollUtil.isEmpty(trainList)) {LOG.info("没有车次基础数据,任务结束");return;}for (Train train : trainList) {genDailyTrain(date, train);}}@Transactionalpublic void genDailyTrain(Date date, Train train) {LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());// 删除该车次已有的数据DailyTrainExample dailyTrainExample = new DailyTrainExample();dailyTrainExample.createCriteria().andDateEqualTo(date).andCodeEqualTo(train.getCode());dailyTrainMapper.deleteByExample(dailyTrainExample);// 生成该车次的数据DateTime now = DateTime.now();DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);dailyTrain.setId(SnowUtil.getSnowflakeNextId());dailyTrain.setCreateTime(now);dailyTrain.setUpdateTime(now);dailyTrain.setDate(date);dailyTrainMapper.insert(dailyTrain);// 生成该车次的车站数据dailyTrainStationService.genDaily(date, train.getCode());// 生成该车次的车厢数据dailyTrainCarriageService.genDaily(date, train.getCode());// 生成该车次的座位数据dailyTrainSeatService.genDaily(date, train.getCode());// 生成该车次的余票数据dailyTrainTicketService.genDaily(date, train.getCode());LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());} }
-
测试
四、生成车次时初始化各种座位的余票数量
-
DailyTrainSeatService.java
public int countSeat(Date date, String trainCode, String seatType) {DailyTrainSeatExample example = new DailyTrainSeatExample();example.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode).andSeatTypeEqualTo(seatType);long l = dailyTrainSeatMapper.countByExample(example);if (l == 0L) {return -1;}return (int) l; }
-
DailyTrainService.java
// 生成该车次的余票数据 dailyTrainTicketService.genDaily(dailyTrain, date, train.getCode());
-
DailyTrainTicketService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjectUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.DailyTrain; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.domain.DailyTrainTicketExample; import com.neilxu.train.business.domain.TrainStation; import com.neilxu.train.business.enums.SeatTypeEnum; import com.neilxu.train.business.enums.TrainTypeEnum; import com.neilxu.train.business.mapper.DailyTrainTicketMapper; import com.neilxu.train.business.req.DailyTrainTicketQueryReq; import com.neilxu.train.business.req.DailyTrainTicketSaveReq; import com.neilxu.train.business.resp.DailyTrainTicketQueryResp; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; import java.util.List;@Service public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);BigDecimal sumKM = BigDecimal.ZERO;for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);sumKM = sumKM.add(trainStationEnd.getKm());DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());// 票价 = 里程之和 * 座位单价 * 车次类型系数String trainType = dailyTrain.getType();// 计算票价系数:TrainTypeEnum.priceRateBigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);dailyTrainTicket.setYdz(ydz);dailyTrainTicket.setYdzPrice(ydzPrice);dailyTrainTicket.setEdz(edz);dailyTrainTicket.setEdzPrice(edzPrice);dailyTrainTicket.setRw(rw);dailyTrainTicket.setRwPrice(rwPrice);dailyTrainTicket.setYw(yw);dailyTrainTicket.setYwPrice(ywPrice);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);} }
-
测试
重新生成车次
五、为余票信息页面增加查询条件
-
DailyTrainTicketQueryReq.java
package com.neilxu.train.business.req;import com.neilxu.train.common.req.PageReq; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat;import java.util.Date;@Data public class DailyTrainTicketQueryReq extends PageReq {/*** 日期*/@DateTimeFormat(pattern = "yyyy-MM-dd")private Date date;/*** 车次编号*/private String trainCode;/*** 出发站*/private String start;/*** 到达站*/private String end;}
-
DailyTrainTicketService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.DailyTrain; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.domain.DailyTrainTicketExample; import com.neilxu.train.business.domain.TrainStation; import com.neilxu.train.business.enums.SeatTypeEnum; import com.neilxu.train.business.enums.TrainTypeEnum; import com.neilxu.train.business.mapper.DailyTrainTicketMapper; import com.neilxu.train.business.req.DailyTrainTicketQueryReq; import com.neilxu.train.business.req.DailyTrainTicketSaveReq; import com.neilxu.train.business.resp.DailyTrainTicketQueryResp; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; import java.util.List;@Service public class DailyTrainTicketService {private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);@Resourceprivate DailyTrainTicketMapper dailyTrainTicketMapper;@Resourceprivate TrainStationService trainStationService;@Resourceprivate DailyTrainSeatService dailyTrainSeatService;public void save(DailyTrainTicketSaveReq req) {DateTime now = DateTime.now();DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);if (ObjectUtil.isNull(dailyTrainTicket.getId())) {dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);} else {dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);}}public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.setOrderByClause("id desc");DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();if (ObjUtil.isNotNull(req.getDate())) {criteria.andDateEqualTo(req.getDate());}if (ObjUtil.isNotEmpty(req.getTrainCode())) {criteria.andTrainCodeEqualTo(req.getTrainCode());}if (ObjUtil.isNotEmpty(req.getStart())) {criteria.andStartEqualTo(req.getStart());}if (ObjUtil.isNotEmpty(req.getEnd())) {criteria.andEndEqualTo(req.getEnd());}LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {dailyTrainTicketMapper.deleteByPrimaryKey(id);}@Transactionalpublic void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);// 删除某日某车次的余票信息DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);// 查出某车次的所有的车站信息List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);if (CollUtil.isEmpty(stationList)) {LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");return;}DateTime now = DateTime.now();for (int i = 0; i < stationList.size(); i++) {// 得到出发站TrainStation trainStationStart = stationList.get(i);BigDecimal sumKM = BigDecimal.ZERO;for (int j = (i + 1); j < stationList.size(); j++) {TrainStation trainStationEnd = stationList.get(j);sumKM = sumKM.add(trainStationEnd.getKm());DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());dailyTrainTicket.setDate(date);dailyTrainTicket.setTrainCode(trainCode);dailyTrainTicket.setStart(trainStationStart.getName());dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());dailyTrainTicket.setStartTime(trainStationStart.getOutTime());dailyTrainTicket.setStartIndex(trainStationStart.getIndex());dailyTrainTicket.setEnd(trainStationEnd.getName());dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());dailyTrainTicket.setEndTime(trainStationEnd.getInTime());dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());// 票价 = 里程之和 * 座位单价 * 车次类型系数String trainType = dailyTrain.getType();// 计算票价系数:TrainTypeEnum.priceRateBigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);dailyTrainTicket.setYdz(ydz);dailyTrainTicket.setYdzPrice(ydzPrice);dailyTrainTicket.setEdz(edz);dailyTrainTicket.setEdzPrice(edzPrice);dailyTrainTicket.setRw(rw);dailyTrainTicket.setRwPrice(rwPrice);dailyTrainTicket.setYw(yw);dailyTrainTicket.setYwPrice(ywPrice);dailyTrainTicket.setCreateTime(now);dailyTrainTicket.setUpdateTime(now);dailyTrainTicketMapper.insert(dailyTrainTicket);}}LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);} }
-
daily-train-ticket.vue
<template><p><a-space><train-select-view v-model="params.trainCode" width="200px"></train-select-view><a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker><station-select-view v-model="params.start" width="200px"></station-select-view><station-select-view v-model="params.end" width="200px"></station-select-view><a-button type="primary" @click="handleQuery()">查找</a-button></a-space></p><a-table :dataSource="dailyTrainTickets":columns="columns":pagination="pagination"@change="handleTableChange":loading="loading"><template #bodyCell="{ column, record }"><template v-if="column.dataIndex === 'operation'"></template></template></a-table> </template><script> import { defineComponent, ref, onMounted } from 'vue'; import {notification} from "ant-design-vue"; import axios from "axios"; import TrainSelectView from "@/components/train-select"; import StationSelectView from "@/components/station-select";export default defineComponent({name: "daily-train-ticket-view",components: {StationSelectView, TrainSelectView},setup() {const visible = ref(false);let dailyTrainTicket = ref({id: undefined,date: undefined,trainCode: undefined,start: undefined,startPinyin: undefined,startTime: undefined,startIndex: undefined,end: undefined,endPinyin: undefined,endTime: undefined,endIndex: undefined,ydz: undefined,ydzPrice: undefined,edz: undefined,edzPrice: undefined,rw: undefined,rwPrice: undefined,yw: undefined,ywPrice: undefined,createTime: undefined,updateTime: undefined,});const dailyTrainTickets = ref([]);// 分页的三个属性名是固定的const pagination = ref({total: 0,current: 1,pageSize: 10,});let loading = ref(false);const params = ref({});const columns = [{title: '日期',dataIndex: 'date',key: 'date',},{title: '车次编号',dataIndex: 'trainCode',key: 'trainCode',},{title: '出发站',dataIndex: 'start',key: 'start',},{title: '出发站拼音',dataIndex: 'startPinyin',key: 'startPinyin',},{title: '出发时间',dataIndex: 'startTime',key: 'startTime',},{title: '出发站序',dataIndex: 'startIndex',key: 'startIndex',},{title: '到达站',dataIndex: 'end',key: 'end',},{title: '到达站拼音',dataIndex: 'endPinyin',key: 'endPinyin',},{title: '到站时间',dataIndex: 'endTime',key: 'endTime',},{title: '到站站序',dataIndex: 'endIndex',key: 'endIndex',},{title: '一等座余票',dataIndex: 'ydz',key: 'ydz',},{title: '一等座票价',dataIndex: 'ydzPrice',key: 'ydzPrice',},{title: '二等座余票',dataIndex: 'edz',key: 'edz',},{title: '二等座票价',dataIndex: 'edzPrice',key: 'edzPrice',},{title: '软卧余票',dataIndex: 'rw',key: 'rw',},{title: '软卧票价',dataIndex: 'rwPrice',key: 'rwPrice',},{title: '硬卧余票',dataIndex: 'yw',key: 'yw',},{title: '硬卧票价',dataIndex: 'ywPrice',key: 'ywPrice',},];const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/business/admin/daily-train-ticket/query-list", {params: {page: param.page,size: param.size,trainCode: params.value.trainCode,date: params.value.date,start: params.value.start,end: params.value.end}}).then((response) => {loading.value = false;let data = response.data;if (data.success) {dailyTrainTickets.value = data.content.list;// 设置分页控件的值pagination.value.current = param.page;pagination.value.total = data.content.total;} else {notification.error({description: data.message});}});};const handleTableChange = (page) => {// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));pagination.value.pageSize = page.pageSize;handleQuery({page: page.current,size: page.pageSize});};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params};}, }); </script>
-
测试
问题:当前页面显示信息太乱,且有多余的列,需要优化
-
优化余票信息页面
-
daily-train-ticket.vue
<template><p><a-space><train-select-view v-model="params.trainCode" width="200px"></train-select-view><a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker><station-select-view v-model="params.start" width="200px"></station-select-view><station-select-view v-model="params.end" width="200px"></station-select-view><a-button type="primary" @click="handleQuery()">查找</a-button></a-space></p><a-table :dataSource="dailyTrainTickets":columns="columns":pagination="pagination"@change="handleTableChange":loading="loading"><template #bodyCell="{ column, record }"><template v-if="column.dataIndex === 'operation'"></template><template v-else-if="column.dataIndex === 'station'">{{record.start}}<br/>{{record.end}}</template><template v-else-if="column.dataIndex === 'time'">{{record.startTime}}<br/>{{record.endTime}}</template><template v-else-if="column.dataIndex === 'duration'">{{calDuration(record.startTime, record.endTime)}}<br/><div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">次日到达</div><div v-else>当日到达</div></template><template v-else-if="column.dataIndex === 'ydz'"><div v-if="record.ydz >= 0">{{record.ydz}}<br/>{{record.ydzPrice}}¥</div><div v-else>--</div></template><template v-else-if="column.dataIndex === 'edz'"><div v-if="record.edz >= 0">{{record.edz}}<br/>{{record.edzPrice}}¥</div><div v-else>--</div></template><template v-else-if="column.dataIndex === 'rw'"><div v-if="record.rw >= 0">{{record.rw}}<br/>{{record.rwPrice}}¥</div><div v-else>--</div></template><template v-else-if="column.dataIndex === 'yw'"><div v-if="record.yw >= 0">{{record.yw}}<br/>{{record.ywPrice}}¥</div><div v-else>--</div></template></template></a-table> </template><script> import { defineComponent, ref, onMounted } from 'vue'; import {notification} from "ant-design-vue"; import axios from "axios"; import TrainSelectView from "@/components/train-select"; import StationSelectView from "@/components/station-select"; import dayjs from "dayjs";export default defineComponent({name: "daily-train-ticket-view",components: {StationSelectView, TrainSelectView},setup() {const visible = ref(false);let dailyTrainTicket = ref({id: undefined,date: undefined,trainCode: undefined,start: undefined,startPinyin: undefined,startTime: undefined,startIndex: undefined,end: undefined,endPinyin: undefined,endTime: undefined,endIndex: undefined,ydz: undefined,ydzPrice: undefined,edz: undefined,edzPrice: undefined,rw: undefined,rwPrice: undefined,yw: undefined,ywPrice: undefined,createTime: undefined,updateTime: undefined,});const dailyTrainTickets = ref([]);// 分页的三个属性名是固定的const pagination = ref({total: 0,current: 1,pageSize: 10,});let loading = ref(false);const params = ref({});const columns = [{title: '日期',dataIndex: 'date',key: 'date',},{title: '车次编号',dataIndex: 'trainCode',key: 'trainCode',},{title: '车站',dataIndex: 'station',},{title: '时间',dataIndex: 'time',},{title: '历时',dataIndex: 'duration',},// {// title: '出发站',// dataIndex: 'start',// key: 'start',// },// {// title: '出发站拼音',// dataIndex: 'startPinyin',// key: 'startPinyin',// },// {// title: '出发时间',// dataIndex: 'startTime',// key: 'startTime',// },// {// title: '出发站序',// dataIndex: 'startIndex',// key: 'startIndex',// },// {// title: '到达站',// dataIndex: 'end',// key: 'end',// },// {// title: '到达站拼音',// dataIndex: 'endPinyin',// key: 'endPinyin',// },// {// title: '到站时间',// dataIndex: 'endTime',// key: 'endTime',// },// {// title: '到站站序',// dataIndex: 'endIndex',// key: 'endIndex',// },{title: '一等座',dataIndex: 'ydz',key: 'ydz',},// {// title: '一等座票价',// dataIndex: 'ydzPrice',// key: 'ydzPrice',// },{title: '二等座',dataIndex: 'edz',key: 'edz',},// {// title: '二等座票价',// dataIndex: 'edzPrice',// key: 'edzPrice',// },{title: '软卧',dataIndex: 'rw',key: 'rw',},// {// title: '软卧票价',// dataIndex: 'rwPrice',// key: 'rwPrice',// },{title: '硬卧',dataIndex: 'yw',key: 'yw',},// {// title: '硬卧票价',// dataIndex: 'ywPrice',// key: 'ywPrice',// },];const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/business/admin/daily-train-ticket/query-list", {params: {page: param.page,size: param.size,trainCode: params.value.trainCode,date: params.value.date,start: params.value.start,end: params.value.end}}).then((response) => {loading.value = false;let data = response.data;if (data.success) {dailyTrainTickets.value = data.content.list;// 设置分页控件的值pagination.value.current = param.page;pagination.value.total = data.content.total;} else {notification.error({description: data.message});}});};const handleTableChange = (page) => {// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));pagination.value.pageSize = page.pageSize;handleQuery({page: page.current,size: page.pageSize});};const calDuration = (startTime, endTime) => {let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {dailyTrainTicket,visible,dailyTrainTickets,pagination,columns,handleTableChange,handleQuery,loading,params,calDuration};}, }); </script>
-
测试
-
六、为会员端增余票查询功能
-
修改乘客管理页面,正常显示页面;修改前端模块的启动指令名称
-
passenger.vue
web/src/views/main/passenger.vue
<template><p><a-space><a-button type="primary" @click="handleQuery()">刷新</a-button><a-button type="primary" @click="onAdd">新增</a-button></a-space></p><a-table :dataSource="passengers":columns="columns":pagination="pagination"@change="handleTableChange":loading="loading"><template #bodyCell="{ column, record }"><template v-if="column.dataIndex === 'operation'"><a-space><a-popconfirmtitle="删除后不可恢复,确认删除?"@confirm="onDelete(record)"ok-text="确认" cancel-text="取消"><a style="color: red">删除</a></a-popconfirm><a @click="onEdit(record)">编辑</a></a-space></template><template v-else-if="column.dataIndex === 'type'"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === record.type">{{item.desc}}</span></span></template></template></a-table><a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"ok-text="确认" cancel-text="取消"><a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }"><a-form-item label="姓名"><a-input v-model:value="passenger.name" /></a-form-item><a-form-item label="身份证"><a-input v-model:value="passenger.idCard" /></a-form-item><a-form-item label="旅客类型"><a-select v-model:value="passenger.type"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-form-item></a-form></a-modal> </template><script> import { defineComponent, ref, onMounted } from 'vue'; import {notification} from "ant-design-vue"; import axios from "axios";export default defineComponent({name: "passenger-view",setup() {const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);let passenger = ref({id: undefined,memberId: undefined,name: undefined,idCard: undefined,type: undefined,createTime: undefined,updateTime: undefined,});const passengers = ref([]);// 分页的三个属性名是固定的const pagination = ref({total: 0,current: 1,pageSize: 10,});let loading = ref(false);const columns = [{title: '会员id',dataIndex: 'memberId',key: 'memberId',},{title: '姓名',dataIndex: 'name',key: 'name',},{title: '身份证',dataIndex: 'idCard',key: 'idCard',},{title: '旅客类型',dataIndex: 'type',key: 'type',},{title: '操作',dataIndex: 'operation'}];const onAdd = () => {passenger.value = {};visible.value = true;};const onEdit = (record) => {passenger.value = window.Tool.copy(record);visible.value = true;};const onDelete = (record) => {axios.delete("/member/passenger/delete/" + record.id).then((response) => {const data = response.data;if (data.success) {notification.success({description: "删除成功!"});handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {notification.error({description: data.message});}});};const handleOk = () => {axios.post("/member/passenger/save", passenger.value).then((response) => {let data = response.data;if (data.success) {notification.success({description: "保存成功!"});visible.value = false;handleQuery({page: pagination.value.current,size: pagination.value.pageSize});} else {notification.error({description: data.message});}});};const handleQuery = (param) => {if (!param) {param = {page: 1,size: pagination.value.pageSize};}loading.value = true;axios.get("/member/passenger/query-list", {params: {page: param.page,size: param.size}}).then((response) => {loading.value = false;let data = response.data;if (data.success) {passengers.value = data.content.list;// 设置分页控件的值pagination.value.current = param.page;pagination.value.total = data.content.total;} else {notification.error({description: data.message});}});};const handleTableChange = (pagination) => {// console.log("看看自带的分页参数都有啥:" + pagination);handleQuery({page: pagination.current,size: pagination.pageSize});};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize});});return {PASSENGER_TYPE_ARRAY,passenger,visible,passengers,pagination,columns,handleTableChange,handleQuery,loading,onAdd,handleOk,onEdit,onDelete};}, }); </script>
-
admin/package.json
"scripts": {"admin-dev": "vue-cli-service serve --mode dev --port 9001","admin-prod": "vue-cli-service serve --mode prod --port 9001","build": "vue-cli-service build","lint": "vue-cli-service lint" },
-
web/package.json
"private": true, "scripts": {"web-dev": "vue-cli-service serve --mode dev --port 9000","web-prod": "vue-cli-service serve --mode prod --port 9000","build": "vue-cli-service build","lint": "vue-cli-service lint" },
-
测试效果
-
-
会员端增加余票查询页面,功能和控台端完全一致
操作:将前面控台端的代码复制到用户端
-
DailyTrainTicketController.java
package com.neilxu.train.business.controller;import com.neilxu.train.business.req.DailyTrainTicketQueryReq; import com.neilxu.train.business.resp.DailyTrainTicketQueryResp; import com.neilxu.train.business.service.DailyTrainTicketService; import com.neilxu.train.common.resp.CommonResp; import com.neilxu.train.common.resp.PageResp; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/daily-train-ticket") public class DailyTrainTicketController {@Resourceprivate DailyTrainTicketService dailyTrainTicketService;@GetMapping("/query-list")public CommonResp<PageResp<DailyTrainTicketQueryResp>> queryList(@Valid DailyTrainTicketQueryReq req) {PageResp<DailyTrainTicketQueryResp> list = dailyTrainTicketService.queryList(req);return new CommonResp<>(list);}}
-
StationController.java
package com.neilxu.train.business.controller;import com.neilxu.train.business.resp.StationQueryResp; import com.neilxu.train.business.service.StationService; import com.neilxu.train.common.resp.CommonResp; import jakarta.annotation.Resource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController @RequestMapping("/station") public class StationController {@Resourceprivate StationService stationService;@GetMapping("/query-all")public CommonResp<List<StationQueryResp>> queryList() {List<StationQueryResp> list = stationService.queryAll();return new CommonResp<>(list);}}
-
TrainController.java
package com.neilxu.train.business.controller;import com.neilxu.train.business.resp.TrainQueryResp; import com.neilxu.train.business.service.TrainService; import com.neilxu.train.common.resp.CommonResp; import jakarta.annotation.Resource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController @RequestMapping("/train") public class TrainController {@Resourceprivate TrainService trainService;@GetMapping("/query-all")public CommonResp<List<TrainQueryResp>> queryList() {List<TrainQueryResp> list = trainService.queryAll();return new CommonResp<>(list);}}
-
station-select.vue
<template><a-select v-model:value="name" show-search allowClear:filterOption="filterNameOption"@change="onChange" placeholder="请选择车站":style="'width: ' + localWidth"><a-select-option v-for="item in stations" :key="item.name" :value="item.name" :label="item.name + item.namePinyin + item.namePy">{{item.name}} {{item.namePinyin}} ~ {{item.namePy}}</a-select-option></a-select> </template><script>import {defineComponent, onMounted, ref, watch} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "station-select-view",props: ["modelValue", "width"],emits: ['update:modelValue', 'change'],setup(props, {emit}) {const name = ref();const stations = ref([]);const localWidth = ref(props.width);if (Tool.isEmpty(props.width)) {localWidth.value = "100%";}// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效watch(() => props.modelValue, ()=>{console.log("props.modelValue", props.modelValue);name.value = props.modelValue;}, {immediate: true});/*** 查询所有的车站,用于车站下拉框*/const queryAllStation = () => {axios.get("/business/station/query-all").then((response) => {let data = response.data;if (data.success) {stations.value = data.content;} else {notification.error({description: data.message});}});};/*** 车站下拉框筛选*/const filterNameOption = (input, option) => {console.log(input, option);return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;};/*** 将当前组件的值响应给父组件* @param value*/const onChange = (value) => {emit('update:modelValue', value);let station = stations.value.filter(item => item.code === value)[0];if (Tool.isEmpty(station)) {station = {};}emit('change', station);};onMounted(() => {queryAllStation();});return {name,stations,filterNameOption,onChange,localWidth};}, }); </script>
-
train-select.vue
<template><a-select v-model:value="trainCode" show-search allowClear:filterOption="filterTrainCodeOption"@change="onChange" placeholder="请选择车次":style="'width: ' + localWidth"><a-select-option v-for="item in trains" :key="item.code" :value="item.code" :label="item.code + item.start + item.end">{{item.code}} {{item.start}} ~ {{item.end}}</a-select-option></a-select> </template><script>import {defineComponent, onMounted, ref, watch} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "train-select-view",props: ["modelValue", "width"],emits: ['update:modelValue', 'change'],setup(props, {emit}) {const trainCode = ref();const trains = ref([]);const localWidth = ref(props.width);if (Tool.isEmpty(props.width)) {localWidth.value = "100%";}// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效watch(() => props.modelValue, ()=>{console.log("props.modelValue", props.modelValue);trainCode.value = props.modelValue;}, {immediate: true});/*** 查询所有的车次,用于车次下拉框*/const queryAllTrain = () => {axios.get("/business/train/query-all").then((response) => {let data = response.data;if (data.success) {trains.value = data.content;} else {notification.error({description: data.message});}});};/*** 车次下拉框筛选*/const filterTrainCodeOption = (input, option) => {console.log(input, option);return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;};/*** 将当前组件的值响应给父组件* @param value*/const onChange = (value) => {emit('update:modelValue', value);let train = trains.value.filter(item => item.code === value)[0];if (Tool.isEmpty(train)) {train = {};}emit('change', train);};onMounted(() => {queryAllTrain();});return {trainCode,trains,filterTrainCodeOption,onChange,localWidth};}, }); </script>
-
the-header.vue和the-sider.vue
<a-menu-item key="/ticket"><router-link to="/ticket"><user-outlined /> 余票查询</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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> </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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <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> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/>选座类型chooseSeatType:{{chooseSeatType}}<br/>选座对象chooseSeatType:{{chooseSeatObj}}<br/>座位类型SEAT_COL_ARRAY:{{SEAT_COL_ARRAY}}</div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; } </style>
-
效果
3.根据购票列表,展示选座按钮
-
order.vue
<template><div class="order-train"><span class="order-train-main">{{dailyTrainTicket.date}}</span> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/>选座对象chooseSeatType:{{chooseSeatObj}}<br/><div v-if="chooseSeatType === 0" style="color: red;">您购买的车票不支持选座<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div><div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div></div><div v-else style="text-align: center"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" /><div v-if="tickets.length > 1"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" /></div><div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div></div></div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; }.order-tickets .choose-seat-item {margin: 5px 5px; } </style>
-
效果
4.余票小于20张时,不允许选座
-
order.vue
<template><div class="order-train"><span class="order-train-main">{{dailyTrainTicket.date}}</span> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/><div v-if="chooseSeatType === 0" style="color: red;">您购买的车票不支持选座<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div><div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div></div><div v-else style="text-align: center"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" /><div v-if="tickets.length > 1"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" /></div><div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div></div></div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票if (chooseSeatType.value !== 0) {for (let i = 0; i < seatTypes.length; i++) {let seatType = seatTypes[i];// 找到同类型座位if (ticketSeatTypeCodesSet[0] === seatType.code) {// 判断余票,小于20张就不支持选座if (seatType.count < 20) {console.log("余票小于20张就不支持选座")chooseSeatType.value = 0;break;}}}}}// 弹出确认界面visible.value = true;};onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; }.order-tickets .choose-seat-item {margin: 5px 5px; } </style>
5.确认提交时,计算出最终每个乘客所选的座位
-
order.vue
<template><div class="order-train"><span class="order-train-main">{{dailyTrainTicket.date}}</span> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"@ok="handleOk"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/><div v-if="chooseSeatType === 0" style="color: red;">您购买的车票不支持选座<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div><div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div></div><div v-else style="text-align: center"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" /><div v-if="tickets.length > 1"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" /></div><div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div></div><br/>最终购票:{{tickets}}</div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票if (chooseSeatType.value !== 0) {for (let i = 0; i < seatTypes.length; i++) {let seatType = seatTypes[i];// 找到同类型座位if (ticketSeatTypeCodesSet[0] === seatType.code) {// 判断余票,小于20张就不支持选座if (seatType.count < 20) {console.log("余票小于20张就不支持选座")chooseSeatType.value = 0;break;}}}}}// 弹出确认界面visible.value = true;};const handleOk = () => {console.log("选好的座位:", chooseSeatObj.value);// 设置每张票的座位// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍for (let i = 0; i < tickets.value.length; i++) {tickets.value[i].seat = null;}let i = -1;// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)for (let key in chooseSeatObj.value) {if (chooseSeatObj.value[key]) {i++;if (i > tickets.value.length - 1) {notification.error({description: '所选座位数大于购票数'});return;}tickets.value[i].seat = key;}}if (i > -1 && i < (tickets.value.length - 1)) {notification.error({description: '所选座位数小于购票数'});return;}console.log("最终购票:", tickets.value);}onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,handleOk,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; }.order-tickets .choose-seat-item {margin: 5px 5px; } </style>
-
效果
6.chooseSeatObj先清空,再初始化,保证两排座位是有序的
-
order.vue
<template><div class="order-train"><span class="order-train-main">{{dailyTrainTicket.date}}</span> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"@ok="handleOk"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/><div v-if="chooseSeatType === 0" style="color: red;">您购买的车票不支持选座<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div><div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div></div><div v-else style="text-align: center"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" /><div v-if="tickets.length > 1"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" /></div><div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div></div><br/>最终购票:{{tickets}}最终选座:{{chooseSeatObj}}</div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1",// seat: "C1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {chooseSeatObj.value = {};for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票if (chooseSeatType.value !== 0) {for (let i = 0; i < seatTypes.length; i++) {let seatType = seatTypes[i];// 找到同类型座位if (ticketSeatTypeCodesSet[0] === seatType.code) {// 判断余票,小于20张就不支持选座if (seatType.count < 20) {console.log("余票小于20张就不支持选座")chooseSeatType.value = 0;break;}}}}}// 弹出确认界面visible.value = true;};const handleOk = () => {console.log("选好的座位:", chooseSeatObj.value);// 设置每张票的座位// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍for (let i = 0; i < tickets.value.length; i++) {tickets.value[i].seat = null;}let i = -1;// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)for (let key in chooseSeatObj.value) {if (chooseSeatObj.value[key]) {i++;if (i > tickets.value.length - 1) {notification.error({description: '所选座位数大于购票数'});return;}tickets.value[i].seat = key;}}if (i > -1 && i < (tickets.value.length - 1)) {notification.error({description: '所选座位数小于购票数'});return;}console.log("最终购票:", tickets.value);}onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,handleOk,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; }.order-tickets .choose-seat-item {margin: 5px 5px; } </style>
-
效果
十、增加确认订单表并生成前后端代码
-
新增表
sql/business.sql
这里的车票使用json类型,实际上也可以使用子表来做
drop table if exists `confirm_order`; create table `confirm_order` (`id` bigint not null comment 'id',`member_id` bigint not null comment '会员id',`date` date not null comment '日期',`train_code` varchar(20) not null comment '车次编号',`start` varchar(20) not null comment '出发站',`end` varchar(20) not null comment '到达站',`daily_train_ticket_id` bigint not null comment '余票ID',`tickets` json not null comment '车票',`status` char(1) not null comment '订单状态|枚举[ConfirmOrderStatusEnum]',`create_time` datetime(3) comment '新增时间',`update_time` datetime(3) comment '修改时间',primary key (`id`),index `date_train_code_index` (`date`, `train_code`) ) engine=innodb default charset=utf8mb4 comment='确认订单';
-
ConfirmOrderStatusEnum
enum也是可以用lombok注解的
package com.neilxu.train.business.enums;public enum ConfirmOrderStatusEnum {INIT("I", "初始"),PENDING("P", "处理中"),SUCCESS("S", "成功"),FAILURE("F", "失败"),EMPTY("E", "无票"),CANCEL("C", "取消");private String code;private String desc;ConfirmOrderStatusEnum(String code, String desc) {this.code = code;this.desc = desc;}@Override public String toString() {return "ConfirmOrderStatusEnum{" +"code='" + code + '\'' +", desc='" + desc + '\'' +"} " + super.toString();}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public void setDesc(String desc) {this.desc = desc;}public String getDesc() {return desc;}}
-
修改generator-config-business.xml、ServerGenerator.java、EnumGenerator.java生成代码
操作同之前,注意是admin
-
修改路由、侧边栏
操作同之前,注意是admin
-
效果
十一、后端增加确认下单购票接口
这节整理下后端确认下单购票接口的逻辑
-
com.neilxu.train.business.req.ConfirmOrderTicketReq
package com.neilxu.train.business.req;import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data;@Data public class ConfirmOrderTicketReq {/*** 乘客ID*/@NotNull(message = "【乘客ID】不能为空")private Long passengerId;/*** 乘客票种*/@NotBlank(message = "【乘客票种】不能为空")private String passengerType;/*** 乘客名称*/@NotBlank(message = "【乘客名称】不能为空")private String passengerName;/*** 乘客身份证*/@NotBlank(message = "【乘客身份证】不能为空")private String passengerIdCard;/*** 座位类型code*/@NotBlank(message = "【座位类型code】不能为空")private String seatTypeCode;/*** 选座,可空,值示例:A1*/private String seat;}
-
ConfirmOrderSaveReq.java ——> ConfirmOrderDoReq.java
package com.neilxu.train.business.req;import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data;import java.util.Date; import java.util.List;@Data public class ConfirmOrderDoReq {/*** 会员id*/@NotNull(message = "【会员id】不能为空")private Long memberId;/*** 日期*/@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")@NotNull(message = "【日期】不能为空")private Date date;/*** 车次编号*/@NotBlank(message = "【车次编号】不能为空")private String trainCode;/*** 出发站*/@NotBlank(message = "【出发站】不能为空")private String start;/*** 到达站*/@NotBlank(message = "【到达站】不能为空")private String end;/*** 余票ID*/@NotNull(message = "【余票ID】不能为空")private Long dailyTrainTicketId;/*** 车票*/@NotBlank(message = "【车票】不能为空")private List<ConfirmOrderTicketReq> tickets;@Overridepublic String toString() {return "ConfirmOrderDoReq{" +"memberId=" + memberId +", date=" + date +", trainCode='" + trainCode + '\'' +", start='" + start + '\'' +", end='" + end + '\'' +", dailyTrainTicketId=" + dailyTrainTicketId +", tickets=" + tickets +'}';} }
-
ConfirmOrderAdminController.java
package com.neilxu.train.business.controller.admin;import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.service.ConfirmOrderService; import com.neilxu.train.common.resp.CommonResp; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;;@RestController @RequestMapping("/admin/confirm-order") public class ConfirmOrderAdminController {@Resourceprivate ConfirmOrderService confirmOrderService;@PostMapping("/save")public CommonResp<Object> save(@Valid @RequestBody ConfirmOrderDoReq req) {confirmOrderService.save(req);return new CommonResp<>();} }
-
ConfirmOrderService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.ObjectUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import com.neilxu.train.business.domain.ConfirmOrder; import com.neilxu.train.business.domain.ConfirmOrderExample; import com.neilxu.train.business.mapper.ConfirmOrderMapper; import com.neilxu.train.business.req.ConfirmOrderQueryReq; import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.resp.ConfirmOrderQueryResp; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service;import java.util.List;@Service public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {//省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,ticket条数>0,同乘客同车次是否已经买过//保存确认订单表,状态初始//查出余票记录,需要得到真实的库存//扣减余票数量,并判断余票是否足够//选座//一个车厢一个车厢的获取座位数据//挑选符合条件的座位,如果这个车厢不满足,则进入下一个车厢(多个选座应该在同一车厢)//选中座位后事务处理//座位表修改售卖情况sell//余票详情表修改余票//为会员增加购票记录//更新确认订单为成功} }
-
ConfirmOrderController.java
package com.neilxu.train.business.controller;import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.service.ConfirmOrderService; import com.neilxu.train.common.resp.CommonResp; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/confirm-order") public class ConfirmOrderController {@Resourceprivate ConfirmOrderService confirmOrderService;@PostMapping("/do")public CommonResp<Object> doConfirm(@Valid @RequestBody ConfirmOrderDoReq req) {confirmOrderService.doConfirm(req);return new CommonResp<>();}}
-
order.vue
<template><div class="order-train"><span class="order-train-main">{{dailyTrainTicket.date}}</span> <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站<span class="order-train-main">({{dailyTrainTicket.startTime}})</span> <span class="order-train-main">——</span> <span class="order-train-main">{{dailyTrainTicket.end}}</span>站<span class="order-train-main">({{dailyTrainTicket.endTime}})</span> <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> <span class="order-train-ticket-main">{{item.count}}</span> 张票 </span></div></div><a-divider></a-divider><b>勾选要购票的乘客:</b> <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" /><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="2">乘客</a-col><a-col :span="6">身份证</a-col><a-col :span="4">票种</a-col><a-col :span="4">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="2">{{ticket.passengerName}}</a-col><a-col :span="6">{{ticket.passengerIdCard}}</a-col><a-col :span="4"><a-select v-model:value="ticket.passengerType" style="width: 100%"><a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col><a-col :span="4"><a-select v-model:value="ticket.seatTypeCode" style="width: 100%"><a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">{{item.desc}}</a-select-option></a-select></a-col></a-row></div><div v-if="tickets.length > 0"><a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button></div><a-modal v-model:visible="visible" title="请核对以下信息"style="top: 50px; width: 800px"ok-text="确认" cancel-text="取消"@ok="handleOk"><div class="order-tickets"><a-row class="order-tickets-header" v-if="tickets.length > 0"><a-col :span="3">乘客</a-col><a-col :span="15">身份证</a-col><a-col :span="3">票种</a-col><a-col :span="3">座位类型</a-col></a-row><a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId"><a-col :span="3">{{ticket.passengerName}}</a-col><a-col :span="15">{{ticket.passengerIdCard}}</a-col><a-col :span="3"><span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code"><span v-if="item.code === ticket.passengerType">{{item.desc}}</span></span></a-col><a-col :span="3"><span v-for="item in seatTypes" :key="item.code"><span v-if="item.code === ticket.seatTypeCode">{{item.desc}}</span></span></a-col></a-row><br/><div v-if="chooseSeatType === 0" style="color: red;">您购买的车票不支持选座<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div><div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div></div><div v-else style="text-align: center"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" /><div v-if="tickets.length > 1"><a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" /></div><div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div></div><br/>最终购票:{{tickets}}最终选座:{{chooseSeatObj}}</div></a-modal> </template><script>import {defineComponent, ref, onMounted, watch, computed} from 'vue'; import axios from "axios"; import {notification} from "ant-design-vue";export default defineComponent({name: "order-view",setup() {const passengers = ref([]);const passengerOptions = ref([]);const passengerChecks = ref([]);const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};console.log("下单的车次信息", dailyTrainTicket);const SEAT_TYPE = window.SEAT_TYPE;console.log(SEAT_TYPE)// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:// {// type: "YDZ",// code: "1",// desc: "一等座",// count: "100",// price: "50",// }// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]const seatTypes = [];for (let KEY in SEAT_TYPE) {let key = KEY.toLowerCase();if (dailyTrainTicket[key] >= 0) {seatTypes.push({type: KEY,code: SEAT_TYPE[KEY]["code"],desc: SEAT_TYPE[KEY]["desc"],count: dailyTrainTicket[key],price: dailyTrainTicket[key + 'Price'],})}}console.log("本车次提供的座位:", seatTypes)// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票// {// passengerId: 123,// passengerType: "1",// passengerName: "张三",// passengerIdCard: "12323132132",// seatTypeCode: "1",// seat: "C1"// }const tickets = ref([]);const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;const visible = ref(false);// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表watch(() => passengerChecks.value, (newVal, oldVal)=>{console.log("勾选乘客发生变化", newVal, oldVal)// 每次有变化时,把购票列表清空,重新构造列表tickets.value = [];passengerChecks.value.forEach((item) => tickets.value.push({passengerId: item.id,passengerType: item.type,seatTypeCode: seatTypes[0].code,passengerName: item.name,passengerIdCard: item.idCard}))}, {immediate: true});// 0:不支持选座;1:选一等座;2:选二等座const chooseSeatType = ref(0);// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDFconst SEAT_COL_ARRAY = computed(() => {return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);});// 选择的座位// {// A1: false, C1: true,D1: false, F1: false,// A2: false, C2: false,D2: true, F2: false// }const chooseSeatObj = ref({});watch(() => SEAT_COL_ARRAY.value, () => {chooseSeatObj.value = {};for (let i = 1; i <= 2; i++) {SEAT_COL_ARRAY.value.forEach((item) => {chooseSeatObj.value[item.code + i] = false;})}console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);}, {immediate: true});const handleQueryPassenger = () => {axios.get("/member/passenger/query-mine").then((response) => {let data = response.data;if (data.success) {passengers.value = data.content;passengers.value.forEach((item) => passengerOptions.value.push({label: item.name,value: item}))} else {notification.error({description: data.message});}});};const finishCheckPassenger = () => {console.log("购票列表:", tickets.value);if (tickets.value.length > 5) {notification.error({description: '最多只能购买5张车票'});return;}// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足// 前端校验不一定准,但前端校验可以减轻后端很多压力// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存let seatTypesTemp = Tool.copy(seatTypes);for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];for (let j = 0; j < seatTypesTemp.length; j++) {let seatType = seatTypesTemp[j];// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验if (ticket.seatTypeCode === seatType.code) {seatType.count--;if (seatType.count < 0) {notification.error({description: seatType.desc + '余票不足'});return;}}}}console.log("前端余票校验通过");// 判断是否支持选座,只有纯一等座和纯二等座支持选座// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]let ticketSeatTypeCodes = [];for (let i = 0; i < tickets.value.length; i++) {let ticket = tickets.value[i];ticketSeatTypeCodes.push(ticket.seatTypeCode);}// 为购票列表中的所有座位类型去重:[1, 2]const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));console.log("选好的座位类型:", ticketSeatTypeCodesSet);if (ticketSeatTypeCodesSet.length !== 1) {console.log("选了多种座位,不支持选座");chooseSeatType.value = 0;} else {// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {console.log("一等座选座");chooseSeatType.value = SEAT_TYPE.YDZ.code;} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {console.log("二等座选座");chooseSeatType.value = SEAT_TYPE.EDZ.code;} else {console.log("不是一等座或二等座,不支持选座");chooseSeatType.value = 0;}// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票if (chooseSeatType.value !== 0) {for (let i = 0; i < seatTypes.length; i++) {let seatType = seatTypes[i];// 找到同类型座位if (ticketSeatTypeCodesSet[0] === seatType.code) {// 判断余票,小于20张就不支持选座if (seatType.count < 20) {console.log("余票小于20张就不支持选座")chooseSeatType.value = 0;break;}}}}}// 弹出确认界面visible.value = true;};const handleOk = () => {console.log("选好的座位:", chooseSeatObj.value);// 设置每张票的座位// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍for (let i = 0; i < tickets.value.length; i++) {tickets.value[i].seat = null;}let i = -1;// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)for (let key in chooseSeatObj.value) {if (chooseSeatObj.value[key]) {i++;if (i > tickets.value.length - 1) {notification.error({description: '所选座位数大于购票数'});return;}tickets.value[i].seat = key;}}if (i > -1 && i < (tickets.value.length - 1)) {notification.error({description: '所选座位数小于购票数'});return;}console.log("最终购票:", tickets.value);axios.post("/business/confirm-order/do", {dailyTrainTicketId: dailyTrainTicket.id,date: dailyTrainTicket.date,trainCode: dailyTrainTicket.trainCode,start: dailyTrainTicket.start,end: dailyTrainTicket.end,tickets: tickets.value}).then((response) => {let data = response.data;if (data.success) {notification.success({description: "下单成功!"});} else {notification.error({description: data.message});}});}onMounted(() => {handleQueryPassenger();});return {passengers,dailyTrainTicket,seatTypes,passengerOptions,passengerChecks,tickets,PASSENGER_TYPE_ARRAY,visible,finishCheckPassenger,chooseSeatType,chooseSeatObj,SEAT_COL_ARRAY,handleOk,};}, }); </script><style> .order-train .order-train-main {font-size: 18px;font-weight: bold; } .order-train .order-train-ticket {margin-top: 15px; } .order-train .order-train-ticket .order-train-ticket-main {color: red;font-size: 18px; }.order-tickets {margin: 10px 0; } .order-tickets .ant-col {padding: 5px 10px; } .order-tickets .order-tickets-header {background-color: cornflowerblue;border: solid 1px cornflowerblue;color: white;font-size: 16px;padding: 5px 0; } .order-tickets .order-tickets-row {border: solid 1px cornflowerblue;border-top: none;vertical-align: middle;line-height: 30px; }.order-tickets .choose-seat-item {margin: 5px 5px; } </style>
十二、确认下单接口数据初始化
小tips:
ctrl + alt + v 可以快速提取变量
-
DailyTrainTicketService.java
public DailyTrainTicket selectByUnique(Date date, String trainCode, String start, String end) {DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();dailyTrainTicketExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode).andStartEqualTo(start).andEndEqualTo(end);List<DailyTrainTicket> list = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);if (CollUtil.isNotEmpty(list)) {return list.get(0);} else {return null;} }
-
ConfirmOrderService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.ObjectUtil; import com.alibaba.fastjson.JSON; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.enums.ConfirmOrderStatusEnum; import com.neilxu.train.common.context.LoginMemberContext; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import com.neilxu.train.business.domain.ConfirmOrder; import com.neilxu.train.business.domain.ConfirmOrderExample; import com.neilxu.train.business.mapper.ConfirmOrderMapper; import com.neilxu.train.business.req.ConfirmOrderQueryReq; import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.resp.ConfirmOrderQueryResp; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service;import java.util.Date; import java.util.List;@Service public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过Date date = req.getDate();String trainCode = req.getTrainCode();String start = req.getStart();String end = req.getEnd();// 保存确认订单表,状态初始DateTime now = DateTime.now();ConfirmOrder confirmOrder = new ConfirmOrder();confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrder.setMemberId(LoginMemberContext.getId());confirmOrder.setDate(date);confirmOrder.setTrainCode(trainCode);confirmOrder.setStart(start);confirmOrder.setEnd(end);confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());confirmOrder.setTickets(JSON.toJSONString(req.getTickets()));confirmOrderMapper.insert(confirmOrder);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 扣减余票数量,并判断余票是否足够// 选座// 一个车厢一个车厢的获取座位数据// 挑选符合条件的座位,如果这个车厢不满足,则进入下个车厢(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功} }
十三、预扣减库存并判断余票是否足够
-
BusinessExceptionEnum.java
CONFIRM_ORDER_TICKET_COUNT_ERROR("余票不足"),
-
ConfirmOrderService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjectUtil; import com.alibaba.fastjson.JSON; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.ConfirmOrder; import com.neilxu.train.business.domain.ConfirmOrderExample; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.enums.ConfirmOrderStatusEnum; import com.neilxu.train.business.enums.SeatTypeEnum; import com.neilxu.train.business.mapper.ConfirmOrderMapper; import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.req.ConfirmOrderQueryReq; import com.neilxu.train.business.req.ConfirmOrderTicketReq; import com.neilxu.train.business.resp.ConfirmOrderQueryResp; import com.neilxu.train.common.context.LoginMemberContext; import com.neilxu.train.common.exception.BusinessException; import com.neilxu.train.common.exception.BusinessExceptionEnum; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service;import java.util.Date; import java.util.List;@Service public class ConfirmOrderService {private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过Date date = req.getDate();String trainCode = req.getTrainCode();String start = req.getStart();String end = req.getEnd();// 保存确认订单表,状态初始DateTime now = DateTime.now();ConfirmOrder confirmOrder = new ConfirmOrder();confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrder.setMemberId(LoginMemberContext.getId());confirmOrder.setDate(date);confirmOrder.setTrainCode(trainCode);confirmOrder.setStart(start);confirmOrder.setEnd(end);confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());confirmOrder.setTickets(JSON.toJSONString(req.getTickets()));confirmOrderMapper.insert(confirmOrder);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 预扣减余票数量,并判断余票是否足够reduceTickets(req, dailyTrainTicket);// 选座// 一个车箱一个车箱的获取座位数据// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功}private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {String seatTypeCode = ticketReq.getSeatTypeCode();SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);switch (seatTypeEnum) {case YDZ -> {int countLeft = dailyTrainTicket.getYdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYdz(countLeft);}case EDZ -> {int countLeft = dailyTrainTicket.getEdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setEdz(countLeft);}case RW -> {int countLeft = dailyTrainTicket.getRw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setRw(countLeft);}case YW -> {int countLeft = dailyTrainTicket.getYw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYw(countLeft);}}}} }
十四、计算多个选座之间的偏移值
-
ConfirmOrderService.java
package com.neilxu.train.business.service;import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.neilxu.train.business.domain.ConfirmOrder; import com.neilxu.train.business.domain.ConfirmOrderExample; import com.neilxu.train.business.domain.DailyTrainTicket; import com.neilxu.train.business.enums.ConfirmOrderStatusEnum; import com.neilxu.train.business.enums.SeatColEnum; import com.neilxu.train.business.enums.SeatTypeEnum; import com.neilxu.train.business.mapper.ConfirmOrderMapper; import com.neilxu.train.business.req.ConfirmOrderDoReq; import com.neilxu.train.business.req.ConfirmOrderQueryReq; import com.neilxu.train.business.req.ConfirmOrderTicketReq; import com.neilxu.train.business.resp.ConfirmOrderQueryResp; import com.neilxu.train.common.context.LoginMemberContext; import com.neilxu.train.common.exception.BusinessException; import com.neilxu.train.common.exception.BusinessExceptionEnum; import com.neilxu.train.common.resp.PageResp; import com.neilxu.train.common.util.SnowUtil; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service;import java.util.ArrayList; import java.util.Date; import java.util.List;@Service public class ConfirmOrderService {public static final ArrayList<Object> OBJECT = new ArrayList<>();private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);@Resourceprivate ConfirmOrderMapper confirmOrderMapper;@Resourceprivate DailyTrainTicketService dailyTrainTicketService;public void save(ConfirmOrderDoReq req) {DateTime now = DateTime.now();ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);if (ObjectUtil.isNull(confirmOrder.getId())) {confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrderMapper.insert(confirmOrder);} else {confirmOrder.setUpdateTime(now);confirmOrderMapper.updateByPrimaryKey(confirmOrder);}}public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();confirmOrderExample.setOrderByClause("id desc");ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();LOG.info("查询页码:{}", req.getPage());LOG.info("每页条数:{}", req.getSize());PageHelper.startPage(req.getPage(), req.getSize());List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);LOG.info("总行数:{}", pageInfo.getTotal());LOG.info("总页数:{}", pageInfo.getPages());List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(list);return pageResp;}public void delete(Long id) {confirmOrderMapper.deleteByPrimaryKey(id);}public void doConfirm(ConfirmOrderDoReq req) {// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过Date date = req.getDate();String trainCode = req.getTrainCode();String start = req.getStart();String end = req.getEnd();List<ConfirmOrderTicketReq> tickets = req.getTickets();// 保存确认订单表,状态初始DateTime now = DateTime.now();ConfirmOrder confirmOrder = new ConfirmOrder();confirmOrder.setId(SnowUtil.getSnowflakeNextId());confirmOrder.setCreateTime(now);confirmOrder.setUpdateTime(now);confirmOrder.setMemberId(LoginMemberContext.getId());confirmOrder.setDate(date);confirmOrder.setTrainCode(trainCode);confirmOrder.setStart(start);confirmOrder.setEnd(end);confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());confirmOrder.setTickets(JSON.toJSONString(tickets));confirmOrderMapper.insert(confirmOrder);// 查出余票记录,需要得到真实的库存DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);LOG.info("查出余票记录:{}", dailyTrainTicket);// 预扣减余票数量,并判断余票是否足够reduceTickets(req, dailyTrainTicket);// 计算相对第一个座位的偏移值// 比如选择的是C1,D2,则偏移值是:[0,5]// 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]ConfirmOrderTicketReq ticketReq0 = tickets.get(0);if(StrUtil.isNotBlank(ticketReq0.getSeat())) {LOG.info("本次购票有选座");// 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());LOG.info("本次选座的座位类型包含的列:{}", colEnumList);// 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}List<String> referSeatList = new ArrayList<>();for (int i = 1; i <= 2; i++) {for (SeatColEnum seatColEnum : colEnumList) {referSeatList.add(seatColEnum.getCode() + i);}}LOG.info("用于作参照的两排座位:{}", referSeatList);List<Integer> offsetList = new ArrayList<>();// 绝对偏移值,即:在参照座位列表中的位置List<Integer> aboluteOffsetList = new ArrayList<>();for (ConfirmOrderTicketReq ticketReq : tickets) {int index = referSeatList.indexOf(ticketReq.getSeat());aboluteOffsetList.add(index);}LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);for (Integer index : aboluteOffsetList) {int offset = index - aboluteOffsetList.get(0);offsetList.add(offset);}LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);} else {LOG.info("本次购票没有选座");}// 选座// 一个车箱一个车箱的获取座位数据// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)// 选中座位后事务处理:// 座位表修改售卖情况sell;// 余票详情表修改余票;// 为会员增加购票记录// 更新确认订单为成功}private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {String seatTypeCode = ticketReq.getSeatTypeCode();SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);switch (seatTypeEnum) {case YDZ -> {int countLeft = dailyTrainTicket.getYdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYdz(countLeft);}case EDZ -> {int countLeft = dailyTrainTicket.getEdz() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setEdz(countLeft);}case RW -> {int countLeft = dailyTrainTicket.getRw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setRw(countLeft);}case YW -> {int countLeft = dailyTrainTicket.getYw() - 1;if (countLeft < 0) {throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);}dailyTrainTicket.setYw(countLeft);}}}} }
-
测试