> 技术文档 > 抽奖系统(5)——抽奖模块

抽奖系统(5)——抽奖模块

目录

抽奖设计

抽奖业务流程图/技术实现:

一、RabbitMq

MQ主要作用

为何选用RabbitMq

RabbitMq核心概念 

Web端访问页面:

​编辑 RabbitMq的配置与使用

二、抽奖请求处理:

时序图:

约定前后端交互接口:

Controller 层接口设计:

抽奖请求参数:

Service层接口设计与实现:

三、MQ异步抽奖逻辑执⾏(消费者):

时序图:

为什么要设置消息重发次数呢?

 消费代码的实现:

校验抽奖请求是否有效流程:

DrawPrizeService 核对抽奖信息有效性接口设计与实现:

四、状态转换

活动/奖品/参与者状态转换设计 

扭转状态的逻辑如下所示: 

状态转换类的参数: 

活动状态管理的总接口:

活动状态类设计与实现:

策略模式:

该模式就是将状态扭转的抽象操作(判断是否转换,需要转换)封装起来,其次根据三个类的各自详细操作来完成对抽象类的实现。

 AbstractActivityOperator总体活动抽象操作类:

奖品操作的实现

人员操作的实现

最后活动操作的实现

责任链模式

五、保存中奖者记录:

时序图:

Service层接口设计与实现(保存中奖者信息): 

 六、状态回滚:

rollback方法的分析 :

1.状态扭转分析

  2、回滚中奖者名单

附加:

  • 本项目的抽奖系统测试计划测试计划(抽奖系统)-CSDN博客
  • 本项目抽奖系统源代码:https://gitee.com/jia-lixuan/lucky-bullet-bullet-bullet

抽奖设计

抽奖业务流程图/技术实现:

抽奖过程是抽奖系统中最重要的核心环节,它需要确保公平、透明且高效。以下是详细的抽奖过程设计:

1.参与者注册与奖品建立

  • 参与者注册:管理员通过管理端新增用户必要的信息,,如姓名、联系方式等。
  • 奖品建立:奖品需要提前建立好

2.抽奖活动设置

  • 活动创建:管理员在系统中创建抽奖活动,输入活动名称、描述、奖品列表等信息。
  • 圈选人员:关联该抽奖活动的参与者。
  • 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等。
  • 活动发布:活动信息发布后,系统通过管理端界面展示活动列表。

3.抽奖请求处理 (重要)

  • 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
  • 请求提交:在活动进行时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖人员等附加信息。
  • 消息队列通知:有效的抽奖请求被发送至MQ队列中,等待MQ消费者真正处理抽奖逻辑。
  • 请求返回:抽奖的请求处理接口将不再完成任何的事情,直接返回。

4.抽奖结果公布

  • ·前端展示:中奖名单通过前端随机抽取的人员,公布展示出来。

5.抽奖逻辑执行 (重要)

  • 消息消费:MQ消费者收到异步消息,系统开始执行以下抽奖逻辑。

6.中奖结果处理 (重要)

  • 请求验证:
    •  系统验证抽奖请求的有效性,如是否满足系统根据设定的规则(如奖品数量、每人中奖次数限制等)等;
    •  幂等性:若消息多发,已抽取的内容不能再次抽取
  • 状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,人员是否已中奖等。
  • 结果记录:中奖结果被记录在数据库中,并同步更新 Redis 缓存。

7.中奖者通知(未实现)

  • 通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)
  • 奖品领取:中奖者根据通知中的指引领取奖品。

8.抽奖异常处理

  • 回滚处理:当抽奖过程中发生异常,需要保证事务一致性。
  • 补救措施:抽奖行为是一次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施

技术实现: 

  • 异步处理:提高抽奖性能,不影响抽奖流程,将抽奖处理放入队列中进行异步处理,且保证了幂等性。
  • 活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要高扩展性(设计模式)与维护性。
  • 并发处理:中奖者通知,可能要通知多系统,但相互解耦,可以设计为并发处理,加快抽奖效率作用。
  • 事务处理:在抽奖逻辑执行时,如若发生异常,需要确保数据库表原子性、事务一致性,因此要做好事务处理。

通过以上流程,抽奖系统能够确保抽奖过程的公平性和高效性,同时提供良好的用户体验。而且还整合了 Redis 和 MQ ,进一步提高系统的性能。

 

一、RabbitMq

 MQ( Message queue ),从字⾯意思上看,本质是个队列,FIFO先⼊先出,只不过队列中存放的内容 是消息(message)⽽已.消息可以⾮常简单,⽐如只包含⽂本字符串,JSON等,也可以很复杂,⽐如内嵌对象. MQ多⽤于分布式系统之间进⾏通信,系统之间的调⽤通常有两种⽅式:

1.异步通信

直接调用对方的服务,数据从一端发出后迅速到达另一端,(请求 - 响应的时序同步)

  

2.异步通信

数据从一端发出后,先进入一个容器进行临时存储,当达到某种条件后,容器的一个具体实现就是MQ( message queue)

 其中RabbitMq就是MQ的一种实现

MQ主要作用

MQ主要⼯作是接收并转发消息,在不同的应⽤场景下可以展现不同的作⽤:

可以把MQ想象成一个仓库.采购部门进货之后,把零件放进仓库里,生产部门从仓库中取出零件,并加工成产品.MQ和仓库的区别是,仓库里放的是物品,MQ里放的是消息,仓库负责存储物品,并转发物品,MQ负责存储和转发消息

  

  1.  异步解耦:在业务流程中,一些操作可能非常耗时,但并不需要即时返回结果.可以借助MQ把这些操作异步化,比如用户注册后发送注册短信或邮件通知,可以作为异步任务处理,而不必等待这些操作完成后才告知用户注册成功,
  2. 流量削峰:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是是这样的突发流量并不常见.如果以能处理这类峰值为标准而投入资源,无疑是巨大的浪费.使用MQ能够使关键组件支撑突发访问压力,不会因为突发流量而崩溃.比如秒杀或者促销活动,可以使用MQ来控制流量,将请求排队,然后系统根据自己的处理能力逐步处理这些请求
  3. 异步通信:在很多时候应用不需要立即处理消息,MQ提供了异步处理机制,允许应用把一些消息放入MQ中,但并不立即处理它,在需要的时候再慢慢处理
  4. 消息分发:当多个系统需要对同一数据做出响应时,可以使用MQ进行消息分发.比如支付成功后,支付系统可以向MQ发送消息,其他系统订阅该消息,而无需轮询数据库,
  5. 延迟通知:在需要在特定时间后发送通知的场景中,可以使用MQ的延迟消息功能,比如在电子商务平台中,如果用户下单后一定时间内未支付,可以使用延迟队列在超时后自动取消订单  

