1 概述
项目开发中经常会有抽奖这样的营销活动的需求,例如:积分大转盘、刮刮乐、老虎机等等多种形式,其实后台的实现方法是一样的,本文介绍一种常用的抽奖实现方法。
整个抽奖过程包括以下几个方面:
奖品
奖品池
抽奖算法
奖品限制
奖品发放
2 奖品
奖品包括奖品、奖品概率和限制、奖品记录。
奖品表:
CREATE TABLE `points_luck_draw_prize` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL COMMENT '奖品名称', `url` varchar(50) DEFAULT NULL COMMENT '图片地址', `value` varchar(20) DEFAULT NULL, `type` tinyint(4) DEFAULT NULL COMMENT '类型1:红包2:积分3:体验金4:谢谢惠顾5:自定义', `status` tinyint(4) DEFAULT NULL COMMENT '状态', `is_del` bit(1) DEFAULT NULL COMMENT '是否删除', `position` int(5) DEFAULT NULL COMMENT '位置', `phase` int(10) DEFAULT NULL COMMENT '期数', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=164 DEFAULT CHARSET=utf8mb4 COMMENT='奖品表';
奖品概率限制表:
CREATE TABLE `points_luck_draw_probability` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `points_prize_id` bigint(20) DEFAULT NULL COMMENT '奖品ID', `points_prize_phase` int(10) DEFAULT NULL COMMENT '奖品期数', `probability` float(4,2) DEFAULT NULL COMMENT '概率', `frozen` int(11) DEFAULT NULL COMMENT '商品抽中后的冷冻次数', `prize_day_max_times` int(11) DEFAULT NULL COMMENT '该商品平台每天最多抽中的次数', `user_prize_month_max_times` int(11) DEFAULT NULL COMMENT '每位用户每月最多抽中该商品的次数', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4 COMMENT='抽奖概率限制表';
奖品记录表:
CREATE TABLE `points_luck_draw_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `member_id` bigint(20) DEFAULT NULL COMMENT '用户ID', `member_mobile` varchar(11) DEFAULT NULL COMMENT '中奖用户手机号', `points` int(11) DEFAULT NULL COMMENT '消耗积分', `prize_id` bigint(20) DEFAULT NULL COMMENT '奖品ID', `result` smallint(4) DEFAULT NULL COMMENT '1:中奖 2:未中奖', `month` varchar(10) DEFAULT NULL COMMENT '中奖月份', `daily` date DEFAULT NULL COMMENT '中奖日期(不包括时间)', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3078 DEFAULT CHARSET=utf8mb4 COMMENT='抽奖记录表';
3 奖品池
奖品池是根据奖品的概率和限制组装成的抽奖用的池子。主要包括奖品的总池值和每个奖品所占的池值(分为开始值和结束值)两个维度。
奖品的总池值:所有奖品池值的总和。
每个奖品的池值:算法可以变通,常用的有以下两种方式 :
奖品的概率*10000(保证是整数)
奖品的概率10000奖品的剩余数量
奖品池bean:
public class PrizePool implements Serializable{ /** * 总池值 */ private int total; /** * 池中的奖品 */ private List<PrizePoolBean> poolBeanList; }
池中的奖品bean:
public class PrizePoolBean implements Serializable{ /** * 数据库中真实奖品的ID */ private Long id; /** * 奖品的开始池值 */ private int begin; /** * 奖品的结束池值 */ private int end; }
奖品池的组装代码:
/** * 获取超级大富翁的奖品池 * @param zillionaireProductMap 超级大富翁奖品map * @param flag true:有现金 false:无现金 * @return */ private PrizePool getZillionairePrizePool(Map<Long, ActivityProduct> zillionaireProductMap, boolean flag) { //总的奖品池值 int total = 0; List<PrizePoolBean> poolBeanList = new ArrayList<>(); for(Entry<Long, ActivityProduct> entry : zillionaireProductMap.entrySet()){ ActivityProduct product = entry.getValue(); //无现金奖品池,过滤掉类型为现金的奖品 if(!flag && product.getCategoryId() == ActivityPrizeTypeEnums.XJ.getType()){ continue; } //组装奖品池奖品 PrizePoolBean prizePoolBean = new PrizePoolBean(); prizePoolBean.setId(product.getProductDescriptionId()); prizePoolBean.setBengin(total); total = total + product.getEarnings().multiply(new BigDecimal("10000")).intValue(); prizePoolBean.setEnd(total); poolBeanList.add(prizePoolBean); } PrizePool prizePool = new PrizePool(); prizePool.setTotal(total); prizePool.setPoolBeanList(poolBeanList); return prizePool; }
4 抽奖算法
整个抽奖算法为:
- 随机奖品池总池值以内的整数
- 循环比较奖品池中的所有奖品,随机数落到哪个奖品的池区间即为哪个奖品中奖。
抽奖代码:
public static PrizePoolBean getPrize(PrizePool prizePool){ //获取总的奖品池值 int total = prizePool.getTotal(); //获取随机数 Random rand=new Random(); int random=rand.nextInt(total); //循环比较奖品池区间 for(PrizePoolBean prizePoolBean : prizePool.getPoolBeanList()){ if(random >= prizePoolBean.getBengin() && random < prizePoolBean.getEnd()){ return prizePoolBean; } } return null; }
5 奖品限制
实际抽奖中对一些比较大的奖品往往有数量限制,比如:某某奖品一天最多被抽中5次、某某奖品每位用户只能抽中一次。。等等类似的限制,对于这样的限制我们分为两种情况来区别对待:
- 限制的奖品比较少,通常不多于3个:这种情况我们可以再组装奖品池的时候就把不符合条件的奖品过滤掉,这样抽中的奖品都是符合条件的。例如,在上面的超级大富翁抽奖代码中,我们规定现金奖品一天只能被抽中5次,那么我们可以根据判断条件分别组装出有现金的奖品和没有现金的奖品。
- 限制的奖品比较多,这样如果要采用第一种方式,就会导致组装奖品非常繁琐,性能低下,我们可以采用抽中奖品后校验抽中的奖品是否符合条件,如果不符合条件则返回一个固定的奖品即可。
6 奖品发放
奖品发放可以采用工厂模式进行发放:不同的奖品类型走不同的奖品发放处理器,示例代码如下:
奖品发放:
/** * 异步分发奖品 * @param prizeList * @throws Exception */ @Async("myAsync") @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public Future<Boolean> sendPrize(Long memberId, List<PrizeDto> prizeList){ try { for(PrizeDto prizeDto : prizeList){ //过滤掉谢谢惠顾的奖品 if(prizeDto.getType() == PointsLuckDrawTypeEnum.XXHG.getType()){ continue; } //根据奖品类型从工厂中获取奖品发放类 SendPrizeProcessor sendPrizeProcessor = sendPrizeProcessorFactory.getSendPrizeProcessor( PointsLuckDrawTypeEnum.getPointsLuckDrawTypeEnumByType(prizeDto.getType())); if(ObjectUtil.isNotNull(sendPrizeProcessor)){ //发放奖品 sendPrizeProcessor.send(memberId, prizeDto); } } return new AsyncResult<>(Boolean.TRUE); }catch (Exception e){ //奖品发放失败则记录日志 saveSendPrizeErrorLog(memberId, prizeList); LOGGER.error("积分抽奖发放奖品出现异常", e); return new AsyncResult<>(Boolean.FALSE); } }
工厂类:
@Component public class SendPrizeProcessorFactory implements ApplicationContextAware{ private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public SendPrizeProcessor getSendPrizeProcessor(PointsLuckDrawTypeEnum typeEnum){ String processorName = typeEnum.getSendPrizeProcessorName(); if(StrUtil.isBlank(processorName)){ return null; } SendPrizeProcessor processor = applicationContext.getBean(processorName, SendPrizeProcessor.class); if(ObjectUtil.isNull(processor)){ throw new RuntimeException("没有找到名称为【" + processorName + "】的发送奖品处理器"); } return processor; } }
奖品发放类举例:
/** * 红包奖品发放类 */ @Component("sendHbPrizeProcessor") public class SendHbPrizeProcessor implements SendPrizeProcessor{ private Logger LOGGER = LoggerFactory.getLogger(SendHbPrizeProcessor.class); @Resource private CouponService couponService; @Resource private MessageLogService messageLogService; @Override public void send(Long memberId, PrizeDto prizeDto) throws Exception { // 发放红包 Coupon coupon = couponService.receiveCoupon(memberId, Long.parseLong(prizeDto.getValue())); //发送站内信 messageLogService.insertActivityMessageLog(memberId, "你参与积分抽大奖活动抽中的" + coupon.getAmount() + "元理财红包已到账,谢谢参与", "积分抽大奖中奖通知"); //输出log日志 LOGGER.info(memberId + "在积分抽奖中抽中的" + prizeDto.getPrizeName() + "已经发放!"); } }