使用RocketMQ组件对请求做削峰处理

发布时间 2023-06-21 11:41:50作者: 夏雪冬蝉

内容

  • rocketMQ基本介绍
  • 使用MQ,将购票流程一分为二。目前系统的吞吐量低,用户从购买车票到拿到票花费的时间较长。
  • 增加排队购票功能。排队提示loading。

购票时序图

目前的时序图,用户发送购票请求,服务端校验验证码,拿令牌,拿锁,然后选座购票,结束流程才会返回。服务器执行时间太长。

 增加异步,拿锁分为两步,拿令牌锁要放在同步里,拿车次锁要放在异步里。用来防止机器人和超卖。拿到后便响应给用户,告诉用户有资格买票。异步线程中选座购票。之后前端发起轮询,调用后端的查询接口。

 再次改进,将异步操作放到出票模块,接收购票请求的模块与购票模块分开,可以用不同的服务器,从而为选座购票功能分配更多的节点。服务端发送消息给MQ,出票模块监听MQ的消息,有购票请求就选座购票。过一段时间进行轮询,查看购票结果。

 初始RocketMQ

 https://rocketmq.apache.org/

快速开始

相关概念

  • 生产者:负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。
    • topic,表示要发送的消息的主题。
    • body 表示消息的存储内容
    • properties 表示消息属性
    • transactionId 会在事务消息中使用。
    • 普通消息、顺序消息、延迟消息、批量消息、事务消息。
  • 消费者:负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。
    • push消费:MQ主动将消息推给客户端。
    • pull消费:消费者主动拉取消息。
    • 一个消息可以支持多个消费者or一个消息由一个消费者消费。
  • 主题:表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

RocketMQ初体验

 jdk17+roketmq4.9.5启动失败,参考

(76条消息) windows10 + jdk17安装rocketmq4.9.2_合肥芥子网络的博客-CSDN博客

 

使用RocketMQ将购票流程一分为二

一部分处理验证码,令牌,车次锁;另一部分处理选座购票逻辑。

拿到车次锁,就代表用户有条件购票,然后快速反馈用户。

 下单购票接口,只处理验证码、令牌、车次锁,不执行选座购票逻辑,之后发送MQ消息(未做)。

 1 @Service
 2 public class BeforeConfirmOrderService {
 3 
 4     private static final Logger LOG = LoggerFactory.getLogger(BeforeConfirmOrderService.class);
 5 
 6     @Resource
 7     private ConfirmOrderMapper confirmOrderMapper;
 8 
 9     @Resource
10     private DailyTrainTicketService dailyTrainTicketService;
11 
12     @Resource
13     private DailyTrainCarriageService dailyTrainCarriageService;
14 
15     @Resource
16     private DailyTrainSeatService dailyTrainSeatService;
17 
18     @Resource
19     private AfterConfirmOrderService afterConfirmOrderService;
20 
21     @Autowired
22     private StringRedisTemplate redisTemplate;
23 
24     @Autowired
25     private SkTokenService skTokenService;
26 
27     @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
28     public void beforeDoConfirm(ConfirmOrderDoReq req) {
29 
30         // 校验令牌余量
31         boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
32         if (validSkToken) {
33             LOG.info("令牌校验通过");
34         } else {
35             LOG.info("令牌校验不通过");
36             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
37         }
38 
39         // 获取车次锁
40         String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(req.getDate()) + "-" + req.getTrainCode();
41         // setIfAbsent就是对应redis的setnx
42         Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
43         if (Boolean.TRUE.equals(setIfAbsent)) {
44             LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
45         } else {
46             // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
47             LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
48             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
49         }
50 
51         // 可以购票:TODO: 发送MQ,等待出票
52         LOG.info("准备发送MQ,等待出票");
53 
54     }
55 
56     /**
57      * 降级方法,需包含限流方法的所有参数和BlockException参数
58      * @param req
59      * @param e
60      */
61     public void beforeDoConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
62         LOG.info("购票请求被限流:{}", req);
63         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
64     }
65 }
BeforeConfirmOrderService.java

ConfirmOrderController.java中注入的ConfirmOrderService也改为BeforeConfirmOrderService

 

实现RocketMQ发送,spring.factories功能在Spring Boot 3.0被移除,替代方案为META-INFO/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

添加RocketMQ依赖

1             <dependency>
2                 <groupId>org.apache.rocketmq</groupId>
3                 <artifactId>rocketmq-spring-boot-starter</artifactId>
4                 <version>2.2.3</version>
5             </dependency>

配置rocketmq

# rocketmq
rocketmq:
  name-server: http://localhost:9876
  producer:
    group: default

主题枚举类

 1 public enum RocketMQTopicEnum {
 2 
 3     CONFIRM_ORDER("CONFIRM_ORDER", "确认订单排队");
 4 
 5     private String code;
 6 
 7     private String desc;
 8 
 9     RocketMQTopicEnum(String code, String desc) {
10         this.code = code;
11         this.desc = desc;
12     }
13 
14     @Override
15     public String toString() {
16         return "RocketMQTopicEnum{" +
17                 "code='" + code + '\'' +
18                 ", desc='" + desc + '\'' +
19                 "} " + super.toString();
20     }
21 
22     public String getCode() {
23         return code;
24     }
25 
26     public void setCode(String code) {
27         this.code = code;
28     }
29 
30     public void setDesc(String desc) {
31         this.desc = desc;
32     }
33 
34     public String getDesc() {
35         return desc;
36     }
37 }
RocketMQTopicEnum.java

发送rocketmq   (RocketMQTemplate),将购票的请求参数转成json,导入工具类,然后发送。

 1 @Service
 2 public class BeforeConfirmOrderService {
 3 
 4     private static final Logger LOG = LoggerFactory.getLogger(BeforeConfirmOrderService.class);
 5 
 6     @Autowired
 7     private StringRedisTemplate redisTemplate;
 8 
 9     @Autowired
10     private SkTokenService skTokenService;
11 
12     @Resource
13     public RocketMQTemplate rocketMQTemplate;
14 
15     @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
16     public void beforeDoConfirm(ConfirmOrderDoReq req) {
17 
18         // 校验令牌余量
19         boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
20         if (validSkToken) {
21             LOG.info("令牌校验通过");
22         } else {
23             LOG.info("令牌校验不通过");
24             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
25         }
26 
27         // 获取车次锁
28         String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(req.getDate()) + "-" + req.getTrainCode();
29         // setIfAbsent就是对应redis的setnx
30         Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
31         if (Boolean.TRUE.equals(setIfAbsent)) {
32             LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
33         } else {
34             // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
35             LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
36             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
37         }
38 
39         // 发送MQ排队购票
40         String reqJson = JSON.toJSONString(req);
41         LOG.info("排队购票,发送mq开始,消息:{}", reqJson);
42         rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
43         LOG.info("排队购票,发送mq结束");
44 
45     }
46 
47     /**
48      * 降级方法,需包含限流方法的所有参数和BlockException参数
49      * @param req
50      * @param e
51      */
52     public void beforeDoConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
53         LOG.info("购票请求被限流:{}", req);
54         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
55     }
56 }
BeforeConfirmOrderService.java

此时可以发送rocketmq

实现rocketmq接收

消费类,消费发送的topic,接收收到的json

 1 import org.apache.rocketmq.common.message.MessageExt;
 2 import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
 3 import org.apache.rocketmq.spring.core.RocketMQListener;
 4 import org.slf4j.Logger;
 5 import org.slf4j.LoggerFactory;
 6 import org.springframework.stereotype.Service;
 7 
 8 @Service
 9 @RocketMQMessageListener(consumerGroup = "default", topic = "CONFIRM_ORDER")
10 public class ConfirmOrderConsumer implements RocketMQListener<MessageExt> {
11 
12     private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderConsumer.class);
13 
14     @Override
15     public void onMessage(MessageExt messageExt) {
16         byte[] body = messageExt.getBody();
17         LOG.info("ROCKETMQ收到消息:{}", new String(body));
18     }
19 }
ConfirmOrderConsumer.java

 

完成MQ消费的购票功能

完成MQ消费里的购票功能:发送MQ之前应该先保存;获取分布式锁,应该跟随购票逻辑

