从零开始的抽奖系统创作(4)
目录
1.活动创建模块
Service:
1.校验活动信息
1.校验人员是否存在
2.校验奖品是否存在
3.校验奖品数量是否大于人员数量
4.校验活动奖品等级有效性
2.保存活动信息
3.保存活动相关的奖品信息
4.保存活动相关的人员信息
5.整合完整的活动信息
6.存放Redis
7.构造返回
2.查询活动列表
1.查询活动总数
2.查询活动列表
3.构造返回
3.从Redis中获取数据
1.查询Redis
2.如果Redis中不存在,查表
3.整合活动完整信息
4.缓存到Redis中
4.抽奖模块
1.从前端获取中奖信息
2. 成功接收到队列中的消息,放入消息队列
3.处理消息队列任务
3.1校验抽奖信息是否有效
1.活动创建模块
Service:
其他模块较为简单不再概述
该模块开启一个事务,当活动创建错误时需要将数据库中的数据回滚
@Transactional(rollbackFor = Exception.class) // 涉及多表
1.校验活动信息
//1.校验活动信息checkActivityInfo(param);
1.校验人员是否存在
//获取输入的人员idList<Long> userIds = param.getActivityUserList().stream().map(CreateUserByActivityParam::getUserId).distinct() //去重.collect(Collectors.toList());
掌握:
流式读取List数据,利用.distinct()进行数据去重
lambda表达式写法:CreateUserByActivityParam::getUserId直接提取ActivityUserList中的用户id
//在数据库中找寻存在的人员List<Long> existsIds = userMapper.selectUserListByIds(userIds);
通过刚才在 传入的数据中获取的用户id 进行数据库查询前端 输入的人员中 在数据库中存在的人员
Mybatis:
@Select("<script>" +" select id from user " +" where id in" +" <foreach item='item' collection='items' open='(' separator=',' close=')'>" +" #{item}" +" </foreach>" +" </script>")List<Long> selectUserListByIds(@Param("items") List<Long> ids);
然后校验数据库中获取的人员id是否为空
if(existsIds == null || existsIds.isEmpty()) {throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_USER_ERROR);}
不为空后,循环遍历userIds 判断是否有人员不存在数据库中
userIds.forEach(id -> {if(!existsIds.contains(id)) {throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_USER_ERROR);}});
2.校验奖品是否存在
//获取输入的奖品idList<Long> prizeIds = param.getActivityPrizeList().stream().map(CreatePrizeByActivityParam::getPrizeId).distinct() //去重.collect(Collectors.toList());
同理crud一下
//在数据库中找寻存在的奖品List<Long> existsPrizeIds = prizeMapper.selectPrizeListByActivity(prizeIds);//判断是否存在prizeIds.forEach(id -> {if(!existsPrizeIds.contains(id)) {throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_PRIZE_ERROR);}});
3.校验奖品数量是否大于人员数量
long userCount = param.getActivityUserList().size();long prizeCount = param.getActivityPrizeList().stream().mapToLong(CreatePrizeByActivityParam::getPrizeAmount).sum();if(userCount > prizeCount) {throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_USER_PRIZE_ERROR);}
掌握:.mapToLong()可以将内部的数据转化为Long类型
.sum() 对数据进行求和(流中获取的)
4.校验活动奖品等级有效性
param.getActivityPrizeList().forEach(prize -> {if(ActivityPrizeTiersEnum.forName(prize.getPrizeTiers()) == null) {throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_PRIZE_TIERS_ERROR);}});
2.保存活动信息
ActivityDO activityDO = new ActivityDO();activityDO.setActivityName(param.getActivityName());activityDO.setDescription(param.getDescription());activityDO.setStatus(ActivityStatusEnum.RUNNING.name());activityMapper.insert(activityDO);
直接将状态设置为 :活动进行中
insert:
@Insert("<script>" +" insert into activity (activity_name, description, status)" +" values (#{activityName}, #{description}, #{status})" +" </script>")@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")int insert(ActivityDO activityDO);
3.保存活动相关的奖品信息
//先获取奖品信息List<CreatePrizeByActivityParam> prizeParams = param.getActivityPrizeList();List<ActivityPrizeDO> activityPrizeDOList = prizeParams.stream().map(prizeParam -> {ActivityPrizeDO activityPrizeDO = new ActivityPrizeDO();activityPrizeDO.setActivityId(activityDO.getId());activityPrizeDO.setPrizeId(prizeParam.getPrizeId());activityPrizeDO.setPrizeAmount(prizeParam.getPrizeAmount());activityPrizeDO.setPrizeTiers(prizeParam.getPrizeTiers());activityPrizeDO.setStatus(ActivityPrizeStatusEnum.INIT.name());return activityPrizeDO;}).collect(Collectors.toList());activityPrizeMapper.batchInsert(activityPrizeDOList);
@Mapper
public interface ActivityPrizeMapper {@Insert("<script>" +" insert into activity_prize (activity_id, prize_id, prize_amount, prize_tiers, status)" +" <foreach collection='items' item='item' index='index' separator=',' >" +" values (#{item.activityId}, #{item.prizeId},#{item.prizeAmount},#{item.prizeTiers}, #{item.status})" +" </foreach>" +" </script>" )@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")int batchInsert(@Param("items") List<ActivityPrizeDO> activityPrizeDOList);
4.保存活动相关的人员信息
//获取人员信息List<CreateUserByActivityParam> userParams = param.getActivityUserList();List<ActivityUserDO> activityUserDOS = userParams.stream().map(userParam -> {ActivityUserDO activityUserDO = new ActivityUserDO();activityUserDO.setActivityId(activityDO.getId());activityUserDO.setUserId(userParam.getUserId());activityUserDO.setUserName(userParam.getUserName());activityUserDO.setStatus(ActivityUserStatusEnum.INIT.name());return activityUserDO;}).collect(Collectors.toList());activityUserMapper.batchInsert(activityUserDOS);
5.整合完整的活动信息
这一步要先获取奖品ids,用于从数据库中获取奖品的详细信息
List<ActivityDetailDTO> activityDetailDTOList = new ArrayList<>();//先获取奖品ids 用于获取数据库中的奖品表基础属性List<Long> prizeIds = param.getActivityPrizeList().stream().map(CreatePrizeByActivityParam::getPrizeId).distinct().collect(Collectors.toList());//从数据库中获取List<PrizeDO> prizeDOList = prizeMapper.selectPrizeListByIds(prizeIds);//整合ActivityDetailDTO detailDTO = convertToActivityDetailDTO(activityDO,activityPrizeDOList, activityUserDOS, prizeDOList);
在整合全部数据这一步是很复杂的一步:
1.先将活动的信息放入整合数据中
ActivityDetailDTO activityDetailDTO = new ActivityDetailDTO();activityDetailDTO.setActivityId(activityDO.getId());activityDetailDTO.setActivityName(activityDO.getActivityName());activityDetailDTO.setDesc(activityDO.getDescription());activityDetailDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
2.将用户数据放入 整合数据
//构造一个UserDTOList<ActivityDetailDTO.ActivityUserDTO> auDTOs = activityUserDOS.stream().map(auDO -> {ActivityDetailDTO.ActivityUserDTO auDTO = new ActivityDetailDTO.ActivityUserDTO();auDTO.setActivityUserId(auDO.getUserId());auDTO.setActivityUserName(auDO.getUserName());auDTO.setStatus(ActivityUserStatusEnum.forName(auDO.getStatus()));return auDTO;}).collect(Collectors.toList());activityDetailDTO.setUserDTOList(auDTOs);
3.将奖品数据放入 整合数据
但是要注意奖品数据来源有两个对象:
List<ActivityPrizeDO>:活动与奖品关联--->可以得到奖品与活动关联的属性
List<PrizeDO>:奖品--->可以得到奖品的基本属性
//构造一个pDTO 其中使用Optional进行遍历中遍历List<ActivityDetailDTO.PrizeDTO> aDTOs = activityPrizeDOList.stream().map(apDO -> {ActivityDetailDTO.PrizeDTO aDTO = new ActivityDetailDTO.PrizeDTO();aDTO.setId(apDO.getPrizeId());//从prizeDOList中循环遍历//运用 filter 方法筛选出 PrizeDO 对象,筛选条件为该对象的 id 与 apDO 的 prizeId 相等。//利用 findFirst 方法返回符合条件的第一个元素,将其封装到 Optional 对象中。// 若列表为空或者没有匹配项,Optional 对象为空。Optional<PrizeDO> optional = prizeDOList.stream().filter(pDO -> pDO.getId().equals(apDO.getPrizeId())).findFirst();//如果optional为空不执行该方法optional.ifPresent(pDO -> {aDTO.setName(pDO.getName());aDTO.setDescription(pDO.getDescription());aDTO.setPrice(pDO.getPrice());aDTO.setImageUrl(pDO.getImageUrl());});aDTO.setPrizeAmount(apDO.getPrizeAmount());aDTO.setPrizeTiers(ActivityPrizeTiersEnum.forName(apDO.getPrizeTiers()));aDTO.setStatus(ActivityPrizeStatusEnum.forName(apDO.getStatus()));return aDTO;}).collect(Collectors.toList());
我们选择流式遍历奖品活动关联数据,其中基本属性要从奖品数据中循环读取
所以有流中套流:
利用:Optional<PrizeDO> 过滤不要的数据(根据两者中的公共属性奖品id)
.filter() :与map使用类似
findFirst():取满足.filter() 中的第一个数据(达到去重)
optional.ifPresent():如果内部数据为空就不会执行流式遍历(不用使用if判空了)
6.存放Redis
cacheActivityDetailDTO(detailDTO);
为了事务在数据存入Redis中失败时不进行回滚 (校验时可以再从数据库中读取),
try捕获一下异常
/*** 缓存活动信息到Redis中** @param detailDTO*/private void cacheActivityDetailDTO(ActivityDetailDTO detailDTO) {if(detailDTO == null || detailDTO.getActivityId() == null) {logger.warn("cacheActivityDetailDTO 缓存到Redis中失败!, detailDTO:{}",JacksonUtil.writeValueAsString(detailDTO));}//当缓存失败后但是sql没存储失败,不希望回滚try{//TODO 缓存到Redis中redisUtil.set(ACTIVITY_PREFIX + detailDTO.getActivityId(),JacksonUtil.writeValueAsString(detailDTO), ACTIVITY_OUT_TIME);}catch (Exception e) {logger.error("cacheActivityDetailDTO 缓存到Redis中失败!", e);}}
7.构造返回
CreateActivityDTO createActivityDTO = new CreateActivityDTO();createActivityDTO.setActivityId(activityDO.getId());return createActivityDTO;
2.查询活动列表
向前端返回两个参数:
活动总数据量:便于用于计算页数
活动列表:前端展示的数据
@Data
public class FindActivityListResult implements Serializable {/*** 活动总数据量*/private Integer total;/*** 活动列表*/private List<ActivityInfo> records;@Datapublic static class ActivityInfo implements Serializable {/*** 活动id*/private Long activityId;/*** 活动名称*/private String activityName;/*** 活动描述*/private String description;/*** 活动是否有效*/private Boolean valid;}
}
业务:
1.查询活动总数
Integer total = activityMapper.count();
2.查询活动列表
List<ActivityDO> activityDOList = activityMapper.selectActivityList(param.offset(), param.getPageSize());
查找翻表数据(根据两个参数:起始页,一页展示的数据个数)
/*** 获取翻页数据* @param offset 展示的起始索引* @param pageSize 一页展示个数* @return*/@Select("Select * from activity order by id desc limit #{offset}, #{pageSize}")List<ActivityDO> selectActivityList(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);
3.构造返回
List<ActivityDTO> activityDTOList = activityDOList.stream().map(activityDO -> {ActivityDTO activityDTO = new ActivityDTO();activityDTO.setActivityId(activityDO.getId());activityDTO.setActivityName(activityDO.getActivityName());activityDTO.setDescription(activityDO.getDescription());activityDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));return activityDTO;}).collect(Collectors.toList());
3.从Redis中获取数据
从Redis中获取活动信息,如果不存在,从数据库中获取并保存到Redis中
1.查询Redis
ActivityDetailDTO detailDTO = getActivityFromCache(activityId);if(detailDTO != null){logger.info("从Redis中获取活动信息成功!, activityId:{}", activityId);return detailDTO;}
调用RedisUtil方法:
/*** 通过活动id在Redis中获取活动信息* @param activityId* @return*/private ActivityDetailDTO getActivityFromCache(Long activityId) {if(activityId == null) {logger.warn("getActivityFromCache 获取Redis的activityId异常!, activityId:{}",activityId);}try{String activityJson = redisUtil.get(ACTIVITY_PREFIX + activityId);if(activityJson == null) {logger.info("getActivityFromCache 获取Redis中的活动信息失败!, key:{}",ACTIVITY_PREFIX + activityId);return null;}return JacksonUtil.readValue(activityJson, ActivityDetailDTO.class);} catch (Exception e) {logger.error("从Redis中获取活动信息异常, key:{}", ACTIVITY_PREFIX + activityId);return null;}}
2.如果Redis中不存在,查表
//活动表ActivityDO activityDO = activityMapper.selectById(activityId);//活动奖品表List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectAByActivityId(activityId);//活动人员表List<ActivityUserDO> activityUserDOS = activityUserMapper.selectByActivityId(activityId);//奖品表//先获取奖品ids 用于获取数据库中的奖品表基础属性List<Long> prizeIds = activityPrizeDOList.stream().distinct().map(ActivityPrizeDO::getPrizeId).collect(Collectors.toList());//从数据库中获取List<PrizeDO> prizeDOList = prizeMapper.selectPrizeListByIds(prizeIds);
与上一个模块有点像,先获取奖品id再查询数据库
3.整合活动完整信息
detailDTO = convertToActivityDetailDTO(activityDO,activityPrizeDOList, activityUserDOS, prizeDOList);
4.缓存到Redis中
cacheActivityDetailDTO(detailDTO);
4.抽奖模块
涉及到rabbitMQ的使用,这里不做介绍,以后会有更详细的记载
1.从前端获取中奖信息
直接返回前端信息,然后通过异步处理 中奖信息 将其放入消息队列中加快前端返回给用户的速度,提高用户体验。
/*** 异步抽奖,接⼝只做奖品数校验即可返回。** @param param* @return*/@RequestMapping("/draw-prize")public CommonResult<Boolean> drawPrize(@Validated @RequestBody DrawPrizeParam param) {logger.info("DrawPrizeController drawPrize param:{}", JacksonUtil.writeValueAsString(param));drawPrizeService.drawPrize(param);return CommonResult.success(true);}
2. 成功接收到队列中的消息,放入消息队列
public void drawPrize(DrawPrizeParam param) {logger.info("DrawPrizeServiceImpl drawPrize param:{}",param);// 发送消息到消息队列 交换机,路由键(绑定的key),消息体Map<String, String> map = new HashMap<>();map.put("messageId", String.valueOf(UUID.randomUUID()));map.put("messageDate", JacksonUtil.writeValueAsString(param));rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING, map);logger.info("mq消息发送成功, map:{}", JacksonUtil.writeValueAsString(map));}
3.处理消息队列任务
logger.info("Mq成功接收到消息 message:{}", JacksonUtil.writeValueAsString(message));String paramString = message.get("messageDate");DrawPrizeParam drawPrizeParam = JacksonUtil.readValue(paramString, DrawPrizeParam.class);
使用:
@RabbitListener(queues = "DirectQueue"):rabbitMQ绑定
@RabbitHandler:监听器
3.1校验抽奖信息是否有效
1.校验活动与奖品是否存在
2.活动是否有效 根据活动状态判断
3.奖品是否有效
4.获奖人数与奖品数量是否相等
@Overridepublic void checkDrawPrizeParam(DrawPrizeParam param) {//1.校验活动与奖品是否存在// 根据活动ID查询活动信息ActivityDO activityDO = activityMapper.selectById(param.getActivityId());// 根据活动ID和奖品ID查询活动奖品信息ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());if(activityDO == null || activityPrizeDO == null){throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_OR_PRIZE_EMPTY);}//2.活动是否有效 根据活动状态判断if(ActivityStatusEnum.COMPLETED.name().equalsIgnoreCase(activityDO.getStatus())){throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_NOT_RUNNING);}//3.奖品是否有效if(ActivityPrizeStatusEnum.COMPLETED.name().equalsIgnoreCase(activityPrizeDO.getStatus())){throw new ServiceException(ServiceErrorCodeConstant.ACTIVITY_PRIZE_NOT_RUNNING);}//4.获奖人数与奖品数量是否相等if(param.getWinnerList().size() != activityPrizeDO.getPrizeAmount()){throw new ServiceException(ServiceErrorCodeConstant.WINNER_PRIZE_AMOUNT_ERROR);}}