Spring项目-抽奖系统(实操项目-用户管理接口)(END)

news2025/4/26 9:11:22

  ^__^
 (oo)\______

 (__)\      )\/\
     ||----w |
     ||     ||   

一:前言:

      活动创建及展示博客链接:Spring项目-抽奖系统(实操项目-用户管理接口)(THREE)-CSDN博客

        上一次完成了活动的创建和活动的展示,接下来就是重头戏——抽奖及结果展示。

二:抽奖设计:

        首先我们要搞清楚整个业务的流程才能开始:

        

这张图可以反应整个抽奖流程!

当然一些细节部分还是会有些模糊:

        例如究竟是一次性抽出中奖者,还是按等级去抽?还是按奖品去抽?

        那么就需要结合UI图和前后端的约定去理解和确定了:

        

 抽奖时序图:

[ 请求 ] /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": ""
}

从需求上,我们可以直到每次抽奖都是从等级最高的奖品开始抽,也就是每次前端向后端发送一个表单,包括该奖品中奖名单、活动id、奖品id、奖品等级、中奖时间

        画图表示如下:

        

总结:

        前端拿到活动详情,之后进行抽奖行为,每抽一个类型的奖品后将数据传回后端进行处理,后端存储详细信息,返回给前端,前端进行展示!!

2.1:RabbitMq消息队列中间件:

        其中我们为了让用户体验更好,每次将抽奖后的处理流程交给RabbitMq消息队列进行进一步的处理!!

        RabbitMq起到了异步解耦、流量削峰、消息分发等作用。

        对于流量比较大的业务来说,起到了非常大的作用!!

pom.xml文件坐标:

1 <!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency

properties配置:

## mq ##
spring.rabbitmq.host=124.71.229.73
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
# 消息确认机制,默认 auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
# 设置失败重试 5
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5

RabbitMq工具类:

