背景
最近,由于项目需要,在产品同事的合作下专门设计并开发了一个基于概率的抽奖、开奖程序。我们先看下需求:
需求
基于现有的用户积分信息开发一套世界杯抽奖、开奖程序。首先,每个用户每天完成日常任务后可参与现金红包抽奖,中奖概率为随机的,不做人为隐形设定。但是系统可以配置现金奖池大小,根据奖池大小,每天最多发出相应金额的红包。其次,现金红包有不同的金额范围限制,不同的范围中奖概率不同。另外,根据用户操作等,积分榜是实时变动的。活动结束后积分榜前三名和积分榜4-50中随机抽取的一名,共4名小伙伴可获得系统大奖。设计一个公开、公正的算法从第4名至第50名中随机抽取一个幸运儿。
设计
由于奖池和红包部分相对复杂,所以编写程序处理;随机抽取幸运儿相对简单,利用第三方数据源和独立公开算法计算出幸运儿名次即可。
幸运的宝宝
这部分相对简单,先说下规则:
LuckyNumber = Parameter % (50 - 3) + 4
Parameter取值为 中国银行美元外汇牌价 2018/07/16 12:00前(世界杯结束后,平台于07/16中午公开开奖)最后一条记录的现汇买入价去小数点后的值;为了保证公平性或防止意外,可从多平台取值,即
LuckyNumber = (Parameter1 + Parameter2 + Parameter3) % (50 - 3) + 4
50 - 3的意思是只有47人参与抽奖
余数是从0开始的,而中奖者序号从4开始,所以+4
- 举例
假如2018/07/16查询得到如下外汇信息:
则:LuckyNumber = Parameter % (50 - 3) + 4 = 66132 % 47 + 4 = 3 + 4 = 7
榜单第7名即为幸运用户。
红包抽奖
配置
- level=N # 0-9
- level.prize=p0, p1, p2, … , pn
- level.rate=r0, r1 … , rn-1
配置示例
- level=3
- level.amount=100, 1000, 9000, 10000
- level.rate=100, 20, 10
示例含义
红包分为三个等级,[1.00, 10.00)的概率是1%(100/10000),[10.00-90.00)的概率是0.2%,[90.00-100.00]的概率是0.1%。
示例程序
package test;
import java.util.Random;
import com.alibaba.fastjson.JSONObject;
public class T {
private static final int LEVEL = 3;
private static final int[] LEVEL_AMOUNT = new int[] { 100, 1000, 9000, 10000 };
private static final int[] LEVEL_RATE = new int[] { 100, 20, 10 };
public static void main(String[] args) {
System.out.println(prize());
}
/**
* {"lucky":9282,"prize":false}
* {"lucky":50,"amount":980,"level":0,"prize":true}
* lucky:幸运数
* prize:中奖标识,true-中奖,false-没中奖
* level:中奖等级
* amount:中奖金额
* @return
*/
public static JSONObject prize() {
JSONObject result = new JSONObject();
Random r = new Random();
// 服务器每天第一次收到抽奖请求或相关请求时初始化当日奖池信息,
// 或定时器每天自动初始化奖池信息(凌晨高并发请求时可能查不到当日奖池信息)
// 生成幸运数
int lucky = r.nextInt(10000);
result.put("lucky", lucky);
// 声明中奖等级,默认不中奖,值为-1;或中奖值为0,1,2
int level = -1;
// 若lucky < level.rate0,则level=0;
// 否则,若lucky < level.rate0 + level.rate1,则level=1
// ...
int rate = 0;
for (int i = 0; i < LEVEL_RATE.length; i++) {
rate += LEVEL_RATE[i];
if (lucky < rate) {
level = i;
break;
}
}
if (level > -1) {
result.put("prize", true);
result.put("level", level);
int amount = 0;
// 最后一个中奖等级的奖金范围是闭区间,其他都是左闭右开
if (level + 1 == LEVEL) {
// nextInt(N)的范围是[0,N),nextInt(N * 10) / 10的范围是[0,N]
int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level] + 1) * 10;
amount = r.nextInt(target) / 10 + (int) LEVEL_AMOUNT[level];
} else {
// nextInt(N-M)的范围是[0, N-M),nextInt(N-M) + M的范围是[M,N)
int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level]);
amount = r.nextInt(target) + (int) LEVEL_AMOUNT[level];
}
int pool_unusedAmount = 1000000; // 查询当日奖池可用金额
// 奖池可用金额不足时,按奖池剩余金额支付
if (amount > pool_unusedAmount) {
amount = pool_unusedAmount;
}
// 更新当日奖池信息,保存用户中奖数据
result.put("amount", amount);
} else {
result.put("prize", false);
}
return result;
}
}