【Java 实战】实现大转盘抽奖
项目场景
实现一个大转盘抽奖的功能,能后台自定义奖项,各奖项中奖概率,奖品数量,当日抽奖最大次数等。
一、设计思路
这里简单分享下思路:
1.奖品中奖概率
所有参与抽奖的奖项中奖概率之和为 1
2.抽奖规则
这里首先需要明确如何中奖?
一般来说是生成随机数,然后将随机数与奖品的中奖概率相比较,如果小于中奖概率则中奖。
但是,如果每个奖项或者几个奖项的概率一样,上面的方法就会出现每次抽奖,中奖都是同一个奖品的情况
所以我们采用中奖概率累加的方法,如图所示:
抽奖规则:
- 获取该游戏的奖品列表,按照中奖概率升序排列 (可以不用排序)
- 奖品列表中的概率为累加概率,需要按照添加进列表的顺序进行累加
- 生成一个0-1的随机数,与设置好的奖品概率循环比较
- 若随机数小于概率值,则抽中该奖项
3.奖品发放
在设置奖品的时需要设置类型,例如:优惠券
、积分
、商品(实物)
、虚拟商品
等等。
每个类型的奖品发放规则不同,这里可以利用Java多态的特性建立一个工厂类,用不同的实现类来分别实现不同类型的奖品的中奖处理逻辑
这里由于将使用的微服务,所以直接通过feign调用对应的服务方法
二、数据库设计
1.转盘游戏表
存储转盘抽奖信息
字段 | 描述 |
---|---|
id(varchar) | 主键id |
name(varchar) | 活动名称 |
start_time(datetime) | 开始时间 |
end_time(datetime) | 结束时间 |
description(varchar) | 描述 |
day_limit(int) | 单人当天限制次数 0:代表不限制 |
single_limit(int) | 单人总次数限制 0:代表不限制 |
state(int) | 活动状态,1 开启 2 关闭 |
2.奖品表
存储抽奖奖品信息
字段 | 描述 |
---|---|
id(varchar) | 主键id |
game_id(varchar) | 游戏id |
prize_type(int) | 奖品类型(1优惠券,2商品,3积分,4谢谢参与) |
prize_name(varchar) | 奖品名字 |
prize_id(varchar) | 奖品id |
prize_value(int) | 奖品值(数量) |
ratio(double) | 中奖几率 |
current_num(int) | 当前命中 |
max_num(int) | 最大中奖数 0:代表不限制 |
3.抽奖记录表
存储抽奖记录信息
字段 | 描述 |
---|---|
id(varchar) | 主键id |
game_id(varchar) | 游戏id |
user_id(datetime) | 用户id |
user_name(datetime) | 用户名 |
draw_time(varchar) | 抽奖时间 |
is_hit(int) | 是否中奖 0:未中奖 1:中奖 |
hit_prize(varchar) | 中奖奖品 |
is_send(int) | 是否发放 1未发放,2 已发放 3 发放失败 |
send_msg(text) | 发放结果 |
三、业务逻辑
1.提供接口
(1)获取转盘游戏
(2)获取用户的抽奖记剩余次数
(3)参与转盘抽奖
(4)抽奖记录
2.核心代码
抽奖逻辑代码
// 获取奖品信息列表
List<Entity> prizeList = service.list();
// 奖品列表中的概率为累加概率
// 需要按照添加进列表的顺序进行累加,为了数据处理方便中奖几率*100
double sum = 0;
List<Entity> newList = new ArrayList<>();
for (int i = 0; i < prizeList.size(); i++) {
Entity entity = prizeList.get(i);
Entity newEntity = new Entity();
BeanUtils.copyProperties(entity, newEntity);
sum = sum + (entity.getRatio() * 100);
newEntity.setRatio(sum);
newList.add(newEntity);
}
// 生成一个随机数
Random random = new Random();
Double userSelect = random.nextDouble() * 10000;
for (Entity prize : prizeList) {
// 随机数小于中奖几率,则中奖
if (userSelect < prize.getRatio()) {
// 最大中奖数(0:代表不限制次数)
int maxNum = prize.getMaxNum();
// 判断游戏奖品当前中奖数及最大中奖数
if (maxNum != 0 && maxNum <= prize.getCurrentNum()) {
// 超过最大中奖数则不中
break;
} else {
return prize;
}
}
}
// 谢谢参与
List<Entity> prize = prizeList.stream().filter(item -> item.getPrizeType() == 4).collect(Collectors.toList());
if (prize.size() > 0) {
return prize.get(0);
}
return null;
抽奖发放:根据抽中的奖品类型调用对应的服务进行奖品发放;(比如抽中优惠券,就调用优惠券的服务进行发放)
int type = prize.getPrizeType();
try {
switch (type) {
// 发放优惠券
case 1:
ApiResult res = couponFeign.sendCouponToUser(userId, prize.getPrizeId(), prize.getPrizeValue());
// 其他业务处理
break;
// 发放商品
case 2:
break;
// 发放积分
case 3:
ApiResult res2 = pointFeign.addPointToUser(gameId, userId, prize.getPrizeValue());
// 其他业务处理
break;
// 谢谢参与
case 4:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
注意事项:抽奖容易出现并发问题,所以在抽奖的地方,需要加上分布式锁,这里使用Redis官方推荐的Java版的Redis客户端
Redisson中的分布式锁功能
RLock lock = redisson.getLock("turntable:" + gameId);
try {
// 指定超时时间
if (lock.tryLock(10, TimeUnit.SECONDS)) {
// 抽奖业务处理
}
} finally {
lock.unlock();
}
创作不易,关注、点赞就是对作者最大的鼓励,欢迎在下方评论留言
欢迎关注微信公众号:键指JAVA,定期分享Java知识,一起学习,共同成长。