为何选用RabbitMq

采用Erlang语言开发,MQ 功能比较完备,能较好,吞吐量能达到万级,那么高的场景.且几乎支持所有主流语言,开源提供的界面也非常友好,性社区活跃度也比较高,比较适合中小型公司,数据量没那么大,且并发没综合: 由于 RabbitMQ 的综合能力较强,咱们这边的项目没有那么大的高并发,熟,管理界面友好

RabbitMq核心概念 

RabbitMQ是一个消息中间件,也是一个生产者消费者模型.它负责接收,存储并转发消息.

1.Broker
        RabbitMQ服务器,负责接收和分发消息的应用。

2.Virtual Host
        虚拟主机,是RabbitMQ中的逻辑容器,用于隔离不同环境或不同应用程序的信息流。每个虚拟主机都有自己的队列、交换机等设置,可以理解为一个独立的RabbitMQ服务。每个VirtualHost相当于一个相对独立的RabbitMQ服务器;每个VirtualHost之间是相互隔离的,exchange、queue、message不能互通。 

拿数据库(用MySQL)来类比:RabbitMq相当于MySQL,RabbitMq中的VirtualHost就相当于MySQL中的一个库。

3.Connection 连接
        管理和维护与RabbitMQ服务器的TCP连接,生产者、消费者通过这个连接和 Broker 建立物理网络连接

4. Channel通道
        是在Connection 内创建的轻量级通信通道,用于进行消息的传输和交互。应用程序通过Channel进行消息的发送和接收。通常一个 Connection 可以建立多个 Channel。

5. Exchange交换机
        交换机是消息的中转站,负责接收来自生产者的消息,并将其路由到一个或多个队列中。RabbitMQ 提供了多种不同类型的交换机,每种类型的交换机都有不同的消息路由规则。

        1>.不同的交换机类型

  • 直连交换机:Direct exchange
  •  直连交换机是一种带路由功能的交换机,一个队列会和一个交换机绑定,除此之外再绑定一个routing_key,当消息被发送的时候,需要指定一个binding_key,这个消息被送达交换机的时候,就会被这个交换机送到指定的队列里面去。同样的一个binding_key也是支持应用到多个队列中的。这样当一个交换机绑定多个队列,就会被送到对应的队列去处理。
  • 扇形交换机:Fanout exchange
  • ​ 扇形交换机是最基本的交换机类型,它所能做的事情非常简单———广播消息。扇形交换机会把能接收到的消息全部发送给绑定在自己身上的队列。因为广播不需要“思考”,所以扇形交换机处理消息的速度也是所有的交换机类型里面最快的。
  • 主题交换机:Topic exchange
  • RabbitMQ提供了一种主题交换机,发送到主题交换机上的消息需要携带指定规则的routing_key,主题交换机会根据这个规则将数据发送到对应的(多个)队列上。主题交换机的routing_key需要有一定的规则,交换机和队列的binding_key需要采用*.#.*.....的格式,每个部分用.分开,其中:* 表示一个单词 # 表示任意数量(零个或多个)单词。

6.Queue队列:
队列是消息的存储位置。每个队列都有一个唯一的名称。消息从交换机路由到队列,然后等待消费者来获取和处理。

7.Binding绑定关系:
Binding 是 Exchange 和 Queue 之间的关联规则,定义了消息如何从交换机路由到特定的队列

注释
        routing_key:
                路由键,生产者将消息发送给交换机的时候一般会指定一个RoutingKey,用于指定这个消息的路由规则。

        binding_key:
                绑定key,RabbitMq通过绑定将交换机与队列关联起来,绑定的时候一般会指定一个绑定key,结合路由键使路由规则生效。
 

Web端访问页面:

本地地址访问:http://localhost:15672

默认账号:guest

默认密码:guest

 RabbitMq的配置与使用

 pom.xml文件引入依赖:

   org.springframework.boot spring-boot-starter-amqp 

application.properties 配置 MQ选项:

## mq ##spring.rabbitmq.host=localhostspring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guest

DirectRabbitConfig 配置类实现:

