一次简单的转盘抽奖活动我是怎么做的

/ 后端 / 没有评论 / 267浏览

活动结束的截图

1.抽奖功能及难点

功能: (1)每种奖品设立每日库存,每天指定时间刷新库存 (2)奖品有消费门槛,例如消费过10000元的用户优先抽实体物品,并且是尽量每个消费够的用户尽量都能抽到实物 (3)每个人只能抽中一次实体奖品 (4)所有人都有概率抽中谢谢惠顾,并且每日也是有限额的 难点: (1)业务需求实现 (2)避免人多抢购并发请求导致数据与预期值不一致,也就是如秒杀导致库存数据错误

2.伪业务代码的实现

    @ApiOperation("进行抽奖")
    @LoginLimitSubmit(lockTime = 3000)  //防重复提交防护
    @PostMapping(value = "/go")
    public Rest<> go() {
        //一系列校验检查:活动时间,用户身份,是否存在抽奖次数等
        checkResidueCount();

        //由于用户可能有多次抢购次数,根据用户id,进行防抖处理,3秒内只能请求一次
        if (!RedisLockUtil.tryLock("/raffle/go" + userIdInt, WfIdWorkerUtil.getIdStr(), 3000)) {
                    return Rest.error("您点的太快了,请稍后再试~");
        }

        //活动暂停判断,预防突发问题,可随时终止抽奖
        Integer h5RafflePause = distributionService.queryCacheDistributionByKey("h5_raffle_pause").getValue();
        if (h5RafflePause != null && h5RafflePause == 1) {
            return Rest.error("活动太火爆了,请稍后再试~");
        }

         //随机抽到谢谢参与
         if (isRandomThank()) {
             saveThanksLog(userIdInt);
             WfLuckyEntity thankEntity = new WfLuckyEntity();
             thankEntity.setId(-1);
             thankEntity.setGood("谢谢参与");
             vo.setWfLuckyEntity(thankEntity);
             return Rest.ok(vo);
         }

         //进行抽奖奖品
         WfLuckyEntity wfLuckyEntity = randomRaffle();

         //进行发放虚拟或保存实物奖品,以下代码忽略...
    }

    private boolean isRandomThank() {
         //缓存中获取抽奖谢谢参与概率及次数
         String configStr = distributionService.queryCacheDistributionByKey("h5_raffle_config").getRemark();
         if (StringUtils.isBlank(configStr)) {
             return false;
         }
         //每天抽中次数
         int thankCount = 0;
         //概率
         double thankProbability = 0;
         try {
             JSONObject jsonObject = JSON.parseObject(configStr);
             thankCount = jsonObject.getIntValue("thankCount");
             thankProbability = jsonObject.getDoubleValue("thankProbability");
         } catch (Exception e) {
             log.error("转换失败", e);
             return false;
         }
         if (thankCount < 1 || thankProbability < 0.1) {
             return false;
         }

         WfLuckyEntity thankLucky = new WfLuckyEntity();
         thankLucky.setId(-1);
         thankLucky.setEveryDayCount(thankCount);

         //此处默认thankProbability=0.3,及大概百分之30的人抽中,与1-10随机数除以10进行大于等于对比
         if (!(thankProbability >= RandomUtil.randomInt(1, 11) / 10f)) {
             return false;
         }
         return checkDistributeAndIncr(thankLucky);
     }

     //进行抽奖
     public WfLuckyEntity randomRaffle() {
         //从缓存中获取奖品
         List<WfLuckyEntity> luckys = luckyService.queryCache();
         //根据用户消费金额,筛选可以抽中的礼物,优先实物还是虚拟,如果实物还要筛选去除用户已经抽中过的实物
         List<WfLuckyEntity> userLuckys = queryLuckysByUser();
         //缓存判断实物商品当前剩余库存,尽可能的让消费够的用户抽中其中一款实物,此处逻辑为每3秒从redis中获取一次实时抽奖数量
         List<WfLuckyEntity> newLuckList = WfLocalCacheTimeUtil.get(3, "getHaveList" + JSON.toJSONString(userLuckys), List.class, () -> {
                 List<WfLuckyEntity> newList = new ArrayList<>();
                 try (Jedis jedis = jedisPool.getResource()) {
                     jedis.select(6);
                     for (WfLuckyEntity luckyEntity : userLuckys) {
                         WfLuckyEntity newLuck = new WfLuckyEntity();
                         String key = "talkshow-raffle:" + luckyEntity.getId() + "-" + LocalDateTimeUtil.getTimeStr(LocalDateTime.now(), "yyyy-MM-dd");
                         String countStr = jedis.get(key);
                         if (StringUtils.isBlank(countStr)) {
                             countStr = "0";
                         }
                         BeanUtils.copyProperties(luckyEntity, newLuck);
                         newLuck.setCurrentCount(Long.parseLong(countStr));
                         newList.add(newLuck);
                     }
                 } catch (Exception e) {
                 }
                 return newList;
         });

         //其中省略虚拟物品抽奖,虚拟物品中有设置库存无限的商品,即用户最后至少能获得虚拟物品

         //将还有库存的商品进行抽奖
         return userLuckys.get(RandomUtil.randomInt(0, userLuckys.size()));
     }

     //减库存判断抽中逻辑,核心扣减逻辑
     public boolean checkDistributeAndIncr(WfLuckyEntity wfLuckyEntity) {
             //奖品id
             Integer luckyEntityId = wfLuckyEntity.getId();
             //每日奖品数量
             Integer everyDayCount = wfLuckyEntity.getEveryDayCount();
             if (everyDayCount == null) {
                 log.error("没有库存" + everyDayCount);
                 return false;
             }

             //使用redis对日期和奖品组成的key,进行递增处理,判断递增后的值是否不大于当日最大抽奖数,如不大于则代表抽成功
             String key = "talkshow-raffle:" + luckyEntityId + "-" + LocalDateTimeUtil.getTimeStr(LocalDateTime.now(), "yyyy-MM-dd");
             try (Jedis jedis = jedisPool.getResource()) {
                 jedis.select(6);
                 Long incr = jedis.incr(key);
                 if (incr == null) {
                     return false;
                 }
                 return incr <= everyDayCount;
             } catch (Exception e) {
             }
             return false;
     }

3.代码解析

(1)做好接口防护,前后端都要做好防重复提交和防抖处理,还可加入限流; (通过业务方式前期拦截大部分用户抽奖或者伪抽奖方式此处不谈) (2)其中查询的数据能用缓存就用缓存,优先运行时缓存,其次使用redis等

(3)此处避免库存超抢,用到了redis的原子性保障的递增,也可以用递减方法; 原理就是先用redis进行操作库存后,那么后续所有操作都不存在并发数据问题,当然此方法要保障redis的可靠性;