内容
- rocketMQ基本介绍
- 使用MQ,将购票流程一分为二。目前系统的吞吐量低,用户从购买车票到拿到票花费的时间较长。
- 增加排队购票功能。排队提示loading。
购票时序图
目前的时序图,用户发送购票请求,服务端校验验证码,拿令牌,拿锁,然后选座购票,结束流程才会返回。服务器执行时间太长。
增加异步,拿锁分为两步,拿令牌锁要放在同步里,拿车次锁要放在异步里。用来防止机器人和超卖。拿到后便响应给用户,告诉用户有资格买票。异步线程中选座购票。之后前端发起轮询,调用后端的查询接口。
再次改进,将异步操作放到出票模块,接收购票请求的模块与购票模块分开,可以用不同的服务器,从而为选座购票功能分配更多的节点。服务端发送消息给MQ,出票模块监听MQ的消息,有购票请求就选座购票。过一段时间进行轮询,查看购票结果。
初始RocketMQ
相关概念
- 生产者:负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到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> 4 <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 5 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站 6 <span class="order-train-main">({{dailyTrainTicket.startTime}})</span> 7 <span class="order-train-main">——</span> 8 <span class="order-train-main">{{dailyTrainTicket.end}}</span>站 9 <span class="order-train-main">({{dailyTrainTicket.endTime}})</span> 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> 15 <span class="order-train-ticket-main">{{item.count}}</span> 张票 16 </span> 17 </div> 18 </div> 19 <a-divider></a-divider> 20 <b>勾选要购票的乘客:</b> 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> 4 <span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次 5 <span class="order-train-main">{{dailyTrainTicket.start}}</span>站 6 <span class="order-train-main">({{dailyTrainTicket.startTime}})</span> 7 <span class="order-train-main">——</span> 8 <span class="order-train-main">{{dailyTrainTicket.end}}</span>站 9 <span class="order-train-main">({{dailyTrainTicket.endTime}})</span> 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> 15 <span class="order-train-ticket-main">{{item.count}}</span> 张票 16 </span> 17 </div> 18 </div> 19 <a-divider></a-divider> 20 <b>勾选要购票的乘客:</b> 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 }
标签:info,LOG,req,削峰,购票,value,confirmOrder,组件,RocketMQ From: https://www.cnblogs.com/szhNJUPT/p/17483359.html