当前位置: 首页 > article >正文

[项目总结] 抽奖系统项目技术应用总结

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (93平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(97平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(95平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

目录

  • 1. Redis的使用
  • 2. RabbitMQ的使用
  • 3. 抽奖业务逻辑详细梳理
  • 4. 阿里云短信服务与邮件服务
    • 4.1 中奖通知服务
    • 4.2 短信验证码服务
  • 5. 数据库表设计

1. Redis的使用

  1. 在向用户发送验证码之后,我们需要把验证码存储到Redis中,方便后面校验
//校验手机号
if (!StringUtils.hasText(phoneNumber) || !RegexUtil.checkMobile(phoneNumber)){throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
String captcha = CaptchaUtil.generateCaptchaCode(4);//生成4位验证码
Map<String,String> map = new HashMap<>();
map.put("code",captcha);//把验证码设置进Map中,再转换为json字符串,之后就会替换掉模版中的对应参数.
smsUtil.sendMessage(VERIFICATION_TEMPLATE_CODE,phoneNumber,objectMapper.writeValueAsString(map));
redisUtil.set(VERIFICATION_PREFIX + phoneNumber,captcha,300L);//把验证码放入redis中
  1. 把活动详细信息存储到Redis中.如果ActivityId和原来一致,就会把原来的信息覆盖掉.
//整合活动整体信息,存储在redis中.
//首先获取活动中对应的奖品信息
List<Long> prizeIdList = new ArrayList<>();
for (ActivityPrizeDO activityPrizeDO :activityPrizeDOList) {prizeIdList.add(activityPrizeDO.getPrizeId());
}
List<PrizeDO> prizeDOList = prizeMapper.selectByIdList(prizeIdList);
//把信息整合为存储在Redis中的活动详细信息
ActivityDetailDTO activityDetailDTO = convertToActivityDetailDTO(activityDO,prizeDOList,activityPrizeDOList,activityUserDOList
);
//缓存活动信息
cacheActivity(activityDetailDTO);

之所以要把活动信息缓存到Redis中,有一下的几点:
1. 首先是因为由于活动信息涉及到三张表的相关操作,如果后期再查询活动数据的时候,直接在MySQL中查询,需要查询三张表的数据,那么就会很慢,给用户带来不好的体验,所以要把活动信息提前存储到Redis中.
2. 其次是因为,活动信息是系统的核心数据,访问频率较高,使用Redis可以轻松应对高QPS的场景.

  1. 从Redis中获取活动详细信息,如果Redis中没有查询到,再去MySQL中查询,之后将MySQL中查询到的数据同步到Redis中.
@Overridepublic ActivityDetailDTO getActivityDetail(Long activityId) throws JsonProcessingException {if (activityId == null){log.warn("活动id为空");return null;}//首先从缓存中获取信息ActivityDetailDTO activityDetail = getActivityFromCache(activityId);if (activityDetail != null){return activityDetail;}//缓存中没有查询到的时候,去数据库中查ActivityDO activityDO = activityMapper.selectById(activityId);List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectByActivityId(activityId);List<ActivityUserDO> activityUserDOList = activityUserMapper.selectByActivityId(activityId);List<Long> prizeId = new ArrayList<>();for (ActivityPrizeDO prizeDO : activityPrizeDOList) {prizeId.add(prizeDO.getPrizeId());}List<PrizeDO> prizeDOList = prizeMapper.selectByIdList(prizeId);activityDetail = convertToActivityDetailDTO(activityDO,prizeDOList,activityPrizeDOList,activityUserDOList);cacheActivity(activityDetail);//缓存活动信息return activityDetail;}

其中下面这几行代码就是在查询三张表的数据,这个过程非常慢,所以我们前期就需要把信息缓存到Redis中.

ActivityDO activityDO = activityMapper.selectById(activityId);
List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectByActivityId(activityId);
List<ActivityUserDO> activityUserDOList = activityUserMapper.selectByActivityId(activityId);
  1. 抽奖操作完成之后,需要对之前存储的Redis中的详细信息进行更新,把活动的状态,活动奖品的状态和活动用户的状态进行翻转.
if (update){//扭转之后,更新活动相关的信息到缓存中activityService.cacheActivity(activityStatusConvertDTO.getActivityId());
}
  1. 抽奖过程中发生异常的时候,把Redis中的记录删除掉.
drawPrizeService.deleteRecords(drawPrizeParam.getActivityId(),drawPrizeParam.getPrizeId());

2. RabbitMQ的使用

  • 由于抽奖的业务逻辑比较复杂,所以我们选择使用RabbitMQ把抽奖请求和抽奖的主业务逻辑做了异步处理,当用户发起抽奖请求之后,前端就可以立即返回结果,后台消费者独立处理业务请求,系统响应时间从秒级降到毫秒级,大大提升了用户的体验.
  • 其次由于抽奖逻辑是系统的主逻辑,有可能有很大的QPS,所以我们可以使用RabbitMQ对请求进行削峰处理,以免系统被压垮.

下面是给队列中发送请求的过程.

@Override
public void drawPrize(DrawPrizeParam param) throws JsonProcessingException {String messageId = UUID.randomUUID().toString();String messageData = objectMapper.writeValueAsString(param);String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));//格式化时间Map<String,String> map = new HashMap<>();map.put("messageId",messageId);map.put("createTime",createTime);map.put("messageData",messageData);//添加中奖信息rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,map);log.info("mq发送成功");
}

消费者从消息队列中收到消息

@RabbitHandler
public void process(Map<String,String> message) throws JsonProcessingException {log.info("接收到生产者消息:{}",message.toString());String messageData = message.get("messageData");//从map中获取到中奖信息DrawPrizeParam drawPrizeParam = objectMapper.readValue(messageData, DrawPrizeParam.class);//将指定的字符串转化为指定类的对象try {//这里之所以使用Boolean作为返回值,是为了用户重复发送抽奖信息,如果采用抛出异常的方式的话,// 在第二次发送之后,就会触发回滚操作,这时候又会把奖品和用户状态全部回滚回来,显然不符合逻辑if (!drawPrizeService.checkDrawPrizeValid(drawPrizeParam)){//对参数进行校验,如果校验不成功,直接返回return;}convertStatus(drawPrizeParam);//翻转状态List<WinningRecordDO> winningRecordDOList = drawPrizeService.saveWinningRecords(drawPrizeParam);//存储获奖者信息syncExecute(winningRecordDOList);//拿到中奖记录之后,去发邮件和短信} catch (ServiceException e){log.error("处理mq消息异常:{},{},{}",e.getCode(),e.getCode(),e.getMessage());rollback(drawPrizeParam);//异常之后需要把数据回滚掉throw e;}catch (Exception e){log.error("处理mq消息异常:{}",e.getMessage());rollback(drawPrizeParam);//异常之后需要把数据回滚掉throw e;}
}
  • 注意: 对抽奖的参数进行校验的时候,我们不可以使用抛出异常的方式,我们应该使用直接返回的方式.如果我们直接抛出异常,会使得异常被捕捉,这样会引起活动的奖品状态和活动的用户状态发生回滚,不符合我们预期的业务逻辑.

3. 抽奖业务逻辑详细梳理

  • 首先就是前端发起抽奖请求,之后把请求发送到消息队列,之后消费者接收到消息,就是我们上面RabbitMQ的逻辑,这里不再多余赘述.
  • 从消息队列里接收到消息之后,对消息队列中的JSON字符串格式的消息转化为对应的对象
DrawPrizeParam drawPrizeParam = objectMapper.readValue(messageData, DrawPrizeParam.class);//将指定的字符串转化为指定类的对象
  • 之后对获取到的抽奖参数进行校验,注意这里我们使用直接返回的方式来表示没有通过校验(原因见上),checkDrawPrizeValid方法也是直接使用返回false的方式,没有使用抛出异常的方式.
if (!drawPrizeService.checkDrawPrizeValid(drawPrizeParam)){//对参数进行校验,如果校验不成功,直接返回return;
}
@Override
public Boolean checkDrawPrizeValid(DrawPrizeParam param) {//校验奖品和活动是否存在ActivityDO activityDO = activityMapper.selectById(param.getActivityId());ActivityPrizeDO activityPrizeDO =activityPrizeMapper.selectByActivityAndPrizeId(param.getActivityId(),param.getPrizeId());if (activityDO == null || activityPrizeDO == null){return false;}//判断奖品是否足够if (param.getWinnerList().size() > activityPrizeDO.getPrizeAmount()){return false;}//判断活动或者奖品是否有效if (activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())){return false;}if (activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())){return false;}return true;
}
  • 之后对关联表中的活动奖品状态和活动用户状态进行翻转.
convertStatus(drawPrizeParam);//翻转状态

首先构造状态翻转参数,构造活动奖品的状态和活动用户的状态全部为已完成,之后调用活动状态管理器中的handlerEvent方法,对活动相关的信息进行状态翻转.

/*** 扭转活动相关信息的状态* @param drawPrizeParam 活动相关信息*/
private void convertStatus(DrawPrizeParam drawPrizeParam) throws JsonProcessingException {//构造状态翻转参数,将活动状态和奖品状态都翻转成已完成ActivityStatusConvertDTO statusConvertDTO = new ActivityStatusConvertDTO();statusConvertDTO.setActivityId(drawPrizeParam.getActivityId());statusConvertDTO.setPrizeId(drawPrizeParam.getPrizeId());statusConvertDTO.setActivityTargetStatus(ActivityStatusEnum.COMPLETED);statusConvertDTO.setPrizeTargetStatus(ActivityPrizeStatusEnum.COMPLETED);List<Long> winnerIdList = new ArrayList<>();for (DrawPrizeParam.Winner winner : drawPrizeParam.getWinnerList()) {winnerIdList.add(winner.getUserId());}statusConvertDTO.setUserIds(winnerIdList);statusConvertDTO.setUserTargetStatus(ActivityUserStatusEnum.COMPLETED);//使用活动状态管理器进行状态翻转activityStatusManager.handlerEvent(statusConvertDTO);
}

首先这里我们使用到了@Transactional注解,如果翻转状态的中间发生了异常,也就是调用processStatusConversion方法的时候抛出了ServiceException(ServiceErrorCodeConstants.CONVERT_ACTIVITY_STATUS_ERROR);异常,那么就会进行事务回滚,之前翻转的奖品,用户的状态全部回滚.使用processStatusConversion方法对用户状态和活动状态进行翻转.这里采用了责任链模式和策略模式,我们放到后面细说.扭转完成奖品,活动,用户状态之后,把活动的详细信息更新到Redis中.

/*** 转换状态* @param activityStatusConvertDTO 状态转换参数*/
@Override
@Transactional(rollbackFor = Exception.class)
public void handlerEvent(ActivityStatusConvertDTO activityStatusConvertDTO) throws JsonProcessingException {if (operatorMap == null || operatorMap.isEmpty()){log.warn("AbstractActivityOperatorMap 为空");return;}//扭转活动状态Boolean update = false;//先扭转奖品和用户的状态Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);update = processStatusConversion(activityStatusConvertDTO,currMap,1);//奖品和用户状态都扭转完成之后,后扭转奖品的状态update = processStatusConversion(activityStatusConvertDTO,currMap,2) || update;if (update){//扭转之后,更新活动相关的信息到缓存中activityService.cacheActivity(activityStatusConvertDTO.getActivityId());}
}

在扭转用户和奖品状态的时候,我们使用的是责任链模式和策略模式,我们使用的是Map来管理责任链.通过@Autowired注入Map中的是Bean的名称和抽象类的子类的具体实现.

@Autowired
private Map<String, AbstractActivityOperator> operatorMap;//注入抽象类对应继承的子类

我们首先定义了一个抽象类(抽象操作器),确定针对活动,奖品,用户的状态如何进行翻转,即采用什么样的策略进行翻转.采用的具体策略是:确定转换的次序,用户,活动,奖品的状态,先转换哪一个,后转换那一个,用一个整形表示.之后查看当前的状态是否需要翻转,主要是查询数据库中的状态和传入的状态是否一致,如果一致,就不需要翻转.最后翻转状态.

/*** 转换抽奖活动相关参数的状态* 使用策略模式*/
public abstract class AbstractActivityOperator {public abstract Integer sequence();//转换的次序.使用责任链模式public abstract Boolean needConvert(ActivityStatusConvertDTO statusConvertDTO);//是否需要转换状态public abstract Boolean convertStatus(ActivityStatusConvertDTO statusConvertDTO);//转换状态
}

针对这个抽象类有三个子类继承与实现,分别是活动的操作器,奖品操作器,用户操作器,首先是奖品操作和用户操作,首先实现的是责任链顺序,他们在public Integer sequence()中返回的是1,是责任链的第一层,实现的第二个方法是查看状态是否需要翻转,首先保证状态翻转参数正确,如果不正确,返回false,之后从数据库中查询相关活动奖品/用户的数据,如果不存在,返回false,最后查看传入的参数中的状态和数据库中的状态是否一致,如果一致,则不需要翻转,返回false,走到最后,说明需要翻转,返回true.实现的第三个方法就是翻转数据库中的状态.

/*** 转换奖品状态*/
@Component
public class PrizeOperator extends AbstractActivityOperator{@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Overridepublic Integer sequence() {return 1;}@Overridepublic Boolean needConvert(ActivityStatusConvertDTO statusConvertDTO) {if (statusConvertDTO.getPrizeId() == null ||statusConvertDTO.getActivityTargetStatus() == null){//如果状态转换种不存在相关参数,直接返回falsereturn false;}//从数据库中查询活动奖品ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityAndPrizeId(statusConvertDTO.getActivityId(),statusConvertDTO.getPrizeId());//如果没有查询到,返回falseif (activityPrizeDO == null){return false;}//和查询出的数据的状态比较,状态和传入的"完成"状态一致的时候不许要反转if (activityPrizeDO.getStatus().equals(statusConvertDTO.getPrizeTargetStatus().name())){return false;}return true;}@Overridepublic Boolean convertStatus(ActivityStatusConvertDTO statusConvertDTO) {activityPrizeMapper.updateStatus(statusConvertDTO.getActivityId(),statusConvertDTO.getPrizeId(),statusConvertDTO.getActivityTargetStatus().name());return true;}
}
/*** 转换人员状态*/
@Component
public class UserOperator extends AbstractActivityOperator{@Autowiredprivate ActivityUserMapper activityUserMapper;@Overridepublic Integer sequence() {return 1;}@Overridepublic Boolean needConvert(ActivityStatusConvertDTO statusConvertDTO) {if (statusConvertDTO.getUserIds() == null ||statusConvertDTO.getActivityId() == null ||statusConvertDTO.getUserIds().isEmpty()){return false;}Long activityId = statusConvertDTO.getActivityId();//从mapper层查询用户信息for (Long userId :statusConvertDTO.getUserIds()) {ActivityUserDO activityUserDO = activityUserMapper.selectUserById(userId,activityId);if (activityUserDO == null){//没有查询到活动对应的用户return false;}//查询状态是否和传入的"完成一致if (activityUserDO.getStatus().equals(statusConvertDTO.getUserTargetStatus().name())){return false;}}return true;}@Overridepublic Boolean convertStatus(ActivityStatusConvertDTO statusConvertDTO) {Long activityId = statusConvertDTO.getActivityId();for (Long userId : statusConvertDTO.getUserIds()) {activityUserMapper.batchUpdateStatus(statusConvertDTO.getUserTargetStatus().name(),activityId,userId);}return true;}
}

之后是活动操作器,实现的第一个方法还是处于责任链中的位置,sequence()方法返回的是2,处于责任链中的第二个位置.之后是实现的第二个方法,首先对参数进行校验,如果校验不通过,返回false,之后查看数据库中的状态和传入的状态是否一致,如果一致,就不需要翻转,返回false,之后校验和活动相关的奖品是否全部抽取完成,如果还有奖品没有抽取完成,证明活动还为结束,返回false,如果以上校验全部通过,返回true,证明活动已经结束,可以翻转.第三个方法和上面的一样,翻转活动状态.

@Component
public class ActivityOperator extends AbstractActivityOperator{@Autowiredprivate ActivityMapper activityMapper;@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Overridepublic Integer sequence() {return 2;}@Overridepublic Boolean needConvert(ActivityStatusConvertDTO statusConvertDTO) {//判断后续需要校验的参数是否为空if (statusConvertDTO.getActivityId() == null ||statusConvertDTO.getActivityTargetStatus() == null){return false;}//校验活动状态是否和数据库中的一致,如果一致就是翻转过的String activityStatus =activityMapper.selectById(statusConvertDTO.getActivityId()).getStatus();if (activityStatus.equals(statusConvertDTO.getActivityTargetStatus().name())){return false;}//校验活动奖品是否均抽取完成int count = activityPrizeMapper.countRunningPrizeByActivityId(statusConvertDTO.getActivityId(),ActivityPrizeStatusEnum.INIT.name());if (count > 0){return false;}return true;}@Overridepublic Boolean convertStatus(ActivityStatusConvertDTO statusConvertDTO) {activityMapper.convert(statusConvertDTO.getActivityId(),statusConvertDTO.getActivityTargetStatus().name());return true;}
}

我们使用processStatusConversion方法来对活动,用户以及奖品的相关信息来进行翻转.首先是对奖品和用户状态的扭转,传入的责任链顺序是1,之后就是对活动状态的扭转,传入的责任链顺序是2.

//先扭转奖品和用户的状态
Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
update = processStatusConversion(activityStatusConvertDTO,currMap,1);
//奖品和用户状态都扭转完成之后,后扭转奖品的状态
update = processStatusConversion(activityStatusConvertDTO,currMap,2) || update;

首先把Map转换为可迭代的Iterator,对Iterator进行遍历,知道遇到Map中的sequence和传入参数一致的sequence,证明找到了Map中想要执行的责任链,即operator.sequence() != sequence逻辑,之后判断状态是否需要翻转,即!operator.needConvert(convertActivityStatusDTO)逻辑,只要这连个条件有一个满足,就直接跳过Map中的当前执行器(Operator)的Bean.如果不满足,就证明找到了需要转换状态的实体,调用convertStatus进行状态转换.

private Boolean processStatusConversion(ActivityStatusConvertDTO convertActivityStatusDTO,Map<String, AbstractActivityOperator> currMap,int sequence) {Boolean update = false;// 遍历currMapIterator<Map.Entry<String, AbstractActivityOperator>> iterator = currMap.entrySet().iterator();while (iterator.hasNext()) {AbstractActivityOperator operator = iterator.next().getValue();// Operator 是否需要转换if (operator.sequence() != sequence|| !operator.needConvert(convertActivityStatusDTO)) {continue;}// 需要转换:转换if (!operator.convertStatus(convertActivityStatusDTO)) {log.error("{}状态转换失败!", operator.getClass().getName());throw new ServiceException(ServiceErrorCodeConstants.CONVERT_ACTIVITY_STATUS_ERROR);}// currMap 删除当前 Operatoriterator.remove();update = true;}// 返回return update;
}

翻转状态之后,需要把中奖人的名单保存进入数据库中,数据中保存的数据包括活动,奖品,人员三方面的信息,具体见WinningRecordDO.

List<WinningRecordDO> winningRecordDOList = drawPrizeService.saveWinningRecords(drawPrizeParam);//存储获奖者信息
/*** 中奖记录DO*/
@Data
public class WinningRecordDO extends BaseDO{private Long activityId;//活动idprivate String activityName;//活动名称private Long prizeId;//奖品idprivate String prizeName;//奖品名称private String prizeTier;//奖品等级private Long winnerId;//获奖者idprivate String winnerName;//获奖者名称private String winnerEmail;//获奖者邮箱private Encrypt winnerPhoneNumber;//获奖者手机号码private Date winningTime;//获奖时间
}

saveWinningRecords方法返回中奖记录之后,拿到用户的手机号和邮箱,使用阿里云的短信服务和邮箱服务,为指定的用户发送中奖记录.

/*** 抽奖之后进行邮件和短信的发送* @param winningRecordDOList 获奖记录*/
public void syncExecute(List<WinningRecordDO> winningRecordDOList){//利用并发给中奖者发信息asyncServiceExecutor.execute(() -> sendMessage(winningRecordDOList));asyncServiceExecutor.execute(() -> sendMail(winningRecordDOList));
}

如果在消费者收到消息的过程中发生了异常,则异常会被捕捉,最后把活动相关的数据全部回滚掉

} catch (ServiceException e){log.error("处理mq消息异常:{},{},{}",e.getCode(),e.getCode(),e.getMessage());rollback(drawPrizeParam);//异常之后需要把数据回滚掉throw e;
}catch (Exception e){log.error("处理mq消息异常:{}",e.getMessage());rollback(drawPrizeParam);//异常之后需要把数据回滚掉throw e;
}

还是和之前的状态翻转的流程一样,首先构造回滚参数,之后调用活动状态管理器中的rollback方法进行回滚.只不过就是把之前的convertStatus方法中的完成状态改为初始化状态,把之前handlerEvent方法中的Redis更新变为删除.这里需要注意的是,在回滚之前,需要先判断数据是否有真的落库,如果没有落库,就不需要回滚,直接返回.

/*** 回滚活动相关信息的状态* @param drawPrizeParam 抽奖参数*/
private void rollback(DrawPrizeParam drawPrizeParam) throws JsonProcessingException {if (!convertStatusSuccess(drawPrizeParam)){//没有翻转成功,直接返回return;}//构建状态翻转参数,回滚回原来的状态ActivityStatusConvertDTO statusConvertDTO = new ActivityStatusConvertDTO();statusConvertDTO.setActivityId(drawPrizeParam.getActivityId());statusConvertDTO.setPrizeId(drawPrizeParam.getPrizeId());for (DrawPrizeParam.Winner winner :drawPrizeParam.getWinnerList()) {List<Long> userIds = new ArrayList<>();userIds.add(winner.getUserId());statusConvertDTO.setUserIds(userIds);}statusConvertDTO.setActivityTargetStatus(ActivityStatusEnum.RUNNING);statusConvertDTO.setPrizeTargetStatus(ActivityPrizeStatusEnum.INIT);statusConvertDTO.setUserTargetStatus(ActivityUserStatusEnum.INIT);activityStatusManager.rollback(statusConvertDTO);//回滚数据//回滚之后判断中奖记录是否已经落库if (!hasRecords(drawPrizeParam)){return;//如果没有落库,直接返回}//如果落库,删除其中的数据,包括缓存和数据库中的数据drawPrizeService.deleteRecords(drawPrizeParam.getActivityId(),drawPrizeParam.getPrizeId());
}

针对数据的状态进行回滚的时候,我们可以对上面的状态翻转接口进行复用,即operator.convertStatus(convertDTO);,因为他们本质上都是对状态进行翻转,只不过一个是翻转到完成状态,一个是翻转到初始化状态.

@Override
public void rollback(ActivityStatusConvertDTO convertDTO) throws JsonProcessingException {if (convertDTO == null){log.warn("无需回滚状态");return;}Collection<AbstractActivityOperator> values = operatorMap.values();//获取所有需要回滚的类for (AbstractActivityOperator operator : values) {//回滚全部类的状态operator.convertStatus(convertDTO);}//回滚缓存中的数据activityService.cacheActivity(convertDTO.getActivityId());
}

4. 阿里云短信服务与邮件服务

4.1 中奖通知服务

在活动结束,即抽奖完成之后,我们使用线程池的方式,去同时为用户的手机和邮箱发送中奖通知.

/*** 抽奖之后进行邮件和短信的发送* @param winningRecordDOList 获奖记录*/
public void syncExecute(List<WinningRecordDO> winningRecordDOList){//利用并发给中奖者发信息asyncServiceExecutor.execute(() -> sendMessage(winningRecordDOList));//todoasyncServiceExecutor.execute(() -> sendMail(winningRecordDOList));
}

在为用户发送短信的时候,首先针对List<WinningRecordDO>中的每一条记录进行遍历,拿到其中的相关信息放入templateParam中,以便后面对短信模版中的相关参数进行替换,在使用smsUtil.sendMessage调用阿里云短信服务的时候,传入的参数包括短信模版id,获奖者手机号,替换短信模版的参数的JSON字符串.

/*** 发送短信给中奖者* @param winningRecordDOList 中奖记录*/
private void sendMessage(List<WinningRecordDO> winningRecordDOList){//对参数进行校验if (winningRecordDOList == null || winningRecordDOList.isEmpty()){log.warn("中奖名单为空!");return;}winningRecordDOList.forEach(record -> {Map<String, String> templateParam = new HashMap<>();templateParam.put("name", record.getWinnerName());templateParam.put("activityName", record.getActivityName());templateParam.put("prizeTiers", ActivityPrizeTiersEnum.checkForName(record.getPrizeTier()).getMessage());templateParam.put("prizeName", record.getPrizeName());templateParam.put("winningTime",DateUtil.formatTime(record.getWinningTime()));try {smsUtil.sendMessage(WINNING_TEMPLATE_CODE,record.getWinnerPhoneNumber().getValue(),objectMapper.writeValueAsString(templateParam));} catch (JsonProcessingException e) {throw new RuntimeException(e);}});
}

之后就是发邮件,在调用mailUtil.sendSampleMail阿里云发送邮件的接口的时候,需要传入的参数为获奖者的邮件,和提前构造好的邮件内容.

/*** 给中奖者发送邮件* @param recordDOList 中奖记录*/private void sendMail(List<WinningRecordDO> recordDOList) {if(CollectionUtils.isEmpty(recordDOList)) {log.warn("中奖名单为空!");return;}for (WinningRecordDO winningRecordDO : recordDOList) {// Hi,xxx。恭喜你在抽奖活动活动中获得二等奖:吹风机。获奖奖时间为18:18:44,请尽快领取您的奖励String context = "Hi," + winningRecordDO.getWinnerName() + "。恭喜你在"+ winningRecordDO.getActivityName() + "活动中获得"+ ActivityPrizeTiersEnum.checkForName(winningRecordDO.getPrizeTier()).getMessage()+ ":" + winningRecordDO.getPrizeName() + "。获奖时间为"+ DateUtil.formatTime(winningRecordDO.getWinningTime()) + ",请尽快领 取您的奖励!";mailUtil.sendSampleMail(winningRecordDO.getWinnerEmail(),"中奖通知", context);}}

4.2 短信验证码服务

和上面的道理一样,为短信服务传入的参数还是短信的模版id,用户的手机号,还有填充短信模版中参数的JSON字符串.

@Override
public void sendVerificationCode(String phoneNumber) throws JsonProcessingException {//校验手机号if (!StringUtils.hasText(phoneNumber) || !RegexUtil.checkMobile(phoneNumber)){throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}String captcha = CaptchaUtil.generateCaptchaCode(4);//生成4位验证码Map<String,String> map = new HashMap<>();map.put("code",captcha);//把验证码设置进Map中,再转换为json字符串,之后就会替换掉模版中的对应参数.smsUtil.sendMessage(VERIFICATION_TEMPLATE_CODE,phoneNumber,objectMapper.writeValueAsString(map));redisUtil.set(VERIFICATION_PREFIX + phoneNumber,captcha,300L);//把验证码放入redis中
}

5. 数据库表设计

其中包含三张基本表,三张联合表,基本表包含用户,活动,奖品,联合表包含用户活动联合表,主要用于记录一个用户在一个活动中的状态,所以有status字段.还有奖品活动联合表,主要用于记录奖品在当前活动中的数量,奖品在当前活动中属于几等奖,还有奖品的状态.还有中奖记录表,主要用于记录那个用户,在那个活动中,中了什么奖.
在这里插入图片描述

相关文章:

[项目总结] 抽奖系统项目技术应用总结

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…...

Axios替代品Alova

介绍alova | Alova.JS Multipart 实体请求 | Axios中文文档 | Axios中文网 1. 极致的轻量与性能 Tree-shaking优化&#xff1a;仅打包使用到的功能模块 零依赖&#xff1a;基础包仅 4KB&#xff08;Axios 12KB&#xff09; 2. 智能请求管理&#xff08;开箱即用&#xff0…...

Python OpenCV性能优化与部署实战指南

在计算机视觉领域&#xff0c;OpenCV作为开源视觉库的标杆&#xff0c;其性能表现直接影响着从工业检测到AI模型推理的各类应用场景。本文结合最新技术趋势与生产实践&#xff0c;系统性梳理Python环境下OpenCV的性能优化策略与部署方案。 一、性能优化核心技术矩阵 1.1 内存…...

k8s的flannel生产实战与常见问题排查

关于 Kubernetes Flannel 插件的详细教程及生产环境实战指南&#xff0c;涵盖核心概念、安装配置、常见问题排查与优化策略 Flannel通信流程 一、Flannel 概述 Flannel 是 Kubernetes 最常用的 CNI&#xff08;Container Network Interface&#xff09;插件之一&#xff0c;…...

删除链表倒数第N个节点

Leetcode&#xff08;19&#xff09;&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 分析&#xff1a; 首要目标就是找到第N个节点的前一个节点&#xff0c;因为只有通过这个节点&#xff08;cur&#xff09;才可进行对…...

互联网大厂Java面试实录:Spring Boot与微服务架构在电商场景中的应用解析

&#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通 &#x1f601; 2. 毕业设计专栏&#xff0c;毕业季咱们不慌忙&#xff0c;几百款毕业设计等你选。 ❤️ 3. Python爬虫专栏…...

UGUI如何使用EventTrigger

前言 在 Unity 的 UGUI 系统中,EventTrigger 是一个强大的组件,允许开发者监听和处理多种 UI 交互事件。以下是详细的使用方法、示例代码、优缺点分析以及注意事项。 一、EventTrigger 基本用法 1. 添加 EventTrigger 组件 在 Unity 编辑器中选中 UI 对象(如 But…...

从代码学习深度学习 - 单发多框检测(SSD)PyTorch版

文章目录 前言工具函数数据处理工具 (`utils_for_data.py`)训练工具 (`utils_for_train.py`)检测相关工具 (`utils_for_detection.py`)可视化工具 (`utils_for_huitu.py`)模型类别预测层边界框预测层连接多尺度预测高和宽减半块基础网络块完整的模型训练模型读取数据集和初始化…...

机器视觉的平板电脑屏幕组件覆膜应用

在现代智能制造业中&#xff0c;平板电脑屏幕组件覆膜工序是确保产品外观和功能完整性的重要环节。随着技术的进步&#xff0c;传统的覆膜方式已经无法满足高速度、高精度的生产需求。而MasterAlign视觉系统的出现&#xff0c;将传统覆膜工艺转变为智能化、自动化的生产流程。在…...

更换内存条会影响电脑的IP地址吗?——全面解析

在日常电脑维护和升级过程中&#xff0c;许多用户都会遇到需要更换内存条的情况。与此同时&#xff0c;不少用户也担心硬件更换是否会影响电脑的网络配置&#xff0c;特别是IP地址的设置。本文将详细探讨更换内存条与IP地址之间的关系&#xff0c;帮助读者理解这两者之间的本质…...

SQLite数据库加密(Java语言、python语言)

1. 背景与需求 SQLite 是一种轻量级的关系型数据库,广泛应用于嵌入式设备、移动应用、桌面应用等场景。为了保护数据的隐私与安全,SQLite 提供了加密功能(通过 SQLCipher 扩展)。在 Java 中,可以使用 sqlite-jdbc 驱动与 SQLCipher 集成来实现 SQLite 数据库的加密。 本…...

RISC-V入门资料

以下是获取 RISC-V 相关资料的权威渠道和推荐资源&#xff0c;涵盖技术文档、开发工具、社区支持等&#xff1a; 1. 官方资料 RISC-V 国际基金会官网 https://riscv.org 核心文档&#xff1a;ISA 规范&#xff08;包括基础指令集&#xff08;RV32I/RV64I&#xff09;、扩展指令…...

C++访问权限控制符

访问权限控制符 在C中&#xff0c;访问权限控制符是用来限制类或结构体成员&#xff08;例如&#xff1a;变量、函数等&#xff09;的访问级别的。C提供了三种访问权限级别&#xff1a; Public 访问权限&#xff1a; 公共成员可以在任何地方被访问&#xff0c;包括类的内部、…...

VMware安装CentOS Stream10

文章目录 安装下载iso文件vmware安装CentOS Stream创建新虚拟机安装CentOS Stream10 安装 下载iso文件 官方地址&#xff1a;跳转链接 vmware安装CentOS Stream 创建新虚拟机 参考以下步骤 安装CentOS Stream10 指定ISO文件 开启虚拟机选择Install CentOS Stream 10 鼠…...

互联网大厂Java求职面试:云原生与AI融合下的系统设计挑战-2

互联网大厂Java求职面试&#xff1a;云原生与AI融合下的系统设计挑战-2 第一轮提问&#xff1a;云原生架构选型与微服务治理 面试官&#xff08;技术总监&#xff09;&#xff1a;郑薪苦&#xff0c;我们先从一个基础问题开始。你了解Spring Cloud和Kubernetes在微服务架构中…...

基于Dify实现对Excel的数据分析

在dify部署完成后&#xff0c;大家就可以基于此进行各种应用场景建设&#xff0c;目前dify支持聊天助手&#xff08;包括对话工作流&#xff09;、工作流、agent等模式的场景建设&#xff0c;我们在日常工作中经常会遇到各种各样的数据清洗、格式转换处理、数据统计成图等数据分…...

资产月报怎么填?资产月报填报指南

资产月报是企业对固定资产进行定期检查和管理的重要工具&#xff0c;它能够帮助管理者了解资产的使用情况、维护状况和财务状况&#xff0c;从而为资产的优化配置和决策提供依据。填写资产月报时&#xff0c;除了填报内容外&#xff0c;还需要注意格式的规范性和数据的准确性。…...

MIT XV6 - 1.3 Lab: Xv6 and Unix utilities - primes

接上文 MIT XV6 - 1.2 Lab: Xv6 and Unix utilities - pingpong primes 继续实验&#xff0c;实验介绍和要求如下 (原文链接 译文链接) : Write a concurrent prime sieve program for xv6 using pipes and the design illustrated in the picture halfway down this page and…...

Android学习总结之kotlin协程面试篇

一、协程基础概念与原理类真题 真题 1&#xff1a;协程是线程吗&#xff1f;为什么说它是轻量级的&#xff1f;&#xff08;字节跳动 / 美团&#xff09; 解答&#xff1a; 本质区别&#xff1a; 线程是操作系统调度的最小单位&#xff08;内核态&#xff09;&#xff0c;协…...

从前端视角看网络协议的演进

别再让才华被埋没&#xff0c;别再让github 项目蒙尘&#xff01;github star 请点击 GitHub 在线专业服务直通车GitHub赋能精灵 - 艾米莉&#xff0c;立即加入这场席卷全球开发者的星光革命&#xff01;若你有快速提升github Star github 加星数的需求&#xff0c;访问taimili…...

Docker中运行的Chrome崩溃问题解决

问题 各位看官是否在 Docker 容器中的 Linux 桌面环境&#xff08;如Xfce&#xff09;上启动Chrome &#xff0c;遇到了令人沮丧的频繁崩溃问题&#xff1f;尤其是在打开包含图片、视频的网页&#xff0c;或者进行一些稍复杂的操作时&#xff0c;窗口突然消失&#xff1f;如果…...

【沉浸式求职学习day36】【初识Maven】

沉浸式求职学习 Maven1. Maven项目架构管理工具2.下载安装Maven3.利用Tomcat和Maven进入一个网站 Maven 为什么要学习这个技术&#xff1f; 在Java Web开发中&#xff0c;需要使用大量的jar包&#xff0c;我们手动去导入&#xff0c;这种操作很麻烦&#xff0c;PASS&#xff01…...

ES面试题系列「一」

1、Elasticsearch 是什么&#xff1f;它与传统数据库有什么区别&#xff1f; 答案&#xff1a;Elasticsearch 是一个基于 Lucene 的分布式、开源的搜索和分析引擎&#xff0c;主要用于处理大量的文本数据&#xff0c;提供快速的搜索和分析功能。与传统数据库相比&#xff0c;E…...

【音视频工具】MP4BOX使用

这里写目录标题 使用介绍 使用 下面这个网站直接使用&#xff1a; MP4Box.js - JavaScript MP4 Reader/Fragmenter (gpac.github.io) 介绍 MMP4Box 是 GPAC 项目开发的一款命令行工具&#xff0c;专门用于处理 MP4 格式多媒体文件&#xff0c;也可操作 AVI、MPG、TS 等格…...

Linux中常见开发工具简单介绍

目录 apt/yum 介绍 常用命令 install remove list vim 介绍 常用模式 命令模式 插入模式 批量操作 底行模式 模式替换图 vim的配置文件 gcc/g 介绍 处理过程 预处理 编译 汇编 链接 库 静态库 动态库&#xff08;共享库&#xff09; make/Makefile …...

laravel 使用异步队列,context带的上下文造成反序列化出问题

2025年5月8日17:03:44 如果你是单个应用&#xff0c;异步递交任务&#xff0c;是在应用内部使用&#xff0c;一般不会发生这样的问题 但是现在app项目是 app是一个应用&#xff0c;admin是一个应用&#xff0c;app吧为了接口性能吧异步任务丢给admin去执行&#xff0c;如果两个…...

flow-matching 之学习matcha-tts cosyvoice

文章目录 matcha 实现cosyvoice 实现chunk_fmchunk_maskcache_attn stream token2wav 关于flow-matching 很好的原理性解释文章&#xff0c; 值得仔细读&#xff0c;多读几遍&#xff0c;关于文章Flow Straight and Fast: Learning to Generate and Transfer Data with Rectifi…...

视频编解码学习三之显示器续

一、现在主流的显示器是LCD显示器吗&#xff1f; 是的&#xff0c;现在主流的显示器仍然是 LCD&#xff08;液晶显示器&#xff0c;Liquid Crystal Display&#xff09;&#xff0c;但它已经细分为多种技术类型&#xff0c;并和其他显示技术&#xff08;如OLED&#xff09;形成…...

ubuntu22.04在 Docker容器中安装 ROS2-Humble

22.04 安装 docker 容器并实现rviz功能 1 docker pull命令拉取包含ROS-Humble的镜像&#xff1a; docker pull osrf/ros:humble-desktop-full-jammy docker images验证该镜像是否拉取成功。 使用镜像osrf/ros:humble-desktop-full-jammy创建并运行容器 sudo docker run -it…...

【JavaWeb+后端常用部件】

回顾内容看&#xff1a; 一、获取请求参数的方法 参考&#xff1a;[JavaWeb]——获取请求参数的方式(全面&#xff01;&#xff01;&#xff01;)_java 获取请求参数-CSDN博客 Json格式的Body加备注RequestBody{id}动态路径加备注PathVariableid?&name?直接接收就好 i…...