PHP 简易抽奖

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
<?php

function luck_draw() {
$prize = [
[
'id' => 1, // 奖品 ID
'name' => '奖品1', // 奖品名称
'type' => 1, // 奖品类型 1 实体 2 未中奖
'stock' => 123, // 奖品剩余库存
'total' => 123, // 奖品总量 -1 代表无限制
'probability' => 10.000, // 奖品概率
'upper_limit' => 1, // 奖品单用户获取上限
], [
'id' => 2,
'name' => '未中奖-兜底',
'type' => 2,
'stock' => 123,
'total' => 123,
'probability' => 10.0000,
'upper_limit' => 123,
],
];

// 生成一个 1 - 100W 的随机数
// 意味我们的概率总和应该是 0.0001% - 100%
$value = random_int(1, 100 * 10000);

// 避免运营不会计算导致概率超过 100%,这里我们可以对奖品列表再来个随机
shuffle($prize);

// 当前奖品索引临时存储
$i = 0;

// 区间数值临时存储
$v = 0;

// 中奖奖品存储
$p = null;

while(true) {
if(!isset($prize[$i])) {
break;
}

// 当前循环的奖品
$item = $prize[$i];

// 当前概率最大区间值
$v = $v + (int) bcmul($item['probability'], 10000, 1);

// 不在中奖区间内
if($value > $v) {
$i++;

continue;
}

// 没有库存了
if($item['total'] !== -1 && $item['stock'] <= 0) {
break;
}

// 判断是不是上限了
// 其他逻辑判断
// 如果不符合拿奖那应该未 `break` 退出循环

$p = $item;

break;
}

// 没有拿到奖品,那应该用未中奖来替代,确保记录可以便于存储
// 没有中奖存在很多情况,大概就是中了但是不符合具体逻辑要求或者区间都不到那边等等
if(is_null($p)) {
$p = array_filter($prize, function($item) {
// 非未中奖类型
if($item['type'] !== 2) {
return false;
}

// 无限库存
if($item['total'] === -1) {
return true;
}

// 这里可能还要判断下是否上限,业务应该调高未中奖上限的。

return $item['stock'] > 0;
});

if(!$p) {
return '需配置未中奖兜底';
}

$p = current($p);
}

return $p['id'];
}

luck_draw(); // 返回字符串说明出错了,返回 int 则为奖品 ID

由于我们这个项目的并发不是很大,固没有使用库存锁等来尽可能避免超额发奖等情况,具体看业务需要。

往上