@Configuration
public class DirectRabbitConfig {
 public static final String QUEUE_NAME = "DirectQueue";
 public static final String EXCHANGE_NAME = "DirectExchange";
 public static final String ROUTING = "DirectRouting";
 /**
 * 队列 起名: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);
 }
 /**
 * 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);
 }
 @Bean
 public MessageConverter jsonMessageConverter(){
 return new Jackson2JsonMessageConverter();
 }
}

2.2:抽奖请求处理:

2.2.1:controller层:

        注意,这里接收到参数以后,不进行任何处理,然后直接抛给RabbitMq去处理!!

@RequestMapping("/draw-prize")
    public CommonResult<Boolean> drawPrize(@RequestBody @Valid DrawPrizeParam param) {
        log.info("drawPrize DrawPrizeParam:{}", JacksonUtil.writeValueAsString(param));
        drawPrizeService.drawPrize(param);
        return CommonResult.succcess(true);
    }

2.2.2:Service层:

@Service
public interface DrawPrizeService {
    void drawPrize(DrawPrizeParam param);
}

serviceImpl:

    @Override
    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:dd:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId",messageId);
        // 可以加type区分消息类型
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        //将消息携带绑定键值:DirectRouting 发送到交换机DirectExchange
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING, map);
        log.info("发送mq完成!");
    }

2.2.3:dao层:

        这里只是给出一部分代码,重点是逻辑,完整代码,可以参考码云中的!!

@Mapper
public interface WinningRecordMapper {

    Integer batchinsert(@Param("items") List<WinningRecordDO> winningRecordDO);

    @Select("select * from winning_record where activity_id = #{activityId}")
    List<WinningRecordDO> selectByActivityId(Long activityId);
    @Select("select count(1) from winning_record where activity_id = #{activityId} and prize_id = #{prizeId}")
    int countByAPId(Long activityId, Long prizeId);
}

2.3:获取活动完整信息:

        该接口是在抽奖请求之前需要进行调用,分别获取该活动的人员详情信息,奖品详情信息,

活动详情信息!!

2.3.1::controller层:

@RequestMapping("/activity-detail/find")
     public CommonResult<FindActivityDetailListResult> activityDetailFind(Long activityId) {
        log.info("activityDetailFind activityId:{}",activityId);
         ActivityDetailDTO activityDetailDTO = createActivityService.getActivityDetail(activityId);
         return CommonResult.succcess(convertToGetActivityResult(activityDetailDTO));
     }

    private FindActivityDetailListResult convertToGetActivityResult(ActivityDetailDTO activityDetailDTO) {
        if(activityDetailDTO == null) {
            throw new ControllerException(ControllerErrorCodeConstants.FIND_ACITVITY_LIST_ERROR);
        }
        FindActivityDetailListResult findActivityDetailListResult = new FindActivityDetailListResult();
        findActivityDetailListResult.setActivityId(activityDetailDTO.getActivityId());
        findActivityDetailListResult.setActivityName(activityDetailDTO.getActivityName());
        findActivityDetailListResult.setDescription(activityDetailDTO.getDescription());
        findActivityDetailListResult.setValid(activityDetailDTO.valid());
        findActivityDetailListResult.setPrizes(activityDetailDTO.getActivityPrizeList().stream()
                .map(detailDTO->{
                    FindActivityDetailListResult.Prize prize = new FindActivityDetailListResult.Prize();
                    prize.setPrizeId(detailDTO.getPrizeId());
                    prize.setName(detailDTO.getPrizeName());
                    prize.setImageUrl(detailDTO.getImageUrl());
                    prize.setPrice(detailDTO.getPrice());
                    prize.setPrizeAmount(detailDTO.getPrizeAmount());
                    prize.setDescription(detailDTO.getDescription());
                    prize.setPrizeTierName(detailDTO.getPrizeTiers().getMessage());
                    prize.setValid(detailDTO.valid());
                    return prize;
                }).collect(Collectors.toList()));
        findActivityDetailListResult.setUsers(activityDetailDTO.getActivityUserList().stream()
                .map(detailDTO->{
                    FindActivityDetailListResult.User user = new FindActivityDetailListResult.User();
                    user.setUserId(detailDTO.getUserId());
                    user.setUserName(detailDTO.getUserName());
                    user.setValid(detailDTO.valid());
                    return user;}
                ).collect(Collectors.toList()));
        return findActivityDetailListResult;
    }

2.3.2:service层:

    ActivityDetailDTO getActivityDetail(Long activityId);

serviceimpl:
        需要注意的是,之前createActivity时已经将详情信息存入redis缓存,当我们需要时首先从redis中查询相关信息;

        如果redis中不存在时,需要从数据库中再次查找,查找出的结果再次存入redis中供以后使用!!

 @Override
    public ActivityDetailDTO getActivityDetail(Long activityId) {
        if(null == activityId) {
            throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_ID_IS_EMPTY);
        }
        // 从redis缓存中获取
        ActivityDetailDTO activityDetailDTO = getActivityFromCache(activityId);
        if (null != activityDetailDTO) {
            logger.info("从redis缓存中获取活动信息成功:{}",
                    JacksonUtil.writeValueAsString(activityDetailDTO));
            return activityDetailDTO;
        }
        // 从数据库获取,并缓存活动数据
        activityDetailDTO = getActivityDetailDTO(activityId);
        cacheActivity(activityDetailDTO);
        logger.info("从数据库中获取活动信息成功:{}",
                JacksonUtil.writeValueAsString(activityDetailDTO));
        return activityDetailDTO;
    }
 /**
     * 从数据库中获取详细活动信息
     * @param activityId
     * @return
     */
    private ActivityDetailDTO getActivityDetailDTO(Long activityId) {
        if(activityId == null) {
            log.error("查询活动失败!,activityId为空!");
            return null;
        }
        //查询redis
        ActivityDetailDTO activityDetailDTO = getActivityFromCache(activityId);
        if(activityDetailDTO != null) {
            log.info("查询活动信息成功!:{}",JacksonUtil.writeValueAsString(activityDetailDTO));
            return activityDetailDTO;
        }

        //如果redis中不存在,就在数据库中查
        //查询活动信息
        ActivityDO activityDO = activityMapper.selectByActivityId(activityId);
        if(activityDO == null) {
            log.info("getActivityDetailDTO ActivityDO:{}",activityDO);
            return null;
        }
        //查询活动奖品信息
        List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.batchByActivityId(activityId);

        //查询活动人员信息
        List<ActivityUserDO> activityUserDOList = activityUserMapper.batchByActivityId(activityId);

        //奖品表:先要查寻关联奖品id
        List<Long> prizeIds = activityPrizeDOList.stream()
                .map(ActivityPrizeDO::getPrizeId).toList();
        List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIds);