ConfirmOrderService可以去掉锁和令牌校验,纯粹用来选座购票。

  1  @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock")
  2     public void doConfirm(ConfirmOrderDoReq req) {
  3 
  4         // // 校验令牌余量
  5         // boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
  6         // if (validSkToken) {
  7         //     LOG.info("令牌校验通过");
  8         // } else {
  9         //     LOG.info("令牌校验不通过");
 10         //     throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
 11         // }
 12         //
 13         // 获取分布式锁
 14         String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(req.getDate()) + "-" + req.getTrainCode();
 15         // setIfAbsent就是对应redis的setnx
 16         Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
 17         if (Boolean.TRUE.equals(setIfAbsent)) {
 18             LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
 19         } else {
 20             // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
 21             LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
 22             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
 23         }
 24 
 25         // RLock lock = null;
 26         /*
 27             关于红锁,看16.7节:
 28             A B C D E
 29             1: A B C D E
 30             2: C D E
 31             3: C
 32         */
 33         try {
 34             // // 使用redisson,自带看门狗
 35             // lock = redissonClient.getLock(lockKey);
 36             //
 37             // // 红锁的写法
 38             // // RedissonRedLock redissonRedLock = new RedissonRedLock(lock, lock, lock);
 39             // // boolean tryLock1 = redissonRedLock.tryLock(0, TimeUnit.SECONDS);
 40             //
 41             // /**
 42             //   waitTime – the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回false
 43             //   leaseTime – lease time 锁时长,即n秒后自动释放锁
 44             //   time unit – time unit 时间单位
 45             //  */
 46             // // boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不带看门狗
 47             // boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 带看门狗
 48             // if (tryLock) {
 49             //     LOG.info("恭喜,抢到锁了!");
 50             //     // 可以把下面这段放开,只用一个线程来测试,看看redisson的看门狗效果
 51             //     // for (int i = 0; i < 30; i++) {
 52             //     //     Long expire = redisTemplate.opsForValue().getOperations().getExpire(lockKey);
 53             //     //     LOG.info("锁过期时间还有:{}", expire);
 54             //     //     Thread.sleep(1000);
 55             //     // }
 56             // } else {
 57             //     // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
 58             //     LOG.info("很遗憾,没抢到锁");
 59             //     throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
 60             // }
 61 
 62             // 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过
 63 
 64             Date date = req.getDate();
 65             String trainCode = req.getTrainCode();
 66             String start = req.getStart();
 67             String end = req.getEnd();
 68             List<ConfirmOrderTicketReq> tickets = req.getTickets();
 69             //
 70             // // 保存确认订单表,状态初始
 71             // DateTime now = DateTime.now();
 72             // ConfirmOrder confirmOrder = new ConfirmOrder();
 73             // confirmOrder.setId(SnowUtil.getSnowflakeNextId());
 74             // confirmOrder.setCreateTime(now);
 75             // confirmOrder.setUpdateTime(now);
 76             // confirmOrder.setMemberId(req.getMemberId());
 77             // confirmOrder.setDate(date);
 78             // confirmOrder.setTrainCode(trainCode);
 79             // confirmOrder.setStart(start);
 80             // confirmOrder.setEnd(end);
 81             // confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
 82             // confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
 83             // confirmOrder.setTickets(JSON.toJSONString(tickets));
 84             // confirmOrderMapper.insert(confirmOrder);
 85 
 86             // 从数据库里查出订单
 87             ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
 88             confirmOrderExample.setOrderByClause("id asc");
 89             ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
 90             criteria.andDateEqualTo(req.getDate())
 91                     .andTrainCodeEqualTo(req.getTrainCode())
 92                     .andMemberIdEqualTo(req.getMemberId())
 93                     .andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
 94             List<ConfirmOrder> list = confirmOrderMapper.selectByExampleWithBLOBs(confirmOrderExample);
 95             ConfirmOrder confirmOrder;
 96             if (CollUtil.isEmpty(list)) {
 97                 LOG.info("找不到原始订单,结束");
 98                 return;
 99             } else {
100                 LOG.info("本次处理{}条确认订单", list.size());
101                 confirmOrder = list.get(0);
102             }
103 
104             // 查出余票记录,需要得到真实的库存
105             DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
106             LOG.info("查出余票记录:{}", dailyTrainTicket);
107 
108             // 预扣减余票数量,并判断余票是否足够
109             reduceTickets(req, dailyTrainTicket);
110 
111             // 最终的选座结果
112             List<DailyTrainSeat> finalSeatList = new ArrayList<>();
113             // 计算相对第一个座位的偏移值
114             // 比如选择的是C1,D2,则偏移值是:[0,5]
115             // 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]
116             ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
117             if (StrUtil.isNotBlank(ticketReq0.getSeat())) {
118                 LOG.info("本次购票有选座");
119                 // 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值
120                 List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
121                 LOG.info("本次选座的座位类型包含的列:{}", colEnumList);
122 
123                 // 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}
124                 List<String> referSeatList = new ArrayList<>();
125                 for (int i = 1; i <= 2; i++) {
126                     for (SeatColEnum seatColEnum : colEnumList) {
127                         referSeatList.add(seatColEnum.getCode() + i);
128                     }
129                 }
130                 LOG.info("用于作参照的两排座位:{}", referSeatList);
131 
132                 List<Integer> offsetList = new ArrayList<>();
133                 // 绝对偏移值,即:在参照座位列表中的位置
134                 List<Integer> aboluteOffsetList = new ArrayList<>();
135                 for (ConfirmOrderTicketReq ticketReq : tickets) {
136                     int index = referSeatList.indexOf(ticketReq.getSeat());
137                     aboluteOffsetList.add(index);
138                 }
139                 LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);
140                 for (Integer index : aboluteOffsetList) {
141                     int offset = index - aboluteOffsetList.get(0);
142                     offsetList.add(offset);
143                 }
144                 LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);
145 
146                 getSeat(finalSeatList,
147                         date,
148                         trainCode,
149                         ticketReq0.getSeatTypeCode(),
150                         ticketReq0.getSeat().split("")[0], // 从A1得到A
151                         offsetList,
152                         dailyTrainTicket.getStartIndex(),
153                         dailyTrainTicket.getEndIndex()
154                 );
155 
156             } else {
157                 LOG.info("本次购票没有选座");
158                 for (ConfirmOrderTicketReq ticketReq : tickets) {
159                     getSeat(finalSeatList,
160                             date,
161                             trainCode,
162                             ticketReq.getSeatTypeCode(),
163                             null,
164                             null,
165                             dailyTrainTicket.getStartIndex(),
166                             dailyTrainTicket.getEndIndex()
167                     );
168                 }
169             }
170 
171             LOG.info("最终选座:{}", finalSeatList);
172 
173             // 选中座位后事务处理:
174             // 座位表修改售卖情况sell;
175             // 余票详情表修改余票;
176             // 为会员增加购票记录
177             // 更新确认订单为成功
178             try {
179                 afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
180             } catch (Exception e) {
181                 LOG.error("保存购票信息失败", e);
182                 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
183             }
184             // LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);
185             // redisTemplate.delete(lockKey);
186             // } catch (InterruptedException e) {
187             //     LOG.error("购票异常", e);
188         } finally {
189             // try finally不能包含加锁的那段代码,否则加锁失败会走到finally里,从而释放别的线程的锁
190             // LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);
191             // redisTemplate.delete(lockKey);
192             // LOG.info("购票流程结束,释放锁!");
193             // if (null != lock && lock.isHeldByCurrentThread()) {
194             //     lock.unlock();
195             // }
196         }
197 
198     }
ConfirmOrderService.java

消费方注入ConfirmOrderService来调用选座购票接口

 1 @Service
 2 @RocketMQMessageListener(consumerGroup = "default", topic = "CONFIRM_ORDER")
 3 public class ConfirmOrderConsumer implements RocketMQListener<MessageExt> {
 4 
 5     private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderConsumer.class);
 6 
 7     @Resource
 8     private ConfirmOrderService confirmOrderService;
 9 
10     @Override
11     public void onMessage(MessageExt messageExt) {
12         byte[] body = messageExt.getBody();
13         LOG.info("ROCKETMQ收到消息:{}", new String(body));
14         ConfirmOrderDoReq req = JSON.parseObject(new String(body), ConfirmOrderDoReq.class);
15         confirmOrderService.doConfirm(req);
16     }
17 }
ConfirmOrderService.java

拦截器只能在接口入口生效,选座购票请求是MQ调用的,拿不到拦截器得到的id。可以在before时手动设置memberid。

 

还有一个比较严重的问题,由于抢锁和选座购票业务分开,那么车次锁就不能在抢锁阶段实现,而要放在选座购票业务里。因为即使车次锁只能一个人获得,但是消费时多个人可以同时抢一辆车的票,又会造成车票超卖。

请求进来后,先保存订单信息,再发MQ等待出票,给用户响应。

 1 @SentinelResource(value = "beforeDoConfirm", blockHandler = "beforeDoConfirmBlock")
 2     public void beforeDoConfirm(ConfirmOrderDoReq req) {
 3         req.setMemberId(LoginMemberContext.getId());
 4         // 校验令牌余量
 5         boolean validSkToken = skTokenService.validSkToken(req.getDate(), req.getTrainCode(), LoginMemberContext.getId());
 6         if (validSkToken) {
 7             LOG.info("令牌校验通过");
 8         } else {
 9             LOG.info("令牌校验不通过");
10             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
11         }
12 
13         Date date = req.getDate();
14         String trainCode = req.getTrainCode();
15         String start = req.getStart();
16         String end = req.getEnd();
17         List<ConfirmOrderTicketReq> tickets = req.getTickets();
18 
19         // 保存确认订单表,状态初始
20         DateTime now = DateTime.now();
21         ConfirmOrder confirmOrder = new ConfirmOrder();
22         confirmOrder.setId(SnowUtil.getSnowflakeNextId());
23         confirmOrder.setCreateTime(now);
24         confirmOrder.setUpdateTime(now);
25         confirmOrder.setMemberId(req.getMemberId());
26         confirmOrder.setDate(date);
27         confirmOrder.setTrainCode(trainCode);
28         confirmOrder.setStart(start);
29         confirmOrder.setEnd(end);
30         confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
31         confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
32         confirmOrder.setTickets(JSON.toJSONString(tickets));
33         confirmOrderMapper.insert(confirmOrder);
34 
35         // 发送MQ排队购票
36         String reqJson = JSON.toJSONString(req);
37         LOG.info("排队购票,发送mq开始,消息:{}", reqJson);
38         rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
39         LOG.info("排队购票,发送mq结束");
40 
41     }
BeforeConfirmOrderService.java

由于保存过订单,所以MQ从数据库中拿数据,取第0条。

 

为同转异增加logId,方便日志跟踪

