内容
- 余票查询(控台端)
余票初始化、余票查询
- 选座购票(会员端)
余票查询、选择乘客、选择座位类型、选择座位、下单购票
增加余票信息表以提高余票查询性能
第一步:建表
1 drop table if exists `daily_train_ticket`; 2 create table `daily_train_ticket` ( 3 `id` bigint not null comment 'id', 4 `date` date not null comment '日期', 5 `train_code` varchar(20) not null comment '车次编号', 6 `start` varchar(20) not null comment '出发站', 7 `start_pinyin` varchar(50) not null comment '出发站拼音', 8 `start_time` time not null comment '出发时间', 9 `start_index` int not null comment '出发站序|本站是整个车次的第几站', 10 `end` varchar(20) not null comment '到达站', 11 `end_pinyin` varchar(50) not null comment '到达站拼音', 12 `end_time` time not null comment '到站时间', 13 `end_index` int not null comment '到站站序|本站是整个车次的第几站', 14 `ydz` int not null comment '一等座余票', 15 `ydz_price` decimal(8, 2) not null comment '一等座票价', 16 `edz` int not null comment '二等座余票', 17 `edz_price` decimal(8, 2) not null comment '二等座票价', 18 `rw` int not null comment '软卧余票', 19 `rw_price` decimal(8, 2) not null comment '软卧票价', 20 `yw` int not null comment '硬卧余票', 21 `yw_price` decimal(8, 2) not null comment '硬卧票价', 22 `create_time` datetime(3) comment '新增时间', 23 `update_time` datetime(3) comment '修改时间', 24 primary key (`id`), 25 unique key `date_train_code_start_end_unique` (`date`, `train_code`, `start`, `end`) 26 ) engine=innodb default charset=utf8mb4 comment='余票信息';
比如说有5个座位ABCDE,那么可售区间有4个,1111就是所有区间已售空。0000就是所有区间未售。如果想要买A-C的座位,如果A-C的售票区间中包含1,那么则不可售(A-B为1,B-C为0,那么A-C肯定不可售)。
余票查询会显示还有多少张票,票数如果实时通过每日座位表的sell来计算,会影响性能,所以要另外做张表,直接存储余票数。
一个火车经过5个站,就能生成10个余票信息(4+3+2+1),会影响10个余票记录。
难点:如何把售票信息转换为单表的信息
第二步:用代码生成器生成dao、service、controller、请求和响应、前端页面。
生成车次时初始化余票信息
问题:
- 表的数据什么时候初始化
车次生成,该表应该也生成,也就是每日自动生成。
- 数据是怎么来的(出发站、到达站怎么形成?不同座位类型车票数怎么来)
G1 ABCDE
G2 CD
此时用户查找CD会出现G1、G2。
车站是一个嵌套循环
AB BC CD DE
AC BD CE
AD BE
AE
车票就把每日座位的记录copy一下
DailyTrainTicketService需要增加genDaily方法,用来创建哪一天哪个车次的数据,用来初始化车站。
1 @Transactional 2 public void genDaily(Date date, String trainCode) { 3 LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode); 4 5 // 删除某日某车次的余票信息 6 DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample(); 7 dailyTrainTicketExample.createCriteria() 8 .andDateEqualTo(date) 9 .andTrainCodeEqualTo(trainCode); 10 dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample); 11 12 // 查出某车次的所有的车站信息 13 List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode); 14 if (CollUtil.isEmpty(stationList)) { 15 LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束"); 16 return; 17 } 18 19 DateTime now = DateTime.now(); 20 for (int i = 0; i < stationList.size(); i++) { 21 // 得到出发站 22 TrainStation trainStationStart = stationList.get(i); 23 for (int j = (i + 1); j < stationList.size(); j++) { 24 TrainStation trainStationEnd = stationList.get(j); 25 26 DailyTrainTicket dailyTrainTicket = new DailyTrainTicket(); 27 28 dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId()); 29 dailyTrainTicket.setDate(date); 30 dailyTrainTicket.setTrainCode(trainCode); 31 dailyTrainTicket.setStart(trainStationStart.getName()); 32 dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin()); 33 dailyTrainTicket.setStartTime(trainStationStart.getOutTime()); 34 dailyTrainTicket.setStartIndex(trainStationStart.getIndex()); 35 dailyTrainTicket.setEnd(trainStationEnd.getName()); 36 dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin()); 37 dailyTrainTicket.setEndTime(trainStationEnd.getInTime()); 38 dailyTrainTicket.setEndIndex(trainStationEnd.getIndex()); 39 dailyTrainTicket.setYdz(0); 40 dailyTrainTicket.setYdzPrice(BigDecimal.ZERO); 41 dailyTrainTicket.setEdz(0); 42 dailyTrainTicket.setEdzPrice(BigDecimal.ZERO); 43 dailyTrainTicket.setRw(0); 44 dailyTrainTicket.setRwPrice(BigDecimal.ZERO); 45 dailyTrainTicket.setYw(0); 46 dailyTrainTicket.setYwPrice(BigDecimal.ZERO); 47 dailyTrainTicket.setCreateTime(now); 48 dailyTrainTicket.setUpdateTime(now); 49 dailyTrainTicketMapper.insert(dailyTrainTicket); 50 } 51 } 52 LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode); 53 54 }
一定要加事务@TranSaction,防止生成失败。
下面生成余票数量
只要计算在每日车次表中每一类座位的数量,因此在DailyTrainSeatService中添加countSeat函数,如果没有该座位类型,则返回-1。
1 public int countSeat(Date date, String trainCode, String seatType) { 2 DailyTrainSeatExample example = new DailyTrainSeatExample(); 3 example.createCriteria() 4 .andDateEqualTo(date) 5 .andTrainCodeEqualTo(trainCode) 6 .andSeatTypeEqualTo(seatType); 7 long l = dailyTrainSeatMapper.countByExample(example); 8 if (l == 0L) { 9 return -1; 10 } 11 return (int) l; 12 }
DailyTrainTicketService就可以调用该方法。
1 int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode()); 2 int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode()); 3 int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode()); 4 int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode()); 5 // 票价 = 里程之和 * 座位单价 * 车次类型系数 6 String trainType = dailyTrain.getType(); 7 // 计算票价系数:TrainTypeEnum.priceRate 8 BigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType); 9 BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP); 10 BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP); 11 BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP); 12 BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
将dailyTrainTicketService.genDaily(dailyTrain, date, train.getCode())方法注入DailyTrainService.java。
对于车票查询,增加四个判断
1 if (ObjectUtil.isNotNull(req.getDate())) { 2 criteria.andDateEqualTo(req.getDate()); 3 } 4 if (ObjectUtil.isNotEmpty(req.getTrainCode())) { 5 criteria.andTrainCodeEqualTo(req.getTrainCode()); 6 } 7 if (ObjectUtil.isNotEmpty(req.getStart())) { 8 criteria.andStartEqualTo(req.getStart()); 9 } 10 if (ObjectUtil.isNotEmpty(req.getEnd())) { 11 criteria.andEndEqualTo(req.getEnd()); 12 }
为会员端增加余票查询功能
businiss的controller增加一个admin包,用来给web访问。
会员端只能通过日期、始发站、终点站查询。查询功能和上面差不多,只是要通过三个条件查询。
1 package com.zihans.train.business.controller; 2 3 import com.zihans.train.business.req.DailyTrainTicketQueryReq; 4 import com.zihans.train.business.resp.DailyTrainTicketQueryResp; 5 import com.zihans.train.business.service.DailyTrainTicketService; 6 import com.zihans.train.common.resp.CommonResp; 7 import com.zihans.train.common.resp.PageResp; 8 import jakarta.annotation.Resource; 9 import jakarta.validation.Valid; 10 import org.springframework.web.bind.annotation.GetMapping; 11 import org.springframework.web.bind.annotation.RequestMapping; 12 import org.springframework.web.bind.annotation.RestController; 13 14 @RestController 15 @RequestMapping("/daily-train-ticket") 16 public class DailyTrainTicketController { 17 @Resource 18 private DailyTrainTicketService dailyTrainTicketService; 19 20 @GetMapping("/query-list") 21 public CommonResp<PageResp<DailyTrainTicketQueryResp>> queryList(@Valid DailyTrainTicketQueryReq req) { 22 PageResp<DailyTrainTicketQueryResp> list = dailyTrainTicketService.queryList(req); 23 return new CommonResp<>(list); 24 } 25 }
订票页面
可以查询所有乘客
1 /** 2 * 查询我的所有乘客 3 */ 4 public List<PassengerQueryResp> queryMine() { 5 PassengerExample passengerExample = new PassengerExample(); 6 passengerExample.setOrderByClause("name asc"); 7 PassengerExample.Criteria criteria = passengerExample.createCriteria(); 8 criteria.andMemberIdEqualTo(LoginMemberContext.getId()); 9 List<Passenger> list = passengerMapper.selectByExample(passengerExample); 10 return BeanUtil.copyToList(list, PassengerQueryResp.class); 11 }
1 @GetMapping("/query-mine") 2 public CommonResp<List<PassengerQueryResp>> queryMine() { 3 List<PassengerQueryResp> list = passengerService.queryMine(); 4 return new CommonResp<>(list); 5 }
分解选座购票功能的前后端逻辑
12306规则:
只有全部是一等座或全部是二等座才支持选座
余票小于一定数量时,不允许选座(以20为例)
选座效果
A B C D F
A B C D F
后端购票逻辑
1、不选座,遍历一等座车厢,每个车厢从1号座位找,未购买就选中。
2、选座,以购买两张一等座为例,先遍历一遍一等座车箱,每个车厢从第一个座位开始找A,未被购买就预定;再根据B相对于A的偏移值找B。
售卖情况:如果有ABCDE五个站,sell=0110,则AB可买,AC不可买。
增加确认订单表并生成前后端代码
1 drop table if exists `confirm_order`; 2 create table `confirm_order` ( 3 `id` bigint not null comment 'id', 4 `member_id` bigint not null comment '会员id', 5 `date` date not null comment '日期', 6 `train_code` varchar(20) not null comment '车次编号', 7 `start` varchar(20) not null comment '出发站', 8 `end` varchar(20) not null comment '到达站', 9 `daily_train_ticket_id` bigint not null comment '余票ID', 10 `tickets` json not null comment '车票', 11 `status` char(1) not null comment '订单状态|枚举[ConfirmOrderStatusEnum]', 12 `create_time` datetime(3) comment '新增时间', 13 `update_time` datetime(3) comment '修改时间', 14 primary key (`id`), 15 index `date_train_code_index` (`date`, `train_code`) 16 ) engine=innodb default charset=utf8mb4 comment='确认订单';
对于重要的功能,要在接口入口落库,留下痕迹。可以方便统计买票高峰,购买率等。
增加确认下单购票接口
1 package com.zihans.train.business.req; 2 3 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.NotNull; 5 6 public class ConfirmOrderTicketReq { 7 8 /** 9 * 乘客ID 10 */ 11 @NotNull(message = "【乘客ID】不能为空") 12 private Long passengerId; 13 14 /** 15 * 乘客票种 16 */ 17 @NotBlank(message = "【乘客票种】不能为空") 18 private String passengerType; 19 20 /** 21 * 乘客名称 22 */ 23 @NotBlank(message = "【乘客名称】不能为空") 24 private String passengerName; 25 26 /** 27 * 乘客身份证 28 */ 29 @NotBlank(message = "【乘客身份证】不能为空") 30 private String passengerIdCard; 31 32 /** 33 * 座位类型code 34 */ 35 @NotBlank(message = "【座位类型code】不能为空") 36 private String seatTypeCode; 37 38 /** 39 * 选座,可空,值示例:A1 40 */ 41 private String seat; 42 43 public Long getPassengerId() { 44 return passengerId; 45 } 46 47 public void setPassengerId(Long passengerId) { 48 this.passengerId = passengerId; 49 } 50 51 public String getPassengerType() { 52 return passengerType; 53 } 54 55 public void setPassengerType(String passengerType) { 56 this.passengerType = passengerType; 57 } 58 59 public String getPassengerName() { 60 return passengerName; 61 } 62 63 public void setPassengerName(String passengerName) { 64 this.passengerName = passengerName; 65 } 66 67 public String getPassengerIdCard() { 68 return passengerIdCard; 69 } 70 71 public void setPassengerIdCard(String passengerIdCard) { 72 this.passengerIdCard = passengerIdCard; 73 } 74 75 public String getSeatTypeCode() { 76 return seatTypeCode; 77 } 78 79 public void setSeatTypeCode(String seatTypeCode) { 80 this.seatTypeCode = seatTypeCode; 81 } 82 83 public String getSeat() { 84 return seat; 85 } 86 87 public void setSeat(String seat) { 88 this.seat = seat; 89 } 90 91 @Override 92 public String toString() { 93 final StringBuilder sb = new StringBuilder("ConfirmOrderTicketReq{"); 94 sb.append("passengerId=").append(passengerId); 95 sb.append(", passengerType='").append(passengerType).append('\''); 96 sb.append(", passengerName='").append(passengerName).append('\''); 97 sb.append(", passengerIdCard='").append(passengerIdCard).append('\''); 98 sb.append(", seatTypeCode='").append(seatTypeCode).append('\''); 99 sb.append(", seat='").append(seat).append('\''); 100 sb.append('}'); 101 return sb.toString(); 102 } 103 }
1 package com.zihans.train.business.req; 2 3 import com.fasterxml.jackson.annotation.JsonFormat; 4 import jakarta.validation.constraints.NotBlank; 5 import jakarta.validation.constraints.NotNull; 6 7 import java.util.Date; 8 import java.util.List; 9 10 public class ConfirmOrderDoReq { 11 12 /** 13 * 会员id 14 */ 15 @NotNull(message = "【会员id】不能为空") 16 private Long memberId; 17 18 /** 19 * 日期 20 */ 21 @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8") 22 @NotNull(message = "【日期】不能为空") 23 private Date date; 24 25 /** 26 * 车次编号 27 */ 28 @NotBlank(message = "【车次编号】不能为空") 29 private String trainCode; 30 31 /** 32 * 出发站 33 */ 34 @NotBlank(message = "【出发站】不能为空") 35 private String start; 36 37 /** 38 * 到达站 39 */ 40 @NotBlank(message = "【到达站】不能为空") 41 private String end; 42 43 /** 44 * 余票ID 45 */ 46 @NotNull(message = "【余票ID】不能为空") 47 private Long dailyTrainTicketId; 48 49 /** 50 * 车票 51 */ 52 @NotBlank(message = "【车票】不能为空") 53 private List<ConfirmOrderTicketReq> tickets; 54 55 56 public Long getMemberId() { 57 return memberId; 58 } 59 60 public void setMemberId(Long memberId) { 61 this.memberId = memberId; 62 } 63 64 public Date getDate() { 65 return date; 66 } 67 68 public void setDate(Date date) { 69 this.date = date; 70 } 71 72 public String getTrainCode() { 73 return trainCode; 74 } 75 76 public void setTrainCode(String trainCode) { 77 this.trainCode = trainCode; 78 } 79 80 public String getStart() { 81 return start; 82 } 83 84 public void setStart(String start) { 85 this.start = start; 86 } 87 88 public String getEnd() { 89 return end; 90 } 91 92 public void setEnd(String end) { 93 this.end = end; 94 } 95 96 public Long getDailyTrainTicketId() { 97 return dailyTrainTicketId; 98 } 99 100 public void setDailyTrainTicketId(Long dailyTrainTicketId) { 101 this.dailyTrainTicketId = dailyTrainTicketId; 102 } 103 104 105 @Override 106 public String toString() { 107 return "ConfirmOrderDoReq{" + 108 "memberId=" + memberId + 109 ", date=" + date + 110 ", trainCode='" + trainCode + '\'' + 111 ", start='" + start + '\'' + 112 ", end='" + end + '\'' + 113 ", dailyTrainTicketId=" + dailyTrainTicketId + 114 ", tickets=" + tickets + 115 '}'; 116 } 117 118 public List<ConfirmOrderTicketReq> getTickets() { 119 return tickets; 120 } 121 122 public void setTickets(List<ConfirmOrderTicketReq> tickets) { 123 this.tickets = tickets; 124 } 125 126 }
校验订单
1 package com.zihans.train.business.service; 2 3 import cn.hutool.core.bean.BeanUtil; 4 import cn.hutool.core.collection.CollUtil; 5 import cn.hutool.core.date.DateTime; 6 import cn.hutool.core.util.EnumUtil; 7 import cn.hutool.core.util.NumberUtil; 8 import cn.hutool.core.util.ObjectUtil; 9 import cn.hutool.core.util.StrUtil; 10 import com.alibaba.fastjson.JSON; 11 import com.github.pagehelper.PageHelper; 12 import com.github.pagehelper.PageInfo; 13 import com.zihans.train.business.domain.*; 14 import com.zihans.train.business.enums.ConfirmOrderStatusEnum; 15 import com.zihans.train.business.enums.SeatColEnum; 16 import com.zihans.train.business.enums.SeatTypeEnum; 17 import com.zihans.train.business.mapper.ConfirmOrderMapper; 18 import com.zihans.train.business.req.ConfirmOrderDoReq; 19 import com.zihans.train.business.req.ConfirmOrderQueryReq; 20 import com.zihans.train.business.req.ConfirmOrderTicketReq; 21 import com.zihans.train.business.resp.ConfirmOrderQueryResp; 22 import com.zihans.train.common.context.LoginMemberContext; 23 import com.zihans.train.common.exception.BusinessException; 24 import com.zihans.train.common.exception.BusinessExceptionEnum; 25 import com.zihans.train.common.resp.PageResp; 26 import com.zihans.train.common.util.SnowUtil; 27 import jakarta.annotation.Resource; 28 import org.slf4j.Logger; 29 import org.slf4j.LoggerFactory; 30 import org.springframework.stereotype.Service; 31 32 import java.util.ArrayList; 33 import java.util.Date; 34 import java.util.List; 35 36 @Service 37 public class ConfirmOrderService { 38 39 private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class); 40 41 @Resource 42 private ConfirmOrderMapper confirmOrderMapper; 43 44 @Resource 45 private DailyTrainTicketService dailyTrainTicketService; 46 47 @Resource 48 private DailyTrainCarriageService dailyTrainCarriageService; 49 50 @Resource 51 private DailyTrainSeatService dailyTrainSeatService; 52 53 @Resource 54 private AfterConfirmOrderService afterConfirmOrderService; 55 56 public void save(ConfirmOrderDoReq req) { 57 DateTime now = DateTime.now(); 58 ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class); 59 if (ObjectUtil.isNull(confirmOrder.getId())) { 60 confirmOrder.setId(SnowUtil.getSnowflakeNextId()); 61 confirmOrder.setCreateTime(now); 62 confirmOrder.setUpdateTime(now); 63 confirmOrderMapper.insert(confirmOrder); 64 } else { 65 confirmOrder.setUpdateTime(now); 66 confirmOrderMapper.updateByPrimaryKey(confirmOrder); 67 } 68 } 69 70 public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) { 71 ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample(); 72 confirmOrderExample.setOrderByClause("id desc"); 73 ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria(); 74 75 LOG.info("查询页码:{}", req.getPage()); 76 LOG.info("每页条数:{}", req.getSize()); 77 PageHelper.startPage(req.getPage(), req.getSize()); 78 List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample); 79 80 PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList); 81 LOG.info("总行数:{}", pageInfo.getTotal()); 82 LOG.info("总页数:{}", pageInfo.getPages()); 83 84 List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class); 85 86 PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>(); 87 pageResp.setTotal(pageInfo.getTotal()); 88 pageResp.setList(list); 89 return pageResp; 90 } 91 92 public void delete(Long id) { 93 confirmOrderMapper.deleteByPrimaryKey(id); 94 } 95 96 public void doConfirm(ConfirmOrderDoReq req) { 97 // 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过 98 99 Date date = req.getDate(); 100 String trainCode = req.getTrainCode(); 101 String start = req.getStart(); 102 String end = req.getEnd(); 103 List<ConfirmOrderTicketReq> tickets = req.getTickets(); 104 105 // 保存确认订单表,状态初始 106 DateTime now = DateTime.now(); 107 ConfirmOrder confirmOrder = new ConfirmOrder(); 108 confirmOrder.setId(SnowUtil.getSnowflakeNextId()); 109 confirmOrder.setCreateTime(now); 110 confirmOrder.setUpdateTime(now); 111 confirmOrder.setMemberId(LoginMemberContext.getId()); 112 confirmOrder.setDate(date); 113 confirmOrder.setTrainCode(trainCode); 114 confirmOrder.setStart(start); 115 confirmOrder.setEnd(end); 116 confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId()); 117 confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode()); 118 confirmOrder.setTickets(JSON.toJSONString(tickets)); 119 confirmOrderMapper.insert(confirmOrder); 120 121 // 查出余票记录,需要得到真实的库存 122 DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end); 123 LOG.info("查出余票记录:{}", dailyTrainTicket); 124 125 // 预扣减余票数量,并判断余票是否足够 126 reduceTickets(req, dailyTrainTicket); 127 128 // 最终的选座结果 129 List<DailyTrainSeat> finalSeatList = new ArrayList<>(); 130 // 计算相对第一个座位的偏移值 131 // 比如选择的是C1,D2,则偏移值是:[0,5] 132 // 比如选择的是A1,B1,C1,则偏移值是:[0,1,2] 133 ConfirmOrderTicketReq ticketReq0 = tickets.get(0); 134 if(StrUtil.isNotBlank(ticketReq0.getSeat())) { 135 LOG.info("本次购票有选座"); 136 // 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值 137 List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode()); 138 LOG.info("本次选座的座位类型包含的列:{}", colEnumList); 139 140 // 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2} 141 List<String> referSeatList = new ArrayList<>(); 142 for (int i = 1; i <= 2; i++) { 143 for (SeatColEnum seatColEnum : colEnumList) { 144 referSeatList.add(seatColEnum.getCode() + i); 145 } 146 } 147 LOG.info("用于作参照的两排座位:{}", referSeatList); 148 149 List<Integer> offsetList = new ArrayList<>(); 150 // 绝对偏移值,即:在参照座位列表中的位置 151 List<Integer> aboluteOffsetList = new ArrayList<>(); 152 for (ConfirmOrderTicketReq ticketReq : tickets) { 153 int index = referSeatList.indexOf(ticketReq.getSeat()); 154 aboluteOffsetList.add(index); 155 } 156 LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList); 157 for (Integer index : aboluteOffsetList) { 158 int offset = index - aboluteOffsetList.get(0); 159 offsetList.add(offset); 160 } 161 LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList); 162 163 getSeat(finalSeatList, 164 date, 165 trainCode, 166 ticketReq0.getSeatTypeCode(), 167 ticketReq0.getSeat().split("")[0], // 从A1得到A 168 offsetList, 169 dailyTrainTicket.getStartIndex(), 170 dailyTrainTicket.getEndIndex() 171 ); 172 173 } else { 174 LOG.info("本次购票没有选座"); 175 for (ConfirmOrderTicketReq ticketReq : tickets) { 176 getSeat(finalSeatList, 177 date, 178 trainCode, 179 ticketReq.getSeatTypeCode(), 180 null, 181 null, 182 dailyTrainTicket.getStartIndex(), 183 dailyTrainTicket.getEndIndex() 184 ); 185 } 186 } 187 188 LOG.info("最终选座:{}", finalSeatList); 189 190 // 选中座位后事务处理: 191 // 座位表修改售卖情况sell; 192 // 余票详情表修改余票; 193 // 为会员增加购票记录 194 // 更新确认订单为成功 195 afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder); 196 197 198 199 } 200 201 /** 202 * 挑座位,如果有选座,则一次性挑完,如果无选座,则一个一个挑 203 * @param date 204 * @param trainCode 205 * @param seatType 206 * @param column 207 * @param offsetList 208 */ 209 private void getSeat(List<DailyTrainSeat> finalSeatList, Date date, String trainCode, String seatType, String column, List<Integer> offsetList, Integer startIndex, Integer endIndex) { 210 List<DailyTrainSeat> getSeatList = new ArrayList<>(); 211 List<DailyTrainCarriage> carriageList = dailyTrainCarriageService.selectBySeatType(date, trainCode, seatType); 212 LOG.info("共查出{}个符合条件的车厢", carriageList.size()); 213 214 // 一个车箱一个车箱的获取座位数据 215 for (DailyTrainCarriage dailyTrainCarriage : carriageList) { 216 LOG.info("开始从车厢{}选座", dailyTrainCarriage.getIndex()); 217 getSeatList = new ArrayList<>(); 218 List<DailyTrainSeat> seatList = dailyTrainSeatService.selectByCarriage(date, trainCode, dailyTrainCarriage.getIndex()); 219 LOG.info("车厢{}的座位数:{}", dailyTrainCarriage.getIndex(), seatList.size()); 220 for (int i = 0; i < seatList.size(); i++) { 221 DailyTrainSeat dailyTrainSeat = seatList.get(i); 222 Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex(); 223 String col = dailyTrainSeat.getCol(); 224 225 // 判断当前座位不能被选中过 226 boolean alreadyChooseFlag = false; 227 for (DailyTrainSeat finalSeat : finalSeatList){ 228 if (finalSeat.getId().equals(dailyTrainSeat.getId())) { 229 alreadyChooseFlag = true; 230 break; 231 } 232 } 233 if (alreadyChooseFlag) { 234 LOG.info("座位{}被选中过,不能重复选中,继续判断下一个座位", seatIndex); 235 continue; 236 } 237 238 // 判断column,有值的话要比对列号 239 if (StrUtil.isBlank(column)) { 240 LOG.info("无选座"); 241 } else { 242 if (!column.equals(col)) { 243 LOG.info("座位{}列值不对,继续判断下一个座位,当前列值:{},目标列值:{}", seatIndex, col, column); 244 continue; 245 } 246 } 247 248 boolean isChoose = calSell(dailyTrainSeat, startIndex, endIndex); 249 if (isChoose) { 250 LOG.info("选中座位"); 251 getSeatList.add(dailyTrainSeat); 252 } else { 253 continue; 254 } 255 256 // 根据offset选剩下的座位 257 boolean isGetAllOffsetSeat = true; 258 if (CollUtil.isNotEmpty(offsetList)) { 259 LOG.info("有偏移值:{},校验偏移的座位是否可选", offsetList); 260 // 从索引1开始,索引0就是当前已选中的票 261 for (int j = 1; j < offsetList.size(); j++) { 262 Integer offset = offsetList.get(j); 263 // 座位在库的索引是从1开始 264 // int nextIndex = seatIndex + offset - 1; 265 int nextIndex = i + offset; 266 267 // 有选座时,一定是在同一个车箱 268 if (nextIndex >= seatList.size()) { 269 LOG.info("座位{}不可选,偏移后的索引超出了这个车箱的座位数", nextIndex); 270 isGetAllOffsetSeat = false; 271 break; 272 } 273 274 DailyTrainSeat nextDailyTrainSeat = seatList.get(nextIndex); 275 boolean isChooseNext = calSell(nextDailyTrainSeat, startIndex, endIndex); 276 if (isChooseNext) { 277 LOG.info("座位{}被选中", nextDailyTrainSeat.getCarriageSeatIndex()); 278 getSeatList.add(nextDailyTrainSeat); 279 } else { 280 LOG.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex()); 281 isGetAllOffsetSeat = false; 282 break; 283 } 284 } 285 } 286 if (!isGetAllOffsetSeat) { 287 getSeatList = new ArrayList<>(); 288 continue; 289 } 290 291 // 保存选好的座位 292 finalSeatList.addAll(getSeatList); 293 return; 294 } 295 } 296 } 297 298 /** 299 * 计算某座位在区间内是否可卖 300 * 例:sell=10001,本次购买区间站1~4,则区间已售000 301 * 全部是0,表示这个区间可买;只要有1,就表示区间内已售过票 302 * 303 * 选中后,要计算购票后的sell,比如原来是10001,本次购买区间站1~4 304 * 方案:构造本次购票造成的售卖信息01110,和原sell 10001按位与,最终得到11111 305 */ 306 private boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex) { 307 // 00001, 00000 308 String sell = dailyTrainSeat.getSell(); 309 // 000, 000 310 String sellPart = sell.substring(startIndex, endIndex); 311 if (Integer.parseInt(sellPart) > 0) { 312 LOG.info("座位{}在本次车站区间{}~{}已售过票,不可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex); 313 return false; 314 } else { 315 LOG.info("座位{}在本次车站区间{}~{}未售过票,可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex); 316 // 111, 111 317 String curSell = sellPart.replace('0', '1'); 318 // 0111, 0111 319 curSell = StrUtil.fillBefore(curSell, '0', endIndex); 320 // 01110, 01110 321 curSell = StrUtil.fillAfter(curSell, '0', sell.length()); 322 323 // 当前区间售票信息curSell 01110与库里的已售信息sell 00001按位与,即可得到该座位卖出此票后的售票详情 324 // 15(01111), 14(01110 = 01110|00000) 325 int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell); 326 // 1111, 1110 327 String newSell = NumberUtil.getBinaryStr(newSellInt); 328 // 01111, 01110 329 newSell = StrUtil.fillBefore(newSell, '0', sell.length()); 330 LOG.info("座位{}被选中,原售票信息:{},车站区间:{}~{},即:{},最终售票信息:{}" 331 , dailyTrainSeat.getCarriageSeatIndex(), sell, startIndex, endIndex, curSell, newSell); 332 dailyTrainSeat.setSell(newSell); 333 return true; 334 335 } 336 } 337 338 private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) { 339 for (ConfirmOrderTicketReq ticketReq : req.getTickets()) { 340 String seatTypeCode = ticketReq.getSeatTypeCode(); 341 SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode); 342 switch (seatTypeEnum) { 343 case YDZ -> { 344 int countLeft = dailyTrainTicket.getYdz() - 1; 345 if (countLeft < 0) { 346 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); 347 } 348 dailyTrainTicket.setYdz(countLeft); 349 } 350 case EDZ -> { 351 int countLeft = dailyTrainTicket.getEdz() - 1; 352 if (countLeft < 0) { 353 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); 354 } 355 dailyTrainTicket.setEdz(countLeft); 356 } 357 case RW -> { 358 int countLeft = dailyTrainTicket.getRw() - 1; 359 if (countLeft < 0) { 360 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); 361 } 362 dailyTrainTicket.setRw(countLeft); 363 } 364 case YW -> { 365 int countLeft = dailyTrainTicket.getYw() - 1; 366 if (countLeft < 0) { 367 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); 368 } 369 dailyTrainTicket.setYw(countLeft); 370 } 371 } 372 } 373 } 374 }