        //将查询结果打包成ActivityDetail
        activityDetailDTO = convertToActivityDetilDTO(activityDO,activityPrizeDOList,activityUserDOList,prizeDOList);
        //放入redis
        cacheActivity(activityDetailDTO);
        return activityDetailDTO;
    }

2.4MQ异步抽奖逻辑:

        

2.4.1:消费类MqReceiver:
        

@Component
@Slf4j
@RabbitListener(queues = QUEUE_NAME)
public class MqReceive {
    @Autowired
    private SMSUtil smsUtil;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private DrawPrizeService drawPrizeService;
    @Autowired
    private ActivityStatusManager activityStatusManager;
    @Autowired
    private ThreadPoolTaskExecutor asyncServiceExecutor;
    @Autowired
    private MailUtil mailUtil;

    @Autowired
    private WinningRecordMapper recordMapper;

    @RabbitHandler
    public void process(Map<String, String> message) {
        log.info("DirectReceiver消费者收到消息 : " + message.toString());
        String msgData = message.get("messageData");
        DrawPrizeParam param = JacksonUtil.readValue(msgData,
                DrawPrizeParam.class);
        try {
            // 1、核对抽奖信息有效性
            drawPrizeService.checkDrawPrizeValid(param);
            // 2、扭转活动状态
            convertStatus(param);
            // 3、保存中奖结果
            List<WinningRecordDO> recordDOList =
                    drawPrizeService.saveWinningRecords(param);
            // 4、并发处理后续流程
            // 通知中奖者(邮箱、短信)
            // 抽奖之后的后续流程,异步(并发)处理
            syncExecute(recordDOList);
        } catch (ServiceException e) {
            log.error("mq消息处理异常:{}", e.getCode(), e);
            // 异常回滚中奖结果+活动/奖品状态,保证事务⼀致性
            //此消息自动东进入死信队列
            rollback(param);
        } catch (Exception e) {
            log.error("处理 MQ 消息异常!", e);
            // 需要保证事务一致性(回滚)
            //此消息自动东进入死信队列
            rollback(param);
            // 抛出异常
            throw e;
        }
    }

2.4.2:请求验证:

        接收到信息之后,需要进行对结果的验证操作!!

  @Override
    public void checkDrawPrizeValid(DrawPrizeParam param) {
        //奖品id和活动id对应的奖品活动必须存在
        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityIdAndPrizeId(param.getActivityId(),
                param.getPrizeId());
        //活动id对应的活动必须存在
        ActivityDO activityDO = activityMapper.selectByActivityId(param.getActivityId());
        if(activityPrizeDO == null || activityDO == null) {
            throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_OR_PRIZE_NOT_EXIST);
        }

        //验证活动是否有效
        if(activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())) {
            throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_IS_FAILURE);
        }

        //验证奖品是否有效
        if(activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())) {
            throw new ServiceException(ServiceErrorCodeConstatns.PRIZE_IS_FAILURE);
        }

        //验证中奖人数和奖品数量是否一致
        if(!(param.getWinnerList().size() == activityPrizeDO.getPrizeAmount())) {
            throw new ServiceException(ServiceErrorCodeConstatns.WIINER_PRIZE_AMOUNT_ERROR);
        }
    }

2.5:状态转换:

        验证消息结束之后,就需要对之前的所有状态进行转换!

