首页 > 编程语言 >用 Java 徒手写一个抽奖系统,拿去用吧

用 Java 徒手写一个抽奖系统,拿去用吧

时间:2023-10-12 13:06:04浏览次数:50  
标签:COMMENT 奖品 抽奖 Java DEFAULT int NULL 徒手


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、抽奖算法

整个抽奖算法为:

1. 随机奖品池总池值以内的整数 

2. 循环比较奖品池中的所有奖品,随机数落到哪个奖品的池区间即为哪个奖品中奖。

抽奖代码:

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次、某某奖品每位用户只能抽中一次。。等等类似的限制,对于这样的限制我们分为两种情况来区别对待:

1. 限制的奖品比较少,通常不多于3个:这种情况我们可以再组装奖品池的时候就把不符合条件的奖品过滤掉,这样抽中的奖品都是符合条件的。例如,在上面的超级大富翁抽奖代码中,我们规定现金奖品一天只能被抽中5次,那么我们可以根据判断条件分别组装出有现金的奖品和没有现金的奖品。

2. 限制的奖品比较多,这样如果要采用第一种方式,就会导致组装奖品非常繁琐,性能低下,我们可以采用抽中奖品后校验抽中的奖品是否符合条件,如果不符合条件则返回一个固定的奖品即可。

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() + "已经发放!");
    }
}

PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦

标签:COMMENT,奖品,抽奖,Java,DEFAULT,int,NULL,徒手
From: https://blog.51cto.com/zhongmayisheng/7825626

相关文章

  • Java常用类,这一次帮你总结好
    常用类概述:内部类Object类包装类数学类时间类字符串StringBuilder和StringBufferDecimalFormat-   一、内部类  -概念:在一个类内部再定义一个完整的类。一般情况下类与类之间是相互独立的,内部类的意思就是打破这种独立思想,让一个类成为另一个类的内部信息,和成员变量、成......
  • JAVA
    1.JVM相关对于刚刚接触Java的人来说,JVM相关的知识不一定需要理解很深,对此里面的概念有一些简单的了解即可。不过对于一个有着3年以上Java经验的资深开发者来说,不会JVM几乎是不可接受的。JVM作为java运行的基础,很难相信对于JVM一点都不了解的人可以把java语言吃得很透。我在面......
  • 几个 Java 性能调优技巧,YYDS
    大多数开发者认为性能优化是一个复杂的话题,它需要大量的工作经验和相关知识理论。好吧,这也不完全错。优化一个应用做到性能最优化可能不是件容易的任务,但是这并不意味着你没有相关的知识就什么也做不了。这里有一些易于遵循的建议和最佳实践可以帮助你创建一个性能良好的应用程序。......
  • Java 18 要来了,你不会还在用Java 8吧
    Java开发工具包(JDK)18将于2022年3月22日发布。新版本的标准Java将有九个新特性,该特性集已于12月9日冻结,进入Rampdown第一阶段。值得注意的是:JDK17是一个长期支持(LTS)版本,将获得Oracle至少八年的支持,但JDK18将是一个短期功能版本,只支持六个月。可以在......
  • Java通过itext解析PDF中的关键字得到坐标进行插入印章图片或签名
    需求因需提高公司运转效率,提倡去无纸化操作,减少人力等前提;通过系统将审核通过后的pdf文档进行盖电子印章或电子签名等功能;测试效果如下:图1图2实现思路因如上图1中,存在动态表格,所以文档的布局是随数据而变的,可能是多页,可能是一页,且内容上下浮动,所以得通过解析文档内容,通过......
  • 报错解决:java.security.InvalidKeyException: Illegal key size(微信支付v3遇到的问
    前言在使用微信支付v3生成jar包后本地测试没有问题在开发小程序支付功能的时候:本地开发好好的,放在linux服务器上运行时碰到报错原因是因为微信支付256位秘钥加密解密策略 可能会导致某些jdk的版本加密解密出现问题解决首先观察你这个目录下的文件根据文件内容做判断看下......
  • 为什么 Java 中“1000==1000”为false,而”100==100“为true?
    这是一个挺有意思的讨论话题。如果你运行下面的代码:Integera=1000,b=1000;System.out.println(a==b);//1Integerc=100,d=100;System.out.println(c==d);//2你会得到:falsetrue基本知识:我们知道,如果两个引用指向同一个对象,用表示它们......
  • Java 线程池
    目录线程池线程池创建方式通过ThreadPoolExecutor创建线程池ThreadPoolExecutor的总体设计ThreadPoolExecutor的继承关系ThreadPoolExecutor的运行机制ThreadPoolExecutor生命周期管理任务执行机制线程池线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池......
  • Java word文本分词器简单使用
    1、引入依赖<dependency><groupId>org.apdplat</groupId><artifactId>word</artifactId><version>1.2</version></dependency>2、使用@OverridepublicList&l......
  • 87基于java的流浪动物领养系统设计与实现(配套lun文,PPT,可参考做毕业设计)
    本章节给大家带来一个基于java流浪动物领养系统设计与实现,可适用于流浪动物救助及领养管理系统,宠物教学、领养宠物、宠物认领、领养申请、动物认领信息,动物申请认领等等;项目背景科学技术日新月异的如今,计算机在生活各个领域都占有重要的作用,尤其在信息管理方面,在这样的大背景......