九、车票预定功能开发

发布时间 2023-05-26 20:57:13作者: 夏雪冬蝉

内容

  • 余票查询(控台端)

    余票初始化、余票查询

  • 选座购票(会员端)

    余票查询、选择乘客、选择座位类型、选择座位、下单购票

增加余票信息表以提高余票查询性能

第一步:建表

 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     }
DailyTrainTicketService.java

一定要加事务@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.java

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 }
DailyTrainTicketController.java

订票页面

可以查询所有乘客

 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     }
PassengerService.java
1     @GetMapping("/query-mine")
2     public CommonResp<List<PassengerQueryResp>> queryMine() {
3         List<PassengerQueryResp> list = passengerService.queryMine();
4         return new CommonResp<>(list);
5     }
PassengerController.java

分解选座购票功能的前后端逻辑

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 }
ConfirmOrderTicketReq.java
  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 }
ConfirmOrderDoReq.java

校验订单

  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 }
ConfirmOrderService.java