注意事项:

        1.状态转化时应该最后转换的时活动状态!!

        2.如果人员\奖品信息全部转换完成以后,才能对活动状态完成转换!

        3.如果人员\奖品信息转换失败时需要进行事务的回滚操作!

        4.如果日后添加新的模块,也需要等待其他模块状态转换完毕之后,活动状态才可以转换!

    综上,面对以上的问题,这里采用两种设计模式合理解决!

   责任链模式+策略模式  
代码如下:

//map注入常被用在策略模式中
    @Autowired
    private final Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();
    @Autowired
    private ActivityService activityService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
        // 1、活动状态扭转有依赖性,导致代码维护性差
        // 2、状态扭转条件可能会扩展,当前写法,扩展性差,维护性差
        if(CollectionUtils.isEmpty(operatorMap)) {
            log.warn("operatorMap 为空! 无法处理活动扭转");
            return ;
        }
        Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
        Boolean update;
        //先处理人员和奖品
        update = processConvertStatus(convertActivityStatusDTO,currMap,1);
        //最后处理活动
        update = (processConvertStatus(convertActivityStatusDTO,currMap,2) || update);

        //更新缓存
        if(update) {
            log.info("更新缓存成功!");
            activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
        }

    }

注:

        1.这里 resquence的设计,就是责任链模式,也就是如果其他的方式没有执行结束,该方法就不能被执行!

        2.这里Map的设计就是策略模式,每个模块有自己的处理扭转状态的方式!!

2.5.1:ActivityOperator:

 关于活动相关的处理方法:

@Component
@Slf4j
public class ActivityOperator extends AbstractActivityOperator {
    @Autowired
    private ActivityMapper activityMapper;

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

    @Override
    public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        ActivityStatusEnum tagertEnum = convertActivityStatusDTO.getTargetActivityStatus();
        if(null == activityId || tagertEnum == null) {
            log.error("ActivityOperator  needCovert 活动id:{}错误",activityId);
            return false;
        }

        ActivityDO activityDO = activityMapper.selectByActivityId(activityId);
        if(activityDO == null) {
            log.error("ActivityOperator needCovert 活动信息错误:{}",activityDO);
            return false;
        }

        //判断当前活动状态是否一致
        //如果一致就不需要更新
        if(activityDO.getStatus().equals(tagertEnum.name())) {
            log.error("ActivityOperator  needCovert 活动状态错误:{}",activityDO.getStatus());
            return false;
        }

        //需要判断奖品是否全部抽完
        //查询INIT状态下奖品的数量
        int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
        if(count>0) {
            log.info("ActivityOperator  needCovert 奖品还剩:{}",count);
            return false;
        }
        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //更新数据库状态
        try{
            activityMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
                    convertActivityStatusDTO.getTargetActivityStatus().name());
            log.info("activityMapper 更新成功!");
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}
2.5.2:UserOperator:

与人员有关的处理方法:

        

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

    @Override
    public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        List<Long> userIds = convertActivityStatusDTO.getUserIds();
        ActivityUserStatusEnum activityUserStatusEnum = convertActivityStatusDTO.getTargetUserStatus();
        if(userIds == null || activityUserStatusEnum == null ||
                activityId == null) {
            log.info("所传参数为空 不更新!");
            return false;
        }
        //通过id查询活动人员表
        List<ActivityUserDO> activityUserDOList = activityUserMapper.batchSelectByAUIds(activityId,userIds);
        if(activityUserDOList == null) {
            log.info("人员表为空 不更新!");
            return false;
        }
        //判断当前人员状态是否与转换状态一致
        for(ActivityUserDO activityUserDO:activityUserDOList) {
            if(activityUserDO.getStatus().equalsIgnoreCase(activityUserStatusEnum.name())) {
                log.info("状态一致 不更新!");
                return false;
            }
        }
        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        try {
            activityUserMapper.batchUpdateStatus(convertActivityStatusDTO
                    .getTargetUserStatus().name(),convertActivityStatusDTO.getUserIds(),
                    convertActivityStatusDTO.getActivityId());
            log.info("activityUserMapper 更新成功!");
            return true;
        }catch (Exception e){
            return false;
        }
    }
}
2.5.3:PrizeOperator:

        与奖品状态有关的处理方式:

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

    @Override
    public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        Long prizeId = convertActivityStatusDTO.getPrizeId();
        ActivityPrizeStatusEnum activityPrizeStatusEnum = convertActivityStatusDTO.getTargetPrizeStatus();
        if(prizeId == null || activityPrizeStatusEnum == null ||
        activityId == null) {
            log.info("所传参数为空 不更新!");
            return false;
        }
        //通过id查询活动奖品表
        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityIdAndPrizeId(activityId,prizeId);
        if(activityPrizeDO == null) {
            log.info("活动奖品表为哦空 不更新!");
            return false;
        }
        //判断当前奖品状态是否与转换状态一致
        if(activityPrizeStatusEnum.name().equals(activityPrizeDO.getStatus())) {
            log.info("奖品状态与期望状态一致 不更新!");
            return false;
        }

        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        //更新数据库状态
        try{
            activityPrizeMapper.updateStatus(convertActivityStatusDTO.getPrizeId(),
                    convertActivityStatusDTO.getActivityId(),
                    convertActivityStatusDTO.getTargetPrizeStatus().name());
            log.info("activityPrizeMapper 更新成功!");
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}