日志跟踪号在拦截器,所以mq没有日志跟踪。直接将日志跟踪号放进消息里,消费时则取出。使请求和选座购票日志跟踪号相同。

  1 public class ConfirmOrderDoReq {
  2 
  3     /**
  4      * 会员id
  5      */
  6     private Long memberId;
  7 
  8     /**
  9      * 日期
 10      */
 11     @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
 12     @NotNull(message = "【日期】不能为空")
 13     private Date date;
 14 
 15     /**
 16      * 车次编号
 17      */
 18     @NotBlank(message = "【车次编号】不能为空")
 19     private String trainCode;
 20 
 21     /**
 22      * 出发站
 23      */
 24     @NotBlank(message = "【出发站】不能为空")
 25     private String start;
 26 
 27     /**
 28      * 到达站
 29      */
 30     @NotBlank(message = "【到达站】不能为空")
 31     private String end;
 32 
 33     /**
 34      * 余票ID
 35      */
 36     @NotNull(message = "【余票ID】不能为空")
 37     private Long dailyTrainTicketId;
 38 
 39     /**
 40      * 车票
 41      */
 42     @NotEmpty(message = "【车票】不能为空")
 43     private List<ConfirmOrderTicketReq> tickets;
 44 
 45     /**
 46      * 验证码
 47      */
 48     @NotBlank(message = "【图片验证码】不能为空")
 49     private String imageCode;
 50 
 51     /**
 52      * 图片验证码token
 53      */
 54     @NotBlank(message = "【图片验证码】参数非法")
 55     private String imageCodeToken;
 56 
 57     /**
 58      * 日志跟踪号
 59      */
 60     private String logId;
 61 
 62     public Long getMemberId() {
 63         return memberId;
 64     }
 65 
 66     public void setMemberId(Long memberId) {
 67         this.memberId = memberId;
 68     }
 69 
 70     public Date getDate() {
 71         return date;
 72     }
 73 
 74     public void setDate(Date date) {
 75         this.date = date;
 76     }
 77 
 78     public String getTrainCode() {
 79         return trainCode;
 80     }
 81 
 82     public void setTrainCode(String trainCode) {
 83         this.trainCode = trainCode;
 84     }
 85 
 86     public String getStart() {
 87         return start;
 88     }
 89 
 90     public void setStart(String start) {
 91         this.start = start;
 92     }
 93 
 94     public String getEnd() {
 95         return end;
 96     }
 97 
 98     public void setEnd(String end) {
 99         this.end = end;
100     }
101 
102     public Long getDailyTrainTicketId() {
103         return dailyTrainTicketId;
104     }
105 
106     public void setDailyTrainTicketId(Long dailyTrainTicketId) {
107         this.dailyTrainTicketId = dailyTrainTicketId;
108     }
109 
110     public List<ConfirmOrderTicketReq> getTickets() {
111         return tickets;
112     }
113 
114     public void setTickets(List<ConfirmOrderTicketReq> tickets) {
115         this.tickets = tickets;
116     }
117 
118     public String getImageCode() {
119         return imageCode;
120     }
121 
122     public void setImageCode(String imageCode) {
123         this.imageCode = imageCode;
124     }
125 
126     public String getImageCodeToken() {
127         return imageCodeToken;
128     }
129 
130     public void setImageCodeToken(String imageCodeToken) {
131         this.imageCodeToken = imageCodeToken;
132     }
133 
134     public String getLogId() {
135         return logId;
136     }
137 
138     public void setLogId(String logId) {
139         this.logId = logId;
140     }
141 
142     @Override
143     public String toString() {
144         return "ConfirmOrderDoReq{" +
145                 "memberId=" + memberId +
146                 ", date=" + date +
147                 ", trainCode='" + trainCode + '\'' +
148                 ", start='" + start + '\'' +
149                 ", end='" + end + '\'' +
150                 ", dailyTrainTicketId=" + dailyTrainTicketId +
151                 ", tickets=" + tickets +
152                 ", imageCode='" + imageCode + '\'' +
153                 ", imageCodeToken='" + imageCodeToken + '\'' +
154                 ", logId='" + logId + '\'' +
155                 '}';
156     }
157 }
ConfirmOrderDoReq.java

BeforeConfirmOrderService.java添加

// 发送MQ排队购票
req.setLogId(MDC.get("LOG_ID"));

ConfirmOrderConsumer.java添加

MDC.put("LOG_ID", req.getLogId());
LOG.info("ROCKETMQ收到消息: {}", new String(body));

 

增加排队功能思路

 有一个问题,拿锁的时候有可能失败,没拿到锁的会快速失败,会抛异常。正确的方法是让订单更新成失败,用户查询到失败会重新发起购票。

但是拿不到锁还会使令牌消耗过大,拿到令牌后就有买票的资格,不能因为没抢到车次锁就买票失败,因此要有排队功能

新的时序图

 上述循环在拿锁之后

一个消费者拿到了某个车次锁,则改车次下的所有票都由他来出,一张一张出,直到所有的订单出完。

轮询一直查询出票结果。

 

完成排队出票功能

MQ首先通知出票模块有一个车次要售票。

修改MQ消息内容,只需要通知出哪个车次的票(即:组成锁的内容),不需要具体到哪个人

不需要传递订单所有的信息,只用传递锁相关信息,真正的消息只有日期+车次。内部之间的传递用dto类

 1 public class ConfirmOrderMQDto {
 2     /**
 3      * 日志流程号,用于同转异时,用同一个流水号
 4      */
 5     private String logId;
 6 
 7     /**
 8      * 日期
 9      */
10     private Date date;
11 
12     /**
13      * 车次编号
14      */
15     private String trainCode;
16 
17     public String getLogId() {
18         return logId;
19     }
20 
21     public void setLogId(String logId) {
22         this.logId = logId;
23     }
24 
25     public Date getDate() {
26         return date;
27     }
28 
29     public void setDate(Date date) {
30         this.date = date;
31     }
32 
33     public String getTrainCode() {
34         return trainCode;
35     }
36 
37     public void setTrainCode(String trainCode) {
38         this.trainCode = trainCode;
39     }
40 
41     @Override
42     public String toString() {
43         final StringBuilder sb = new StringBuilder("ConfirmOrderMQDto{");
44         sb.append("logId=").append(logId);
45         sb.append(", date=").append(date);
46         sb.append(", trainCode='").append(trainCode).append('\'');
47         sb.append('}');
48         return sb.toString();
49     }
50 }
ConfirmOrderMQDto.java

购票之前排队购票,写入dto

        // 发送MQ排队购票
        ConfirmOrderMQDto confirmOrderMQDto = new ConfirmOrderMQDto();
        confirmOrderMQDto.setDate(req.getDate());
        confirmOrderMQDto.setTrainCode(req.getTrainCode());
        confirmOrderMQDto.setLogId(MDC.get("LOG_ID"));

 

