依機率分布抽獎:一等獎 0.12%、二等獎 3%、三等獎 12%、其餘「謝謝惠顧」。核心挑戰:機率精確控制、大量抽獎效能、機率動態調整。
function drawPrizeArrayExpansion(array $prizes, int $total = 10000): string {
// $prizes = ['一等獎' => 0.12, '二等獎' => 3, '三等獎' => 12, '謝謝惠顧' => 84.88]; // 百分比
$pool = [];
foreach ($prizes as $prize => $percent) {
$count = (int) round($total * $percent / 100);
$pool = array_merge($pool, array_fill(0, $count, $prize));
}
shuffle($pool);
return $pool[array_rand($pool)];
}
$total 需足夠大以精確表達小數機率(0.12% 需至少 10000)、記憶體隨 $total 線性增長、shuffle O(n) 耗時。/**
* 加權隨機抽獎
* @param array $prizes ['獎項名' => 機率權重(整數或浮點數), ...]
* @return string 中獎獎項
*/
function drawPrizeWeighted(array $prizes): string {
// 1. 計算總權重
$totalWeight = array_sum($prizes);
if ($totalWeight <= 0) throw new InvalidArgumentException('Total weight must be > 0');
// 2. 產生隨機點
$rand = mt_rand(1, (int) $totalWeight * 10000) / 10000; // 支援小數權重
// 3. 區間查找
$accumulated = 0;
foreach ($prizes as $prize => $weight) {
$accumulated += $weight;
if ($rand <= $accumulated) {
return $prize;
}
}
// 理論不會到達,保底回傳最後一項
return array_key_last($prizes);
}
// 使用範例
$prizes = [
'一等獎' => 0.12,
'二等獎' => 3,
'三等獎' => 12,
'謝謝惠顧' => 84.88,
];
// 測試 100000 次驗證機率
$stats = [];
for ($i = 0; $i < 100000; $i++) {
$stats[drawPrizeWeighted($prizes)]++;
}
print_r($stats);
/*
Array
(
[一等獎] => 119 // 0.119% ≈ 0.12%
[二等獎] => 3002 // 3.002% ≈ 3%
[三等獎] => 12015 // 12.015% ≈ 12%
[謝謝惠顧] => 84864 // 84.864% ≈ 84.88%
)
*/
適合獎項數極多(>1000)、高頻抽獎(每秒萬次)場景。此處略過實作,可參考 php-alias-method 套件。
class LotteryService {
private $pdo;
private $prizes = [
'一等獎' => ['weight' => 0.12, 'stock' => 10],
'二等獎' => ['weight' => 3, 'stock' => 100],
'三等獎' => ['weight' => 12, 'stock' => 500],
'謝謝惠顧' => ['weight' => 84.88, 'stock' => INF],
];
public function draw(int $userId): array {
// 1. 防刷:Redis 限流 + 用戶每日上限
$key = "lottery:user:$userId:" . date('Ymd');
$count = $this->redis->incr($key);
if ($count === 1) $this->redis->expire($key, 86400);
if ($count > 5) throw new RuntimeException('Daily limit exceeded');
// 2. 過濾無庫存獎項
$available = [];
foreach ($this->prizes as $name => $info) {
if ($info['stock'] === INF || $info['stock'] > 0) {
$available[$name] = $info['weight'];
}
}
if (empty($available)) return ['prize' => '活動結束'];
// 3. 抽獎
$prize = drawPrizeWeighted($available);
// 4. 扣減庫存(Redis 原子操作 + DB 最終一致性)
if ($prize !== '謝謝惠顧') {
$this->redis->decr("lottery:stock:$prize");
// 異步寫入 DB 訂單表
}
// 5. 記錄日誌
$this->log($userId, $prize);
return ['prize' => $prize];
}
}
prize_config 表存 name, weight, stock, start_time, end_time,後台可視化調整、即時生效。[12, 300, 1200, 8488] 對應萬分比。user_id, prize, weight_snapshot, timestamp, ip,防舞弊、支援爭議查證。抽獎核心在「加權隨機」演算法,陣列展開法適合原型開發,生產環境請使用加權隨機(區間查找),配合 Redis 防刷、庫存原子扣減、機率資料庫化管理、完整審計日誌,構建公平、高效、可審計的抽獎系統。