2.5:状态回滚:

        如果此时我们正在抽奖,发生了以外,例如网络突然断开,或者页面突然关闭等不可预知的操作时!

        此时除了我们保存到的数据之外,发生意外后上传的数据应该进行数据回滚操作!!保证事务的统一性,也避免出现不可预知的bug!!

       

    private void rollback(DrawPrizeParam param) {
        // 1、回滚状态:活动、奖品、人员
        // 状态是否需要回滚
        if (!statusNeedRollback(param)) {
            // 不需要:return
            return;
        }
        // 需要回滚: 回滚
        rollbackStatus(param);

        // 2、回滚中奖者名单
        // 是否需要回滚
        if (!winnerNeedRollback(param)) {
            // 不需要:return
            return;
        }
        // 需要: 回滚
        rollbackWinner(param);
    }
    private boolean statusNeedRollback(DrawPrizeParam param) {
        // 判断活动+奖品+人员表相关状态是否已经扭转(正常思路)
        // 扭转状态时,保证了事务一致性,要么都扭转了,要么都没扭转(不包含活动):
        // 因此,只用判断人员/奖品是否扭转过,就能判断出状态是否全部扭转
        // 不能判断活动是否已经扭转
        // 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转
        ActivityPrizeDO activityPrizeDO =
                activityPrizeMapper.selectByActivityIdAndPrizeId(param.getActivityId(), param.getPrizeId());
        // 已经扭转了,需要回滚
        return activityPrizeDO.getStatus()
                .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
    }

   private void rollbackWinner(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }

    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);
    }

步骤:

        1.先判断是否需要回滚!

        2.如果此时发生意外,抛出异常,此时需要判断一下人员\奖品是否扭转,如果其中之一已经扭转了,那么剩下的奖品\人员与活动状态均需要进行状态扭转

        3.接下来判断中奖者名单需不需要扭转(删除)。

2.6:线程池配置:

        当活动完成后,如果没有什么异常出现,此时就需要将获奖信息发送给获奖者!!

        发送的形式分为两种:

        1.短信发送

        2.邮箱发送

        这里可能获奖人数非常多,也可能使用该产品的用户非常多!

        因此为了避免出现卡顿等延迟现象,这里采用多线程的方式进行短信和邮件的发送!!

properties.xml相关配置:

## 线程池 ##
##核心线程数
async.executor.thread.core_pool_size=10 
##最大线程数
async.executor.thread.max_pool_size=20
##队列容量
async.executor.thread.queue_capacity=20
##线程前缀
async.executor.thread.name.prefix=async-service-

