积分抽奖方案设计
前言
积分抽奖作为一种创新的激励机制,正逐渐成为各大平台吸引用户、增强用户粘性、促进用户活跃度的关键手段。积分抽奖方案的设计,不仅仅是简单的奖励设置与概率调配,它更是一种深度洞察用户心理、巧妙平衡成本效益与用户体验的艺术。本文聚焦于“积分抽奖方案”的设计,旨在深入剖析其背后的逻辑、策略与实践。
原理分析
概率抽奖方案
概率抽奖原理参考于扇形统计图和转盘抽奖,当我们某一奖品占比越大,那么它在我们图形中所占用权重面积也就越多,相对应被抽中的概率也就越大。当我们把扇形统计图摊开成为矩形时(如下图),根据我们所规定各奖品的抽中概率去分配各奖品所占用的权重区域。
上图中,假设把我们手指当作转盘抽奖上的指针,那么手指随意在
0-1000总权重线段上左右摆动(随机数)最后停留在某个奖品区间内的概率与它的区间大小是息息相关的。“天命抽奖”方案
此方案参考于某游戏或者某刀刀那0.00000001%的抽奖概率(个人观点-对于公布抽取概率与规则略表怀疑)。此抽奖规则跟概率抽奖原理相差不多,区别在于可控制大奖所出现的区间范围,比如在前850次时,
特等奖不会出现在奖池中,在850次到900次时将大奖加入奖池中,如果在区间内没抽取到,则在第900次进行保底抽中。
当然上述的两种规则只是通用的基础方案,实际很多大平台中抽奖规则可能来源于多方面因素。比如新用户几率会比老用户高、平台内金额消费的越多中奖概率越小、同个地区内人数越多概率越小、通过多个平台抽取用户画像去识别消费能力从而定义不同的概率高低等等(这些只是个人观点及使用某些平台后的一些猜想,城市套路深。。。🙄)
代码示例
创建统一奖品实体类
1 | /*** |
概率抽奖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93/***
* 积分抽奖 概率随机
* @author deng
* @date 2024/7/4
*/
public class DrawTest {
private static Integer count = 1;
/***
* 抽奖返回奖品方法
* @param prizeList
* @return
*/
private static Prize lottery(List<Prize> prizeList) {
// 按照权重从小到大排序奖品
prizeList.sort(Comparator.comparingInt(Prize::getWeight));
// 计算节点 节点的数量比奖品的数量多一个,即 0
List<Integer> nodeList = new ArrayList<>();
// 第一个节点为 0
nodeList.add(0);
for (Prize prize : prizeList) {
// 每一个节点等于前一个节点+当前奖品的权重
nodeList.add(nodeList.get(nodeList.size() - 1) + prize.getWeight());
}
// 获取最后一个节点的权重值,确定随机数的最大范围
Integer currentNode = nodeList.get(nodeList.size() - 1);
// 递归结束点,如果都为0则代表奖品池已清空
if (currentNode == 0) {
return null;
}
// 生成随机数,当前抽取 0-节点最大权重的任意值
int randomInt = RandomUtil.randomInt(0, currentNode);
// 最终抽奖逻辑 此处需要从第二个节点开始遍历
for (int i = 1; i < nodeList.size(); i++) {
// 当前奖品
Prize prize = prizeList.get(i - 1);
// 本次节点
Integer endNode = nodeList.get(i);
// 前一个节点
Integer startNode = nodeList.get(i - 1);
// 若随机数大于等于前一个节点并且小于本节点,则当前奖品满足随机值且为所抽中奖品
if (randomInt >= startNode && randomInt < endNode) {
// 判断当前奖品是否还有库存,没有则从奖池中移出并进行递归重新抽奖
if (prize.getNumber() == 0) {
prizeList.remove(i - 1);
return lottery(prizeList);
}
// 抽中奖品,当前奖品数量递减
prize.setNumber(prize.getNumber() - 1);
// 当前奖品抽中后数量为0,则代表已抽取完
if (prize.getNumber() == 0) {
System.out.println("在抽奖过程中 => " + prize.getName() + ",在第【" + count + "】次被抽空");
}
count++;
return prize;
}
}
throw new RuntimeException("程序异常 生成的随机数不在任何奖品权重区间内");
}
/***
* 抽奖程序
* @param args
*/
public static void main(String[] args) {
int drawCount = 1000;
System.out.println("==========================当前抽奖次数为:" + drawCount + "次=============================");
// 创建奖池,给每个奖品分配不同的权重和对应的库存
List<Prize> prizeList = new ArrayList<>();
prizeList.add(new Prize(1, "特等奖", 20, 20));
prizeList.add(new Prize(2, "一等奖", 50, 50));
prizeList.add(new Prize(3, "二等奖", 100, 100));
prizeList.add(new Prize(4, "三等奖", 150, 150));
prizeList.add(new Prize(5, "四等奖", 250, 250));
prizeList.add(new Prize(6, "安慰奖", 430, 430));
// 所抽取的奖品集合
List<Prize> lotteryResult = new ArrayList<>();
// 模拟一千次抽奖验证概率
for (int i = 1; i <= drawCount; i++) {
// 从奖池中进行单次奖品抽取
Prize prize = lottery(prizeList);
if (prize != null) {
// 抽中奖品后加入集合中
lotteryResult.add(prize);
}
}
// 根据奖品名称进行分组,从而得到每个奖品被抽中的次数
Map<String, List<Prize>> collect = lotteryResult.stream().collect(Collectors.groupingBy(Prize::getName));
System.out.println("===================本批次抽奖结果为======================");
collect.forEach((k, v) -> System.out.println(k + " 被抽中 " + v.size() + " 次"));
}
}模拟用户一千次抽奖操作,抽取完所有奖项,抽取结果如下:
“天命抽奖”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154/***
* 积分抽奖 奖项后移
* @author deng
* @date 2024/7/4
*/
public class DrawTest1 {
/***
* 抽奖次数
*/
private static Integer drawCount = 2000;
/***
* 奖池
*/
private static List<Prize> prizeList = new ArrayList<>();
/***
* 抽取总次数
*/
private static int countTotal = 1;
/***
* 周期次数
*/
private static int count = 1;
/***
* 周期阈值 100% 1000次
*/
private static int thresholdCount = 1000;
/***
* 特等奖出现阈值 85%-90% 850-900之间
*/
private static int specialPrizeStartThreshold = 850;
private static int specialPrizeEndThreshold = 900;
private static Integer specialId = 1;
/***
* 初始化奖池 给每个奖品分配不同的权重和对应的库存
* @return
*/
private static List<Prize> initPrize() {
prizeList.add(new Prize(2, "一等奖", 50, 50));
prizeList.add(new Prize(3, "二等奖", 100, 100));
prizeList.add(new Prize(4, "三等奖", 150, 150));
prizeList.add(new Prize(5, "四等奖", 250, 250));
prizeList.add(new Prize(6, "安慰奖", 449, 449));
return prizeList;
}
/***
* 抽奖返回奖品方法
* @return
*/
private static Prize lottery() {
// 判断是否加入特等奖
if (count == specialPrizeStartThreshold) {
// 加入特等奖项,权重值调大 提高中奖率
prizeList.add(new Prize(specialId, "特等奖", 500, 1));
}
// 判断当前次数如达到特等奖结束阈值则直接返回
if (count == specialPrizeEndThreshold) {
for (Prize prize : prizeList) {
// 如果当前特等奖项还存在,到达结束阈值则直接返回奖项
if (Objects.equals(specialId, prize.getId()) && prize.getNumber() > 0) {
prize.setNumber(prize.getNumber() - 1);
System.out.println("在抽奖过程中 => 特等奖,在第【" + count + "】次被抽空");
count++;
countTotal++;
return prize;
}
}
}
// 按照权重从小到大排序奖品
prizeList.sort(Comparator.comparingInt(Prize::getWeight));
// 计算节点 节点的数量比奖品的数量多一个,即 0
List<Integer> nodeList = new ArrayList<>();
// 第一个节点为 0
nodeList.add(0);
for (Prize prize : prizeList) {
// 每一个节点等于前一个节点+当前奖品的权重
nodeList.add(nodeList.get(nodeList.size() - 1) + prize.getWeight());
}
// 获取最后一个节点的权重值,确定随机数的最大范围
Integer currentNode = nodeList.get(nodeList.size() - 1);
// 递归结束点,如果都为0则代表奖品池已清空
if (currentNode == 0) {
return null;
}
// 生成随机数,当前抽取 0-节点最大权重的任意值
int randomInt = RandomUtil.randomInt(0, currentNode);
// 最终抽奖逻辑 此处需要从第二个节点开始遍历
for (int i = 1; i < nodeList.size(); i++) {
// 当前奖品
Prize prize = prizeList.get(i - 1);
// 本次节点
Integer endNode = nodeList.get(i);
// 前一个节点
Integer startNode = nodeList.get(i - 1);
// 若随机数大于等于前一个节点并且小于本节点,则当前奖品满足随机值且为所抽中奖品
if (randomInt >= startNode && randomInt < endNode) {
// 判断当前奖品是否还有库存,没有则从奖池中移出并进行递归重新抽奖
if (prize.getNumber() == 0) {
prizeList.remove(i - 1);
return lottery();
}
// 抽中奖品,当前奖品数量递减
prize.setNumber(prize.getNumber() - 1);
// 当前奖品抽中后数量为0,则代表已抽取完
if (prize.getNumber() == 0) {
System.out.println("在抽奖过程中 => " + prize.getName() + ",在第【" + count + "】次被抽空");
}
// 判断如果达到一个周期后 重置奖池 将周期次数归0
if (countTotal % thresholdCount == 0 && countTotal < drawCount) {
System.out.println("=================重置奖池=================");
count = 0;
initPrize();
}
count++;
countTotal++;
return prize;
}
}
throw new RuntimeException("程序异常 生成的随机数不在任何奖品区间内");
}
/***
* 抽奖程序
* @param args
*/
public static void main(String[] args) {
System.out.println("==========================当前抽奖次数为:" + drawCount + "次=============================");
// 创建奖池
initPrize();
// 所抽取的奖品集合
List<Prize> lotteryResult = new ArrayList<>();
// 进行两千次抽奖验证概率
for (int i = 1; i <= drawCount; i++) {
// 从奖池中进行单次奖品抽取
Prize prize = lottery();
if (prize != null) {
// 抽中奖品后加入集合中
lotteryResult.add(prize);
}
}
// 根据奖品名称进行分组,从而得到每个奖品被抽中的次数
Map<String, List<Prize>> collect = lotteryResult.stream().collect(Collectors.groupingBy(Prize::getName));
System.out.println("===================本批次抽奖结果为======================");
collect.forEach((k, v) -> System.out.println(k + " 被抽中 " + v.size() + " 次"));
}
}参考某游戏部分抽奖规则(201次必中 抽取水晶后重置奖池),本示例为在85%-90%(850-900次)区间内加入特等奖,如若未抽中,则在90%节点上必中特等奖。以一千次为周期,一千次抽取完所有奖品后重置奖池,进入下一个周期抽取。模拟用户两千次抽取结果如下:
法律法规
平台抽奖概率的法律规定主要涉及到 《中华人民共和国反不正当竞争法》 的相关规定。根据该法,抽奖式的有奖销售需要满足一定的法律要求:
- 抽奖概率必须公示:抽奖活动应当公示其中奖概率,抽奖活动的信息应当真实、准确,不得夸大中奖概率或虚构奖品。
- 抽奖信息应当明确:经营者在进行有奖销售时,所设奖的种类、兑奖条件、奖金金额或者奖品等信息应当明确,不得影响兑奖。
- 禁止欺骗性抽奖:经营者不得采用谎称有奖或者故意让内定人员中奖的欺骗方式进行有奖销售。
- 最高奖金额限制:抽奖式的有奖销售,最高奖的金额不能超过五万元。
平台在开展积分抽奖时需要遵守反不正当竞争法、消费者权益保护法、广告法以及其他相关法律法规和规范性文件。平台应当严格遵守这些法律法规的规定,确保积分抽奖活动合法合规。