出票功能改为循环排队出票,按车次(锁)来循环出票

 原来拿到锁按具体订单选座购票,现在拿锁之后循环,按日期+车次+订单状态(初始)查询,把所有购买该车次的票查出来。然后分页查询,有值就分批卖。
  1 @Service
  2 public class ConfirmOrderService {
  3 
  4     private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
  5 
  6     @Resource
  7     private ConfirmOrderMapper confirmOrderMapper;
  8 
  9     @Resource
 10     private DailyTrainTicketService dailyTrainTicketService;
 11 
 12     @Resource
 13     private DailyTrainCarriageService dailyTrainCarriageService;
 14 
 15     @Resource
 16     private DailyTrainSeatService dailyTrainSeatService;
 17 
 18     @Resource
 19     private AfterConfirmOrderService afterConfirmOrderService;
 20 
 21     @Autowired
 22     private StringRedisTemplate redisTemplate;
 23 
 24     @Autowired
 25     private SkTokenService skTokenService;
 26 
 27     // @Autowired
 28     // private RedissonClient redissonClient;
 29 
 30     public void save(ConfirmOrderDoReq req) {
 31         DateTime now = DateTime.now();
 32         ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
 33         if (ObjectUtil.isNull(confirmOrder.getId())) {
 34             confirmOrder.setId(SnowUtil.getSnowflakeNextId());
 35             confirmOrder.setCreateTime(now);
 36             confirmOrder.setUpdateTime(now);
 37             confirmOrderMapper.insert(confirmOrder);
 38         } else {
 39             confirmOrder.setUpdateTime(now);
 40             confirmOrderMapper.updateByPrimaryKey(confirmOrder);
 41         }
 42     }
 43 
 44     public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
 45         ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
 46         confirmOrderExample.setOrderByClause("id desc");
 47         ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
 48 
 49         LOG.info("查询页码:{}", req.getPage());
 50         LOG.info("每页条数:{}", req.getSize());
 51         PageHelper.startPage(req.getPage(), req.getSize());
 52         List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);
 53 
 54         PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
 55         LOG.info("总行数:{}", pageInfo.getTotal());
 56         LOG.info("总页数:{}", pageInfo.getPages());
 57 
 58         List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);
 59 
 60         PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
 61         pageResp.setTotal(pageInfo.getTotal());
 62         pageResp.setList(list);
 63         return pageResp;
 64     }
 65 
 66     public void delete(Long id) {
 67         confirmOrderMapper.deleteByPrimaryKey(id);
 68     }
 69 
 70     @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock")
 71     public void doConfirm(ConfirmOrderMQDto dto) {
 72 
 73         // // 校验令牌余量
 74         // boolean validSkToken = skTokenService.validSkToken(dto.getDate(), dto.getTrainCode(), LoginMemberContext.getId());
 75         // if (validSkToken) {
 76         //     LOG.info("令牌校验通过");
 77         // } else {
 78         //     LOG.info("令牌校验不通过");
 79         //     throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_SK_TOKEN_FAIL);
 80         // }
 81         //
 82         // 获取分布式锁
 83         String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(dto.getDate()) + "-" + dto.getTrainCode();
 84         // setIfAbsent就是对应redis的setnx
 85         Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
 86         if (Boolean.TRUE.equals(setIfAbsent)) {
 87             LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
 88         } else {
 89             // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
 90             LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
 91             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
 92         }
 93 
 94         // RLock lock = null;
 95         /*
 96             关于红锁,看16.7节:
 97             A B C D E
 98             1: A B C D E
 99             2: C D E
100             3: C
101         */
102         try {
103             // // 使用redisson,自带看门狗
104             // lock = redissonClient.getLock(lockKey);
105             //
106             // // 红锁的写法
107             // // RedissonRedLock redissonRedLock = new RedissonRedLock(lock, lock, lock);
108             // // boolean tryLock1 = redissonRedLock.tryLock(0, TimeUnit.SECONDS);
109             //
110             // /**
111             //   waitTime – the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回false
112             //   leaseTime – lease time 锁时长,即n秒后自动释放锁
113             //   time unit – time unit 时间单位
114             //  */
115             // // boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不带看门狗
116             // boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 带看门狗
117             // if (tryLock) {
118             //     LOG.info("恭喜,抢到锁了!");
119             //     // 可以把下面这段放开,只用一个线程来测试,看看redisson的看门狗效果
120             //     // for (int i = 0; i < 30; i++) {
121             //     //     Long expire = redisTemplate.opsForValue().getOperations().getExpire(lockKey);
122             //     //     LOG.info("锁过期时间还有:{}", expire);
123             //     //     Thread.sleep(1000);
124             //     // }
125             // } else {
126             //     // 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
127             //     LOG.info("很遗憾,没抢到锁");
128             //     throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
129             // }
130 
131             while (true) {
132                 // 取确认订单表的记录,同日期车次,状态是I,分页处理,每次取N条
133                 ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
134                 confirmOrderExample.setOrderByClause("id asc");
135                 ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
136                 criteria.andDateEqualTo(dto.getDate())
137                         .andTrainCodeEqualTo(dto.getTrainCode())
138                         .andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
139                 PageHelper.startPage(1, 5);
140                 List<ConfirmOrder> list = confirmOrderMapper.selectByExampleWithBLOBs(confirmOrderExample);
141 
142                 if (CollUtil.isEmpty(list)) {
143                     LOG.info("没有需要处理的订单,结束循环");
144                     break;
145                 } else {
146                     LOG.info("本次处理{}条订单", list.size());
147                 }
148 
149                 // 一条一条的卖
150                 list.forEach(this::sell);
151             }
152 
153             // LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);
154             // redisTemplate.delete(lockKey);
155             // } catch (InterruptedException e) {
156             //     LOG.error("购票异常", e);
157         } finally {
158             // try finally不能包含加锁的那段代码,否则加锁失败会走到finally里,从而释放别的线程的锁
159             LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);
160             redisTemplate.delete(lockKey);
161             // LOG.info("购票流程结束,释放锁!");
162             // if (null != lock && lock.isHeldByCurrentThread()) {
163             //     lock.unlock();
164             // }
165         }
166 
167     }
168 
169     /**
170      * 售票
171      * @param confirmOrder
172      */
173     private void sell(ConfirmOrder confirmOrder) {
174         // 构造ConfirmOrderDoReq
175         ConfirmOrderDoReq req = new ConfirmOrderDoReq();
176         req.setMemberId(confirmOrder.getMemberId());
177         req.setDate(confirmOrder.getDate());
178         req.setTrainCode(confirmOrder.getTrainCode());
179         req.setStart(confirmOrder.getStart());
180         req.setEnd(confirmOrder.getEnd());
181         req.setDailyTrainTicketId(confirmOrder.getDailyTrainTicketId());
182         req.setTickets(JSON.parseArray(confirmOrder.getTickets(), ConfirmOrderTicketReq.class));
183         req.setImageCode("");
184         req.setImageCodeToken("");
185         req.setLogId("");
186 
187         // 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过
188 
189         Date date = req.getDate();
190         String trainCode = req.getTrainCode();
191         String start = req.getStart();
192         String end = req.getEnd();
193         List<ConfirmOrderTicketReq> tickets = req.getTickets();
194         //
195         // // 保存确认订单表,状态初始
196         // DateTime now = DateTime.now();
197         // ConfirmOrder confirmOrder = new ConfirmOrder();
198         // confirmOrder.setId(SnowUtil.getSnowflakeNextId());
199         // confirmOrder.setCreateTime(now);
200         // confirmOrder.setUpdateTime(now);
201         // confirmOrder.setMemberId(req.getMemberId());
202         // confirmOrder.setDate(date);
203         // confirmOrder.setTrainCode(trainCode);
204         // confirmOrder.setStart(start);
205         // confirmOrder.setEnd(end);
206         // confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
207         // confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
208         // confirmOrder.setTickets(JSON.toJSONString(tickets));
209         // confirmOrderMapper.insert(confirmOrder);
210 
211         // // 从数据库里查出订单
212         // ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
213         // confirmOrderExample.setOrderByClause("id asc");
214         // ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
215         // criteria.andDateEqualTo(req.getDate())
216         //         .andTrainCodeEqualTo(req.getTrainCode())
217         //         .andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
218         // List<ConfirmOrder> list = confirmOrderMapper.selectByExampleWithBLOBs(confirmOrderExample);
219         // ConfirmOrder confirmOrder;
220         // if (CollUtil.isEmpty(list)) {
221         //     LOG.info("找不到原始订单,结束");
222         //     return;
223         // } else {
224         //     LOG.info("本次处理{}条确认订单", list.size());
225         //     confirmOrder = list.get(0);
226         // }
227 
228         // 查出余票记录,需要得到真实的库存
229         DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
230         LOG.info("查出余票记录:{}", dailyTrainTicket);
231 
232         // 预扣减余票数量,并判断余票是否足够
233         reduceTickets(req, dailyTrainTicket);
234 
235         // 最终的选座结果
236         List<DailyTrainSeat> finalSeatList = new ArrayList<>();
237         // 计算相对第一个座位的偏移值
238         // 比如选择的是C1,D2,则偏移值是:[0,5]
239         // 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]
240         ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
241         if (StrUtil.isNotBlank(ticketReq0.getSeat())) {
242             LOG.info("本次购票有选座");
243             // 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值
244             List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
245             LOG.info("本次选座的座位类型包含的列:{}", colEnumList);
246 
247             // 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}
248             List<String> referSeatList = new ArrayList<>();
249             for (int i = 1; i <= 2; i++) {
250                 for (SeatColEnum seatColEnum : colEnumList) {
251                     referSeatList.add(seatColEnum.getCode() + i);
252                 }
253             }
254             LOG.info("用于作参照的两排座位:{}", referSeatList);
255 
256             List<Integer> offsetList = new ArrayList<>();
257             // 绝对偏移值,即:在参照座位列表中的位置
258             List<Integer> aboluteOffsetList = new ArrayList<>();
259             for (ConfirmOrderTicketReq ticketReq : tickets) {
260                 int index = referSeatList.indexOf(ticketReq.getSeat());
261                 aboluteOffsetList.add(index);
262             }
263             LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);
264             for (Integer index : aboluteOffsetList) {
265                 int offset = index - aboluteOffsetList.get(0);
266                 offsetList.add(offset);
267             }
268             LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);
269 
270             getSeat(finalSeatList,
271                     date,
272                     trainCode,
273                     ticketReq0.getSeatTypeCode(),
274                     ticketReq0.getSeat().split("")[0], // 从A1得到A
275                     offsetList,
276                     dailyTrainTicket.getStartIndex(),
277                     dailyTrainTicket.getEndIndex()
278             );
279 
280         } else {
281             LOG.info("本次购票没有选座");
282             for (ConfirmOrderTicketReq ticketReq : tickets) {
283                 getSeat(finalSeatList,
284                         date,
285                         trainCode,
286                         ticketReq.getSeatTypeCode(),
287                         null,
288                         null,
289                         dailyTrainTicket.getStartIndex(),
290                         dailyTrainTicket.getEndIndex()
291                 );
292             }
293         }
294 
295         LOG.info("最终选座:{}", finalSeatList);
296 
297         // 选中座位后事务处理:
298         // 座位表修改售卖情况sell;
299         // 余票详情表修改余票;
300         // 为会员增加购票记录
301         // 更新确认订单为成功
302         try {
303             afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
304         } catch (Exception e) {
305             LOG.error("保存购票信息失败", e);
306             throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
307         }
308     }
309 
310     /**
311      * 挑座位,如果有选座,则一次性挑完,如果无选座,则一个一个挑
312      * @param date
313      * @param trainCode
314      * @param seatType
315      * @param column
316      * @param offsetList
317      */
318     private void getSeat(List<DailyTrainSeat> finalSeatList, Date date, String trainCode, String seatType, String column, List<Integer> offsetList, Integer startIndex, Integer endIndex) {
319         List<DailyTrainSeat> getSeatList = new ArrayList<>();
320         List<DailyTrainCarriage> carriageList = dailyTrainCarriageService.selectBySeatType(date, trainCode, seatType);
321         LOG.info("共查出{}个符合条件的车厢", carriageList.size());
322 
323         // 一个车箱一个车箱的获取座位数据
324         for (DailyTrainCarriage dailyTrainCarriage : carriageList) {
325             LOG.info("开始从车厢{}选座", dailyTrainCarriage.getIndex());
326             getSeatList = new ArrayList<>();
327             List<DailyTrainSeat> seatList = dailyTrainSeatService.selectByCarriage(date, trainCode, dailyTrainCarriage.getIndex());
328             LOG.info("车厢{}的座位数:{}", dailyTrainCarriage.getIndex(), seatList.size());
329             for (int i = 0; i < seatList.size(); i++) {
330                 DailyTrainSeat dailyTrainSeat = seatList.get(i);
331                 Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex();
332                 String col = dailyTrainSeat.getCol();
333 
334                 // 判断当前座位不能被选中过
335                 boolean alreadyChooseFlag = false;
336                 for (DailyTrainSeat finalSeat : finalSeatList){
337                     if (finalSeat.getId().equals(dailyTrainSeat.getId())) {
338                         alreadyChooseFlag = true;
339                         break;
340                     }
341                 }
342                 if (alreadyChooseFlag) {
343                     LOG.info("座位{}被选中过,不能重复选中,继续判断下一个座位", seatIndex);
344                     continue;
345                 }
346 
347                 // 判断column,有值的话要比对列号
348                 if (StrUtil.isBlank(column)) {
349                     LOG.info("无选座");
350                 } else {
351                     if (!column.equals(col)) {
352                         LOG.info("座位{}列值不对,继续判断下一个座位,当前列值:{},目标列值:{}", seatIndex, col, column);
353                         continue;
354                     }
355                 }
356 
357                 boolean isChoose = calSell(dailyTrainSeat, startIndex, endIndex);
358                 if (isChoose) {
359                     LOG.info("选中座位");
360                     getSeatList.add(dailyTrainSeat);
361                 } else {
362                     continue;
363                 }
364 
365                 // 根据offset选剩下的座位
366                 boolean isGetAllOffsetSeat = true;
367                 if (CollUtil.isNotEmpty(offsetList)) {
368                     LOG.info("有偏移值:{},校验偏移的座位是否可选", offsetList);
369                     // 从索引1开始,索引0就是当前已选中的票
370                     for (int j = 1; j < offsetList.size(); j++) {
371                         Integer offset = offsetList.get(j);
372                         // 座位在库的索引是从1开始
373                         // int nextIndex = seatIndex + offset - 1;
374                         int nextIndex = i + offset;
375 
376                         // 有选座时,一定是在同一个车箱
377                         if (nextIndex >= seatList.size()) {
378                             LOG.info("座位{}不可选,偏移后的索引超出了这个车箱的座位数", nextIndex);
379                             isGetAllOffsetSeat = false;
380                             break;
381                         }
382 
383                         DailyTrainSeat nextDailyTrainSeat = seatList.get(nextIndex);
384                         boolean isChooseNext = calSell(nextDailyTrainSeat, startIndex, endIndex);
385                         if (isChooseNext) {
386                             LOG.info("座位{}被选中", nextDailyTrainSeat.getCarriageSeatIndex());
387                             getSeatList.add(nextDailyTrainSeat);
388                         } else {
389                             LOG.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex());
390                             isGetAllOffsetSeat = false;
391                             break;
392                         }
393                     }
394                 }
395                 if (!isGetAllOffsetSeat) {
396                     getSeatList = new ArrayList<>();
397                     continue;
398                 }
399 
400                 // 保存选好的座位
401                 finalSeatList.addAll(getSeatList);
402                 return;
403             }
404         }
405     }
406 
407     /**
408      * 计算某座位在区间内是否可卖
409      * 例:sell=10001,本次购买区间站1~4,则区间已售000
410      * 全部是0,表示这个区间可买;只要有1,就表示区间内已售过票
411      *
412      * 选中后,要计算购票后的sell,比如原来是10001,本次购买区间站1~4
413      * 方案:构造本次购票造成的售卖信息01110,和原sell 10001按位与,最终得到11111
414      */
415     private boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex) {
416         // 00001, 00000
417         String sell = dailyTrainSeat.getSell();
418         //  000, 000
419         String sellPart = sell.substring(startIndex, endIndex);
420         if (Integer.parseInt(sellPart) > 0) {
421             LOG.info("座位{}在本次车站区间{}~{}已售过票,不可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
422             return false;
423         } else {
424             LOG.info("座位{}在本次车站区间{}~{}未售过票,可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
425             //  111,   111
426             String curSell = sellPart.replace('0', '1');
427             // 0111,  0111
428             curSell = StrUtil.fillBefore(curSell, '0', endIndex);
429             // 01110, 01110
430             curSell = StrUtil.fillAfter(curSell, '0', sell.length());
431 
432             // 当前区间售票信息curSell 01110与库里的已售信息sell 00001按位与,即可得到该座位卖出此票后的售票详情
433             // 15(01111), 14(01110 = 01110|00000)
434             int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);
435             //  1111,  1110
436             String newSell = NumberUtil.getBinaryStr(newSellInt);
437             // 01111, 01110
438             newSell = StrUtil.fillBefore(newSell, '0', sell.length());
439             LOG.info("座位{}被选中,原售票信息:{},车站区间:{}~{},即:{},最终售票信息:{}"
440                     , dailyTrainSeat.getCarriageSeatIndex(), sell, startIndex, endIndex, curSell, newSell);
441             dailyTrainSeat.setSell(newSell);
442             return true;
443 
444         }
445     }
446 
447     private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {
448         for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {
449             String seatTypeCode = ticketReq.getSeatTypeCode();
450             SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);
451             switch (seatTypeEnum) {
452                 case YDZ -> {
453                     int countLeft = dailyTrainTicket.getYdz() - 1;
454                     if (countLeft < 0) {
455                         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
456                     }
457                     dailyTrainTicket.setYdz(countLeft);
458                 }
459                 case EDZ -> {
460                     int countLeft = dailyTrainTicket.getEdz() - 1;
461                     if (countLeft < 0) {
462                         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
463                     }
464                     dailyTrainTicket.setEdz(countLeft);
465                 }
466                 case RW -> {
467                     int countLeft = dailyTrainTicket.getRw() - 1;
468                     if (countLeft < 0) {
469                         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
470                     }
471                     dailyTrainTicket.setRw(countLeft);
472                 }
473                 case YW -> {
474                     int countLeft = dailyTrainTicket.getYw() - 1;
475                     if (countLeft < 0) {
476                         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
477                     }
478                     dailyTrainTicket.setYw(countLeft);
479                 }
480             }
481         }
482     }
483 
484     /**
485      * 降级方法,需包含限流方法的所有参数和BlockException参数
486      * @param req
487      * @param e
488      */
489     public void doConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
490         LOG.info("购票请求被限流:{}", req);
491         throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
492     }
493 }
ConfirmOrderService.java

