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

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

🌸个人主页: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与…...

【运维】基于Python打造分布式系统日志聚合与分析利器

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 在分布式系统中,日志数据分散在多个节点,管理和分析变得复杂。本文详细介绍如何基于Python开发一个日志聚合与分析工具,结合Logstash和F…...

MySql(基础)

表名建议用 反引号 包裹&#xff08;尤其是表名包含特殊字符或保留字时&#xff09;&#xff0c;但如果表名是普通字符串&#xff08;如 user&#xff09;&#xff0c;可以省略。 注释&#xff08;COMMENT 姓名&#xff09; 数据库 1.查看数据库&#xff1a;show databases…...

怎样选择成长股 读书笔记(一)

文章目录 第一章 成长型投资的困惑一、市场不可预测性的本质困惑二、成长股的筛选悖论三、管理层评估的认知盲区四、长期持有与估值波动的博弈五、实践中的认知升级路径总结&#xff1a;破解困惑的行动框架 第二章 如何阅读应计制利润表一、应计制利润表的本质与核心原则1. 权责…...

系统思考:个人与团队成长

四年前&#xff0c;我交付的系统思考项目&#xff0c;今天学员的反馈依然深深触动了我。 我常常感叹&#xff0c;系统思考不仅仅是一场培训&#xff0c;更像是一场持续的“修炼”。在这条修炼之路上&#xff0c;最珍贵的&#xff0c;便是有志同道合的伙伴们一路同行&#xff0…...

并行发起http请求

