轉盤抽獎、盲盒開箱、遊戲掉寶、活動營銷等需按設定機率發放獎品的場景。核心需求:獎品列表動態配置、機率精準控制、程式碼可複用、效能高。
$totalWeight。$rand = mt_rand(1, $totalWeight) (或 0 到 total-1)。$accumulated += $weight,當 $rand <= $accumulated 時,該獎品中獎。時間複雜度: O(n) (n 為獎品種類數,通常極小)。若獎品極多可優化為二分搜尋 O(log n)。
<?php
class LotteryTool {
private static array $awards = [];
private static string $weightKey = 'probability'; // 權重欄位名
private static int $totalWeight = 0;
private static bool $validated = false;
public const SUCCESS = 0;
public const FAIL = -1;
// 設定權重欄位名 (預設 'probability')
public static function setWeightKey(string $key): void {
self::$weightKey = $key;
}
// 設定獎品池並驗證
public static function setAwards(array $awards): void {
self::$awards = $awards;
self::validate();
}
private static function validate(): void {
self::$validated = false;
self::$totalWeight = 0;
if (empty(self::$awards)) return;
foreach (self::$awards as $award) {
if (!isset($award[self::$weightKey]) || $award[self::$weightKey] < 0) {
return; // 資料格式錯誤
}
self::$totalWeight += $award[self::$weightKey];
}
if (self::$totalWeight > 0) self::$validated = true;
}
// 執行抽獎
public static function draw(): array {
if (!self::$validated) {
return ['code' => self::FAIL, 'msg' => '獎品池未配置或資料無效'];
}
if (self::$totalWeight === 0) {
return ['code' => self::FAIL, 'msg' => '總權重為 0'];
}
$rand = mt_rand(1, self::$totalWeight); // 1 ~ 總權重
$accumulated = 0;
foreach (self::$awards as $key => $award) {
$accumulated += $award[self::$weightKey];
if ($rand <= $accumulated) {
return [
'code' => self::SUCCESS,
'award_key' => $key,
'award_info' => $award,
'msg' => '中獎'
];
}
}
// 理論上不會執行到這裡
return ['code' => self::FAIL, 'msg' => '邏輯錯誤'];
}
}
?>
<?php
// 1. 定義獎品池 (權重欄位 'pro')
$awards = [
'grand_prize' => ['pro' => 1, 'info' => '大獎: iPhone 15 (1%)'],
'first_prize' => ['pro' => 5, 'info' => '一等獎: 紅包 100元 (5%)'],
'second_prize'=> ['pro' => 20, 'info' => '二等獎: 優惠券 20元 (20%)'],
'third_prize' => ['pro' => 74, 'info' => '三等獎: 積分 100分 (74%)'],
// 'no_prize' => ['pro' => 0, 'info' => '未中獎'], // 權重 0 可不寫或寫 0
];
// 2. 配置工具
LotteryTool::setWeightKey('pro');
LotteryTool::setAwards($awards);
// 3. 模擬 100,000 次抽獎驗證機率
$stats = [];
for ($i = 0; $i < 100000; $i++) {
$result = LotteryTool::draw();
if ($result['code'] === LotteryTool::SUCCESS) {
$key = $result['award_key'];
$stats[$key] = ($stats[$key] ?? 0) + 1;
}
}
// 4. 輸出統計
echo "獎品\t\t設定機率\t實際次數\t實際機率\n";
foreach ($awards as $key => $award) {
$count = $stats[$key] ?? 0;
$actual = ($count / 100000) * 100;
echo "{$award['info']}\t{$award['pro']}%\t\t{$count}\t\t{$actual}%\n";
}
/*
輸出範例:
大獎: iPhone 15 (1%) 1% 1002 1.002%
一等獎: 紅包 100元 (5%) 5% 4987 4.987%
二等獎: 優惠券 20元 (20%) 20% 19956 19.956%
三等獎: 積分 100分 (74%) 74% 74055 74.055%
*/
?>
抽獎前用 Redis Lua 腳本 原子檢查並扣減庫存,庫存為 0 則從獎品池移除或設權重為 0。
// 建立前綴和陣列 [w1, w1+w2, w1+w2+w3, ...]
$prefixSums = [];
$sum = 0;
foreach ($awards as $a) { $sum += $a['pro']; $prefixSums[] = $sum; }
// 二分搜尋
$rand = mt_rand(1, $sum);
$idx = 0;
$low = 0; $high = count($prefixSums) - 1;
while ($low <= $high) {
$mid = ($low + $high) >> 1;
if ($prefixSums[$mid] < $rand) $low = $mid + 1;
else { $idx = $mid; $high = $mid - 1; }
}
$winner = array_keys($awards)[$idx];
記錄用戶連續未中大獎次數,達閾值時強制提升大獎權重或直接發放。