Spring项目-抽奖系统(实操项目-用户管理接口)(END)
^__^
(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.73spring.rabbitmq.port=5672spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 消息确认机制,默认 autospring.rabbitmq.listener.simple.acknowledge-mode=auto# 设置失败重试 5 次spring.rabbitmq.listener.simple.retry.enabled=truespring.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*/@Beanpublic Queue directQueue() {// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启
时仍然存在,暂存队列:当前连接有效// exclusive:默认也是false,只能被当前创建的连接使⽤,⽽且当连接关闭后队列即被
删除。此参考优先级⾼于durable// autoDelete:是否⾃动删除,当没有⽣产者或者消费者使⽤此队列,该队列会⾃动删除。// return new Queue("DirectQueue",true,true,false);// ⼀般设置⼀下队列的持久化就好,其余两个就是默认falsereturn new Queue(QUEUE_NAME,true);}/*** Direct交换机 起名:DirectExchange** @return*/@BeanDirectExchange directExchange() {return new DirectExchange(EXCHANGE_NAME,true,false);}/*** 绑定 将队列和交换机绑定, 并设置⽤于匹配键:DirectRouting** @return*/@BeanBinding bindingDirect() {return BindingBuilder.bind(directQueue()).to(directExchange()).with(ROUTING);}@Beanpublic 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:
@Overridepublic 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 发送到交换机DirectExchangerabbitTemplate.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中供以后使用!!
@Overridepublic 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;}//查询redisActivityDetailDTO 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);//奖品表:先要查寻关联奖品idList<Long> prizeIds = activityPrizeDOList.stream().map(ActivityPrizeDO::getPrizeId).toList();List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIds);//将查询结果打包成ActivityDetailactivityDetailDTO = convertToActivityDetilDTO(activityDO,activityPrizeDOList,activityUserDOList,prizeDOList);//放入rediscacheActivity(activityDetailDTO);return activityDetailDTO;}
2.4MQ异步抽奖逻辑:
2.4.1:消费类MqReceiver:
@Component
@Slf4j
@RabbitListener(queues = QUEUE_NAME)
public class MqReceive {@Autowiredprivate SMSUtil smsUtil;@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Autowiredprivate DrawPrizeService drawPrizeService;@Autowiredprivate ActivityStatusManager activityStatusManager;@Autowiredprivate ThreadPoolTaskExecutor asyncServiceExecutor;@Autowiredprivate MailUtil mailUtil;@Autowiredprivate WinningRecordMapper recordMapper;@RabbitHandlerpublic 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:请求验证:
接收到信息之后,需要进行对结果的验证操作!!
@Overridepublic 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注入常被用在策略模式中@Autowiredprivate final Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();@Autowiredprivate 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 {@Autowiredprivate ActivityMapper activityMapper;@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Overridepublic Integer sequence() {return 2;}@Overridepublic 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;}@Overridepublic 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 {@Autowiredprivate ActivityUserMapper activityUserMapper;@Overridepublic Integer sequence() {return 1;}@Overridepublic 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;}@Overridepublic 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 {@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Overridepublic Integer sequence() {return 1;}@Overridepublic 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;}@Overridepublic 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)) {// 不需要:returnreturn;}// 需要回滚: 回滚rollbackStatus(param);// 2、回滚中奖者名单// 是否需要回滚if (!winnerNeedRollback(param)) {// 不需要:returnreturn;}// 需要: 回滚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) {// 涉及状态的恢复,使用 ActivityStatusManagerConvertActivityStatusDTO 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层:
@Overridepublic 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());// 存放记录到redisif (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,最后进行项目的部署工作啦!!
相关文章:
Spring项目-抽奖系统(实操项目-用户管理接口)(END)
^__^ (oo)\______ (__)\ )\/\ ||----w | || || 一:前言: 活动创建及展示博客链接:Spring项目-抽奖系统(实操项目-用户管理接口)(THREE)-CSDN博客 上一次完成了活动的创建和活动的展示,接下来就是重头戏—…...
Kafka面试题及原理
1. 消息可靠性(不丢失) 使用Kafka在消息的收发过程都会出现消息丢失,Kafka分别给出了解决方案 生产者发送消息到Brocker丢失消息在Brocker中存储丢失消费者从Brocker 幂等方案:【分布式锁、数据库锁(悲观锁、乐观锁…...
Jenkinsfile流水线构建教程
前言 Jenkins 是目前使用非常广泛的自动化流程的执行工具, 我们目前的一些自动化编译, 自动化测试都允许在 Jenkins 上面. 在 Jenkins 的术语里面, 一些自动化工作联合起来称之为流水线, 比如拉取代码, 编译, 运行自动化测试等. 本文的主要目的是引导你快速熟悉 Jenkinsfile …...
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设置字符…...
API接口:企业名称、注册号、统一社会信用代码、企业类型、成立日期和法定代表人等数据 API 接口使用指南
API接口:企业名称、注册号、统一社会信用代码、企业类型、成立日期和法定代表人等数据 API 接口使用指南 本文详细介绍一种基于 Web 搜索方式实现的企业信息查询接口,适用于数据补全、企业资质验证、信息查询等场景。文章内容涵盖接口功能、请求参数、返…...
在.net中,async/await的理解
一、什么是同步?什么是异步? 在.net中,async 和 await 是两个关键字,async 关键字用于声明一个方法是异步方法,该方法可以包含一个或多个 await 表达式。await 关键字是用于在异步方法中等待一个任务(Task…...
水果识别系统 | BP神经网络水果识别系统,含GUI界面(Matlab)
使用说明 代码下载:BP神经网络水果识别系统,含GUI界面(Matlab) BP神经网络水果识别系统 一、引言 1.1、研究背景及意义 在当今科技迅速发展的背景下,人工智能技术尤其是在图像识别领域的应用日益广泛。水果识别作为…...
40岁开始学Java:Java中单例模式(Singleton Pattern),适用场景有哪些?
在Java中,单例模式(Singleton Pattern)用于确保一个类只有一个实例,并提供全局访问点。以下是详细的实现方式、适用场景及注意事项: 一、单例模式的实现方式 1. 饿汉式(Eager Initialization) …...
李宏毅机器学习课程学习笔记04 | 浅谈机器学习-宝可梦、数码宝贝分类器
文章目录 案例:宝可梦、数码宝贝分类器第一步:需要定义一个含有未知数的function第二步:loss of a function如何Sample Training Examples > 如何抽样可以得到一个较好的结果如何权衡模型的复杂程度 Tradeoff of Model Complexity todo 这…...
C++11中的右值引用和完美转发
C11中的右值引用和完美转发 右值引用 右值引用是 C11 引入的一种新的引用类型,用 && 表示。它主要用于区分左值和右值,并且可以实现移动语义,避免不必要的深拷贝,提高程序的性能。左值通常是可以取地址的表达式…...
Redis详解(实战 + 面试)
目录 Redis 是单线程的!为什么 Redis-Key(操作redis的key命令) String 扩展字符串操作命令 数字增长命令 字符串范围range命令 设置过期时间命令 批量设置值 string设置对象,但最好使用hash来存储对象 组合命令getset,先get然后在set Hash hash命令: h…...
ISP CIE-XYZ色彩空间
1. 颜色匹配实验 1931年,CIE综合了前人实验数据,统一采用700nm(红)、546.1nm(绿)、435.8nm(蓝)作为标准三原色波长,绘制了色彩匹配函数,如下图。选定这些波…...
【强化学习笔记1】从强化学习的基本概念到近端策略优化(PPO)
好久没有更新了。最近想学习一下强化学习,本系列是李宏毅老师强化学习的课程笔记。 1. Policy-based Model 1.1 Actor 在policy-based model中,主要的目的就是训练一个actor。 对于一个episode(例如,玩一局游戏)&…...
Deepseek对ChatGPT的冲击?
从测试工程师的视角来看,DeepSeek对ChatGPT的冲击主要体现在**测试场景的垂直化需求与通用模型局限性之间的博弈**。以下从技术适配性、效率优化、风险控制及未来趋势四个维度展开分析: --- ### **一、技术适配性:垂直领域能力决定工具选择…...
STM32中的ADC
目录 一:什么是ADC 二:ADC的用途 三:STM32F103ZET6的ADC 3.1ADC对应的引脚 3.2ADC时钟 3.3ADC的工作模式 编辑3.4ADC校准 3.5ADC转换结构和实际电压的换算 四:ADC配置步骤 五:两个重要的函数 一:…...
开启AI短剧新纪元!SkyReels-V1/A1双剑合璧!昆仑万维开源首个面向AI短剧的视频生成模型
论文链接:https://arxiv.org/abs/2502.10841 项目链接:https://skyworkai.github.io/skyreels-a1.github.io/ Demo链接:https://www.skyreels.ai/ 开源地址:https://github.com/SkyworkAI/SkyReels-A1 https://github.com/Skywork…...
【uniapp】在UniApp中实现持久化存储:安卓--生成写入数据为jsontxt
在移动应用开发中,数据存储是一个至关重要的环节。对于使用UniApp开发的Android应用来说,缓存(Cache)是一种常见的数据存储方式,它能够提高应用的性能和用户体验。然而,缓存数据在用户清除缓存或清除应用数…...
大白话React第十一章React 相关的高级特性以及在实际项目中的应用优化
假设我们已经对 React 前端框架的性能和可扩展性评估有了一定了解,接下来的阶段可以深入学习 React 相关的高级特性以及在实际项目中的应用优化,以下是详细介绍及代码示例: 1. React 高级特性的深入学习 1.1 React 并发模式(Con…...
java容器 LIst、set、Map
Java容器中的List、Set、Map是核心数据结构,各自适用于不同的场景 一、List(有序、可重复) List接口代表有序集合,允许元素重复和通过索引访问,主要实现类包括: ArrayList 底层结构:动态数组…...
使用IDEA如何隐藏文件或文件夹
选择file -> settings 选择Editor -> File Types ->Ignored Files and Folders (忽略文件和目录) 点击号就可以指定想要隐藏的文件或文件夹...
DOM HTML:深入理解与高效运用
DOM HTML:深入理解与高效运用 引言 随着互联网的飞速发展,前端技术逐渐成为软件开发中的关键部分。DOM(文档对象模型)和HTML(超文本标记语言)是前端开发中的基石。本文将深入探讨DOM和HTML的概念、特性以及在实际开发中的应用,帮助读者更好地理解和使用这两项技术。 …...
形象生动讲解Linux 虚拟化 I/O
用现实生活的比喻和简单例子来解释 Linux 虚拟化 I/O,就像给朋友讲故事一样。 虚拟化 I/O 要解决什么问题? 想象你有一栋大房子(物理服务器),想把它分割成多个小公寓(虚拟机)出租。每个租客&…...
git从零学起
从事了多年java开发,一直在用svn进行版本控制,如今更换了公司,使用的是git进行版本控制,所以打算记录一下git学习的点滴,和大家一起分享。 百度百科: Git(读音为/gɪt/)是一个开源…...
汽车低频发射天线介绍
汽车低频PKE天线是基于RFID技术的深度研究及产品开发应用的一种天线,在汽车的智能系统中发挥着重要作用,以下是关于它的详细介绍: 移动管家PKE低频天线结构与原理 结构:产品一般由一个高Q值磁棒天线和一个高压电容组成ÿ…...
【Java分布式】Nacos注册中心
Nacos注册中心 SpringCloudAlibaba 也推出了一个名为 Nacos 的注册中心,相比 Eureka 功能更加丰富,在国内受欢迎程度较高。 官网:https://nacos.io/zh-cn/ 集群 Nacos就将同一机房内的实例划分为一个集群,一个服务可以包含多个集…...
【C++】ImGui:极简化的立即模式GUI开发
如果你是GUI开发的新手,或想试试轻量级、易集成的GUI库,ImGui(即时模式图形用户界面)是个不错的选择。它以简洁的API、跨平台的兼容性和卓越的性能,受到许多开发者的喜爱。无论是为C项目添加调试界面,还是构…...
5G学习笔记之BWP
我们只会经历一种人生,我们选择的人生。 参考:《5G NR标准》、《5G无线系统指南:如微见著,赋能数字化时代》 目录 1. 概述2. BWP频域位置3. 初始与专用BWP4. 默认BWP5. 切换BWP 1. 概述 在LTE的设计中,默认所有终端均能处理最大2…...
1. 搭建前端+后端开发框架
1. 说明 本篇博客主要介绍网页开发中,搭建前端和后端开发框架的具体步骤,框架中所使用的技术栈如下: 前端:VUE Javascript 后端:Python Flask Mysql 其中MySQL主要用来存储需要的数据,在本文中搭建基本…...
深入浅出:插入排序算法完全解析
1. 什么是插入排序? 插入排序(Insertion Sort)是一种简单的排序算法,其基本思想与我们整理扑克牌的方式非常相似。我们将扑克牌从第二张开始依次与前面已排序的牌进行比较,将其插入到合适的位置,直到所有牌…...
(十一)基于vue3+mapbox-GL实现模拟高德实时导航轨迹播放
要在 Vue 3 项目中结合 Mapbox GL 实现类似高德地图的实时导航轨迹功能,您可以按照以下步骤进行: 安装依赖: 首先,安装 mapbox-gl 和 @turf/turf 这两个必要的库: npm install mapbox-gl @turf/turf引入 Mapbox GL: 在组件中引入 mapbox-gl 并初始化地图实例: <templ…...

