核心演算法:加權隨機抽取
/**
* 依權重陣列抽取索引
* @param array $weights ['prize_id' => weight, ...] 權重總和為基數
* @return int|string 抽中鍵名
*/
function weighted_rand(array $weights) {
$total = array_sum($weights);
$rand = mt_rand(1, $total);
foreach ($weights as $key => $weight) {
if ($rand <= $weight) return $key;
$rand -= $weight;
}
// 理論不會到達這裡,保底回傳最後一項
return array_key_last($weights);
}
完整抽獎流程範例
<?php
// 獎項設定:id, 名稱, 權重(整數)
$prizes = [
1 => ['name' => 'iPad Pro', 'weight' => 1], // 0.5%
2 => ['name' => 'AirPods', 'weight' => 5], // 2.5%
3 => ['name' => '星巴克禮券 500', 'weight' => 20], // 10%
4 => ['name' => '折扣碼 9 折', 'weight' => 50], // 25%
5 => ['name' => '再接再厲', 'weight' => 124], // 62%
];
// 基數 = 200,機率 = weight / 200
$weights = array_column($prizes, 'weight', 'id');
// 抽獎
$winId = weighted_rand($weights);
$win = $prizes[$winId];
// 紀錄中獎、扣除庫存、記錄日誌...
echo "恭喜中獎:{$win['name']}";
// 前端 AJAX 回傳 JSON
header('Content-Type: application/json');
echo json_encode(['code'=>0, 'prize'=>$win['name'], 'id'=>$winId]);
大轉盤前端配合
- 後端回傳中獎索引
winId
- 前端計算對應角度:
angle = (winId - 1) * (360 / count) + random(0, sectorAngle)
- CSS
transform: rotate() + transition: transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99) 產生減速效果
- 動畫結束後彈窗顯示獎品
防作弊與公平性機制
| 風險 | 對策 |
| 前端竄改請求 | 簽名驗證 (HMAC)、CSRF Token、Rate Limit |
| 重複抽獎 | Redis SETNX 鎖 + 用戶每日次數計數器 |
| 內部人員操縱 | 權重配置審計日志、上線審核、開獎結果不可逆記錄 |
| 機率顯示不實 | 公開演算法邏輯、第三方公證、區塊鏈存證 |
庫存控制:動態權重調整
function adjust_weights($prizes, $stock) {
foreach ($prizes as $id => &$p) {
if ($stock[$id] <= 0) $p['weight'] = 0; // 售罄機率歸零
}
return $prizes;
}
擴充:保底機制 (Pity System)
// 連續 N 次未中大獎 → 第 N+1 次必中
$userPity = $redis->get("pity:{$uid}") ?: 0;
if ($userPity >= 99) {
$weights[1] = 10000; // 大獎權重拉高
$userPity = 0;
} else {
$userPity++;
}
$redis->set("pity:{$uid}", $userPity);