本文分享适用于公众号抽奖、H5大转盘及小程序扭蛋活动的带库存限制权重抽奖算法。
- 支持为不同奖品设置差异化中奖概率(权重)
- 每个奖品独立管理库存,库存耗尽后自动从候选池剔除
- 采用密码学安全随机数生成器,规避普通随机函数易被预测及刷奖的风险
算法原理详解
以以下配置为例:
- 手机:权重 1,库存 2
- 优惠券:权重 20,库存 50
- 谢谢参与:权重 300,库存 99999
计算逻辑:
- 过滤无效奖品:每次抽奖前,先剔除库存为 0 的奖品。
- 计算总权重:将剩余所有有库存奖品的权重相加。例如初始总权重 = 1 + 20 + 300 = 321。
- 生成随机数:在 `1 ~ 总权重` 区间内生成一个安全随机数。
- 区间匹配:按顺序累加奖品权重,随机数落在哪个区间即命中对应奖品。
- 随机数 = 1 → 中手机
- 随机数 2~21 → 中优惠券
- 随机数 22~321 → 谢谢参与
- 动态调整:若手机库存抽完归零,下次抽奖时手机被过滤,总权重变为 320,区间自动重新计算。
方案优缺点分析
优点:
- 概率可控,可通过低权重设置大奖稀缺性。
- 库存严格锁定,杜绝超发,预算可控。
- 实时出结果,适配大转盘等即时交互场景。
- 使用 `random_int()` 实现密码级安全随机,安全性远高于 `mt_rand`。
缺点:
- 属于即时概率抽奖,不支持事后验算,公信力弱于哈希种子抽签。
- 无法保证固定人数内必然产出固定数量的大奖,仅符合概率分布。
注:若需求为报名结束后统一开奖且需公示验真,建议改用哈希公平抽签算法。
PHP 核心代码实现
<?php
/**
* 带库存控制的权重抽奖算法
* 适用:公众号H5大转盘、小程序抽奖、扭蛋机、即时抽奖
* @param array $prizeList 奖品数组,结构要求:name奖品名称, weight权重, stock库存
* @return array 中奖奖品信息
*/
function weightDrawWithStock(array $prizeList): array
{
// 第一步:过滤库存为0的奖品,只保留还有库存的奖品
$validPrize = [];
$totalWeight = 0;
foreach ($prizeList as $item) {
// 必须同时存在三个字段,并且库存大于0才进入候选池
if (isset($item['name'], $item['weight'], $item['stock']) && $item['stock'] > 0) {
$validPrize[] = $item;
$totalWeight += $item['weight'];
}
}
// 没有任何可抽奖品
if ($totalWeight <= 0 || empty($validPrize)) {
return [
'name' => '奖品已全部抽完',
'weight' => 0,
'stock' => 0
];
}
// 第二步:密码安全随机,生成 1 ~ 总权重 的随机数
$randNum = random_int(1, $totalWeight);
// 第三步:区间遍历,命中奖品
$currentRange = 0;
foreach ($validPrize as $prize) {
$currentRange += $prize['weight'];
if ($randNum <= $currentRange) {
// 命中该奖品,直接返回
return $prize;
}
}
// 兜底返回:谢谢参与
return [
'name' => '谢谢参与',
'weight' => 0,
'stock' => 0
];
}
// ===================== 测试示例 =====================
// 奖品配置,这里可以直接从数据库读取
$prizeConfig = [
[
'name' => '品牌手机',
'weight' => 1,
'stock' => 2
],
[
'name' => '10元无门槛优惠券',
'weight' => 20,
'stock' => 50
],
[
'name' => '谢谢参与',
'weight' => 300,
'stock' => 99999
]
];
// 执行一次抽奖
$winResult = weightDrawWithStock($prizeConfig);
echo "本次抽到奖品:" . $winResult['name'];
关键注意事项与避坑指南
上述函数仅负责判定中奖结果,实际开发中需注意以下核心问题:
1. 并发控制:必须使用数据库事务
抽奖与扣库存操作必须包裹在数据库事务中。在高并发场景下(如数百人同时点击),若不添加事务及行锁,可能导致多人同时读到同一剩余库存(如库存=1),均命中大奖,最终导致库存扣减为负数。
解决方案:使用 InnoDB 引擎,在确定中奖瞬间对奖品记录加行锁。
2. 高频踩坑清单
- 禁用伪随机函数:`mt_rand` / `rand` 种子可预测,极易被技术手段批量刷奖。生产环境务必使用 PHP7+ 原生支持的 `random_int()`。
- 严禁异步扣库存:高并发下异步处理必然导致超发。必须同步完成“判断中奖”与“扣减库存”。
- “谢谢参与”需设大库存:避免将所有实物奖品抽完后程序报错。建议将“谢谢参与”设为极大库存值(如 99999)作为保底。
- 权重数值合理:权重总和应避免超出 PHP 整数范围,一般业务场景中几百至几千的权重值已足够,无需设置过大数据。
- 后端主导逻辑:绝对禁止在前端 JS 中编写抽奖算法。前端仅负责动画展示,结果判定必须由后端完成,防止通过 F12 篡改代码刷奖。
3. 扩展优化建议
- 频次限制:在用户表或独立表中记录每日抽奖次数,限制单用户行为。
- 黑名单机制:识别并拦截恶意刷奖账号。
- 防刷策略:增加 IP 限制、验证码校验及接口请求冷却时间,抵御脚本攻击。
- 分时段概率(慎用):虽可调整不同时段的大奖权重,但容易引发用户关于公平性的质疑,需谨慎使用。