package com.example.lotterysystem.common.config;@Configurationpublic class DirectRabbitConfig { public static final String QUEUE_NAME = \"DirectQueue\"; public static final String EXCHANGE_NAME = \"DirectExchange\"; public static final String ROUTING = \"DirectRouting\"; public static final String DLX_QUEUE_NAME = \"DlxDirectQueue\"; public static final String DLX_EXCHANGE_NAME = \"DlxDirectExchange\"; public static final String DLX_ROUTING = \"DlxDirectRouting\"; /** * 队列 起名:DirectQueue * * @return */ @Bean public Queue directQueue() { // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue(\"DirectQueue\",true,true,false); // 一般设置一下队列的持久化就好,其余两个就是默认false // return new Queue(QUEUE_NAME,true); // 普通队列绑定死信交换机 return QueueBuilder.durable(QUEUE_NAME) .deadLetterExchange(DLX_EXCHANGE_NAME) .deadLetterRoutingKey(DLX_ROUTING).build(); } /** * Direct交换机 起名:DirectExchange * * @return */ @Bean DirectExchange directExchange() { return new DirectExchange(EXCHANGE_NAME,true,false); } /** * 绑定 将队列和交换机绑定, 并设置用于匹配键:DirectRouting * * @return */ @Bean Binding bindingDirect() { return BindingBuilder.bind(directQueue()) .to(directExchange()) .with(ROUTING); } /** * 死信队列 * * @return */ @Bean public Queue dlxQueue() { // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue(\"DirectQueue\",true,true,false); // 一般设置一下队列的持久化就好,其余两个就是默认false return new Queue(DLX_QUEUE_NAME,true); } /** * 死信交换机 * * @return */ @Bean DirectExchange dlxExchange() { return new DirectExchange(DLX_EXCHANGE_NAME,true,false); } /** * 绑定死信队列与交换机 * * @return */ @Bean Binding bindingDlx() { return BindingBuilder.bind(dlxQueue()) .to(dlxExchange()) .with(DLX_ROUTING); } @Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); }}

二、抽奖请求处理:

时序图:

约定前后端交互接口:

【请求]/draw-prize POST{ \"winnerList\":[ { \"userId\":15, \"userName\":\"胡—博 }, { \"userId\"\":21 \"userName\":\"范闲\" } ], \"activityId\":23, \"prizeId\":13, \"prizeTiers\":\"FIRST_PRIZE\", \"winningTime\":\"2024-05-21T11:55:10.000z\"}[响应]{ \"code\": 200, \"data\": true, \"msg\":\"\"}

Controller 层接口设计:

@Slf4j@RestControllerpublic class DrawPrizeController { // 日志对象 private static final Logger logger = LoggerFactory.getLogger(DrawPrizeController.class); @Autowired private DrawPrizeService drawPrizeService; /** * 异步抽奖,此接口只做奖品数校验 * * @param param * @return */ @RequestMapping(\"/draw-prize\") public CommonResult drawPrize(@Validated @RequestBody DrawPrizeParam param) { logger.info(\"drawPrize DrawPrizeParam:{}\", JacksonUtil.writeValueAsString( param)); drawPrizeService.drawPrize( param); return CommonResult.success(true); }}

抽奖请求参数:

@Datapublic class DrawPrizeParam implements Serializable { /** * 中奖用户名单 */ @NotEmpty(message = \"中奖用户名单不能为空!\") @Valid private List winnerList; /** * 活动id */ @NotNull(message = \"活动id不能为空!\") private Long activityId; /** * 奖品id */ @NotNull(message = \"奖品id不能为空!\") private Long prizeId; /** * 中奖时间 **/ @NotNull(message = \"中奖时间不能为空!\") private Date winningTime; @Data public static class Winner implements Serializable{ /** * 中奖用户ID */ @NotNull(message = \"中奖用户ID不能为空!\") private Long userId; /** * 中奖用户名称 */ @NotNull(message = \"中奖用户名称不能为空!\") private String userName; }}

Service层接口设计与实现:

//设计@Servicepublic interface DrawPrizeService { /** * 异步抽奖,此接口只做奖品数校验即可返回 * @param param * @return */ void drawPrize(DrawPrizeParam param);}//实现@Servicepublic class DrawPrizeServiceImpl implements DrawPrizeService { private static final Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class); // 中奖记录有效期 private static final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L; // 缓存中奖记录的key前缀 private final String WINNING_RECORDS_PREFIX = \"WINNING_RECORDS_\"; /** * 使用RabbitTemplate,这提供了接收/发送等等方法 */ @Autowired private RabbitTemplate rabbitTemplate; @Autowired private ActivityMapper activityMapper; @Autowired private ActivityPrizeMapper activityPrizeMapper; @Autowired private UserMapper userMapper; @Autowired private PrizeMapper prizeMapper; @Autowired private WinningRecordMapper winningRecordMapper; @Autowired private RedisUtil redisUtil; public void drawPrize(DrawPrizeParam param) { // 将中奖信息发送mq进行异步处理 String messageId = String.valueOf(UUID.randomUUID()); String messageData = JacksonUtil.writeValueAsString(param); String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\")); Map map=new HashMap(); map.put(\"messageId\",messageId); map.put(\"messageData\",messageData); map.put(\"createTime\",createTime); // 发消息: 交换机、绑定的key、消息体 // 将消息携带绑定键值:DirectRouting 发送到交换机 DirectExchange rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING, map); logger.info(\"mq消息发送成功:map={}\", JacksonUtil.writeValueAsString(map)); }}

三、MQ异步抽奖逻辑执⾏(消费者):

时序图:

     要保证消息必须被成功处理:前端展示的中奖者一定要成功;如果消息处理过程异常,就要保证事务的一致性,即进行回滚操作,将已经修改的数据库表进行回滚,和该消息处理签的状态保持一致。此时就会mq就会进行消息重发,因为配置次数是5次,所以当重发了5次都失败的话,就会禁止重试。

        此时会将这些失败的消息存放进一个新的队列(死信队列)。这时候就会分析消息失败处理的原因?网络:代码导致的异常?服务器压力

        如果是代码导致的异常,就去解决bug。以上三种问题都是能进行解决的。当这些问题都被解决的话,就会重新从死信队列中重新发送。

为什么要设置消息重发次数呢?

        如果不进行消息发送次数设置的话,每次发送失败,就会导致消息堆积。

 消费代码的实现:

@Component@RabbitListener(queues = QUEUE_NAME)public class MqReceiver { private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class); @Autowired private DrawPrizeService drawPrizeService; @RabbitHandler public void process(Map message) { //成功接收消息队列的消息 logger.info(\"MQ消息接收成功:message={}\", JacksonUtil.writeValueAsString( message)); String paranString = message.get(\"messageData\"); DrawPrizeParam param = JacksonUtil.readValue(paranString, DrawPrizeParam.class); // 处理抽奖的流程 try{ // 校验抽奖请求是否有效 drawPrizeService.checkDrawPrizeParam( param); // 状态扭转处理 statusConvert( param); // 保存中奖者名单 List winningRecordDOList =  drawPrizeService.saveWinnerRecords(param); // 通知中奖者 手段有:邮箱、短信(未能实现) // 如果发生异常,需要保证事务一致性(就是回滚) syncExecute(winningRecordDOList); }catch (ServiceException e){ logger.error(\"处理 MQ 消息队列异常!{},{}\",e.getCode(),e.getMessage(),e); // 需要保证事务的一致性(回滚) rollback(param); // 抛出异常:消息重试(解决异常:代码Bug 、网络问题、服务问题) throw e; }catch (Exception e){ logger.error(\"处理 MQ 消息队列异常!\",e); // 需要保证事务的一致性(回滚) rollback(param); // 抛出异常 throw e; } }}

mq普通队列的逻辑分析:

 当消费者收到MQ队列的消息后,首先会对接收到的消息进行校验,查看其格式和信息是否满足接下来处理操作的需求

校验抽奖请求是否有效流程:

首选根据活动ID  查看活动信息,从活动奖品表中查看奖品信息

  1. 判断查询出来的活动信息和奖品信息是否为空
  2. 活动是否未完成(活动的状态为running和 complited 两种),如果为running说明抽奖活动未结束
  3. 奖品是否有效(奖品的状态分为 init初始化 和 complited 已抽取两种)
  4. 中奖者人数和奖品数量的关系判断(需是是人多奖品少的状态)

DrawPrizeService 核对抽奖信息有效性接口设计与实现:

@Servicepublic interface DrawPrizeService { /** * 校验抽奖参数 * @param param */ void checkDrawPrizeParam(DrawPrizeParam param);}@Servicepublic class DrawPrizeServiceImpl implements DrawPrizeService { /** * 校验抽奖参数是否有效 * * @param param 抽奖参数对象,包含活动ID、奖品ID和中奖者列表 * @throws ServiceException 当参数校验失败时抛出异常 */ @Override public void checkDrawPrizeParam(DrawPrizeParam param) { ActivityDO activityDO = activityMapper.selectById(param.getActivityId()); ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId( param.getActivityId(),param.getPrizeId()); // 查看活动是否存在 if(null == activityDO || null == activityPrizeDO){ throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY); } /** * 检查活动状态: * 1. 查询数据库获取活动状态 * 2. 如果活动已完成则抛出异常 */ if(activityDO.getStatus(). equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())){ throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED); } /** * 检查奖品状态: * 1. 查询数据库获取奖品状态 * 2. 如果奖品已发放完毕则抛出异常 */ if(activityPrizeDO.getStatus(). equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name())){ throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED); } /** * 验证奖品数量与中奖人数匹配: * 1. 获取奖品库存数量 * 2. 比对奖品数量与中奖者列表大小 * 3. 不一致时抛出异常 */ if( activityPrizeDO.getPrizeAmount() != param.getWinnerList().size()){ throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR); } }

ActivityPrizeMapper 新增 selectByActivityAndPrizeId 方法:

 /** * 根据活动id查询奖品信息 * @param activityId * @return */ @Select(\"select * from activity_prize where activity_id = #{activityId}\") List selectByActivityId(@Param(\"activityId\") Long activityId);

 ActivityMapper 新增 selectById 方法

 /** * 根据id查询活动信息 */ @Select(\"select * from activity where id = #{id}\") ActivityDO selectById(@Param(\"id\") Long id);

 mq队列中的消息参数信息如下:

@Datapublic class DrawPrizeParam implements Serializable { /** * 中奖用户名单 */ @NotEmpty(message = \"中奖用户名单不能为空!\") @Valid private List winnerList; /** * 活动id */ @NotNull(message = \"活动id不能为空!\") private Long activityId; /** * 奖品id */ @NotNull(message = \"奖品id不能为空!\") private Long prizeId; /** * 中奖时间 **/ @NotNull(message = \"中奖时间不能为空!\") private Date winningTime; @Data public static class Winner implements Serializable{ /** * 中奖用户ID */ @NotNull(message = \"中奖用户ID不能为空!\") private Long userId; /** * 中奖用户名称 */ @NotNull(message = \"中奖用户名称不能为空!\") private String userName; }}

四、状态转换

活动/奖品/参与者状态转换设计 

状态扭转处理:

活动: RUNNING-->COMPLETED ?? 全部奖品抽完之后才改变状态奖品:INIT-->COMPLETED人员列表: INIT-->COMPLETED

 如上所示,当进行抽奖逻辑时,需要对三个表中的状态进行,但是基于三者之间的关系,其处理扭转状态的次序是有被约束的。人员列表中人员的完成状态和奖品的状态扭转并列先处理,其次最后才处理活动列表中活动的状态。

扭转状态的逻辑如下所示: 

/ 1 扭转奖品状态/ 查询活动关联的奖品信息/ 条件判断是否符合扭转奖品状态?判断当前状态不是COMPLETED?要扭转:不去扭转/确保最后的状态为completed/ 2 扭转人员状态/ 查询活动关联的人员信息/ 条件判断是否符合扭转人员状态:判断当前状态不是COMPLETED?要扭转:不去扭转/ 确保最后的状态为completed/ 3 扭转活动状态(必须再扭转奖品状态之后完成)/ 查询活动信息/ 条件判断是否符合扭转活动状态:才改变状态/ 判断当前状态是不是COMPLETED,如果不是/ 且全部奖品抽完之后,才去扭转/等所有的状态都扭转完成之后/ 4 更新活动完整信息缓存

状态转换类的参数: 

@Datapublic class ConvertActivityStatusDTO { /** * 活动id */ private Long activityId; /** * 活动目标状态 */ private ActivityStatusEnum targetActivityStatus; /** * 奖品id */ private Long prizeId; /** * 奖品目标状态 */ private ActivityPrizeStatusEnum targetPrizeStatus; /** * 人员id列表 */ private List userIds; /** * 人员目标状态 */ private ActivityUserStatusEnum targetUserStatus;}

活动状态管理的总接口:

关于活动状态相关的操作都通过这个接口管理(活动转态转换,回滚处理活动相关状态)

@Servicepublic interface ActivityStatusManager { /** * 状态扭转 * @param convertActivityStatusDTO */ void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO); /** * 状态扭转回滚 * @param convertActivityStatusDTO */ void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO);}

活动状态类设计与实现:

在活动状态管理实现类重写ActivityStatusManager接口中的handlerEvent。

handlerEvent方法中,传入活动中的活动id,奖品id,参与人员列表(里面是人员id),以及各自的目标转换的状态。

        该方法中就是按照正确的顺序扭转活动状态,这里加入设计模式,即策略模式和责任链模式。

package com.example.lotterysystem.service.activitystatus.impl;import java.util.HashMap;import java.util.Iterator;import java.util.Map;/** * 活动状态管理实现类 * * 该类负责处理活动、奖品及用户状态的转换操作,通过依赖注入的方式获取各种状态操作器(AbstractActivityOperator), * 并按照预定义顺序执行状态转换逻辑。此类采用组件注解进行 Spring 管理,并支持事务控制。 */@Componentpublic class ActivityStatusManagerImpl implements ActivityStatusManager { private static final Logger logger = LoggerFactory.getLogger(ActivityStatusManagerImpl.class); /** * 存储所有 AbstractActivityOperator 实现类的映射表,用于状态转换处理 */ @Autowired private final Map operatorMap = new HashMap(); /** * Spring 属性解析器,可用于读取配置属性 */ @Autowired private PropertyResolver propertyResolver; /** * 活动服务接口,用于缓存活动信息等操作 */ @Autowired private ActivityService activityService; private AbstractActivityOperator operator; /** * 执行活动状态扭转操作。 * * 该方法根据传入的 DTO 参数判断需要转换的状态类型,并调用相应的处理器进行状态转换。 * 转换完成后会更新活动缓存以保证数据一致性。 * * @param convertActivityStatusDTO 状态转换所需的参数封装对象 */ @Override @Transactional(rollbackFor = Exception.class) public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) { // 日志提示:当前状态扭转存在维护性差和扩展性低的问题,建议后续重构优化 if (CollectionUtils.isEmpty(operatorMap)) { logger.warn(\"operatorMap 为空!\"); return; } // 创建当前操作器副本以避免并发修改 Map curMap = new HashMap(operatorMap); Boolean update = false; // 先处理人员、奖品状态转换(sequence=1) update = processConvertStatus(convertActivityStatusDTO, curMap, 1); // 后处理活动状态转换(sequence=2) update = processConvertStatus(convertActivityStatusDTO, curMap, 2) || update; // 如果有状态发生更新,则刷新缓存 if (update) { activityService.cacheActivity(convertActivityStatusDTO.getActivityId()); } } /** * 回滚处理活动状态事件。 * * 此方法用于将活动及其相关资源的状态回滚到之前的状态。 * 它会遍历operatorMap中的所有操作符,并对每个操作符执行转换逻辑。 * 最后,更新活动的缓存以反映新的状态。 * * @param convertActivityStatusDTO 包含转换所需数据的DTO对象 */ @Override public void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) { // operatorMap : 活动、奖品、人员 // 活动是否需要回滚? 绝对需要 // 原因:奖品状态都为INIT了,那么活动下的奖品绝对没有抽完 for (AbstractActivityOperator operator : operatorMap.values()) { operator.convert(convertActivityStatusDTO); } // 缓存更新 activityService.cacheActivity(convertActivityStatusDTO.getActivityId()); } /** * 执行指定顺序的状态转换处理。 * * 遍历所有的 AbstractActivityOperator 实现类,筛选出满足条件的操作器并执行其转换逻辑。 * * @param convertActivityStatusDTO 状态转换所需参数 * @param curMap 当前可用操作器映射表 * @param sequence 操作器执行顺序标识 * @return 是否进行了状态更新 */ private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,  Map curMap,  int sequence) { Boolean update = false; // 使用迭代器遍历操作器集合 Iterator<Map.Entry> iterator = curMap.entrySet().iterator(); while (iterator.hasNext()) { AbstractActivityOperator operator = iterator.next().getValue(); // 判断是否需要转换以及是否匹配当前处理顺序 if (operator.sequence() != sequence  || !operator.needConvert(convertActivityStatusDTO)) { continue; } // 执行转换逻辑 if (!operator.convert(convertActivityStatusDTO)) { logger.error(\"{}状态转换失败!\", operator.getClass().getName()); throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR); } // 移除已处理的操作器,防止重复执行 iterator.remove(); update = true; } return update; }}
策略模式:
该模式就是将状态扭转的抽象操作(判断是否转换,需要转换)封装起来,其次根据三个类的各自详细操作来完成对抽象类的实现。
 AbstractActivityOperator总体活动抽象操作类:

总体抽象操作有判断是否需要进行状态扭转和执行状态扭转。其次为了后面完成责任链设计模式,定义一个操作的抽象属性sequence; 

@Servicepublic abstract class AbstractActivityOperator { /** * 控制处理顺序 * @return */ public abstract Integer sequence(); /** * 是否需要转换 * @param convertActivityStausDTO */ public abstract Boolean needConvert(ConvertActivityStatusDTO convertActivityStausDTO); /** * 转换方法 * @param convertActivityStausDTO */ public abstract Boolean convert(ConvertActivityStatusDTO convertActivityStausDTO);}
奖品操作的实现
package com.example.lotterysystem.service.activitystatus.operater;import com.example.lotterysystem.dao.dataobject.ActivityPrizeDO;import com.example.lotterysystem.dao.mapper.ActivityPrizeMapper;import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class PrizeOperator extends AbstractActivityOperator{ @Autowired private ActivityPrizeMapper activityPrizeMapper; @Override public Integer sequence() { return 1; } /** * 判断奖品状态是否需要转换 * * @param convertActivityStausDTO 转换活动状态数据传输对象,包含奖品ID、活动ID和目标奖品状态 * @return Boolean 返回是否需要转换奖品状态,true表示需要转换,false表示不需要 */ @Override public Boolean needConvert(ConvertActivityStatusDTO convertActivityStausDTO) { //判断奖品是否全部被抽完 Long prizeId = convertActivityStausDTO.getPrizeId(); Long activityId = convertActivityStausDTO.getActivityId(); ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStausDTO.getTargetPrizeStatus(); //奖品id,活动id,奖品状态 if(null == activityId || null == targetPrizeStatus || null == prizeId){ return false; } // 查询奖品信息 ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(activityId, prizeId); // 判断奖品信息是否存在 if(null == activityPrizeDO){ return false; } // 判断奖品状态是否一致 if(targetPrizeStatus.name().equalsIgnoreCase( activityPrizeDO.getStatus())){ return false; } return true; } /** * 转换奖品活动状态 * @param convertActivityStatusDTO 状态转换参数 * @return Boolean 操作结果,成功返回true,失败返回false */ @Override public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) { Long activityId = convertActivityStatusDTO.getActivityId(); Long prizeId = convertActivityStatusDTO.getPrizeId(); ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStatusDTO.getTargetPrizeStatus(); try { activityPrizeMapper.updateStatus(activityId, prizeId, targetPrizeStatus.name()); return true; } catch (Exception e) { return false; } }}
人员操作的实现
package com.example.lotterysystem.service.activitystatus.operater;import com.example.lotterysystem.dao.dataobject.ActivityUserDO;import com.example.lotterysystem.dao.mapper.ActivityUserMapper;import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.util.CollectionUtils;import java.util.List;@Componentpublic class UserOperator extends AbstractActivityOperator{ @Autowired private ActivityUserMapper activityUserMapper; @Override public Integer sequence() { return 1; } /** * 判断是否需要转换活动用户状态 * * @param convertActivityStausDTO 转换活动状态数据传输对象 * @return Boolean 是否需要转换状态 */ @Override public Boolean needConvert(ConvertActivityStatusDTO convertActivityStausDTO) { Long activityId = convertActivityStausDTO.getActivityId(); List userIds = convertActivityStausDTO.getUserIds(); ActivityUserStatusEnum targetUserStatus = convertActivityStausDTO.getTargetUserStatus(); if(null == activityId || CollectionUtils.isEmpty(userIds) || null == targetUserStatus){ return false; } /** * 查询活动用户信息 * 如果没有找到对应的用户记录,直接返回false */ List activityUserDOS = activityUserMapper.batchSelectByAUIds(activityId, userIds); if (CollectionUtils.isEmpty(activityUserDOS)){ return false; } /** * 遍历用户记录 * 如果任何一个用户的当前状态已经等于目标状态,不需要转换,返回false */ for(ActivityUserDO activityUserDO : activityUserDOS){ if (activityUserDO.getStatus().equalsIgnoreCase(targetUserStatus.name())){ return false; } } return true; } /** * 转换活动用户状态。 * * @param convertActivityStausDTO 转换活动状态的数据传输对象。 * @return Boolean 表示转换是否成功。 */ @Override public Boolean convert(ConvertActivityStatusDTO convertActivityStausDTO) { Long activityId = convertActivityStausDTO.getActivityId(); List userIds = convertActivityStausDTO.getUserIds(); ActivityUserStatusEnum targetUserStatus = convertActivityStausDTO.getTargetUserStatus(); try { // 调用Mapper批量更新用户状态并检查影响行数 int affectedRows = activityUserMapper.batchUpdateStatus (activityId, userIds, targetUserStatus.name()); // 只有实际更新发生时才返回true return affectedRows > 0; } catch (Exception e) { // 捕获异常并记录日志,返回false表示操作失败 // TODO: 可根据实际需要添加日志记录,例如:log.error(\"Failed to update user status\", e); return false; } }}
最后活动操作的实现
package com.example.lotterysystem.service.activitystatus.operater;import com.example.lotterysystem.dao.dataobject.ActivityDO;import com.example.lotterysystem.dao.mapper.ActivityMapper;import com.example.lotterysystem.dao.mapper.ActivityPrizeMapper;import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;import com.example.lotterysystem.service.enums.ActivityStatusEnum;import lombok.extern.slf4j.Slf4j;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class ActivityOperator extends AbstractActivityOperator { @Autowired private ActivityMapper activityMapper; @Autowired private ActivityPrizeMapper activityPrizeMapper; @Override public Integer sequence() { return 2; } /** * 判断是否需要转换活动状态 * @param convertActivityStatusDTO * @return */ @Override public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) { Long activityId = convertActivityStatusDTO.getActivityId(); ActivityStatusEnum targetStatus = convertActivityStatusDTO.getTargetActivityStatus(); if (null == activityId || null == targetStatus) { return false; } ActivityDO activityDO = activityMapper.selectById(activityId); if (null == activityDO) { return false; } // 当前活动状态与传入的状态一致,不处理 if (targetStatus.name().equalsIgnoreCase(activityDO.getStatus())) { return false; } // 需要判断奖品是否全部抽完 // 查询 INIT 状态的奖品个数 int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name()); if (count > 0) { return false; } return true; } /** * 转换活动状态 * @param convertActivityStatusDTO * @return */ @Override public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) { try { activityMapper.updateStatus(convertActivityStatusDTO.getActivityId(),  convertActivityStatusDTO.getTargetActivityStatus().name()); return true; } catch (Exception e) { return false; } }}

责任链模式

 此时针对状态转换的操作已经设计好了,接下来就是要确保人员和奖品的相关操作要在活动相关操作的前面,我们在前面三个具体的操作中增加sequence属性,分别定义为1,2;

        在processConvertStatus中实现责任链模式,约定次序为1的操作先执行,次序为2的操作后执行。使用map容器将三个操作封装注入到ActivityStatusManagerImpl类中作为processConvertStatus方法的参数,

 /** * 执行指定顺序的状态转换处理。 * * 遍历所有的 AbstractActivityOperator 实现类,筛选出满足条件的操作器并执行其转换逻辑。 * * @param convertActivityStatusDTO 状态转换所需参数 * @param curMap 当前可用操作器映射表 * @param sequence 操作器执行顺序标识 * @return 是否进行了状态更新 */ private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,  Map curMap,  int sequence) { Boolean update = false; // 使用迭代器遍历操作器集合 Iterator<Map.Entry> iterator = curMap.entrySet().iterator(); while (iterator.hasNext()) { AbstractActivityOperator operator = iterator.next().getValue(); // 判断是否需要转换以及是否匹配当前处理顺序 if (operator.sequence() != sequence  || !operator.needConvert(convertActivityStatusDTO)) { continue; } // 执行转换逻辑 if (!operator.convert(convertActivityStatusDTO)) { logger.error(\"{}状态转换失败!\", operator.getClass().getName()); throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR); } // 移除已处理的操作器,防止重复执行 iterator.remove(); update = true; } return update; }

等确保状态都成功扭转之后,要更新缓存,首先从活动,活动用户,活动奖品三个数据库表拿出数据,其次将三个表的整合信息缓存到reids内存中。

 /** * 缓存活动详情信息 * * @param activityId */ @Override public void cacheActivity(Long activityId) { if(null == activityId){ logger.warn(\"缓存活动失败,activityId为空!\"); throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_IS_EMPTY); } // 如果redis不存在,查表 // 活动表 ActivityDO aDO = activityMapper.selectById(activityId); if (null == aDO){ logger.warn(\"缓存活动失败,activityId有误!\"); throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_ERROR); } // 活动奖品表 List apDOList = activityPrizeMapper.selectByActivityId(activityId); // 活动人员表 List auDOList = activityUserMapper.selectByActivityId(activityId); // 奖品表: 先获取要查询的奖品id List prizeIds = apDOList.stream() .map(ActivityPrizeDO::getPrizeId) .collect(Collectors.toList()); List pDOList = prizeMapper.batchSelectByIds(prizeIds); // 整合活动详细信息,存放redis ActivityDetailDTO detailDTO = convertToActivityDetailDTO(aDO, auDOList, pDOList, apDOList); cacheActivity(detailDTO); } /** * 将活动相关数据对象转换为活动详情传输对象 * 此方法整合了活动基本信息、活动奖品信息和活动用户信息,构建了一个详细的活动详情DTO * * @param activityDO 活动数据对象,包含活动的基本信息 * @param activityUserDOList 活动用户数据对象列表,包含参与活动的用户信息 * @param prizeDOList 奖品数据对象列表,包含所有可能的奖品信息 * @param activityPrizeDOList 活动奖品数据对象列表,包含活动中的奖品及其数量和状态 * @return 返回一个包含完整活动详情的ActivityDetailDTO对象 */ private ActivityDetailDTO convertToActivityDetailDTO(  ActivityDO activityDO,  List activityUserDOList,  List prizeDOList,  List activityPrizeDOList) { // 构造完整的活动信息 ActivityDetailDTO detailDTO = new ActivityDetailDTO(); detailDTO.setActivityId(activityDO.getId()); detailDTO.setActivityName(activityDO.getActivityName()); detailDTO.setDesc(activityDO.getDescription()); detailDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus())); // apDO: {prizeId,amount, status}, {prizeId,amount, status} // pDO: {prizeid, name....},{prizeid, name....},{prizeid, name....} List prizeDTOList = activityPrizeDOList  .stream()  .map(apDO -> {  ActivityDetailDTO.PrizeDTO prizeDTO = new ActivityDetailDTO.PrizeDTO();  prizeDTO.setPrizeId(apDO.getPrizeId());  Optional optionalPrizeDO = prizeDOList.stream() .filter(prizeDO -> prizeDO.getId().equals(apDO.getPrizeId())) .findFirst();  //将第二行的每一个pdo和第一个的apdo根据奖品id进行比较,  //得到同id的第二行的第一个pdo  // 如果PrizeDO为空,不执行当前方法,不为空才执行  optionalPrizeDO.ifPresent(prizeDO -> { prizeDTO.setName(prizeDO.getName()); prizeDTO.setImageUrl(prizeDO.getImageUrl()); prizeDTO.setPrice(prizeDO.getPrice()); prizeDTO.setDescription(prizeDO.getDescription());  });  prizeDTO.setTiers(ActivityPrizeTiersEnum.forName(apDO.getPrizeTiers()));  prizeDTO.setPrizeAmount(apDO.getPrizeAmount());  prizeDTO.setStatus(ActivityPrizeStatusEnum.forName(apDO.getStatus()));  return prizeDTO;  }).collect(Collectors.toList()); detailDTO.setPrizeDTOList(prizeDTOList); // 处理活动用户信息,将其转换为UserDTO列表 List userDTOList = activityUserDOList.stream()  .map(auDO -> {  ActivityDetailDTO.UserDTO userDTO = new ActivityDetailDTO.UserDTO();  userDTO.setUserId(auDO.getUserId());  userDTO.setUserName(auDO.getUserName());  userDTO.setStatus(ActivityUserStatusEnum.forName(auDO.getStatus()));  return userDTO;  }).collect(Collectors.toList()); detailDTO.setUserDTOList(userDTOList); return detailDTO; }

五、保存中奖者记录:

   根据DrawPrizeParam入参从活动表,人员表,奖品表中查找到相关正确数据。

  • 将这些数据构造之后批量添加到winning_record表中;
  • 缓存中奖记录,从两个维度进行设计:
  1.  查看单个奖品的获奖记录 ,从缓存奖品维度中奖记录(奖品维度的中奖名单),为的是某一个等级的奖品被单一人员抽中之后显示在页面。
  2.  缓存活动维度中奖记录((活动维度的中奖名单)) ,通过活动id查看完整的获奖记录 ,但是需要进行判断, 即当活动已完成再,去存放活动维度中奖记录。

时序图:

 

Service层接口设计与实现(保存中奖者信息): 

 //接口设计 /** * 保存中奖名单 * @param param * @return */ List saveWinnerRecords(DrawPrizeParam param); //接口实现 /** * 保存中奖记录 * * @param param 抽奖参数对象,包含活动ID、奖品ID和中奖者列表 * @return 中奖记录列表 */ @Override public List saveWinnerRecords(DrawPrizeParam param) { // 查询相关信息:活动、人员、奖品、活动关联奖品 ActivityDO activityDO = activityMapper.selectById(param.getActivityId()); List userDOList = userMapper.batchSelectByIds( param.getWinnerList() .stream() .map(DrawPrizeParam.Winner::getUserId) .collect(Collectors.toList()) ); PrizeDO prizeDO = prizeMapper.selectById(param.getPrizeId()); ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId()); // 构造中奖者记录,保存 List winningRecordDOList = userDOList.stream() .map(userDO -> {  WinningRecordDO winningRecordDO = new WinningRecordDO();  winningRecordDO.setActivityId(activityDO.getId());  winningRecordDO.setActivityName(activityDO.getActivityName());  winningRecordDO.setPrizeId(prizeDO.getId());  winningRecordDO.setPrizeName(prizeDO.getName());  winningRecordDO.setPrizeTier(activityPrizeDO.getPrizeTiers());  winningRecordDO.setWinnerId(userDO.getId());  winningRecordDO.setWinnerName(userDO.getUserName());  winningRecordDO.setWinnerEmail(userDO.getEmail());  winningRecordDO.setWinnerPhoneNumber(userDO.getPhoneNumber());  winningRecordDO.setWinningTime(param.getWinningTime());  return winningRecordDO; }).collect(Collectors.toList()); winningRecordMapper.batchInsert(winningRecordDOList); // 缓存中奖者记录 // 1、缓存奖品维度中奖记录(WinningRecord_activityId_prizeId, winningRecordDOList(奖品维度的中奖名单)) cacheWinningRecords(param.getActivityId() + \"_\" + param.getPrizeId(), winningRecordDOList, WINNING_RECORDS_TIMEOUT); // 2、缓存活动维度中奖记录(WinningRecord_activityId, winningRecordDOList(活动维度的中奖名单)) // 当活动已完成再去存放活动维度中奖记录 if (activityDO.getStatus() .equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) { // 查询活动维度的全量中奖记录 List allList = winningRecordMapper.selectByActivityId(param.getActivityId()); cacheWinningRecords(String.valueOf(param.getActivityId()),  allList,  WINNING_RECORDS_TIMEOUT); } return winningRecordDOList; }

 六、状态回滚:

这里,体现了事务一个很重要的特性:原子性

 过程图:

活动状态管理实现类下重写ActivityStatusManager接口的rollbackHandlerEvent方法( 状态回滚方法可以看成是活动状态管理的一个接口)

/** * 活动状态管理实现类 * * 该类负责处理活动、奖品及用户状态的转换操作,通过依赖注入的方式获取各种状态操作器(AbstractActivityOperator), * 并按照预定义顺序执行状态转换逻辑。此类采用组件注解进行 Spring 管理,并支持事务控制。 */@Componentpublic class ActivityStatusManagerImpl implements ActivityStatusManager { /** * 回滚处理活动状态事件。 * * 此方法用于将活动及其相关资源的状态回滚到之前的状态。 * 它会遍历operatorMap中的所有操作符,并对每个操作符执行转换逻辑。 * 最后,更新活动的缓存以反映新的状态。 * * @param convertActivityStatusDTO 包含转换所需数据的DTO对象 */ @Override public void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) { // operatorMap : 活动、奖品、人员 // 活动是否需要回滚? 绝对需要 // 原因:奖品状态都为INIT了,那么活动下的奖品绝对没有抽完 for (AbstractActivityOperator operator : operatorMap.values()) { operator.convert(convertActivityStatusDTO); } // 缓存更新 activityService.cacheActivity(convertActivityStatusDTO.getActivityId()); }}

    前面的步骤都是理想状态下锁链式运行的,但是由于事务一致性的约束,所以前面部分状态没有扭转或者或者一部分扭转之后部分没有保存到数据库表中,对于这些问题的出现要进行事务的回滚,确保相关所有的表状态回滚到最初的状态。

rollback方法的分析 :

1.状态扭转分析

                如果某一表中的状态还未扭转,此时整个活动的状态就未扭转,获奖记录就没有保存。

                判断状态是否存在未扭转:

/ 判断活动+奖品+人员表相关状态是否已经扭转,扭转状态时,由于需要保证事务一致性,所以上面人员和奖品要么都扭转了,要么都没扭转(不包含活动)。/ 1、因为一个活动有多个奖品,一个奖品扭转了,以该奖品为维度的获奖记录需要回滚;/ 2、但是该活动由于其他奖品没有完成,所以整个活动的状态没有扭转,即以活动为维度,当前的活动状态由于没有扭转而不能回滚,实际是需要进行回滚其下面的奖品和中奖人员状态/ 因此,只用判断人员/奖品是否扭转过,就能判断出活动相关下面的状态是否全部扭转,不能判断活动是否已经扭转。// 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转
 /** * 判断状态是否需要回滚 * * @param param * @return */ private boolean statusNeedRollback(DrawPrizeParam param) { // 判断活动+奖品+人员表相关状态是否已经扭转(正常思路) // 扭转状态时,保证了事务一致性,要么都扭转了,要么都没扭转(不包含活动): // 因此,只用判断人员/奖品是否扭转过,就能判断出状态是否全部扭转 // 不能判断活动是否已经扭转 // 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转 ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId()); // 已经扭转了,需要回滚 return activityPrizeDO.getStatus() .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name()); } /** * 恢复相关状态 * * @param param */ private void rollbackStatus(DrawPrizeParam param) { // 涉及状态的恢复,使用 ActivityStatusManager ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO(); convertActivityStatusDTO.setActivityId(param.getActivityId()); convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING); convertActivityStatusDTO.setPrizeId(param.getPrizeId()); convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT); convertActivityStatusDTO.setUserIds( param.getWinnerList().stream() .map(DrawPrizeParam.Winner::getUserId) .collect(Collectors.toList()) ); convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT); activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO); }
  2、回滚中奖者名单

        在获奖记录表中,根据活动id和奖品id查看是否有获奖人员 ,如果有直接删除该条获奖记录

 /** * 判断中奖者名单是否需要回滚 */ private boolean winnerNeedRollback(DrawPrizeParam param) { // 判断活动中的奖品是否存在中奖者 int count = winningRecordMapper.countByAPId(param.getActivityId(), param.getPrizeId()); return count > 0; } /** * 回滚中奖记录:删除奖品下的中奖者 * * @param param */ private void rollbackWinner(DrawPrizeParam param) { drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId()); }