分页是后端处理大批量数据的常用做法,使用分页处理,而不是一次性查到内存里。减小内存压力。

 

要对某一订单开始出票时,先把它更新成处理中,避免重复处理

 1     /**
 2      * 更新状态
 3      * @param confirmOrder
 4      */
 5     public void updateStatus(ConfirmOrder confirmOrder) {
 6         ConfirmOrder confirmOrderForUpdate = new ConfirmOrder();
 7         confirmOrderForUpdate.setId(confirmOrder.getId());
 8         confirmOrderForUpdate.setUpdateTime(new Date());
 9         confirmOrderForUpdate.setStatus(confirmOrder.getStatus());
10         confirmOrderMapper.updateByPrimaryKeySelective(confirmOrderForUpdate);
11     }

// 将订单设置成处理中,避免重复处理
LOG.info("将确认订单更新成处理中,避免重复处理,confirm_order.id: {}", confirmOrder.getId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.PENDING.getCode());
updateStatus(confirmOrder);

 

某一订单余票不足时,继续售卖下一订单

                // 一条一条的卖
                list.forEach(confirmOrder -> {
                    try {
                        sell(confirmOrder);
                    } catch (BusinessException e) {
                        if (e.getE().equals(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR)) {
                            LOG.info("本订单余票不足,继续售卖下一个订单");
                            confirmOrder.setStatus(ConfirmOrderStatusEnum.EMPTY.getCode());
                            updateStatus(confirmOrder);
                        } else {
                            throw e;
                        }
                    }
                });

 

MQ消费里,没抢到锁的,表示有其它消费线程正在出票,不做任何处理

LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
//            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
            LOG.info("没抢到锁,有其它消费线程正在出票,不做任何处理");
            return;

十个人抢票,会有十个Q,只有一个抢到锁,其他直接返回

 

增加轮询购票结果功能

 订单轮询结果:

1.有终态:成功、失败、没票等

2.非终态:告知排队数量

前端确认订单后,显示模态框

 确认订单后,显示模态框:系统处理中

  1 <template>
  2   <div class="order-train">
  3     <span class="order-train-main">{{dailyTrainTicket.date}}</span>&nbsp;
  4     <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次&nbsp;
  5     <span class="order-train-main">{{dailyTrainTicket.start}}</span>站
  6     <span class="order-train-main">({{dailyTrainTicket.startTime}})</span>&nbsp;
  7     <span class="order-train-main">——</span>&nbsp;
  8     <span class="order-train-main">{{dailyTrainTicket.end}}</span>站
  9     <span class="order-train-main">({{dailyTrainTicket.endTime}})</span>&nbsp;
 10 
 11     <div class="order-train-ticket">
 12       <span v-for="item in seatTypes" :key="item.type">
 13         <span>{{item.desc}}</span>:
 14         <span class="order-train-ticket-main">{{item.price}}¥</span>&nbsp;
 15         <span class="order-train-ticket-main">{{item.count}}</span>&nbsp;张票&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 16       </span>
 17     </div>
 18   </div>
 19   <a-divider></a-divider>
 20   <b>勾选要购票的乘客:</b>&nbsp;
 21   <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
 22 
 23   <div class="order-tickets">
 24     <a-row class="order-tickets-header" v-if="tickets.length > 0">
 25       <a-col :span="2">乘客</a-col>
 26       <a-col :span="6">身份证</a-col>
 27       <a-col :span="4">票种</a-col>
 28       <a-col :span="4">座位类型</a-col>
 29     </a-row>
 30     <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
 31       <a-col :span="2">{{ticket.passengerName}}</a-col>
 32       <a-col :span="6">{{ticket.passengerIdCard}}</a-col>
 33       <a-col :span="4">
 34         <a-select v-model:value="ticket.passengerType" style="width: 100%">
 35           <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
 36             {{item.desc}}
 37           </a-select-option>
 38         </a-select>
 39       </a-col>
 40       <a-col :span="4">
 41         <a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
 42           <a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
 43             {{item.desc}}
 44           </a-select-option>
 45         </a-select>
 46       </a-col>
 47     </a-row>
 48   </div>
 49   <div v-if="tickets.length > 0">
 50     <a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
 51   </div>
 52 
 53   <a-modal v-model:visible="visible" title="请核对以下信息"
 54            style="top: 50px; width: 800px"
 55            ok-text="确认" cancel-text="取消"
 56            @ok="showFirstImageCodeModal">
 57     <div class="order-tickets">
 58       <a-row class="order-tickets-header" v-if="tickets.length > 0">
 59         <a-col :span="3">乘客</a-col>
 60         <a-col :span="15">身份证</a-col>
 61         <a-col :span="3">票种</a-col>
 62         <a-col :span="3">座位类型</a-col>
 63       </a-row>
 64       <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
 65         <a-col :span="3">{{ticket.passengerName}}</a-col>
 66         <a-col :span="15">{{ticket.passengerIdCard}}</a-col>
 67         <a-col :span="3">
 68           <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
 69             <span v-if="item.code === ticket.passengerType">
 70               {{item.desc}}
 71             </span>
 72           </span>
 73         </a-col>
 74         <a-col :span="3">
 75           <span v-for="item in seatTypes" :key="item.code">
 76             <span v-if="item.code === ticket.seatTypeCode">
 77               {{item.desc}}
 78             </span>
 79           </span>
 80         </a-col>
 81       </a-row>
 82       <br/>
 83       <div v-if="chooseSeatType === 0" style="color: red;">
 84         您购买的车票不支持选座
 85         <div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
 86         <div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
 87       </div>
 88       <div v-else style="text-align: center">
 89         <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
 90                   v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
 91         <div v-if="tickets.length > 1">
 92           <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
 93                     v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
 94         </div>
 95         <div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
 96       </div>
 97       <!--<br/>-->
 98       <!--最终购票:{{tickets}}-->
 99       <!--最终选座:{{chooseSeatObj}}-->