相关配置说明如下:

  • 核心线程数:线程池创建时候初始化的线程数。当线程数超过核心线程数,则超过的线程则进入任务队列。
  • 最大线程数:只有在任务队列满了之后才会申请超过核心线程数的线程。不能小于核心线程数。
  • 任务队列:线程数大于核心线程数的部分进入任务队列。如果任务队列足够大,超出核心线程数的线程不会被创建,它会等待核心线程执行完它们自己的任务后再执行任务队列的任务,而不会再额外地创建线程。举例:如果有20个任务要执行,核心线程数:10,最大线程数:20,任务队列大小:2。则系统会创建18个线程。这18个线程有执行完任务的,再执行任务队列中的任务。
  • 线程的空闲时间:当 线程池中的线程数量 大于 核心线程数 时,如果某线程空闲时间超过 keepAliveTime ,线程将被终止。这样,线程池可以动态的调整池中的线程数。
  • 拒绝策略:如果(总任务数 - 核心线程数 - 任务队列数)-(最大线程数 - 核心线程数)> 0 的话,则会出现线程拒绝。举例:( 12 - 5 - 2 ) - ( 8 - 5 ) > 0,会出现线程拒绝。线程拒绝又分为 4 种策略,分别为:

    • CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
    • AbortPolicy():直接抛出异常。
    • DiscardPolicy():直接丢弃。
    • DiscardOldestPolicy():丢弃队列中最老的任务。
2.6.1:异步处理方法:
    private void syncExecute(List<WinningRecordDO> recordDOList) {
        // 通过线程池 threadPoolTaskExecutor
        // 扩展:加入策略模式或者其他设计模式来完成后续的异步操作
        // 短信通知
        asyncServiceExecutor.execute(()->sendMessage(recordDOList));
        //邮箱通知
        asyncServiceExecutor.execute(()->sendMail(recordDOList));
    }

         发送的短信和邮件的内容可以自己确定,当然在使用这两者的同时,还需要引入对应的依赖包和配置相关的配置项!!

2.7:展示每次抽奖中奖信息:

        该过程是在每次抽完一种奖品之后需要展示中奖信息:

2.7.1:controller层:

    @RequestMapping("/winning-records/show")
    public CommonResult<List<WinningRecordResult>> showWinningRecord(
            @RequestBody @Validated ShowWinningRecordParam param) {
        log.info("showWinningRecord winningRecordDTO:{}",JacksonUtil.writeValueAsString(param));
        List<WinningRecordDTO> winningRecordDTOList = drawPrizeService.showWinningRecord(param);
        return CommonResult.succcess(convrtToWinningRecordResult(winningRecordDTOList));
    }

2.7.2:service层:

    List<WinningRecordDTO> showWinningRecord(ShowWinningRecordParam param);

serviceimpl层:

        

    @Override
    public List<WinningRecordDTO> showWinningRecord(ShowWinningRecordParam param) {
        // 查询redis: 奖品、活动
        //可以从奖品维度也可以从活动维度
        String key = null == param.getPrizeId()
                ? String.valueOf(param.getActivityId())
                : param.getActivityId() + "_" + param.getPrizeId();
        List<WinningRecordDO> winningRecordDOList = getWinningRecords(key);
        if (!CollectionUtils.isEmpty(winningRecordDOList)) {
            return convertToWinningRecordDTOList(winningRecordDOList);
        }
        //Redis中数据可能过期
        //如果redis不存在,查库
        winningRecordDOList = recordMapper.selectByActivityIdOrPrizeId(
                param.getActivityId(), param.getPrizeId());
        // 存放记录到redis
        if (CollectionUtils.isEmpty(winningRecordDOList)) {
            log.info("查询的中奖记录为空!param:{}",
                    JacksonUtil.writeValueAsString(param));
            return Arrays.asList();
        }
        cacheWinningRecords(key, winningRecordDOList);
        return convertToWinningRecordDTOList(winningRecordDOList);
    }

2.7.3:dao层:

  @Select("<script>" +
            " select * from winning_record" +
            " where activity_id = #{activityId}" +
            " <if test=\"prizeId != null\">" +
            "   and prize_id = #{prizeId}" +
            " </if>" +
            " </script>")
    List<WinningRecordDO> selectByActivityIdOrPrizeId(@Param("activityId") Long activityId,
                                                      @Param("prizeId") Long prizeId);