1. 使用 axios Promise.all <template><input type"file" multiple change"handleFileUpload" /> </template><script> import axios from axios;export default {methods: {async handleFileUpload(event) {const files event…...

【数据结构入门训练DAY-31】组合的输出

本文介绍了如何使用深度优先搜索&#xff08;DFS&#xff09;算法解决数的组合问题。题目要求从1到n的自然数中选取r个数&#xff0c;输出所有可能的组合&#xff0c;并按字典顺序排列。文章详细描述了解题思路&#xff0c;包括建立数组存储数字、使用DFS递归处理候选数、以及如…...

leetcode0815. 公交路线-hard

1 题目&#xff1a; 公交路线 官方标定难度&#xff1a;难 给你一个数组 routes &#xff0c;表示一系列公交线路&#xff0c;其中每个 routes[i] 表示一条公交线路&#xff0c;第 i 辆公交车将会在上面循环行驶。 例如&#xff0c;路线 routes[0] [1, 5, 7] 表示第 0 辆公…...

花朵识别系统Python+深度学习+卷积神经网络算法+TensorFlow+人工智能

一、介绍 花朵识别系统。本系统采用Python作为主要编程语言&#xff0c;基于TensorFlow搭建ResNet50卷积神经网络算法模型&#xff0c;并基于前期收集到的5种常见的花朵数据集&#xff08;向日葵、玫瑰、蒲公英、郁金香、菊花&#xff09;进行处理后进行模型训练&#xff0c;最…...

LLM 论文精读(四)LLM Post-Training: A Deep Dive into Reasoning Large Language Models

这是一篇2025年发表在arxiv中的LLM领域论文&#xff0c;是一篇非常全面的综述类论文&#xff0c;介绍了当前主流的强化学习方法在LLM上的应用&#xff0c;文章内容比较长&#xff0c;但建议LLM方面的从业人员反复认真阅读。 写在最前面 为了方便你的阅读&#xff0c;以下几点的…...

网址为 http://xxx:xxxx/的网页可能暂时无法连接,或者它已永久性地移动到了新网址

这是由于浏览器默认的非安全端口所导致的&#xff0c;所谓非安全端口&#xff0c;就是浏览器出于安全问题&#xff0c;会禁止一些网络浏览向外的端口。 避免使用6000,6666这样的端口 6000-7000有很多都不行&#xff0c;所以尽量避免使用这个区间 还有在云服务器中&#xff0c…...

【C++】16.继承

C三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态 在前面的章节中&#xff0c;我们讲过了封装&#xff0c;也就是通过类和访问修饰符来进行封装。 接下来我们就来认识一下新的特性——继承 1. 继承的概念及定义 1.1 继承的概念 继承(inheritance)机制是面向对…...

LlamaIndex 第七篇 结构化数据提取

大型语言模型&#xff08;LLMs&#xff09;在数据理解方面表现出色&#xff0c;这也促成了它们最重要的应用场景之一&#xff1a;能够将常规的人类语言&#xff08;我们称之为非结构化数据&#xff09;转化为特定的、规范的、可被计算机程序处理的格式。我们将这一过程的输出称…...

PHP API安全设计四要素:构建坚不可摧的接口防护体系

引言&#xff1a;API安全的重要性 在当今前后端分离和微服务架构盛行的时代&#xff0c;API已成为系统间通信的核心枢纽。然而&#xff0c;不安全的API可能导致&#xff1a; 数据泄露&#xff1a;敏感信息被非法获取篡改风险&#xff1a;传输数据被中间人修改重放攻击&#x…...

英语16种时态

时态应用场合格式例子一般现在时表示经常、反复发生的动作&#xff0c;客观事实或普遍真理主语 动词原形&#xff08;第三人称单数作主语时动词加 -s/-es&#xff09;The sun rises in the east.一般过去时表示过去某个时间发生的动作或存在的状态主语 动词的过去式I visited…...

使用 goaccess 分析 nginx 访问日志

介绍 goaccess 是一个在本地解析日志的工具, 可以直接在命令行终端环境中使用 TUI 界面查看分析结果, 也可以导出为更加丰富的 HTML 页面. 官网: https://goaccess.io/ 下载安装 常见的 Linux 包管理器中都包含了 goaccess, 直接安装就行. 以 Ubuntu 为例: sudo apt instal…...

什么是中央税

中央税&#xff08;又称国家税&#xff09;是指由中央政府直接征收、管理和支配的税种&#xff0c;其收入全额纳入中央财政&#xff0c;用于保障国家层面的财政支出和宏观调控。中央税通常具有税基广泛、收入稳定、涉及国家主权或全局性经济调控的特点。 --- 中央税的核心特征…...

AI Agent(10):个人助手应用

引言 本文聚焦AI Agent在个人助手领域的应用,探讨其如何在个人生产力提升、健康与生活管理、学习与教育辅助以及娱乐与社交互动四个方面,为用户创造价值并解决实际问题。 AI个人助手正从简单的指令执行者逐渐发展为具有自主性、适应性和个性化能力的智能伙伴。这一转变不仅…...

力扣70题解

记录 2025.5.8 题目: 思路&#xff1a; 1.初始化&#xff1a;p 和 q 初始化为 0&#xff0c;表示到达第 0 级和第 1 级前的方法数。r 初始化为 1&#xff0c;表示到达第 1 级台阶有 1 种方法。 2.循环迭代&#xff1a;从第 1 级到第 n 级台阶进行迭代&#xff1a; p 更新为前…...

2025御网杯wp(web,misc,crypto)

文章目录 miscxor10图片里的秘密被折叠的显影图纸 Cryptoeasy_rsagift**1. 礼物数学解析****最终答案** 草甸方阵的密语easy-签到题baby_rsa webYWB_Web_xffYWB_Web_未授权访问easywebYWB_Web_命令执行过滤绕过反序列化 misc xor10 ai一把梭 根据题目中的字符串和提示&#…...

【深度学习】将本地工程上传到Colab运行的方法

1、将本地工程&#xff08;压缩包&#xff09;上传到一个新的colab窗口&#xff1a;如下图中的 2.zip&#xff0c;如果工程中有数据集&#xff0c;可以删除掉。 2、解压压缩包。 !unzip /content/2.zip -d /content/2 如果解压出了不必要的文件夹可以递归删除&#xff1a; #…...

多模态大语言模型arxiv论文略读(六十九)

Prompt-Aware Adapter: Towards Learning Adaptive Visual Tokens for Multimodal Large Language Models ➡️ 论文标题&#xff1a;Prompt-Aware Adapter: Towards Learning Adaptive Visual Tokens for Multimodal Large Language Models ➡️ 论文作者&#xff1a;Yue Zha…...

Lua再学习

因为实习的项目用到了Lua&#xff0c;所以再来深入学习一下 函数 函数的的多返回值 Lua中的函数可以实现多返回值&#xff0c;实现方法是再return后列出要返回的值的列表&#xff0c;返回值也可以通过变量接收到&#xff0c;变量不够也不会影响接收对应位置的返回值 Lua中传…...

Linux计划任务与进程

at 命令使用方法 at 命令可在指定时间执行任务&#xff0c;适用于一次性任务调度。以下是基本用法&#xff1a; 安装 atd 服务&#xff08;如未安装&#xff09; # Debian/Ubuntu sudo apt-get install at# CentOS/RHEL sudo yum install at启动服务 sudo systemctl start atd…...

JavaEE--文件操作和IO

目录 一、认识文件 二、 树型结构组织和目录 三、文件路径 1. 绝对路径 2. 相对路径 四、文件类型 五、文件操作 1. 构造方法 2. 方法 六、文件内容的读写——数据流 1. InputStream概述 2. FileInputStream概述 2.1 构造方法 2.2 示例 3. OutputStream概述 3.…...

k8s的节点是否能直接 curl Service 名称

在 Kubernetes 中&#xff0c;节点&#xff08;Node&#xff09;默认情况下不能直接通过 Service 的 DNS 名称&#xff08;如 my-svc.default.svc.cluster.local&#xff09;访问 Service。以下是详细分析和解决方案&#xff1a; 1. 默认情况下节点无法解析 Service 的 DNS 名…...

Mask-aware Pixel-Shuffle Down-Sampling (MPD) 下采样

来源 简介&#xff1a;这个代码实现了一个带有掩码感知的像素重排下采样模块&#xff0c;主要用于图像处理任务&#xff08;如图像修复或分割&#xff09;。 论文题目&#xff1a;HINT: High-quality INpainting Transformer with Mask-Aware Encoding and Enhanced Attentio…...

本贴会成为记录贴

这几天有些心力交瘁了 一方面带着对互联网下行的伤心&#xff0c;一方面是对未来的担忧 一转眼好像就是20 21那个 可以在宿舍肆意玩手机 大学生活 可是我不小了 是个26岁的人了 时间很快 快的就好像和自己开了一个玩笑 我以为可以找到一个自己足够喜欢的 可爱的人 可是我没有 …...

redis数据结构-04 (HINCRBY、HDEL、HKEYS、HVALS)

哈希操作&#xff1a;HINCRBY、HDEL、HKEYS、HVALS Redis 中的哈希功能极其丰富&#xff0c;让您能够以类似于编程语言中对象的方式存储和检索数据。本课将深入探讨具体的哈希操作&#xff0c;这些操作为操作以下结构中的数据提供了强大的工具&#xff1a; HINCRBY 、 HDEL 、…...

python 写一个工作 简单 番茄钟

1、图 2、需求 番茄钟&#xff08;Pomodoro Technique&#xff09;是一种时间管理方法&#xff0c;由弗朗西斯科西里洛&#xff08;Francesco Cirillo&#xff09;在 20 世纪 80 年代创立。“Pomodoro”在意大利语中意为“番茄”&#xff0c;这个名字来源于西里洛最初使用的一个…...

复现MAET的环境问题(自用)

我的配置是3090&#xff0c;CUDA Version: 12.4 配置环境时总有冲突&#xff0c;解决好的环境如下 如果你的配置也是CUDA12.4&#xff0c;可以把下面的配置信息保存成 environment.yml 文件 然后执行下面的代码创建环境即可 conda env export > environment.yml name:…...

PDF2zh插件在zotero中安装并使用

1、首先根据PDF2zh说明文档&#xff0c;安装PDF2zh https://github.com/guaguastandup/zotero-pdf2zh/tree/v2.4.0 我没有使用conda&#xff0c;直接使用pip安装pdf2zh &#xff08;Python版本要求3.10 < version <3.12&#xff09; pip install pdf2zh1.9.6 flask pypd…...

第二十三节:图像金字塔- 图像金字塔应用 (图像融合)

一、引言:视觉信息的层次化表达 在数字图像处理领域,图像金字塔(Image Pyramid)作为一种多尺度表示方法,自20世纪80年代提出以来,始终在计算机视觉领域扮演着关键角色。这种将图像分解为不同分辨率层次的结构化表示方法,完美地模拟了人类视觉系统对场景的多尺度感知特性…...

一种混沌驱动的后门攻击检测指标

摘要 人工智能&#xff08;AI&#xff09;模型在各个领域的进步和应用已经改变了我们与技术互动的方式。然而&#xff0c;必须认识到&#xff0c;虽然人工智能模型带来了显著的进步&#xff0c;但它们也存在固有的挑战&#xff0c;例如容易受到对抗性攻击。目前的工作提出了一…...

LeetCode 高频题实战:如何优雅地序列化和反序列化字符串数组?

文章目录 摘要描述题解答案题解代码分析编码方法解码方法 示例测试及结果时间复杂度空间复杂度总结 摘要 在分布式系统中&#xff0c;数据的序列化与反序列化是常见的需求&#xff0c;尤其是在网络传输、数据存储等场景中。LeetCode 第 271 题“字符串的编码与解码”要求我们设…...

leetcode 15. 三数之和

题目描述 代码&#xff1a; class Solution { public:vector<vector<int>> threeSum(vector<int>& nums) {sort(nums.begin(),nums.end());int len nums.size();int left 0;int right 0;vector<vector<int>> res;for(int i 0;i <len…...

HTML难点小记:一些简单标签的使用逻辑和实用化

HTML难点小记&#xff1a;一些简单标签的使用逻辑和实用化 jarringslee 文章目录 HTML难点小记&#xff1a;一些简单标签的使用逻辑和实用化简单只是你的表象标签不是随便用的<div> 滥用 vs 语义化标签的本质嵌套规则的隐藏逻辑SEO 与可访问性的隐形关联 暗藏玄机的表单…...

Linux : 31个普通信号含义

Linux &#xff1a; 31个普通信号 信号含义特殊的两个信号 信号含义 信号编号信号名信号含义1SIGHUP如果终端接口检测到一个连接断开&#xff0c;则会将此信号发送给与该终端相关的控制进程&#xff0c;该信号的默认处理动作是终止进程。2SIGINT当用户按组合键&#xff08;一般…...

软件测试都有什么???

文章目录 一、白盒测试&#xff08;结构测试&#xff09;二、黑盒测试&#xff08;功能测试&#xff09;三、灰盒测试四、其他测试类型五、覆盖准则对比六、应用场景 软件测试主要根据测试目标、技术手段和覆盖准则进行分类。分为白盒测试、黑盒测试、灰盒测试及其他补充类型 一…...

LangGraph框架中针对MCP协议的变更-20250510

MCP&#xff08;Model Context Protocol&#xff09;的出现为AI Agent与外部工具及数据源的集成提供了标准化接口&#xff0c;而LangGraph作为基于LangChain的智能体开发框架&#xff0c;在MCP协议的影响下也进行了适配性调整&#xff0c;主要体现在工具调用、异步交互和多步推…...

YashanDB(崖山数据库)V23.4 LTS 正式发布

2024年回顾 2024年11月我们受邀去深圳参与了2024国产数据库创新生态大会。在大会上崖山官方发布了23.3。这个也是和Oracle一样采用的事编年体命名。 那次大会官方希望我们这些在一直从事在一线的KOL帮助产品提一些改进建议。对于这样的想法&#xff0c;我们都是非常乐于合作…...

二、transformers基础组件之Tokenizer

在使用神经网络处理自然语言处理任务时&#xff0c;我们首先需要对数据进行预处理&#xff0c;将数据从字符串转换为神经网络可以接受的格式&#xff0c;一般会分为如下几步: - Step1 分词:使用分词器对文本数据进行分词(字、字词);- Step2 构建词典:根据数据集分词的结果,构建…...

git 报错:错误:RPC 失败。curl 28 Failed to connect to github.com port 443 after 75000

错误&#xff1a;RPC 失败。curl 28 Failed to connect to github.com port 443 after 75000 ms: Couldnt connect to server致命错误&#xff1a;在引用列表之后应该有一个 flush 包 方法一&#xff1a; 直接换一个域名&#xff1a;把 git clone https://github.com/zx59530…...

软考 系统架构设计师系列知识点之杂项集萃(56)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之杂项集萃&#xff08;55&#xff09; 第91题 商业智能关注如何从业务数据中提取有用的信息&#xff0c;然后采用这些信息指导企业的业务开展。商业智能系统主要包括数据预处理、建立&#xff08;&#xff09;、数据分…...

数据库的脱敏策略

数据库的脱敏策略&#xff1a;就是屏蔽敏感的数据 脱敏策略三要求&#xff1a; &#xff08;1&#xff09;表对象 &#xff08;2&#xff09;生效条件&#xff08;脱敏列、脱敏函数&#xff09; &#xff08;3&#xff09;二元组 常见的脱敏策略规则&#xff1a; 替换、重排、…...

Lora原理及实现浅析

Lora 什么是Lora Lora的原始论文为《LoRA: Low-Rank Adaptation of Large Language Models》&#xff0c;翻译为中文为“大语言模型的低秩自适应”。最初是为了解决大型语言模在进行任务特定微调时消耗大量资源的问题&#xff1b;随后也用在了Diffusion等领域&#xff0c;用于…...

力扣热题100之合并两个有序链表

题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 代码 方法一&#xff1a;新建一个链表 这里就先随便新建一个节点作为一个链表的头节点&#xff0c;然后每次遍历都将小的那个节点放入这个链表&#xff0c;遍历完一…...

Linux511SSH连接 禁止root登录 服务任务解决方案 scp Vmware三种模式回顾

创造一个临时文件 引用 scp -p 3712 atthistime.txt code11.1.1.100:/at ssh connect to host 11.1.1.100 port 22:No route to host lost connection 对方虚拟机是[rootlocalhost caozx26]# ll -d /at drwxr-xr-x. 2 root root 6 5月 11 11:10 /at sshd_config文件修改了port为…...

python实现用户登录

使用python实现用户登录&#xff0c;输入用户名和密码&#xff0c;进行验证&#xff0c;正确登录成功&#xff0c;错误登录失败&#xff0c;允许用户输入三次。 代码&#xff1a; 下面展示一些 内联代码片。 for i in range(3):username input(请输入用户名&#xff1a;)pas…...

信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(十五)

个人笔记整理---仅供参考 第十五章项目风险管理 15.1管理基础 15.2项目风险管理过程 15.3规划风险管理 15.4识别风险 15.5实施定性风险分析 15.6实施定量风险分析 15.7规划风险应对 15.8实施风险应对 15.9监督风险...