100     </div>
101   </a-modal>
102 
103   <!-- 第二层验证码 后端 -->
104   <a-modal v-model:visible="imageCodeModalVisible" :title="null" :footer="null" :closable="false"
105            style="top: 50px; width: 400px">
106     <p style="text-align: center; font-weight: bold; font-size: 18px">
107       使用服务端验证码削弱瞬时高峰<br/>
108       防止机器人刷票
109     </p>
110     <p>
111       <a-input v-model:value="imageCode" placeholder="图片验证码">
112         <template #suffix>
113           <img v-show="!!imageCodeSrc" :src="imageCodeSrc" alt="验证码" v-on:click="loadImageCode()"/>
114         </template>
115       </a-input>
116     </p>
117     <a-button type="danger" block @click="handleOk">输入验证码后开始购票</a-button>
118   </a-modal>
119 
120   <!-- 第一层验证码 纯前端 -->
121   <a-modal v-model:visible="firstImageCodeModalVisible" :title="null" :footer="null" :closable="false"
122            style="top: 50px; width: 400px">
123     <p style="text-align: center; font-weight: bold; font-size: 18px">
124       使用纯前端验证码削弱瞬时高峰<br/>
125       减小后端验证码接口的压力
126     </p>
127     <p>
128       <a-input v-model:value="firstImageCodeTarget" placeholder="验证码">
129         <template #suffix>
130           {{firstImageCodeSourceA}} + {{firstImageCodeSourceB}}
131         </template>
132       </a-input>
133     </p>
134     <a-button type="danger" block @click="validFirstImageCode">提交验证码</a-button>
135   </a-modal>
136 
137   <a-modal v-model:visible="lineModalVisible" :title="null" :footer="null" :maskClosable="false" :closable="false"
138            style="top: 50px; width: 400px">
139     <div class="book-line">
140       <loading-outlined /> 系统正在处理中...
141     </div>
142   </a-modal>
143 </template>
144 
145 <script>
146 
147 import {defineComponent, ref, onMounted, watch, computed} from 'vue';
148 import axios from "axios";
149 import {notification} from "ant-design-vue";
150 
151 export default defineComponent({
152   name: "order-view",
153   setup() {
154     const passengers = ref([]);
155     const passengerOptions = ref([]);
156     const passengerChecks = ref([]);
157     const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
158     console.log("下单的车次信息", dailyTrainTicket);
159 
160     const SEAT_TYPE = window.SEAT_TYPE;
161     console.log(SEAT_TYPE)
162     // 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
163     // {
164     //   type: "YDZ",
165     //   code: "1",
166     //   desc: "一等座",
167     //   count: "100",
168     //   price: "50",
169     // }
170     // 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
171     const seatTypes = [];
172     for (let KEY in SEAT_TYPE) {
173       let key = KEY.toLowerCase();
174       if (dailyTrainTicket[key] >= 0) {
175         seatTypes.push({
176           type: KEY,
177           code: SEAT_TYPE[KEY]["code"],
178           desc: SEAT_TYPE[KEY]["desc"],
179           count: dailyTrainTicket[key],
180           price: dailyTrainTicket[key + 'Price'],
181         })
182       }
183     }
184     console.log("本车次提供的座位:", seatTypes)
185     // 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
186     // {
187     //   passengerId: 123,
188     //   passengerType: "1",
189     //   passengerName: "张三",
190     //   passengerIdCard: "12323132132",
191     //   seatTypeCode: "1",
192     //   seat: "C1"
193     // }
194     const tickets = ref([]);
195     const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
196     const visible = ref(false);
197     const lineModalVisible = ref(false);
198 
199     // 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
200     watch(() => passengerChecks.value, (newVal, oldVal)=>{
201       console.log("勾选乘客发生变化", newVal, oldVal)
202       // 每次有变化时,把购票列表清空,重新构造列表
203       tickets.value = [];
204       passengerChecks.value.forEach((item) => tickets.value.push({
205         passengerId: item.id,
206         passengerType: item.type,
207         seatTypeCode: seatTypes[0].code,
208         passengerName: item.name,
209         passengerIdCard: item.idCard
210       }))
211     }, {immediate: true});
212 
213     // 0:不支持选座;1:选一等座;2:选二等座
214     const chooseSeatType = ref(0);
215     // 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
216     const SEAT_COL_ARRAY = computed(() => {
217       return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
218     });
219     // 选择的座位
220     // {
221     //   A1: false, C1: true,D1: false, F1: false,
222     //   A2: false, C2: false,D2: true, F2: false
223     // }
224     const chooseSeatObj = ref({});
225     watch(() => SEAT_COL_ARRAY.value, () => {
226       chooseSeatObj.value = {};
227       for (let i = 1; i <= 2; i++) {
228         SEAT_COL_ARRAY.value.forEach((item) => {
229           chooseSeatObj.value[item.code + i] = false;
230         })
231       }
232       console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
233     }, {immediate: true});
234 
235     const handleQueryPassenger = () => {
236       axios.get("/member/passenger/query-mine").then((response) => {
237         let data = response.data;
238         if (data.success) {
239           passengers.value = data.content;
240           passengers.value.forEach((item) => passengerOptions.value.push({
241             label: item.name,
242             value: item
243           }))
244         } else {
245           notification.error({description: data.message});
246         }
247       });
248     };
249 
250     const finishCheckPassenger = () => {
251       console.log("购票列表:", tickets.value);
252 
253       if (tickets.value.length > 5) {
254         notification.error({description: '最多只能购买5张车票'});
255         return;
256       }
257 
258       // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
259       // 前端校验不一定准,但前端校验可以减轻后端很多压力
260       // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
261       let seatTypesTemp = Tool.copy(seatTypes);
262       for (let i = 0; i < tickets.value.length; i++) {
263         let ticket = tickets.value[i];
264         for (let j = 0; j < seatTypesTemp.length; j++) {
265           let seatType = seatTypesTemp[j];
266           // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
267           if (ticket.seatTypeCode === seatType.code) {
268             seatType.count--;
269             if (seatType.count < 0) {
270               notification.error({description: seatType.desc + '余票不足'});
271               return;
272             }
273           }
274         }
275       }
276       console.log("前端余票校验通过");
277 
278       // 判断是否支持选座,只有纯一等座和纯二等座支持选座
279       // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
280       let ticketSeatTypeCodes = [];
281       for (let i = 0; i < tickets.value.length; i++) {
282         let ticket = tickets.value[i];
283         ticketSeatTypeCodes.push(ticket.seatTypeCode);
284       }
285       // 为购票列表中的所有座位类型去重:[1, 2]
286       const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
287       console.log("选好的座位类型:", ticketSeatTypeCodesSet);
288       if (ticketSeatTypeCodesSet.length !== 1) {
289         console.log("选了多种座位,不支持选座");
290         chooseSeatType.value = 0;
291       } else {
292         // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
293         if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
294           console.log("一等座选座");
295           chooseSeatType.value = SEAT_TYPE.YDZ.code;
296         } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
297           console.log("二等座选座");
298           chooseSeatType.value = SEAT_TYPE.EDZ.code;
299         } else {
300           console.log("不是一等座或二等座,不支持选座");
301           chooseSeatType.value = 0;
302         }
303 
304         // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
305         if (chooseSeatType.value !== 0) {
306           for (let i = 0; i < seatTypes.length; i++) {
307             let seatType = seatTypes[i];
308             // 找到同类型座位
309             if (ticketSeatTypeCodesSet[0] === seatType.code) {
310               // 判断余票,小于20张就不支持选座
311               if (seatType.count < 20) {
312                 console.log("余票小于20张就不支持选座")
313                 chooseSeatType.value = 0;
314                 break;
315               }
316             }
317           }
318         }
319       }
320 
321       // 弹出确认界面
322       visible.value = true;
323 
324     };
325 
326     const handleOk = () => {
327       if (Tool.isEmpty(imageCode.value)) {
328         notification.error({description: '验证码不能为空'});
329         return;
330       }
331 
332       console.log("选好的座位:", chooseSeatObj.value);
333 
334       // 设置每张票的座位
335       // 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
336       for (let i = 0; i < tickets.value.length; i++) {
337         tickets.value[i].seat = null;
338       }
339       let i = -1;
340       // 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
341       for (let key in chooseSeatObj.value) {
342         if (chooseSeatObj.value[key]) {
343           i++;
344           if (i > tickets.value.length - 1) {
345             notification.error({description: '所选座位数大于购票数'});
346             return;
347           }
348           tickets.value[i].seat = key;
349         }
350       }
351       if (i > -1 && i < (tickets.value.length - 1)) {
352         notification.error({description: '所选座位数小于购票数'});
353         return;
354       }
355 
356       console.log("最终购票:", tickets.value);
357 
358       axios.post("/business/confirm-order/do", {
359         dailyTrainTicketId: dailyTrainTicket.id,
360         date: dailyTrainTicket.date,
361         trainCode: dailyTrainTicket.trainCode,
362         start: dailyTrainTicket.start,
363         end: dailyTrainTicket.end,
364         tickets: tickets.value,
365         imageCodeToken: imageCodeToken.value,
366         imageCode: imageCode.value,
367       }).then((response) => {
368         let data = response.data;
369         if (data.success) {
370           // notification.success({description: "下单成功!"});
371           visible.value = false;
372           imageCodeModalVisible.value = false;
373           lineModalVisible.value = true;
374         } else {
375           notification.error({description: data.message});
376         }
377       });
378     }
379 
380     /* ------------------- 第二层验证码 --------------------- */
381     const imageCodeModalVisible = ref();
382     const imageCodeToken = ref();
383     const imageCodeSrc = ref();
384     const imageCode = ref();
385     /**
386      * 加载图形验证码
387      */
388     const loadImageCode = () => {
389       imageCodeToken.value = Tool.uuid(8);
390       imageCodeSrc.value = process.env.VUE_APP_SERVER + '/business/kaptcha/image-code/' + imageCodeToken.value;
391     };
392 
393     const showImageCodeModal = () => {
394       loadImageCode();
395       imageCodeModalVisible.value = true;
396     };
397 
398     /* ------------------- 第一层验证码 --------------------- */
399     const firstImageCodeSourceA = ref();
400     const firstImageCodeSourceB = ref();
401     const firstImageCodeTarget = ref();
402     const firstImageCodeModalVisible = ref();
403 
404     /**
405      * 加载第一层验证码
406      */
407     const loadFirstImageCode = () => {
408       // 获取1~10的数:Math.floor(Math.random()*10 + 1)
409       firstImageCodeSourceA.value = Math.floor(Math.random()*10 + 1) + 10;
410       firstImageCodeSourceB.value = Math.floor(Math.random()*10 + 1) + 20;
411     };
412 
413     /**
414      * 显示第一层验证码弹出框
415      */
416     const showFirstImageCodeModal = () => {
417       loadFirstImageCode();
418       firstImageCodeModalVisible.value = true;
419     };
420 
421     /**
422      * 校验第一层验证码
423      */
424     const validFirstImageCode = () => {
425       if (parseInt(firstImageCodeTarget.value) === parseInt(firstImageCodeSourceA.value + firstImageCodeSourceB.value)) {
426         // 第一层验证通过
427         firstImageCodeModalVisible.value = false;
428         showImageCodeModal();
429       } else {
430         notification.error({description: '验证码错误'});
431       }
432     };
433 
434     onMounted(() => {
435       handleQueryPassenger();
436     });
437 
438     return {
439       passengers,
440       dailyTrainTicket,
441       seatTypes,
442       passengerOptions,
443       passengerChecks,
444       tickets,
445       PASSENGER_TYPE_ARRAY,
446       visible,
447       finishCheckPassenger,
448       chooseSeatType,
449       chooseSeatObj,
450       SEAT_COL_ARRAY,
451       handleOk,
452       imageCodeToken,
453       imageCodeSrc,
454       imageCode,
455       showImageCodeModal,
456       imageCodeModalVisible,
457       loadImageCode,
458       firstImageCodeSourceA,
459       firstImageCodeSourceB,
460       firstImageCodeTarget,
461       firstImageCodeModalVisible,
462       showFirstImageCodeModal,
463       validFirstImageCode,
464       lineModalVisible
465     };
466   },
467 });
468 </script>
469 
470 <style>
471 .order-train .order-train-main {
472   font-size: 18px;
473   font-weight: bold;
474 }
475 .order-train .order-train-ticket {
476   margin-top: 15px;
477 }
478 .order-train .order-train-ticket .order-train-ticket-main {
479   color: red;
480   font-size: 18px;
481 }
482 
483 .order-tickets {
484   margin: 10px 0;
485 }
486 .order-tickets .ant-col {
487   padding: 5px 10px;
488 }
489 .order-tickets .order-tickets-header {
490   background-color: cornflowerblue;
491   border: solid 1px cornflowerblue;
492   color: white;
493   font-size: 16px;
494   padding: 5px 0;
495 }
496 .order-tickets .order-tickets-row {
497   border: solid 1px cornflowerblue;
498   border-top: none;
499   vertical-align: middle;
500   line-height: 30px;
501 }
502 
503 .order-tickets .choose-seat-item {
504   margin: 5px 5px;
505 }
506 </style>
order.vue