接下来就可以完善diamagnetic,最后进行项目的部署工作啦!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2309103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka面试题及原理

1. 消息可靠性&#xff08;不丢失&#xff09; 使用Kafka在消息的收发过程都会出现消息丢失&#xff0c;Kafka分别给出了解决方案 生产者发送消息到Brocker丢失消息在Brocker中存储丢失消费者从Brocker 幂等方案&#xff1a;【分布式锁、数据库锁&#xff08;悲观锁、乐观锁…

CSS—text文本、font字体、列表list、表格table、表单input、下拉菜单select

目录 1.文本 2.字体 3.列表list a.无序列表 b.有序列表 c.定义列表 4.表格table a.内容 b.合并单元格 3.表单input a.input标签 b.单选框 c.上传文件 4.下拉菜单 1.文本 属性描述color设置文本颜色。direction指定文本的方向 / 书写方向。letter-spacing设置字符…

水果识别系统 | BP神经网络水果识别系统,含GUI界面(Matlab)

使用说明 代码下载&#xff1a;BP神经网络水果识别系统&#xff0c;含GUI界面&#xff08;Matlab&#xff09; BP神经网络水果识别系统 一、引言 1.1、研究背景及意义 在当今科技迅速发展的背景下&#xff0c;人工智能技术尤其是在图像识别领域的应用日益广泛。水果识别作为…

40岁开始学Java:Java中单例模式(Singleton Pattern),适用场景有哪些?

在Java中&#xff0c;单例模式&#xff08;Singleton Pattern&#xff09;用于确保一个类只有一个实例&#xff0c;并提供全局访问点。以下是详细的实现方式、适用场景及注意事项&#xff1a; 一、单例模式的实现方式 1. 饿汉式&#xff08;Eager Initialization&#xff09; …

李宏毅机器学习课程学习笔记04 | 浅谈机器学习-宝可梦、数码宝贝分类器

文章目录 案例&#xff1a;宝可梦、数码宝贝分类器第一步&#xff1a;需要定义一个含有未知数的function第二步&#xff1a;loss of a function如何Sample Training Examples > 如何抽样可以得到一个较好的结果如何权衡模型的复杂程度 Tradeoff of Model Complexity todo 这…

Redis详解(实战 + 面试)

目录 Redis 是单线程的&#xff01;为什么 Redis-Key(操作redis的key命令) String 扩展字符串操作命令 数字增长命令 字符串范围range命令 设置过期时间命令 批量设置值 string设置对象,但最好使用hash来存储对象 组合命令getset,先get然后在set Hash hash命令: h…

ISP CIE-XYZ色彩空间

1. 颜色匹配实验 1931年&#xff0c;CIE综合了前人实验数据&#xff0c;统一采用700nm&#xff08;红&#xff09;、546.1nm&#xff08;绿&#xff09;、435.8nm&#xff08;蓝&#xff09;​作为标准三原色波长&#xff0c;绘制了色彩匹配函数&#xff0c;如下图。选定这些波…

【强化学习笔记1】从强化学习的基本概念到近端策略优化(PPO)

好久没有更新了。最近想学习一下强化学习&#xff0c;本系列是李宏毅老师强化学习的课程笔记。 1. Policy-based Model 1.1 Actor 在policy-based model中&#xff0c;主要的目的就是训练一个actor。 对于一个episode&#xff08;例如&#xff0c;玩一局游戏&#xff09;&…

STM32中的ADC

目录 一&#xff1a;什么是ADC 二&#xff1a;ADC的用途 三&#xff1a;STM32F103ZET6的ADC 3.1ADC对应的引脚 3.2ADC时钟 3.3ADC的工作模式 ​编辑3.4ADC校准 3.5ADC转换结构和实际电压的换算 四&#xff1a;ADC配置步骤 五&#xff1a;两个重要的函数 一&#xff1a…

开启AI短剧新纪元!SkyReels-V1/A1双剑合璧!昆仑万维开源首个面向AI短剧的视频生成模型

