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博客 上一次完成了活动的创建和活动的展示,接下来就是重头戏—…...
5个GitHub热点开源项目!!
1.自托管 Moonlight 游戏串流服务:Sunshine 主语言:C,Star:14.4k,周增长:500 这是一个自托管的 Moonlight 游戏串流服务器端项目,支持所有 Moonlight 客户端。用户可以在自己电脑上搭建一个游戏…...
数据结构:二叉搜索树(排序树)
1.二叉搜索树的定义 二叉搜索树要么是空树,要么是满足以下特性的树 (1)左子树不为空,那么左子树左右节点的值都小于根节点的值 (2)右子树不为空,那么右子树左右节点的值都大于根节点的值 &#…...
JavaEE--计算机是如何工作的
一、一台计算机的组成部分 1.CPU(中央处理器) 2.主板(一个大插座) 3.内存(存储数据的主要模板) 4.硬盘(存储数据的主要模板) 内存和硬盘对比: 内存硬盘读写速度快慢存…...
Redis 实战篇 ——《黑马点评》(下)
《引言》 (下)篇将记录 Redis 实战篇 最后的一些学习内容,希望大家能够点赞、收藏支持一下 Thanks♪ (・ω・)ノ,谢谢大家。 传送门(上):Redis 实战篇 ——《黑马…...
OpenCV计算摄影学(10)将一组不同曝光的图像合并成一张高动态范围(HDR)图像的实现类cv::MergeDebevec
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 resulting HDR 图像被计算为考虑了曝光值和相机响应的各次曝光的加权平均值。 cv::MergeDebevec 是 OpenCV 中用于将一组不同曝光的图像合并成一…...
Linux驱动开发之串口驱动移植
原理图 从上图可以看到RS232的串口接的是UART3,接下来我们需要使能UART3的收发功能。一般串口的驱动程序在内核中都有包含,我们配置使能适配即可。 设备树 复用功能配置 查看6ull如何进行uart3的串口复用配置: 设备树下添加uart3的串口复用…...
c语言中return 数字代表的含义
return 数字的含义:表示函数返回一个整数值,通常用于向调用者(如操作系统或其他程序)传递程序的执行状态或结果。 核心规则: return 0: 含义:表示程序或函数正常结束。 示例: int m…...
Android 端侧运行 LLM 框架 MNN 及其应用
MNN Chat Android App - 基于 MNN 引擎的智能聊天应用 一、MNN 框架简介与工作原理1.1 什么是 MNN?1.2 MNN 的工作原理 二、MNN Chat Android App2.1 MNN Chat 的功能2.2 MNN Chat 的优势2.3 MNN Chat Android App 的使用 三、总结 随着移动端人工智能需求的日益增长…...
jupyter汉化、修改默认路径详细讲解
1、配置镜像路径 修改第三方库的下载路径,比如:[清华镜像pypi](https://mirrors.tuna.tsinghua.edu.cn/help/pypi/),配置镜像地址。 首先执行 pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple 2、安…...
java面试笔记(二)
1.流程中如何数据回填 (1)方法1: 在工作流中有一个标识,每一次审批的时候去判断是否审批完成,然后调用反射 (2)方法2: 创建一个流程结束的监听器,监听流程是否结束&a…...
【大语言模型笔记进阶一步】提示语设计学习笔记,跳出框架思维,自己构建提示词
一、大语言模型应用场景 1. 文本生成 文本创作: 诗歌故事,剧本,推文帖子 摘要与改写: 长文本摘要与简化,多语言翻译与本地化 结构化生成: 表格,根据需求生成代码片段,API文档生成…...
sql调优:优化响应时间(优化sql) ; 优化吞吐量
Sql性能调优的目的 1.优化响应时间>>优化sql 经过调优后,执行查询、更新等操作的时候,数据库的反应速度更快,花费的时间更少。 2.优化吞吐量 即“并发”, 就是“同时处理请求”的能力。 优化sql 尽量将多条SQL语句压缩到一句>…...
debian/control中的包关系
软件包依赖就是软件包关系的一种,一般用 Depends 表示。 每个软件包都可以和其他软件包有各种不同的关系。除 Depends 外,还有 Recommends、Suggests、Pre-Depends、Breaks、Conflicts、Provides 和 Replaces,软件包管理工具(如 …...
python学习第三天
条件判断 条件判断使用if、elif和else关键字。它们用于根据条件执行不同的代码块。 # 条件判断 age 18 if age < 18:print("你还是个孩子!") elif age 18:print("永远十八岁!") else:print("你还年轻!")…...
k8s架构及服务详解
目录 1.1.容器是什么1.2.Namespace1.3.rootfs5.1.Service介绍5.1.1.Serice简介 5.1.1.1什么是Service5.1.1.2.Service的创建5.1.1.3.检测服务5.1.1.4.在运行的容器中远程执行命令 5.2.连接集群外部的服务 5.2.1.介绍服务endpoint5.2.2.手动配置服务的endpoint5.2.3.为外部服务…...
Unity中动态切换光照贴图LightProbe的方法
关键代码:LightmapSettings.lightmaps lightmapDatas; LightmapData中操作三张图:lightmapColor,lightmapDir,以及一张ShadowMap 这里只操作前两张: using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;public cl…...
基于Matlab的多目标粒子群优化
在复杂系统的设计、决策与优化问题中,常常需要同时兼顾多个相互冲突的目标,多目标粒子群优化(MOPSO)算法应运而生,作为群体智能优化算法家族中的重要成员,它为解决此类棘手难题提供了高效且富有创新性的解决…...
Android Studio 新版本Gradle发布本地Maven仓库示例
发布代码到JitPack示例:https://blog.csdn.net/loutengyuan/article/details/145938967 以下是基于 Android Studio 24.2.2(Gradle 8.10.2 AGP 8.8.0 JDK17) 的本地 Maven 仓库发布示例,包含aar和jar的不同配置: 1.…...
Langchain解锁LLM大语言模型的结构化输出能力(多种实现方案)
在 LangChain解锁LLM大语言模型的结构化输出能力:调用 with_structured_output() 方法 这篇博客中,我们了解了格式化LLM输出内容的必要性以及如何通过调用langchain框架中提供的 with_structured_output() 方法对LLM输出进行格式化(三种可选方…...
深入理解Spring @Async:异步编程的利器与实战指南
一、为什么需要异步编程? 在现代高并发系统中,同步阻塞式编程会带来两大核心问题: // 同步处理示例 public void processOrder(Order order) {// 1. 保存订单(耗时50ms)orderRepository.save(order); // 2. 发送短信…...
让Word插上AI的翅膀:如何把DeepSeek装进Word
在日常办公中,微软的Word无疑是我们最常用的文字处理工具。无论是撰写报告、编辑文档,还是整理笔记,Word都能胜任。然而,随着AI技术的飞速发展,尤其是DeepSeek的出现,我们的文字编辑方式正在发生革命性的变…...
清华DeepSeek深度探索与进阶指南
「清华北大-Deepseek使用手册」 链接:https://pan.quark.cn/s/98782f7d61dc 「清华大学Deepseek整理) 1-6版本链接:https://pan.quark.cn/s/72194e32428a AI学术工具公测链接:https://pan.baidu.com/s/104w_uBB2F42Da0qnk78_ew …...
迁移学习策略全景解析:从理论到产业落地的技术跃迁
(2025年最新技术实践指南) 一、迁移学习的范式革命与核心价值 在人工智能进入"大模型时代"的今天,迁移学习已成为突破数据瓶颈、降低训练成本的关键技术。本文基于2025年最新技术进展,系统梳理六大核心策略及其在产业实…...
WireGuard搭建网络,供整个公司使用
一、清理现有配置(如已有失败尝试) # 停止并删除现有 WireGuard 接口 sudo wg-quick down wg0 sudo rm -rf /etc/wireguard/wg0.conf# 验证接口已删除 (执行后应该看不到 wg0) ifconfig二、服务器端完整配置流程 1. 安装 WireGuard sudo apt update &…...
MyAgent:用AI开发AI,开启智能编程的产业革命
在人工智能技术爆发的2025年,MyAgent智能体平台凭借其独特的“AI开发AI”模式,正在重构全球软件开发行业的底层逻辑。这一创新范式不仅将自然语言处理、机器学习、RPA(机器人流程自动化)等技术深度融合,更通过“…...
Cherno C++ P60 为什么不用using namespace std
这篇文章我们讲一下之前写代码的时候的一个习惯,也就是不使用using namespace std。如果我们接触过最早的C教程,那么第一节课都会让我们写如下的代码: #include<iostream>using namespace std;int main() {cout << "Hello …...
el-select的下拉选择框插入el-checkbox
el-check注意这里要使用model-value绑定数据 <el-selectv-model"selectDevice"multiplecollapse-tags:multiple-limit"5"style"width: 200px"popper-class"select-popover-class" ><el-optionv-for"item in deviceList…...
M系列芯片 MacOS 在 Conda 环境中安装 TensorFlow 2 和 Keras 3 完整指南
目录 1. 引言2. 环境准备3. 安装 TensorFlow 和必要依赖4. 结语Reference 1. 引言 Keras 是搞深度学习很可爱的工具,其友好的接口让我总是将其作为搭建模型原型的首选。然而,当我希望在 M 系列芯片的MacBook Pro上使用 Keras时,使用Conda和P…...
GitHub教程
目录 1.是什么?2.安装3.创建库3.增删改查4.远程仓库5.分支6.标签7.使用流程8.总结 1.是什么? Git 是一个命令行工具,但也有许多图形用户界面可用。本地仓库,安装包下载到本地。Git 的一个流行 GUI 是 GitHub,它可以方便地管理存储库、推送…...
《JavaScript解题秘籍:力扣队列与栈的高效解题策略》
232.用栈实现队列 力扣题目链接(opens new window) 使用栈实现队列的下列操作: push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。 示例: MyQueue queue new MyQueue(); queue…...
Supra软件更新:AGRV2K CPLD支持无源晶体做时钟输入
Supra软件更新:AGRV2K CPLD支持无源晶体做时钟输入 AGRV2K CPLD支持无源晶体做时钟输入,和AG32一样接入OSC_IN和OSC_OUT管脚。 VE管脚文件设为PIN_HSE,如: clk PIN_HSE ledout[0] PIN_31 ledout[1] PIN_32 ...... 在下载烧录文…...
简易的微信聊天网页版【项目测试报告】
文章目录 一、项目背景二、项目简介登录功能好友列表页面好友会话页面 三、测试工具和环境四、测试计划测试用例部分人工手动测试截图web自动化测试测试用例代码框架配置内容代码文件(Utils.py)登录页面代码文件(WeChatLogin.py)好…...
nio使用
NIO : new Input/Output,,在java1.4中引入的一套新的IO操作API,,,旨在替代传统的IO(即BIO:Blocking IO),,,nio提供了更高效的 文件和网络IO的 操作…...
【蓝桥杯单片机】第十二届省赛
一、真题 二、模块构建 1.编写初始化函数(init.c) void Cls_Peripheral(void); 关闭led led对应的锁存器由Y4C控制关闭蜂鸣器和继电器 由Y5C控制 2.编写LED函数(led.c) void Led_Disp(unsigned char ucLed); 将ucLed取反的值赋给P0 开启锁存器…...
Jenkins与Flutter项目持续集成实战指南
一、环境准备 1. 基础环境要求 Jenkins Server:已安装JDK 11,建议使用Linux服务器(Ubuntu/CentOS)Flutter SDK:全局安装或通过工具动态管理构建代理节点: Android构建:需Android SDK、Gradle、…...
linux常见操作命令
查看目录和文件 ls:列出目录内容。 常用选项: -l:以长格式显示,显示文件的权限、所有者、大小、修改时间等详细信息。-a:显示所有文件和目录,包括隐藏文件(以 . 开头的文件)。-h&…...
6.人工智能与机器学习
一、人工智能基本原理 1. 人工智能(AI)定义与范畴 核心目标:模拟人类智能行为(如推理、学习、决策)分类: 弱人工智能(Narrow AI):专精单一任务(如AlphaGo、…...
GPU架构分类
一、NVIDIA的GPU架构 NVIDIA是全球领先的GPU生产商,其GPU架构在图形渲染、高性能计算和人工智能等领域具有广泛应用。NVIDIA的GPU架构经历了多次迭代,以下是一些重要的架构: 1. Tesla(特斯拉)架构(2006年…...
23种设计模式之单例模式(Singleton Pattern)【设计模式】
文章目录 一、简介二、关键点三、实现单例模式的步骤四、C#示例4.1 简单的单例模式4.2 线程安全的单例模式(双重检查锁定)4.3 静态初始化单例模式 五、单例模式优缺点5.1 优点5.2 缺点 六、适用场景七、示例的现实应用 一、简介 单例模式(Si…...
MAX232数据手册:搭建电平转换桥梁,助力串口稳定通信
在现代电子设备的通信领域,串口通信因其简单可靠而被广泛应用。MAX232 芯片作为串口通信中的关键角色,发挥着不可或缺的作用。下面,我们将依据提供的资料,深入解读 MAX232 芯片的各项特性、参数以及应用要点。 一、引脚说明 MAX2…...
Day 55 卡玛笔记
这是基于代码随想录的每日打卡 所有可达路径 题目描述 给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。 输入描述 第一行包含两个整数…...
python量化交易——金融数据管理最佳实践——使用qteasy管理本地数据源
文章目录 统一定义的金融历史数据表最重要的数据表数据表的定义交易日历表的定义:交易日历表: trade_calendar qteasy是一个功能全面且易用的量化交易策略框架, Github地址在这里。使用它,能轻松地获取历史数据,创建交易策略并完…...
AVM 环视拼接 鱼眼相机
https://zhuanlan.zhihu.com/p/651306620 AVM 环视拼接方法介绍 从内外参推导IPM变换方程及代码实现(生成AVM环视拼接图)_avm拼接-CSDN博客 经典文献阅读之--Extrinsic Self-calibration of the Surround-view System: A Weakly... (环视系统的外参自…...
计算机基础面试(数据库)
1. 事务的ACID特性?如何通过日志保证原子性和持久性? 专业解答: ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Dura…...
Self-Pro: A Self-Prompt and Tuning Framework for Graph Neural Networks
Self-Pro: A Self-Prompt and Tuning Framework for Graph Neural Networks #paper/GFM/GNN-BASED# #paper/⭐⭐⭐# 注意:这篇文章是每个图一个GCN模型,而不是所有图一个GCN 模型 算是最早的涉及异配图的prompt了 贡献和动机: 非对…...
Spring Boot 与 MyBatis 版本兼容性
初接触Spring Boot,本次使用Spring Boot版本为3.4.3,mybatis的起步依赖版本为3.0.0,在启动时报错,报错代码如下 org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name userMapper…...
WPF 如何使文本显示控件支持显示内容滚动显示
WPF中如何使文本显示控件支持显示内容滚动显示 在WPF中,TextBlock 控件本身并不直接支持滚动功能,因为它的设计初衷是用于静态文本展示。但是,你可以通过一些技巧和自定义控件来实现 TextBlock 的滚动效果。以下是几种常见的方法:…...
1208. 尽可能使字符串相等
目录 一、题目二、思路2.1 解题思路2.2 代码尝试2.3 疑难问题 三、解法四、收获4.1 心得4.2 举一反三 一、题目 二、思路 2.1 解题思路 2.2 代码尝试 class Solution { public:int equalSubstring(string s, string t, int maxCost) {int curcost0;//统计当前开销int left0;…...
Linux系统管理操作
一、关闭防火墙 默认端口号是22,其他端口用不了,这时候就引出关闭防火墙 1.1、systemctl 1.1.1、基本语法 systemctl start | stop | restart | status 服务名 //启动、关闭、重启、查看状态 1.1.2、查看服务的方法 查看/usr/lib/systemd/syst…...