确认订单接口返回确认订单ID,方便后续做排队查询

  1 <template>
  2   <div class="order-train">
  3     <span class="order-train-main">{{dailyTrainTicket.date}}</span>&nbsp;
  4     <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次&nbsp;
  5     <span class="order-train-main">{{dailyTrainTicket.start}}</span>站
  6     <span class="order-train-main">({{dailyTrainTicket.startTime}})</span>&nbsp;
  7     <span class="order-train-main">——</span>&nbsp;
  8     <span class="order-train-main">{{dailyTrainTicket.end}}</span>站
  9     <span class="order-train-main">({{dailyTrainTicket.endTime}})</span>&nbsp;
 10 
 11     <div class="order-train-ticket">
 12       <span v-for="item in seatTypes" :key="item.type">
 13         <span>{{item.desc}}</span>:
 14         <span class="order-train-ticket-main">{{item.price}}¥</span>&nbsp;
 15         <span class="order-train-ticket-main">{{item.count}}</span>&nbsp;张票&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 16       </span>
 17     </div>
 18   </div>
 19   <a-divider></a-divider>
 20   <b>勾选要购票的乘客:</b>&nbsp;
 21   <a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
 22 
 23   <div class="order-tickets">
 24     <a-row class="order-tickets-header" v-if="tickets.length > 0">
 25       <a-col :span="2">乘客</a-col>
 26       <a-col :span="6">身份证</a-col>
 27       <a-col :span="4">票种</a-col>
 28       <a-col :span="4">座位类型</a-col>
 29     </a-row>
 30     <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
 31       <a-col :span="2">{{ticket.passengerName}}</a-col>
 32       <a-col :span="6">{{ticket.passengerIdCard}}</a-col>
 33       <a-col :span="4">
 34         <a-select v-model:value="ticket.passengerType" style="width: 100%">
 35           <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
 36             {{item.desc}}
 37           </a-select-option>
 38         </a-select>
 39       </a-col>
 40       <a-col :span="4">
 41         <a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
 42           <a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
 43             {{item.desc}}
 44           </a-select-option>
 45         </a-select>
 46       </a-col>
 47     </a-row>
 48   </div>
 49   <div v-if="tickets.length > 0">
 50     <a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
 51   </div>
 52 
 53   <a-modal v-model:visible="visible" title="请核对以下信息"
 54            style="top: 50px; width: 800px"
 55            ok-text="确认" cancel-text="取消"
 56            @ok="showFirstImageCodeModal">
 57     <div class="order-tickets">
 58       <a-row class="order-tickets-header" v-if="tickets.length > 0">
 59         <a-col :span="3">乘客</a-col>
 60         <a-col :span="15">身份证</a-col>
 61         <a-col :span="3">票种</a-col>
 62         <a-col :span="3">座位类型</a-col>
 63       </a-row>
 64       <a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
 65         <a-col :span="3">{{ticket.passengerName}}</a-col>
 66         <a-col :span="15">{{ticket.passengerIdCard}}</a-col>
 67         <a-col :span="3">
 68           <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
 69             <span v-if="item.code === ticket.passengerType">
 70               {{item.desc}}
 71             </span>
 72           </span>
 73         </a-col>
 74         <a-col :span="3">
 75           <span v-for="item in seatTypes" :key="item.code">
 76             <span v-if="item.code === ticket.seatTypeCode">
 77               {{item.desc}}
 78             </span>
 79           </span>
 80         </a-col>
 81       </a-row>
 82       <br/>
 83       <div v-if="chooseSeatType === 0" style="color: red;">
 84         您购买的车票不支持选座
 85         <div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
 86         <div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
 87       </div>
 88       <div v-else style="text-align: center">
 89         <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
 90                   v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
 91         <div v-if="tickets.length > 1">
 92           <a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
 93                     v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
 94         </div>
 95         <div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
 96       </div>
 97       <!--<br/>-->
 98       <!--最终购票:{{tickets}}-->
 99       <!--最终选座:{{chooseSeatObj}}-->
