🌸个人主页: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的使用
- 在向用户发送验证码之后,我们需要把验证码存储到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中
- 把活动详细信息存储到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的场景.
- 从Redis中获取活动详细信息,如果Redis中没有查询到,再去MySQL中查询,之后将MySQL中查询到的数据同步到Redis中.
@Override
public 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);
- 抽奖操作完成之后,需要对之前存储的Redis中的详细信息进行更新,把活动的状态,活动奖品的状态和活动用户的状态进行翻转.
if (update){//扭转之后,更新活动相关的信息到缓存中
activityService.cacheActivity(activityStatusConvertDTO.getActivityId());
}
- 抽奖过程中发生异常的时候,把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{
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needConvert(ActivityStatusConvertDTO statusConvertDTO) {
if (statusConvertDTO.getPrizeId() == null ||
statusConvertDTO.getActivityTargetStatus() == null){//如果状态转换种不存在相关参数,直接返回false
return false;
}
//从数据库中查询活动奖品
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityAndPrizeId(
statusConvertDTO.getActivityId(),statusConvertDTO.getPrizeId()
);
//如果没有查询到,返回false
if (activityPrizeDO == null){
return false;
}
//和查询出的数据的状态比较,状态和传入的"完成"状态一致的时候不许要反转
if (activityPrizeDO.getStatus().equals(statusConvertDTO.getPrizeTargetStatus().name())){
return false;
}
return true;
}
@Override
public Boolean convertStatus(ActivityStatusConvertDTO statusConvertDTO) {
activityPrizeMapper.updateStatus(
statusConvertDTO.getActivityId(),
statusConvertDTO.getPrizeId(),
statusConvertDTO.getActivityTargetStatus().name()
);
return true;
}
}
/**
* 转换人员状态
*/
@Component
public class UserOperator extends AbstractActivityOperator{
@Autowired
private ActivityUserMapper activityUserMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public 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;
}
@Override
public 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{
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 2;
}
@Override
public 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;
}
@Override
public 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;
// 遍历currMap
Iterator<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 删除当前 Operator
iterator.remove();
update = true;
}
// 返回
return update;
}
翻转状态之后,需要把中奖人的名单保存进入数据库中,数据中保存的数据包括活动,奖品,人员三方面的信息,具体见WinningRecordDO
.
List<WinningRecordDO> winningRecordDOList = drawPrizeService.saveWinningRecords(drawPrizeParam);//存储获奖者信息
/**
* 中奖记录DO
*/
@Data
public class WinningRecordDO extends BaseDO{
private Long activityId;//活动id
private String activityName;//活动名称
private Long prizeId;//奖品id
private String prizeName;//奖品名称
private String prizeTier;//奖品等级
private Long winnerId;//获奖者id
private 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));//todo
asyncServiceExecutor.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
字段.还有奖品活动联合表,主要用于记录奖品在当前活动中的数量,奖品在当前活动中属于几等奖,还有奖品的状态.还有中奖记录表,主要用于记录那个用户,在那个活动中,中了什么奖.