【项目】【抽奖系统】抽奖

目录

  • 一、抽奖流程
  • 二、MQ配置
  • 三、MQ生产者(抽奖异步接口)
    • [3.1 抽奖请求处理简介](#3.1 抽奖请求处理简介)
    • [3.2 参数列表](#3.2 参数列表)
    • [3.3 接口规范](#3.3 接口规范)
    • [3.4 controller 层](#3.4 controller 层)
      • [3.4.1 DrawPrizeController 参数接收类](#3.4.1 DrawPrizeController 参数接收类)
    • [3.5 service 层](#3.5 service 层)
      • [3.5.1 创建接口](#3.5.1 创建接口)
      • [3.5.2 实现接口](#3.5.2 实现接口)
    • [3.6 测试](#3.6 测试)
  • 七、MQ消费者
    • [7.1 接收消息功能](#7.1 接收消息功能)
    • [7.2 校验抽奖请求](#7.2 校验抽奖请求)
      • [7.2.1 创建接口](#7.2.1 创建接口)
      • [7.2.2 实现接口](#7.2.2 实现接口)
      • [7.2.3 查表方法](#7.2.3 查表方法)
    • [7.3 状态扭转(责任链模式、策略模式)](#7.3 状态扭转(责任链模式、策略模式))
      • [7.3.1 状态扭转调用](#7.3.1 状态扭转调用)
      • [7.3.2 状态扭转具体实现](#7.3.2 状态扭转具体实现)
        • [7.3.2.1 状态扭转接口](#7.3.2.1 状态扭转接口)
        • [7.3.2.2 实现状态扭转接口](#7.3.2.2 实现状态扭转接口)
          • [7.3.2.2.1 processConvertStatus:扭转状态方法](#7.3.2.2.1 processConvertStatus:扭转状态方法)
          • [7.4.2.2.2 控制状态扭转抽象类](#7.4.2.2.2 控制状态扭转抽象类)
          • [7.4.2.2.3 ActivityOperator :活动状态扭转控制类](#7.4.2.2.3 ActivityOperator :活动状态扭转控制类)
          • [7.4.2.2.4 PrizeOperator :奖品状态扭转控制类](#7.4.2.2.4 PrizeOperator :奖品状态扭转控制类)
          • [7.4.2.2.5 UserOperator :中奖人员状态扭转控制类](#7.4.2.2.5 UserOperator :中奖人员状态扭转控制类)
          • [7.4.2.2.6 缓存Redis更新](#7.4.2.2.6 缓存Redis更新)
          • [7.4.2.2.6.1 接口定义](#7.4.2.2.6.1 接口定义)
          • [7.4.2.2.6.2 接口实现](#7.4.2.2.6.2 接口实现)
      • [7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类](#7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类)
      • [7.3.4 dao层修改](#7.3.4 dao层修改)
      • [7.3.5 更新Redis缓存中的状态](#7.3.5 更新Redis缓存中的状态)
        • [7.3.5.1 接口创建](#7.3.5.1 接口创建)
        • [7.3.5.2 接口实现](#7.3.5.2 接口实现)
      • [7.3.6 总结](#7.3.6 总结)
    • [7.4 保存中奖者名单](#7.4 保存中奖者名单)
      • [7.4.1 接口创建](#7.4.1 接口创建)
      • [7.4.2 实现接口](#7.4.2 实现接口)
      • [7.4.3 WinningRecodesDO 中奖者类](#7.4.3 WinningRecodesDO 中奖者类)
      • [7.4.4 缓存方法与获取方法](#7.4.4 缓存方法与获取方法)
      • [7.4.5 dao层新增Mapper方法](#7.4.5 dao层新增Mapper方法)
    • [7.5 邮箱通知中奖者](#7.5 邮箱通知中奖者)
      • [7.5.1 添加方法](#7.5.1 添加方法)
      • [7.5.2 配置线程池](#7.5.2 配置线程池)
      • [7.5.3 异步发送](#7.5.3 异步发送)
    • [7.6 抽奖回滚实现](#7.6 抽奖回滚实现)
      • [7.6.1 判断状态是否需要回滚](#7.6.1 判断状态是否需要回滚)
      • [7.6.2 状态回滚方法](#7.6.2 状态回滚方法)
        • [7.6.2.1 接口创建](#7.6.2.1 接口创建)
        • [7.6.2.2 接口实现](#7.6.2.2 接口实现)
      • [7.6.3 判断中奖记录是否需要回滚](#7.6.3 判断中奖记录是否需要回滚)
      • [7.6.4 中奖记录回滚](#7.6.4 中奖记录回滚)
        • [7.6.4.1 创建接口](#7.6.4.1 创建接口)
        • [7.6.4.2 实现接口](#7.6.4.2 实现接口)
        • [7.6.4.3 dao层](#7.6.4.3 dao层)
    • [7.7 消费者主流程](#7.7 消费者主流程)
  • 八、死信队列消费者

一、抽奖流程

  1. 参与者注册与奖品建⽴
    1.1. 参与者注册:管理员通过管理端新增⽤⼾, 填写必要的信息,如姓名、联系⽅式等。
    1.2. 奖品建⽴:奖品需要提前建⽴好
  2. 抽奖活动设置
    2.1. 活动创建:管理员在系统中创建抽奖活动,输⼊活动名称、描述、奖品列表等信息。
    2.2. 圈选⼈员: 关联该抽奖活动的参与者。
    2.3. 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等。
    2.4. 活动发布:活动信息发布后,系统通过管理端界⾯展⽰活动列表。
  3. 抽奖请求处理(重要)
    3.1. 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
    3.2. 请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加
    信息。
    3.3. 消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。
    请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。
  4. 抽奖结果公布
    4.1. 前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。
  5. 抽奖逻辑执⾏(重要)
    5.1. 消息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。
  6. 中奖结果处理(重要)
    6.1. 请求验证:
    6.2. 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数限制等)等;
    6.3. 幂等性:若消息多发,已抽取的内容不能再次抽取
    6.4. 状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,⼈员是否已中奖等。
    6.5. 结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存。
  7. 中奖者通知
    7.1. 通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。
    7.2. 奖品领取:中奖者根据通知中的指引领取奖品。
  8. 抽奖异常处理
    8.1. 回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。
    8.2. 补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施

技术实现细节

  • 异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂等性。
  • 活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。
  • 事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要做好事务处理。

二、MQ配置

pom.xml:

xml 复制代码
<!-- RabbitMQ -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.yml :

java 复制代码
spring:
  rabbitmq:                     ## mq ##
    host: 自己服务器地址
    port: 5672
    username: admin(自己的)
    password: admin(自己的)
    
    listener:
      simple:
        acknowledge-mode: auto #消息确认机制,默认auto
        retry:
          enabled: true
          max-attempts: 5    #设置失败重试 5次

配置类:

com.yj.lottery_system.common.config 包下:DirectRabbitConfig 类

java 复制代码
package com.yj.lottery_system.common.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public 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 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);
    }

    /**
     * 死信队列 起名:DirectQueue
     *
     * @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);
    }
    /**
     * Direct交换机 起名:DirectExchange
     *
     * @return
     */
    @Bean
    DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
    }
    /**
     * 绑定 将死信队列和交换机绑定, 并设置⽤于匹配键:DirectRouting
     *
     * @return
     */
    @Bean
    Binding bindingdlx() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(DLX_ROUTING);
    }
    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

三、MQ生产者(抽奖异步接口)

3.1 抽奖请求处理简介

时序图:

3.2 参数列表

参数名 描述 类型 默认值 条件
activityId 活动id Long 必须
prizeId 正在抽的奖品id Long 必须
prizeTiers 正在抽的奖品等级 String 必须
winningTime 中奖时间 Date 必须
winnerList 中奖人员列表 List 必须

3.3 接口规范

java 复制代码
[请求] /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": ""
 }

3.4 controller 层

com.yj.lottery_system.controller 包下:DrawPrizeController 类

  • DrawPrizeController 参数接收类
  • 前端传的JSON,使用@RequestBody,对参数进行校验使用@Valid
  • 打印调用日志
  • 调用service
  • 返回
java 复制代码
package com.yj.lottery_system.controller;

import com.yj.lottery_system.common.pojo.CommonResult;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.service.IDrawPrizeService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DrawPrizeController {
    private static final Logger log = LoggerFactory.getLogger(DrawPrizeController.class);
    @Resource
    private IDrawPrizeService drawPrizeService;

    /**
     * 异步抽奖接口
     * @param param
     * @return
     */
    @RequestMapping("/draw-prize")
    public CommonResult<Boolean> drawPrize(@RequestBody@Valid DrawPrizeParam param){
        //打印日志
        log.info("drawPrize DrawPrizeParam : {}", JacksonUtil.writeValueAsString(param));
        //调用service
        drawPrizeService.drawPrize(param);
        return CommonResult.success(true);
    }
}

3.4.1 DrawPrizeController 参数接收类

com.yj.lottery_system.controller.param 包下:

  • 根据接口规范,有以下参数,并且都是必传参数
java 复制代码
package com.yj.lottery_system.controller.param;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Data
public class DrawPrizeParam implements Serializable {
    //活动id
    @NotNull(message = "活动id不能为空")
    private Long activityId;

    //正在抽的奖品id
    @NotNull(message = "奖品id不能为空")
    private Long prizeId;

    //正在抽的奖品等级
    @NotBlank(message = "奖品等级不能为空")
    private String prizeTiers;

    //中奖时间
    @NotNull(message = "中奖时间不能为空")
    private Date winningTime;

    //中奖人员列表
    @NotEmpty(message = "中奖人员不能为空")
    @Valid
    private List<Winner> winnerList;

    @Data
    public static class Winner {
        //中奖人员id
        @NotNull(message = "中奖人员id不能为空")
        private Long userId;

        //中奖人员名字
        @NotBlank(message = "中奖人员名字不能为空")
        private String userName;
    }
}

3.5 service 层

3.5.1 创建接口

com.yj.lottery_system.service 包下:

创建 抽奖对应的接口类

java 复制代码
package com.yj.lottery_system.service;

import com.yj.lottery_system.controller.param.DrawPrizeParam;
import org.springframework.stereotype.Service;

@Service
public interface IDrawPrizeService {
    /**
     * 异步抽奖接口
     * @param param
     */
    void drawPrize(DrawPrizeParam param);
}

3.5.2 实现接口

com.yj.lottery_system.service.impl 包下:

java 复制代码
package com.yj.lottery_system.service.impl;

import com.yj.lottery_system.common.utils.JacksonUtil;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.service.IDrawPrizeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static com.yj.lottery_system.common.config.DirectRabbitConfig.EXCHANGE_NAME;
import static com.yj.lottery_system.common.config.DirectRabbitConfig.ROUTING;

@Service
public class DrawPrizeServiceImpl implements IDrawPrizeService {
    private final static Logger log = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 异步抽奖接口
     * @param param
     */
    @Override
    public void drawPrize(DrawPrizeParam param) {
        Map<String,String> map = new HashMap<>();
        map.put("messageId", String.valueOf(UUID.randomUUID()));
        map.put("messageData", JacksonUtil.writeValueAsString(param));
        //发消息: 交换机、绑定key、消息体
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING,map);
        log.info("mq消息发送成功: map: {}", JacksonUtil.writeValueAsString(map));

    }
}

3.6 测试


七、MQ消费者

时序图:

7.1 接收消息功能

根据前面的发送消息使用的Map<String,String>,接收就也使用。

@RabbitHandler 方法注解来接收队列消息

@RabbitListener(queues = 队列名) 类注解来对应RabbitMQ

java 复制代码
//成功接收队列消息
        log.info("mq成功接收到消息 message:{}",
                JacksonUtil.writeValueAsString(message));

        String paramString = message.get("messageData");
        DrawPrizeParam param = JacksonUtil.readValue(paramString,DrawPrizeParam.class);

7.2 校验抽奖请求

我们单独写一个方法service层方法实现

7.2.1 创建接口

com/yj/lottery_system/service 包下 IDrawPrizeService.java类中

java 复制代码
    /**
     * 校验抽奖请求
     * @param param
     */
    Boolean checkDrawPrizeParam(DrawPrizeParam param);

7.2.2 实现接口

com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java类中

  • 我们需要校验活动,奖品是否存在,并且还要校验状态
  • 还要校验奖品数和中奖人数是否一样
  • 我们在activity_prize 活动奖品关联表中 在创建活动的时候是使用了本地事务,保证了一致性的,所以我们查这张表就可以拿到正确的奖品信息:奖品是否存在 奖品状态 奖品数(用活动id和奖品id组成唯一索引)
java 复制代码
   /**
     * 校验抽奖请求
     * @param param
     */
    @Override
    public Boolean checkDrawPrizeParam(DrawPrizeParam param) {
        //活动是否存在
        ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
        //奖品是否存在,从activity_prize表获取,因为使用了本地事务,保持一致性
        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());

        //活动和奖品是否存在
        if(null == activityDO || null == activityPrizeDO) {
            //throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY);
            log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY.getMsg());
            return false;
        }

        //活动是否有效
        if(activityDO.getStatus().equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
            //throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED);
            log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_COMPLETED.getMsg());
            return false;
        }

        //奖品是否有效
        if(activityPrizeDO.getStatus().equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name())) {
            //throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED);
            log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED.getMsg());
            return false;
        }

        //中奖者人数是否与奖品数量一致
        if(activityPrizeDO.getPrizeAmount() != param.getWinnerList().size()) {
            //throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR);
            log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR.getMsg());
            return false;
        }
        return true;
    }

7.2.3 查表方法

com/yj/lottery_system/dao/mapper 包下 ActivityPrizeMapper.java 类:

java 复制代码
    /**
     * 根据活动id与奖品id组成唯一索引 查活动关联奖品信息
     * @param activityId
     * @return
     */
    @Select("select * from activity_prize where activity_id = #{activityId} and prize_id = #{prizeId}")
    ActivityPrizeDO selectByAPId(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);

7.3 状态扭转(责任链模式、策略模式)

我们需要扭转活动状态,活动关联奖品状态,活动关联人员状态

  • 活动关联奖品状态:完成一次抽奖动作后,被抽的奖品的状态扭转,INIT ----> COMPLETED
    • 查询本次抽取的奖品
    • 虽然前面 校验抽奖请求中 校验了奖品状态必须是INIT,但是为了降低耦合性,还是需要在这判断
    • 状态为 INIT 扭转
  • 活动关联人员状态:完成一次抽奖动作后,中奖人员的状态扭转,INIT ----> COMPLETED
    • 查询本次中奖人员
    • 虽然前面 校验抽奖请求中 校验了中奖人员状态必须是INIT,但是为了降低耦合性,还是需要在这判断
    • 状态为 INIT 扭转
  • 活动状态:全部奖品抽完之后,状态扭转,RUNNING----> COMPLETED
    • 查询活动信息
    • 判断活动中奖品状态全部为 COMPLETED
  • 虽然前面 校验抽奖请求中 校验了活动状态必须是RUNNING,但是为了降低耦合性,还是需要在这判断
    • 状态为 RUNNING扭转
  • 更新Redis缓存

上诉过程存在的问题:

  1. 活动状态依赖于奖品状态,代码维护性差
  2. 状态扭转条件如果拓展,代码维护性,扩展性差

解决方法:

  • 责任链模式+策略模式:将处理顺序定死,先处理的与后处理的区分开来,状态扭转实现每个类自己的方法。
    • 我们将要处理的状态都单独拿出来一个类表示,都继承一个抽象类,这个抽象类里面有一个抽象方法返回先后处理的标识,我们处理的时候就根据这个标识来区分处理先后顺序。
    • 这样就解决了依赖性
  • 我们在抽象类中写一个判断状态扭转条件的抽象方法,每一个继承类都实现自己的扭转状态条件。
    • 这样就解决了代码扩展性与维护性

7.3.1 状态扭转调用

com/yj/lottery_system/service/mq 包下 MqReceiver.java 类中

  • 设置状态扭转需要的参数,调用状态扭转实现方法
java 复制代码
/**
     * 状态扭转
     * @param param
     */
    private void statusConvert(DrawPrizeParam param) {
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
        convertActivityStatusDTO.setUserIds(param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.COMPLETED);

        //调用状态扭转方法
        activityStatusManager.handleEvent(convertActivityStatusDTO);

    }

7.3.2 状态扭转具体实现

7.3.2.1 状态扭转接口

com.yj.lottery_system.service.activityStatus 包下:

  • 定义状态扭转的接口:
  • 封装一下
java 复制代码
package com.yj.lottery_system.service.activityStatus;

import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import org.springframework.stereotype.Service;

@Service
public interface IActivityStatusManager {
    /**
     * 处理活动相关状态转换
     * @param convertActivityStatusDTO
     */
    void handleEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
}
7.3.2.2 实现状态扭转接口

com.yj.lottery_system.service.activityStatus.impl 包下 ActivityStatusManagerImpl 类

  • 实现状态扭转的主逻辑:
  • 我们将每一个需要扭转的状态都封装成一个类,类里面实现自己的扭转方法,
  • 将需要扭转的状态使用注解注入到一个Map管理起来
  • 非空校验
  • 调用两次processConvertStatus方法先后处理Map里面需要扭转状态的类,并返回扭转 - true,未扭转 - false
  • 如果扭转了,调用缓存更新方法。
java 复制代码
package com.yj.lottery_system.service.activityStatus.impl;

import com.yj.lottery_system.common.errorcode.ServiceErrorCodeConstants;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.service.IActivityService;
import com.yj.lottery_system.service.activityStatus.IActivityStatusManager;
import com.yj.lottery_system.service.activityStatus.operator.AbstractActivityOperator;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@Service
public class ActivityStatusManagerImpl implements IActivityStatusManager {
    private static final Logger log = LoggerFactory.getLogger(ActivityStatusManagerImpl.class);
    @Autowired
    private Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();
   @Resource
   private IActivityService activityService;
    /**
     * 处理活动相关状态转换
     * @param convertActivityStatusDTO
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handleEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
        if(CollectionUtils.isEmpty(operatorMap)) {
            log.warn("operatorMap 为空");
            return;
        }

        //拿到需要处理的
        Map<String, AbstractActivityOperator> curMap = new HashMap<>(operatorMap);

        //更新标志位
        Boolean updateFlag = false;
        //先处理
        updateFlag = processConvertStatus(convertActivityStatusDTO,curMap,1);
        //后处理
        updateFlag = processConvertStatus(convertActivityStatusDTO,curMap,2) || updateFlag;
        //更新过,更新缓存
        if(updateFlag) {
            activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
        }
    }
}
7.3.2.2.1 processConvertStatus:扭转状态方法
  • 遍历当前的Map,拿到需要扭转状态的operator,
  • 根据sequence 方法判断是否是该当前扭转的
  • 调用operator 自己类实现的扭转方法
  • 如果扭转了,从当前Map删除
java 复制代码
/**
     * 扭转 curMap 里面 sequence 的状态
     * @param convertActivityStatusDTO
     * @param curMap
     * @param sequence
     * @return
     */

    private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,
                                         Map<String, AbstractActivityOperator> curMap,
                                         int sequence) {
        //更新标志位
        Boolean updateFlag = false;
        //遍历 curMap
        Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = curMap.entrySet().iterator();
        while(iterator.hasNext()) {
            AbstractActivityOperator operator = iterator.next().getValue();
            //判断operator是否需要扭转状态
            if(operator.sequence() != sequence
            || !operator.needConvert(convertActivityStatusDTO)) {
                continue;
            }

            //需要扭转 扭转状态
            if(!operator.convert(convertActivityStatusDTO)) {
               log.error("{} 状态扭转失败 ",operator.getClass().getName());
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR);
            }
            //在curMap删除当前operator
            iterator.remove();
            updateFlag = true;
        }

        //返回
        return updateFlag;
    }
7.4.2.2.2 控制状态扭转抽象类

com.yj.lottery_system.service.activityStatus.operator :

里面定义三个抽象方法:

  • sequence:返回 继承类状态扭转先后顺序 方法
  • needConvert:判断继承类是否需要状态扭转
  • Convert:继承类自己状态扭转的方法
java 复制代码
package com.yj.lottery_system.service.activityStatus.operator;

import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;

public abstract class AbstractActivityOperator {
    /**
     * 控制处理顺序
     * @return
     */
    public abstract Integer sequence();

    /**
     * 是否需要状态扭转
     * @param convertActivityStatusDTO
     * @return
     */

    public abstract Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO);

    /**
     * 转换方法
     * @param convertActivityStatusDTO
     * @return
     */
    public abstract Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO);

}
7.4.2.2.3 ActivityOperator :活动状态扭转控制类

com.yj.lottery_system.service.activityStatus.operator 包下:

  • 继承抽象类AbstractActivityOperator
    • 实现抽象方法 sequence :返回2 代表扭转顺序 后处理
    • 实现抽象方法 needConvert:判断是否需要扭转状态
      • 校验活动id 和 活动目标扭转状态
      • 调用dao,求该活动下 奖品RUNNING状态 的个数
      • 个数大于0 不扭转
  • 实现抽象方法 convert:扭转状态
    • 调用dao对应方法即可
java 复制代码
package com.yj.lottery_system.service.activityStatus.operator;

import com.yj.lottery_system.dao.dataObject.ActivityDO;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.mapper.ActivityMapper;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class ActivityOperator extends AbstractActivityOperator{

    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Override
    public Integer sequence() {
        return 2;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //活动id
        Long activityId = convertActivityStatusDTO.getActivityId();
        //活动目标扭转状态
        ActivityStatusEnum targetActivityStatus = convertActivityStatusDTO.getTargetActivityStatus();
        if (null == activityId || null == targetActivityStatus) {
            return false;
        }
        //调用dao
        ActivityDO activityDO = activityMapper.selectById(activityId);
        if(null == activityDO) {
            return false;
        }
        //判断当前状态与目标状态
        if(targetActivityStatus.name().equals(activityDO.getStatus())) {
            return false;
        }
        //该活动下 奖品INIT状态 的个数
       int count =  activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
        if(count > 0) {
            return false;
        }
        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {

        //调用dao,更新状态
        try {
            activityMapper.updateStatus( convertActivityStatusDTO.getActivityId(),
                    convertActivityStatusDTO.getTargetActivityStatus().name());
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}
7.4.2.2.4 PrizeOperator :奖品状态扭转控制类

com.yj.lottery_system.service.activityStatus.operator 包下:

  • 继承抽象类AbstractActivityOperator
    • 实现抽象方法 sequence :返回1 代表扭转顺序 先处理
    • 实现抽象方法 needConvert:判断是否需要扭转状态
      • 校验活动id 和 j奖品id 和 奖品目标扭转状态
      • 调用dao,拿到被抽取 的奖品
      • 判断奖品状态与目标状态,一致返回false,不同返回true
  • 实现抽象方法 convert:扭转状态
    • 调用dao对应方法即可
java 复制代码
package com.yj.lottery_system.service.activityStatus.operator;

import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;

@Component
public class PrizeOperator extends AbstractActivityOperator{
    @Resource
    private ActivityPrizeMapper activityPrizeMapper;
    @Override
    public Integer sequence() {
        return 1;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //活动id
        Long activityId = convertActivityStatusDTO.getActivityId();
        //奖品id
        Long prizeId = convertActivityStatusDTO.getPrizeId();
        //目标状态
        ActivityPrizeStatusEnum targetActivityPrizeStatus = convertActivityStatusDTO.getTargetActivityPrizeStatus();
        if(null == activityId || null == prizeId || null == targetActivityPrizeStatus ) {
            return false;
        }
        //拿奖品
        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(activityId, prizeId);

        if(null == activityPrizeDO) {
            return false;
        }
        //当前奖品状态与目标状态
        if(activityPrizeDO.getStatus().equals(targetActivityPrizeStatus.name())) {
            return false;
        }
        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        try {
            activityPrizeMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
                    convertActivityStatusDTO.getPrizeId(),
                    convertActivityStatusDTO.getTargetActivityPrizeStatus().name());
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}
7.4.2.2.5 UserOperator :中奖人员状态扭转控制类

com.yj.lottery_system.service.activityStatus.operator 包下:

  • 继承抽象类AbstractActivityOperator
    • 实现抽象方法 sequence :返回1 代表扭转顺序 先处理
    • 实现抽象方法 needConvert:判断是否需要扭转状态
      • 校验活动id 和 中奖者 id 列表 和 中奖者目标扭转状态
      • 调用dao,拿到中奖者列表
      • 判断中奖者状态与目标状态,一致返回false,不同返回true
  • 实现抽象方法 convert:扭转状态
    • 调用dao对应方法即可
java 复制代码
package com.yj.lottery_system.service.activityStatus.operator;

import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.dataObject.ActivityUserDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.dao.mapper.ActivityUserMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

@Component

public class UserOperator extends AbstractActivityOperator{
   @Resource
   private ActivityUserMapper activityUserMapper;
    @Override
    public Integer sequence() {
        return 1;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //活动id
        Long activityId = convertActivityStatusDTO.getActivityId();
        //人员id列表
        List<Long> userIds = convertActivityStatusDTO.getUserIds();
        //目标状态
        ActivityUserStatusEnum targetActivityUserStatus = convertActivityStatusDTO.getTargetActivityUserStatus();

        if(null == activityId
                || CollectionUtils.isEmpty(userIds)
                || null == targetActivityUserStatus) {
            return false;
        }
        //调用dao
        List<ActivityUserDO> activityUserDOList = activityUserMapper.batchSelectByAUId(activityId,userIds);

        if(CollectionUtils.isEmpty(activityUserDOList)) {
            return false;
        }
        //判断中奖者状态 与目标状态
        for(ActivityUserDO activityUserDO : activityUserDOList ) {
            if(activityUserDO.getStatus().equalsIgnoreCase(targetActivityUserStatus.name())) {
                return false;
            }
        }

        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {

        //活动id
        Long activityId = convertActivityStatusDTO.getActivityId();
        //人员id列表
        List<Long> userIds = convertActivityStatusDTO.getUserIds();
        //目标状态
        ActivityUserStatusEnum targetActivityUserStatus = convertActivityStatusDTO.getTargetActivityUserStatus();

        try{
            activityUserMapper.batchUpdateStatus(activityId, userIds,targetActivityUserStatus.name());
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}
7.4.2.2.6 缓存Redis更新
7.4.2.2.6.1 接口定义

com/yj/lottery_system/service 包下:IActivityService.java类

java 复制代码
    /**
     * 缓存活动详细信息(读取表数据
     */
    void cacheActivity(Long activityId);
7.4.2.2.6.2 接口实现

7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类

com.yj.lottery_system.service.dto 包下:

  • 这个类里面的属性,对应这侧抽奖活动需要状态扭转的类对应的id,和扭转后的状态
java 复制代码
package com.yj.lottery_system.service.dto;

import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class ConvertActivityStatusDTO implements Serializable {
    //活动id
    private Long activityId;

    // 活动目标状态
    private ActivityStatusEnum targetActivityStatus;

    //奖品id
    private Long prizeId;

    // 奖品目标状态
    private ActivityPrizeStatusEnum targetActivityPrizeStatus;

    //人员列表
    private List<Long> userIds;

    // 奖品目标状态
    private ActivityUserStatusEnum targetActivityUserStatus;


}

7.3.4 dao层修改

com/yj/lottery_system/dao/mapper 包下ActivityMapper.java类:

java 复制代码
    /**
     * 根据活动id,更新目标状态
     * @param id 活动id
     * @param status 目标状态
     */

    @Update("update activity set status = #{status} where id = #{id}")
    void updateStatus(@Param("id") Long id, @Param("status")String status);

com/yj/lottery_system/dao/mapper 包下 ActivityPrizeMapper.java 类:

java 复制代码
    /**
     *活动activityId下 奖品RUNNING状态 的个数
     * @param activityId 活动id
     * @param status 奖品状态
     * @return
     */
    @Select("select count(1) from activity_prize where activity_id = #{activityId} and status = #{status}")
    int countPrize(@Param("activityId")Long activityId, @Param("status")String status);

    /**
     * 更新被抽取奖品状态
     * @param activityId 活动id
     * @param prizeId 奖品id
     * @param status 奖品状态
     */
    @Update(("update activity_prize set status = #{status} where activity_id = #{activityId} and prize_id = #{prizeId}"))
    void updateStatus(@Param("activityId")Long activityId,  @Param("prizeId")Long prizeId,  @Param("status")String status);

com/yj/lottery_system/dao/mapper 包下 ActivityUserMapper.java 类:

java 复制代码
    @Select("<script> " +
            "select * from  activity_user  " +
            "where activity_id = #{activityId}  and " +
            "user_id in" +
            "<foreach collection = 'userIds' item = 'userId' open='(' separator=',' close=')'> " +
            "#{userId}" +
            "</foreach>" +
            "</script>")
    List<ActivityUserDO> batchSelectByAUId(@Param("activityId")Long activityId,
                                           @Param("userIds")List<Long> userIds);

    @Update("<script> " +
            "update activity_user set status = #{status}" +
            "where activity_id = #{activityId}  and " +
            "user_id in" +
            "<foreach collection = 'userIds' item = 'userId' open='(' separator=',' close=')'> " +
            "#{userId}" +
            "</foreach>" +
            "</script>")
    void batchUpdateStatus(@Param("activityId")Long activityId,
                                   @Param("userIds")List<Long> userIds,
                                   @Param("status") String status) ;

7.3.5 更新Redis缓存中的状态

7.3.5.1 接口创建

com/yj/lottery_system/service 包下 IActivityService.java 类:

java 复制代码
    /**
     * 缓存活动详细信息(读取表数据
     */
    void cacheActivity(Long activityId);
7.3.5.2 接口实现

com/yj/lottery_system/service/impl 包下 ActivityServiceImpl.java 类:

跟前面创建活动时的逻辑一样:

java 复制代码
    /**
     * 缓存活动详细信息(读取表数据
     */
    @Override
    public void cacheActivity(Long activityId) {
        if(null == activityId) {
            log.warn("缓存的活动ID为空");
            throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_IS_EMPTY);
        }
    //查询表数据 活动表 活动奖品表 活动人员表
        // 查表 活动表
        ActivityDO activityDO = activityMapper.selectById(activityId);
        if(null == activityDO) {
            log.error("缓存的活动ID有误");
            throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_ERROR);
        }
        // 活动奖品表
        List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectByActivityId(activityId);
        // 活动人员表
        List<ActivityUserDO> activityUserDOList = activityUserMapper.selectByActivityId(activityId);
        // 奖品表
        //先拿奖品id
        List<Long> prizeIdList = activityPrizeDOList.stream()
                .map(ActivityPrizeDO::getPrizeId)
                .collect(Collectors.toList());
        List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIdList);

        //整合详细信息,存放Redis
        ActivityDetailDTO activityDetailDTO = convertActivityDetailDTO(activityDO, activityUserDOList, activityPrizeDOList, prizeDOList);
        //存放Redis
        cacheActivity(activityDetailDTO);
    }

7.3.6 总结

模式 在哪里用 承担的角色 带来的好处
责任链模式(Chain of Responsibility) 1. ActivityStatusManagerImplsequence 两次遍历 Map<String, AbstractActivityOperator> 2. 每个 Operator 自己决定"要不要处理"以及"是否甩锅给后续" 奖品、用户、活动 三类状态节点串成 先 1 后 2 的两条链 ① 节点顺序可配置 ② 新增/删除状态类型只新增/删除一个 Operator 类,链自动感知
策略模式(Strategy) 1. 每个 Operator 实现 AbstractActivityOperator三个策略接口sequence() / needConvert() / convert() 2. Spring 启动时把所有具体策略(PrizeOperator/UserOperator/ActivityOperator)注入到 Map------天然 策略注册表 "判断条件""执行算法" 各自独立,可互换 ① 各状态扭转逻辑彻底解耦 ② 单元测试可单独 mock 任一策略

7.4 保存中奖者名单

7.4.1 接口创建

com/yj/lottery_system/service 包下:IDrawPrizeService.java类中:

java 复制代码
    /**
     * 保存中奖者名单
     * @param param
     */
    List<WinningRecodesDO> saveWinnerRecodes(DrawPrizeParam param);

7.4.2 实现接口

com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:

  • 查 活动表activity 奖品表prize 用户表user 活动关联奖品表activity_prize,拿到对应数据
  • 构造中奖记录
  • 缓存奖品维度(activityId_prizeId_ , winningRecodesDOList)
  • 活动已完成 缓存活动维度(activityId_ , winningRecodesDOList)
java 复制代码
/**
     * 保存中奖者名单
     * @param param
     */
    @Override
    public List<WinningRecodesDO> saveWinnerRecodes(DrawPrizeParam param) {
        //查询活动 奖品 人员信息 活动关联奖品表
        //查询活动
        ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
        //查询人员信息
        List<UserDO> 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<WinningRecodesDO> winningRecodesDOList = userDOList.stream()
                .map(userDO -> {
                    WinningRecodesDO winningRecodesDO = new WinningRecodesDO();
                    winningRecodesDO.setActivityId(activityDO.getId());
                    winningRecodesDO.setActivityName(activityDO.getActivityName());
                    winningRecodesDO.setPrizeId(prizeDO.getId());
                    winningRecodesDO.setPrizeName(prizeDO.getName());
                    winningRecodesDO.setPrizeTier(activityPrizeDO.getPrizeTiers());
                    winningRecodesDO.setWinnerId(userDO.getId());
                    winningRecodesDO.setWinnerName(userDO.getUserName());
                    winningRecodesDO.setWinnerEmail(userDO.getEmail());
                    winningRecodesDO.setWinnerPhoneNumber(userDO.getPhoneNumber());
                    winningRecodesDO.setWinningTime(param.getWinningTime());
                    return winningRecodesDO;
                }).collect(Collectors.toList());

        winningRecordMapper.batchInsert(winningRecodesDOList);

        //缓存中奖者记录
        //缓存奖品维度(activityId_prizeId_ , winningRecodesDOList)
        cacheWinningRecords(param.getActivityId()+"_"+param.getPrizeId(),
                winningRecodesDOList,
                WINNING_RECORDS_TIMEOUT);
        //活动已完成 缓存活动维度(activityId_ , winningRecodesDOList)
        if(activityDO.getStatus()
                .equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
            //查询活动维度全量中奖记录
           List<WinningRecodesDO> allList =  winningRecordMapper.selectByActivityId(param.getActivityId());
            cacheWinningRecords(String.valueOf(param.getActivityId()),
                    allList,
                    WINNING_RECORDS_TIMEOUT);
        }
        return winningRecodesDOList;
    }

7.4.3 WinningRecodesDO 中奖者类

与winner_record表对应

com.yj.lottery_system.dao.dataObject 包下:

java 复制代码
package com.yj.lottery_system.dao.dataObject;

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

@Data
@EqualsAndHashCode(callSuper = true)
public class WinningRecodesDO extends BaseDO{
    //活动id
    private Long activityId;
    //活动名
    private String activityName;
    //奖品id
    private Long prizeId;
    //奖品名称
    private String prizeName;
    //奖品等级
    private String prizeTier;
    //中奖者id
    private Long winnerId;
    //中奖者姓名
    private String winnerName;
    //中奖者邮箱
    private String winnerEmail;
    //中奖者电话
    private Encrypt WinnerPhoneNumber;
    //中奖时间
    private Date winningTime;

}

7.4.4 缓存方法与获取方法

com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:

java 复制代码
/**
     * 缓存中奖记录
     * @param key
     * @param winningRecodesDOList
     * @param time
     */

    private void cacheWinningRecords(String key, List<WinningRecodesDO> winningRecodesDOList, Long time) {
        try {
            if(!StringUtils.hasText(key)
                    || CollectionUtils.isEmpty(winningRecodesDOList)) {
                log.warn("要缓存的内容为空:key: {}, value: {}",
                        WINNING_RECORDS_PREFIX+key,
                        JacksonUtil.writeValueAsString(winningRecodesDOList));
                return;
            }
            redisUtil.set(WINNING_RECORDS_PREFIX+key,
                    JacksonUtil.writeValueAsString(winningRecodesDOList),
                    time);
        }catch (Exception e) {
            log.error("缓存中奖记录异常:key: {}, value: {}",
                    WINNING_RECORDS_PREFIX+key,
                    JacksonUtil.writeValueAsString(winningRecodesDOList));
        }
    }
    /**
     * 获取缓存中的中奖记录
     * @param key
     */

    private List<WinningRecodesDO> getWinningRecords(String key) {
        try{
            if(!StringUtils.hasText(key)) {
                log.warn("要查询的缓存中奖记录key为空:key: {}", WINNING_RECORDS_PREFIX+key);
                return Arrays.asList();
            }
            String s = redisUtil.get(WINNING_RECORDS_PREFIX + key);
            if (!StringUtils.hasText(s)) {
                return Arrays.asList();
            }
            List<WinningRecodesDO> winningRecodesDOList = JacksonUtil.readListValue(s, WinningRecodesDO.class);
            return winningRecodesDOList;
        }catch (Exception e) {
            log.error("要查询的缓存中奖记录异常:key: {}", WINNING_RECORDS_PREFIX+key);
            return Arrays.asList();
        }

    }

7.4.5 dao层新增Mapper方法

com/yj/lottery_system/dao/mapper 包下 PrizeMapper.java 类中

java 复制代码
    /**
     * 根据奖品id查奖品信息
     * @param id
     * @return
     */
    @Select("select * from prize where id = #{id}")
    PrizeDO selectById(@Param("id")Long id);

com.yj.lottery_system.dao.mapper 包

java 复制代码
package com.yj.lottery_system.dao.mapper;

import com.yj.lottery_system.dao.dataObject.WinningRecodesDO;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper

public interface WinningRecordMapper {
    @Insert("<script> " +
            "insert into winning_record  " +
            "(activity_id,activity_name, " +
            "prize_id, prize_name, prize_tier," +
            "winner_id,winner_name,winner_email,winner_phone_number,winning_time) " +
            "values <foreach item = 'item' collection='items' index='index' separator=','  > " +
            " (#{item.activityId},#{item.activityName}," +
            "#{item.prizeId},#{item.prizeName},#{item.prizeTier}," +
            "#{item.winnerId},#{item.winnerName},#{item.winnerEmail,},#{item.winnerPhoneNumber},#{item.winningTime}) " +
            "</foreach>" +
            "</script>")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    void batchInsert(@Param("items") List<WinningRecodesDO> winningRecodesDOList);

    /**
     * 根据活动id 查询全部中奖信息
     * @param activityId
     * @return
     */
    @Select("select * from winning_record where activity_id = #{activityId}")
    List<WinningRecodesDO> selectByActivityId(@Param("activityId") Long activityId);
}

7.5 邮箱通知中奖者

7.5.1 添加方法

com/yj/lottery_system/common/utils 包下 EmailCodeUtil.java 添加发送中奖信息的方法:

java 复制代码
    /**
     * 发送邮件
     * @param to
     * @param subject
     * @param context
     * @return
     */
    public Boolean sendSampleMail(String to, String subject, String context) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(context);
        try {
            mailSender.send(message);
        } catch (Exception e) {
            log.error("向{}发送邮件失败!", to, e);
            return false;
        }
        return true;
    }

7.5.2 配置线程池

application.yml :

yml 复制代码
## 线程池 ##
async:
  executor:
    thread:
      core_pool_size: 10
      max_pool_size: 20
      queue_capacity: 20
      name:
        prefix: async-service-

com.yj.lottery_system.common.config 包:

java 复制代码
package com.yj.lottery_system.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class ExecutorConfig {
    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;
    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;


    @Value("${async.executor.thread.queue_capacity}")
    private int queueCapacity;
    @Value("${async.executor.thread.name.prefix}")
    private String namePrefix;
    @Bean(name = "asyncServiceExecutor")
    public ThreadPoolTaskExecutor asyncServiceExecutor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new
                ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
        threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
        threadPoolTaskExecutor.setQueueCapacity(queueCapacity);
        threadPoolTaskExecutor.setKeepAliveSeconds(3);
        threadPoolTaskExecutor.setThreadNamePrefix(namePrefix);
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执⾏任务,⽽是由调⽤者所在的线程来执⾏
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //加载
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

7.5.3 异步发送

我们是在抽奖完成之后异步处理发送,用线程池。

java 复制代码
    //通知
    private void syncExecute(List<WinningRecodesDO> winningRecodesDOList) {
        //邮件通知
        threadPoolTaskExecutor.execute(()->sendMail(winningRecodesDOList));
    }

    /**
     * 发邮件
     * @param winningRecodesDOList
     */
    private void sendMail(List<WinningRecodesDO> winningRecodesDOList) {
        if(CollectionUtils.isEmpty(winningRecodesDOList)) {
            log.info("中奖列表为空,不用发邮件");
            return;
        }
        for(WinningRecodesDO winningRecodesDO : winningRecodesDOList) {
            String context = "您好, "+winningRecodesDO.getWinnerName()+",恭喜您在"+
                    winningRecodesDO.getActivityName()+"抽奖活动中获得"+
                    winningRecodesDO.getPrizeTier()+":"+
                    winningRecodesDO.getPrizeName()+"!获奖时间是:"+
                    winningRecodesDO.getWinningTime()+",请尽快领取您的奖品,祝您生活愉快。";
            emailCodeUtil.sendSampleMail(winningRecodesDO.getWinnerEmail(),
                    "中奖通知", context);
        }
    }

7.6 抽奖回滚实现

为了保证发生异常后,保证数据一致性,恢复处理请求之前的库表状态。

  1. 回滚状态 :活动表,活动关联奖品表,活动关联人员表
    1.1. 状态不需要回滚就不需要回滚中奖者名单
  2. 回滚中奖者名单

com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:

java 复制代码
    /**
     * 处理抽奖异常回滚行为
     * @param param
     */
    private void rollBack(DrawPrizeParam param) {
        //回滚状态:活动表 奖品 人员
        //状态不需要回滚直接return
        if(!statusNeedRollBack(param)) {
            return;
        }
        rollBackStatus(param);
        //回滚中奖者名单
        //是否需要回滚,不需要直接返回
        if(!winnerNeedRollBack(param)) {
            return;
        }
        rollBackWinner(param);
    }

7.6.1 判断状态是否需要回滚

因为我们前面扭转状态时保证了事务一致性,只需要判断 中奖人员 或者 奖品状态 其一就可以。

一个活动多个奖品,多次抽奖,不能使用活动状态判断。

com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:

java 复制代码
    private boolean statusNeedRollBack(DrawPrizeParam param) {
        //扭转状态时保证了事务一致性,只需要判断一个状态(不包含活动)
        ActivityPrizeDO activityPrizeDO =
                activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
        //已经扭转需要回滚
        return activityPrizeDO.getStatus()
                        .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());

    }

7.6.2 状态回滚方法

com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:

  • 全部回滚为初始化的状态:活动 RUNNING 奖品,中奖人员:INIT
java 复制代码
    /**
     * 回滚状态
     * @param param
     */
    private void rollBackStatus(DrawPrizeParam param) {
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.INIT);
        convertActivityStatusDTO.setUserIds(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.INIT);

        activityStatusManager.rollBackHandleEvent(convertActivityStatusDTO);
    }
7.6.2.1 接口创建

com.yj.lottery_system.service.activityStatus 包下:IActivityStatusManager类中:

java 复制代码
    /**
     * 处理活动相关状态回滚
     * @param convertActivityStatusDTO
     */
    void rollBackHandleEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
7.6.2.2 接口实现

com.yj.lottery_system.service.activityStatus.impl 包下:ActivityStatusManagerImpl类中:

  • 这届遍历需要回滚的类,直接调用上面写好的改变状态方法,更新一下缓存即可。
java 复制代码
    /**
     * 处理活动相关状态回滚
     * @param convertActivityStatusDTO
     */
    @Override
    public void rollBackHandleEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //拿到需要处理的
        Map<String, AbstractActivityOperator> curMap = new HashMap<>(operatorMap);
        for(AbstractActivityOperator operator : curMap.values()) {
            operator.convert(convertActivityStatusDTO);
        }
			//更新缓存
        activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
        
    }

7.6.3 判断中奖记录是否需要回滚

com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:

  • 直接调用dao层方法,看中奖人数是否更新了,
java 复制代码
    /**
     * 判断中奖者名单是否需要回滚,需要-true
     * @param param
     */
    private boolean winnerNeedRollBack(DrawPrizeParam param) {
        //判断活动中的奖品是否存在中奖者
        int count = winningRecordMapper.count(param.getActivityId(), param.getPrizeId());

        return count > 0;
    }

dao层 方法:

com.yj.lottery_system.dao.mapper 包下:WinningRecordMapper类中:

java 复制代码
    /**
     * 根据活动id 和 奖品id 查中奖者人数
     * @param activityId
     * @param prizeId
     * @return
     */
    @Select("select count(1) from winning_record where activity_id = #{activityId} and prize_id = #{prizeId}")
    int count(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);

7.6.4 中奖记录回滚

com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:

java 复制代码
    /**
     * 回滚中奖者名单
     * @param param
     */
    private void rollBackWinner(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }
7.6.4.1 创建接口

com.yj.lottery_system.service 包下:IDrawPrizeService类:

java 复制代码
    /**
     * 根据活动id和奖品id 删除中奖记录
     * @param activityId
     * @param prizeId
     */
    void deleteRecords(Long activityId, Long prizeId);
7.6.4.2 实现接口

com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:

java 复制代码
    /**
     * 根据活动id 或 奖品id 删除中奖记录
     * @param activityId
     * @param prizeId
     */
    @Override
    public void deleteRecords(Long activityId, Long prizeId) {
        if(null == activityId ) {
            log.warn("要删除中奖记录活动id为空");
        }
        //删除winner_record表
        winningRecordMapper.deleteRecords(activityId,prizeId);
        //删除缓存(奖品维度,活动维度)
        if(null != prizeId) {
            deleteWinningRecodes(activityId+"_"+prizeId);
        }
        deleteWinningRecodes(String.valueOf(activityId));
    }
   /**
     * 删除缓存
     */
    private void deleteWinningRecodes(String key) {
        try{
            if(redisUtil.hasKey(WINNING_RECORDS_PREFIX+key)) {
                redisUtil.del(WINNING_RECORDS_PREFIX+key);
            }
        }catch (Exception e){
            log.error("删除缓存中奖记录异常:key: {}", WINNING_RECORDS_PREFIX+key);
        }
    }
7.6.4.3 dao层

com.yj.lottery_system.dao.mapper 包下:WinningRecordMapper类中:

java 复制代码
    /**
     *  根据活动id 或 奖品id 删除中奖记录
     * @param activityId
     * @param prizeId
     */
    @Delete("<script>" +
            "delete from winning_record " +
            "where activity_id = #{activityId} " +
            "<if test=\"prizeId!=null\">" +
            "and prize_id = #{prizeId}" +
            "</if>" +
            "</script>")
    void deleteRecords(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);

7.7 消费者主流程

java 复制代码
package com.yj.lottery_system.service.mq;

import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.common.utils.EmailCodeUtil;
import com.yj.lottery_system.common.utils.JacksonUtil;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.dataObject.WinningRecodesDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.dao.mapper.WinningRecordMapper;
import com.yj.lottery_system.service.IDrawPrizeService;
import com.yj.lottery_system.service.activityStatus.IActivityStatusManager;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.yj.lottery_system.common.config.DirectRabbitConfig.QUEUE_NAME;

@RabbitListener(queues = QUEUE_NAME)
@Component
public class MqReceiver {
    private static final Logger log = LoggerFactory.getLogger(MqReceiver.class);
   @Resource
    private IDrawPrizeService drawPrizeService;
   @Resource
   private IActivityStatusManager activityStatusManager;
   @Autowired
   @Qualifier("asyncServiceExecutor")
   private ThreadPoolTaskExecutor threadPoolTaskExecutor;
   @Autowired
    private EmailCodeUtil emailCodeUtil;
   @Resource
   private ActivityPrizeMapper activityPrizeMapper;
   @Resource
   private WinningRecordMapper winningRecordMapper;
    @RabbitHandler
    public void process(Map<String,String> message) throws Exception{
        //成功接收队列消息
        log.info("mq成功接收到消息 message:{}",
                JacksonUtil.writeValueAsString(message));

        String paramString = message.get("messageData");
        DrawPrizeParam param = JacksonUtil.readValue(paramString,DrawPrizeParam.class);

        //处理抽奖
        try {
            //校验抽奖请求
            if(!drawPrizeService.checkDrawPrizeParam(param)) {
                return;
            }

            //活动,奖品,人员 状态扭转
            statusConvert(param);
            //保存中奖者名单
            List<WinningRecodesDO> winningRecodesDOList = drawPrizeService.saveWinnerRecodes(param);
            //通知中奖者
            syncExecute(winningRecodesDOList);
        }catch (ServiceException e) {
            log.error("处理 MQ 消息异常 {}, {} ",e.getCode(),e.getMsg(),e);
            //发生异常,需要回滚 保持事务一致性,
            rollBack(param);
            // 抛出异常
            throw e;
        } catch (Exception e) {
            log.error("处理 MQ 消息异常  ",e);
            //发生异常,需要回滚 保持事务一致性,
            rollBack(param);
            // 抛出异常
            throw e;
        }

    }
}

八、死信队列消费者

死信队列消费者 实时消费,没有给处理异常消息的时间

java 复制代码
package com.yj.lottery_system.service.mq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

import static com.yj.lottery_system.common.config.DirectRabbitConfig.*;
@Slf4j
@RabbitListener(queues = DLX_QUEUE_NAME)
@Component
public class DlxReceiver {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @RabbitHandler
    public void process(Map<String,String> message) {
        log.info("开始处理异常消息");
        //消息重发
        rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,message);

    }
}
  1. 接收到异常消息,可以将异常消息存放到数据库表中
  2. 存放后,当前异常消息消费完成,死信队列消息处理完成,但异常消息被我们持久化存储到表中了
  3. 解决异常
  4. 完成脚本任务,判断异常消息表中是否存在数据,如果存在,表示有消息未完成,此时处理消息
  5. 处理消息:将消息发送给普通队列进行处理
相关推荐
GoogleDocs3 小时前
基于[api-football]数据学习示例
java·学习
卓码软件测评3 小时前
第三方软件验收评测机构【Gatling安装指南:Java环境配置和IDE插件安装】
java·开发语言·ide·测试工具·负载均衡
妮妮分享3 小时前
H5获取定位的方式是什么?
java·前端·javascript
Billow_lamb3 小时前
MyBatis-Plus 的 条件构造器详解(超详细版)
java·mybatis
CoderYanger3 小时前
动态规划算法-两个数组的dp(含字符串数组):48.最长重复子数组
java·算法·leetcode·动态规划·1024程序员节
西召3 小时前
Spring Kafka 动态消费实现案例
java·后端·kafka
镜花水月linyi3 小时前
ThreadLocal 深度解析(上)
java·后端
镜花水月linyi3 小时前
ThreadLocal 深度解析(下)
java·后端
她说..3 小时前
Spring AOP场景2——数据脱敏(附带源码)
java·开发语言·java-ee·springboot·spring aop