100     </div>
101   </a-modal>
102 
103   <!-- 第二层验证码 后端 -->
104   <a-modal v-model:visible="imageCodeModalVisible" :title="null" :footer="null" :closable="false"
105            style="top: 50px; width: 400px">
106     <p style="text-align: center; font-weight: bold; font-size: 18px">
107       使用服务端验证码削弱瞬时高峰<br/>
108       防止机器人刷票
109     </p>
110     <p>
111       <a-input v-model:value="imageCode" placeholder="图片验证码">
112         <template #suffix>
113           <img v-show="!!imageCodeSrc" :src="imageCodeSrc" alt="验证码" v-on:click="loadImageCode()"/>
114         </template>
115       </a-input>
116     </p>
117     <a-button type="danger" block @click="handleOk">输入验证码后开始购票</a-button>
118   </a-modal>
119 
120   <!-- 第一层验证码 纯前端 -->
121   <a-modal v-model:visible="firstImageCodeModalVisible" :title="null" :footer="null" :closable="false"
122            style="top: 50px; width: 400px">
123     <p style="text-align: center; font-weight: bold; font-size: 18px">
124       使用纯前端验证码削弱瞬时高峰<br/>
125       减小后端验证码接口的压力
126     </p>
127     <p>
128       <a-input v-model:value="firstImageCodeTarget" placeholder="验证码">
129         <template #suffix>
130           {{firstImageCodeSourceA}} + {{firstImageCodeSourceB}}
131         </template>
132       </a-input>
133     </p>
134     <a-button type="danger" block @click="validFirstImageCode">提交验证码</a-button>
135   </a-modal>
136 
137   <a-modal v-model:visible="lineModalVisible" :title="null" :footer="null" :maskClosable="false" :closable="false"
138            style="top: 50px; width: 400px">
139     <div class="book-line">
140       <loading-outlined /> 确认订单:{{confirmOrderId}},系统正在处理中...
141     </div>
142   </a-modal>
143 </template>
144 
145 <script>
146 
147 import {defineComponent, ref, onMounted, watch, computed} from 'vue';
148 import axios from "axios";
149 import {notification} from "ant-design-vue";
150 
151 export default defineComponent({
152   name: "order-view",
153   setup() {
154     const passengers = ref([]);
155     const passengerOptions = ref([]);
156     const passengerChecks = ref([]);
157     const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
158     console.log("下单的车次信息", dailyTrainTicket);
159 
160     const SEAT_TYPE = window.SEAT_TYPE;
161     console.log(SEAT_TYPE)
162     // 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
163     // {
164     //   type: "YDZ",
165     //   code: "1",
166     //   desc: "一等座",
167     //   count: "100",
168     //   price: "50",
169     // }
170     // 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
171     const seatTypes = [];
172     for (let KEY in SEAT_TYPE) {
173       let key = KEY.toLowerCase();
174       if (dailyTrainTicket[key] >= 0) {
175         seatTypes.push({
176           type: KEY,
177           code: SEAT_TYPE[KEY]["code"],
178           desc: SEAT_TYPE[KEY]["desc"],
179           count: dailyTrainTicket[key],
180           price: dailyTrainTicket[key + 'Price'],
181         })
182       }
183     }
184     console.log("本车次提供的座位:", seatTypes)
185     // 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
186     // {
187     //   passengerId: 123,
188     //   passengerType: "1",
189     //   passengerName: "张三",
190     //   passengerIdCard: "12323132132",
191     //   seatTypeCode: "1",
192     //   seat: "C1"
193     // }
194     const tickets = ref([]);
195     const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
196     const visible = ref(false);
197     const lineModalVisible = ref(false);
198     const confirmOrderId = ref();
199 
200     // 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
201     watch(() => passengerChecks.value, (newVal, oldVal)=>{
202       console.log("勾选乘客发生变化", newVal, oldVal)
203       // 每次有变化时,把购票列表清空,重新构造列表
204       tickets.value = [];
205       passengerChecks.value.forEach((item) => tickets.value.push({
206         passengerId: item.id,
207         passengerType: item.type,
208         seatTypeCode: seatTypes[0].code,
209         passengerName: item.name,
210         passengerIdCard: item.idCard
211       }))
212     }, {immediate: true});
213 
214     // 0:不支持选座;1:选一等座;2:选二等座
215     const chooseSeatType = ref(0);
216     // 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
217     const SEAT_COL_ARRAY = computed(() => {
218       return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
219     });
220     // 选择的座位
221     // {
222     //   A1: false, C1: true,D1: false, F1: false,
223     //   A2: false, C2: false,D2: true, F2: false
224     // }
225     const chooseSeatObj = ref({});
226     watch(() => SEAT_COL_ARRAY.value, () => {
227       chooseSeatObj.value = {};
228       for (let i = 1; i <= 2; i++) {
229         SEAT_COL_ARRAY.value.forEach((item) => {
230           chooseSeatObj.value[item.code + i] = false;
231         })
232       }
233       console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
234     }, {immediate: true});
235 
236     const handleQueryPassenger = () => {
237       axios.get("/member/passenger/query-mine").then((response) => {
238         let data = response.data;
239         if (data.success) {
240           passengers.value = data.content;
241           passengers.value.forEach((item) => passengerOptions.value.push({
242             label: item.name,
243             value: item
244           }))
245         } else {
246           notification.error({description: data.message});
247         }
248       });
249     };
250 
251     const finishCheckPassenger = () => {
252       console.log("购票列表:", tickets.value);
253 
254       if (tickets.value.length > 5) {
255         notification.error({description: '最多只能购买5张车票'});
256         return;
257       }
258 
259       // 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
260       // 前端校验不一定准,但前端校验可以减轻后端很多压力
261       // 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
262       let seatTypesTemp = Tool.copy(seatTypes);
263       for (let i = 0; i < tickets.value.length; i++) {
264         let ticket = tickets.value[i];
265         for (let j = 0; j < seatTypesTemp.length; j++) {
266           let seatType = seatTypesTemp[j];
267           // 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
268           if (ticket.seatTypeCode === seatType.code) {
269             seatType.count--;
270             if (seatType.count < 0) {
271               notification.error({description: seatType.desc + '余票不足'});
272               return;
273             }
274           }
275         }
276       }
277       console.log("前端余票校验通过");
278 
279       // 判断是否支持选座,只有纯一等座和纯二等座支持选座
280       // 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
281       let ticketSeatTypeCodes = [];
282       for (let i = 0; i < tickets.value.length; i++) {
283         let ticket = tickets.value[i];
284         ticketSeatTypeCodes.push(ticket.seatTypeCode);
285       }
286       // 为购票列表中的所有座位类型去重:[1, 2]
287       const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
288       console.log("选好的座位类型:", ticketSeatTypeCodesSet);
289       if (ticketSeatTypeCodesSet.length !== 1) {
290         console.log("选了多种座位,不支持选座");
291         chooseSeatType.value = 0;
292       } else {
293         // ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
294         if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
295           console.log("一等座选座");
296           chooseSeatType.value = SEAT_TYPE.YDZ.code;
297         } else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
298           console.log("二等座选座");
299           chooseSeatType.value = SEAT_TYPE.EDZ.code;
300         } else {
301           console.log("不是一等座或二等座,不支持选座");
302           chooseSeatType.value = 0;
303         }
304 
305         // 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
306         if (chooseSeatType.value !== 0) {
307           for (let i = 0; i < seatTypes.length; i++) {
308             let seatType = seatTypes[i];
309             // 找到同类型座位
310             if (ticketSeatTypeCodesSet[0] === seatType.code) {
311               // 判断余票,小于20张就不支持选座
312               if (seatType.count < 20) {
313                 console.log("余票小于20张就不支持选座")
314                 chooseSeatType.value = 0;
315                 break;
316               }
317             }
318           }
319         }
320       }
321 
322       // 弹出确认界面
323       visible.value = true;
324 
325     };
326 
327     const handleOk = () => {
328       if (Tool.isEmpty(imageCode.value)) {
329         notification.error({description: '验证码不能为空'});
330         return;
331       }
332 
333       console.log("选好的座位:", chooseSeatObj.value);
334 
335       // 设置每张票的座位
336       // 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
337       for (let i = 0; i < tickets.value.length; i++) {
338         tickets.value[i].seat = null;
339       }
340       let i = -1;
341       // 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
342       for (let key in chooseSeatObj.value) {
343         if (chooseSeatObj.value[key]) {
344           i++;
345           if (i > tickets.value.length - 1) {
346             notification.error({description: '所选座位数大于购票数'});
347             return;
348           }
349           tickets.value[i].seat = key;
350         }
351       }
352       if (i > -1 && i < (tickets.value.length - 1)) {
353         notification.error({description: '所选座位数小于购票数'});
354         return;
355       }
356 
357       console.log("最终购票:", tickets.value);
358 
359       axios.post("/business/confirm-order/do", {
360         dailyTrainTicketId: dailyTrainTicket.id,
361         date: dailyTrainTicket.date,
362         trainCode: dailyTrainTicket.trainCode,
363         start: dailyTrainTicket.start,
364         end: dailyTrainTicket.end,
365         tickets: tickets.value,
366         imageCodeToken: imageCodeToken.value,
367         imageCode: imageCode.value,
368       }).then((response) => {
369         let data = response.data;
370         if (data.success) {
371           // notification.success({description: "下单成功!"});
372           visible.value = false;
373           imageCodeModalVisible.value = false;
374           lineModalVisible.value = true;
375           confirmOrderId.value = data.content;
376         } else {
377           notification.error({description: data.message});
378         }
379       });
380     }
381 
382     /* ------------------- 第二层验证码 --------------------- */
383     const imageCodeModalVisible = ref();
384     const imageCodeToken = ref();
385     const imageCodeSrc = ref();
386     const imageCode = ref();
387     /**
388      * 加载图形验证码
389      */
390     const loadImageCode = () => {
391       imageCodeToken.value = Tool.uuid(8);
392       imageCodeSrc.value = process.env.VUE_APP_SERVER + '/business/kaptcha/image-code/' + imageCodeToken.value;
393     };
394 
395     const showImageCodeModal = () => {
396       loadImageCode();
397       imageCodeModalVisible.value = true;
398     };
399 
400     /* ------------------- 第一层验证码 --------------------- */
401     const firstImageCodeSourceA = ref();
402     const firstImageCodeSourceB = ref();
403     const firstImageCodeTarget = ref();
404     const firstImageCodeModalVisible = ref();
405 
406     /**
407      * 加载第一层验证码
408      */
409     const loadFirstImageCode = () => {
410       // 获取1~10的数:Math.floor(Math.random()*10 + 1)
411       firstImageCodeSourceA.value = Math.floor(Math.random()*10 + 1) + 10;
412       firstImageCodeSourceB.value = Math.floor(Math.random()*10 + 1) + 20;
413     };
414 
415     /**
416      * 显示第一层验证码弹出框
417      */
418     const showFirstImageCodeModal = () => {
419       loadFirstImageCode();
420       firstImageCodeModalVisible.value = true;
421     };
422 
423     /**
424      * 校验第一层验证码
425      */
426     const validFirstImageCode = () => {
427       if (parseInt(firstImageCodeTarget.value) === parseInt(firstImageCodeSourceA.value + firstImageCodeSourceB.value)) {
428         // 第一层验证通过
429         firstImageCodeModalVisible.value = false;
430         showImageCodeModal();
431       } else {
432         notification.error({description: '验证码错误'});
433       }
434     };
435 
436     onMounted(() => {
437       handleQueryPassenger();
438     });
439 
440     return {
441       passengers,
442       dailyTrainTicket,
443       seatTypes,
444       passengerOptions,
445       passengerChecks,
446       tickets,
447       PASSENGER_TYPE_ARRAY,
448       visible,
449       finishCheckPassenger,
450       chooseSeatType,
451       chooseSeatObj,
452       SEAT_COL_ARRAY,
453       handleOk,
454       imageCodeToken,
455       imageCodeSrc,
456       imageCode,
457       showImageCodeModal,
458       imageCodeModalVisible,
459       loadImageCode,
460       firstImageCodeSourceA,
461       firstImageCodeSourceB,
462       firstImageCodeTarget,
463       firstImageCodeModalVisible,
464       showFirstImageCodeModal,
465       validFirstImageCode,
466       lineModalVisible,
467       confirmOrderId
468     };
469   },
470 });
471 </script>
472 
473 <style>
474 .order-train .order-train-main {
475   font-size: 18px;
476   font-weight: bold;
477 }
478 .order-train .order-train-ticket {
479   margin-top: 15px;
480 }
481 .order-train .order-train-ticket .order-train-ticket-main {
482   color: red;
483   font-size: 18px;
484 }
485 
486 .order-tickets {
487   margin: 10px 0;
488 }
489 .order-tickets .ant-col {
490   padding: 5px 10px;
491 }
492 .order-tickets .order-tickets-header {
493   background-color: cornflowerblue;
494   border: solid 1px cornflowerblue;
495   color: white;
496   font-size: 16px;
497   padding: 5px 0;
498 }
499 .order-tickets .order-tickets-row {
500   border: solid 1px cornflowerblue;
501   border-top: none;
502   vertical-align: middle;
503   line-height: 30px;
504 }
505 
506 .order-tickets .choose-seat-item {
507   margin: 5px 5px;
508 }
509 </style>
order.vue

BeforeConfirmOrder将订单表的信息返回给前端   return confirmOrder.getId();

 

增加查询排队数量接口,返回排队数量或订单终态(成功、失败、无票、取消等)

ConfirmOrderService.java

 1  /**
 2      * 查询前面有几个人在排队
 3      * @param id
 4      */
 5     public Integer queryLineCount(Long id) {
 6         ConfirmOrder confirmOrder = confirmOrderMapper.selectByPrimaryKey(id);
 7         ConfirmOrderStatusEnum statusEnum = EnumUtil.getBy(ConfirmOrderStatusEnum::getCode, confirmOrder.getStatus());
 8         int result = switch (statusEnum) {
 9             case PENDING -> 0; // 排队0
10             case SUCCESS -> -1; // 成功
11             case FAILURE -> -2; // 失败
12             case EMPTY -> -3; // 无票
13             case CANCEL -> -4; // 取消
14             case INIT -> 999; // 需要查表得到实际排队数量
15         };
16 
17         if (result == 999) {
18             // 排在第几位,下面的写法:where a=1 and (b=1 or c=1) 等价于 where (a=1 and b=1) or (a=1 and c=1)
19             ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
20             confirmOrderExample.or().andDateEqualTo(confirmOrder.getDate())
21                     .andTrainCodeEqualTo(confirmOrder.getTrainCode())
22                     .andCreateTimeLessThan(confirmOrder.getCreateTime())
23                     .andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
24             confirmOrderExample.or().andDateEqualTo(confirmOrder.getDate())
25                     .andTrainCodeEqualTo(confirmOrder.getTrainCode())
26                     .andCreateTimeLessThan(confirmOrder.getCreateTime())
27                     .andStatusEqualTo(ConfirmOrderStatusEnum.PENDING.getCode());
28             return Math.toIntExact(confirmOrderMapper.countByExample(confirmOrderExample));
29         } else {
30             return result;
31         }
32     }

ConfirmOrderController.java

1     @GetMapping("/query-line-count/{id}")
2     public CommonResp<Integer> queryLineCount(@PathVariable Long id) {
3         Integer count = confirmOrderService.queryLineCount(id);
4         return new CommonResp<>(count);
5     }