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