论文链接&#xff1a;https://arxiv.org/abs/2502.10841 项目链接&#xff1a;https://skyworkai.github.io/skyreels-a1.github.io/ Demo链接&#xff1a;https://www.skyreels.ai/ 开源地址&#xff1a;https://github.com/SkyworkAI/SkyReels-A1 https://github.com/Skywork…

【uniapp】在UniApp中实现持久化存储:安卓--生成写入数据为jsontxt

在移动应用开发中&#xff0c;数据存储是一个至关重要的环节。对于使用UniApp开发的Android应用来说&#xff0c;缓存&#xff08;Cache&#xff09;是一种常见的数据存储方式&#xff0c;它能够提高应用的性能和用户体验。然而&#xff0c;缓存数据在用户清除缓存或清除应用数…

使用IDEA如何隐藏文件或文件夹

选择file -> settings 选择Editor -> File Types ->Ignored Files and Folders (忽略文件和目录) 点击号就可以指定想要隐藏的文件或文件夹

git从零学起

从事了多年java开发&#xff0c;一直在用svn进行版本控制&#xff0c;如今更换了公司&#xff0c;使用的是git进行版本控制&#xff0c;所以打算记录一下git学习的点滴&#xff0c;和大家一起分享。 百度百科&#xff1a; Git&#xff08;读音为/gɪt/&#xff09;是一个开源…

汽车低频发射天线介绍

汽车低频PKE天线是基于RFID技术的深度研究及产品开发应用的一种天线&#xff0c;在汽车的智能系统中发挥着重要作用&#xff0c;以下是关于它的详细介绍&#xff1a; 移动管家PKE低频天线结构与原理 结构&#xff1a;产品一般由一个高Q值磁棒天线和一个高压电容组成&#xff…

【Java分布式】Nacos注册中心

Nacos注册中心 SpringCloudAlibaba 也推出了一个名为 Nacos 的注册中心&#xff0c;相比 Eureka 功能更加丰富&#xff0c;在国内受欢迎程度较高。 官网&#xff1a;https://nacos.io/zh-cn/ 集群 Nacos就将同一机房内的实例划分为一个集群&#xff0c;一个服务可以包含多个集…

5G学习笔记之BWP

我们只会经历一种人生&#xff0c;我们选择的人生。 参考&#xff1a;《5G NR标准》、《5G无线系统指南:如微见著&#xff0c;赋能数字化时代》 目录 1. 概述2. BWP频域位置3. 初始与专用BWP4. 默认BWP5. 切换BWP 1. 概述 在LTE的设计中&#xff0c;默认所有终端均能处理最大2…

(十一)基于vue3+mapbox-GL实现模拟高德实时导航轨迹播放

要在 Vue 3 项目中结合 Mapbox GL 实现类似高德地图的实时导航轨迹功能,您可以按照以下步骤进行: 安装依赖: 首先,安装 mapbox-gl 和 @turf/turf 这两个必要的库: npm install mapbox-gl @turf/turf引入 Mapbox GL: 在组件中引入 mapbox-gl 并初始化地图实例: <templ…

【Transformer模型学习】第三篇:位置编码

文章目录 0. 前言1. 为什么需要位置编码&#xff1f;2. 如何进行位置编码&#xff1f;3. 正弦和余弦位置编码4. 举个例子4.1 参数设置4.2 计算分母项4.3 计算位置编码4.4 位置编码矩阵 5. 相对位置信息6. 改进的位置编码方式——RoPE6.1 RoPE的核心思想6.2 RoPE的优势 7. 总结 …

(十 六)趣学设计模式 之 责任链模式!

目录 一、 啥是责任链模式&#xff1f;二、 为什么要用责任链模式&#xff1f;三、 责任链模式的实现方式四、 责任链模式的优缺点五、 责任链模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;…

20250225-代码笔记03-class CVRPModel AND other class

文章目录 前言一、class CVRPModel(nn.Module):__init__(self, **model_params)函数功能函数代码 二、class CVRPModel(nn.Module):pre_forward(self, reset_state)函数功能函数代码 三、class CVRPModel(nn.Module):forward(self, state)函数功能函数代码 四、def _get_encodi…