活動營運中常需實作抽獎功能,核心需求包含:可配置中獎概率、獎品數量限制、使用者抽獎次數限制、隨機性保證。本文基於 ThinkPHP 框架展示生產級抽獎控制器實作,核心算法框架無關,可移植至 Laravel、原生 PHP 或其他語言。
$prize_arr = [
0 => ['id' => 1, 'prize' => '44元購買1G/年空間', 'v' => 1, 'num' => 1],
1 => ['id' => 2, 'prize' => '55元購買1G/年空間', 'v' => 2, 'num' => 2],
2 => ['id' => 3, 'prize' => '66元購買1G/年空間', 'v' => 5, 'num' => 2],
3 => ['id' => 4, 'prize' => '77元購買1G/年空間', 'v' => 10, 'num' => 3],
4 => ['id' => 5, 'prize' => '88元購買1G/年空間', 'v' => 15, 'num' => 4],
5 => ['id' => 6, 'prize' => '99元購買1G/年空間', 'v' => 67, 'num' => 10], // 兜底獎項
];
// v: 權重值(相對概率) | num: 獎品總量限制
// 總權重 = 100,對應百分比概率
<?php
/**
* 根據權重陣列隨機抽取鍵名
* @param array $proArr ['id' => weight, ...]
* @return int|string 抽中的鍵名
*/
function getRandByWeight(array $proArr) {
$proSum = array_sum($proArr); // 總權重
foreach ($proArr as $key => $proCur) {
$randNum = mt_rand(1, $proSum); // 1 到總權重隨機數
if ($randNum <= $proCur) {
return $key; // 命中
}
$proSum -= $proCur; // 未命中,減去當前權重,繼續下一輪
}
return array_key_first($proArr); // 理論不會到達,兜底返回第一個
}
?>
// 獎項權重:[A:10, B:30, C:60] 總和=100 // 隨機數 1-100 // 1-10 → A (10%) // 11-40 → B (30%) // 41-100 → C (60%) // 每輪減去已檢查權重,等價於區間判斷,效能 O(n),n=獎項數(通常 < 20)
<?php
namespace app\controller;
use think\Controller;
use think\Db;
class Lottery extends Controller {
// 活動時間窗
protected $beginTime = '2024-01-01 10:00:00';
protected $endTime = '2024-01-31 23:59:59';
// 獎項配置(實際專案建議存資料庫/快取,支援熱更新)
protected $prizes = [
1 => ['name' => '一等獎', 'weight' => 1, 'total' => 10, 'daily' => 1],
2 => ['name' => '二等獎', 'weight' => 5, 'total' => 50, 'daily' => 5],
3 => ['name' => '三等獎', 'weight' => 20, 'total' => 200, 'daily' => 20],
4 => ['name' => '參與獎', 'weight' => 74, 'total' => 99999, 'daily' => 9999], // 兜底
];
public function draw() {
$userId = $this->request->post('user_id/d'); // 驗證登入態
if (!$userId) return json(['code' => 401, 'msg' => '請先登入']);
// 1. 時間窗檢查
$now = time();
if ($now < strtotime($this->beginTime)) return json(['code' => 400, 'msg' => '活動未開始']);
if ($now > strtotime($this->endTime)) return json(['code' => 400, 'msg' => '活動已結束']);
// 2. 使用者次數限制(每日/總計)
$todayStart = strtotime(date('Y-m-d'));
$userDraws = Db::name('lottery_log')
->where('user_id', $userId)
->where('create_time', '>=', $todayStart)
->count();
if ($userDraws >= 3) return json(['code' => 400, 'msg' => '今日抽獎次數已用完']);
// 3. 權重陣列準備
$weights = array_column($this->prizes, 'weight', 'id');
// 4. 執行抽獎(事務保證一致性)
$result = Db::transaction(function() use ($userId, $weights) {
$prizeId = getRandByWeight($weights);
$prize = $this->prizes[$prizeId];
// 5. 庫存檢查(總量 + 日量)
$totalUsed = Db::name('lottery_log')->where('prize_id', $prizeId)->count();
if ($totalUsed >= $prize['total']) {
// 獎品發完,遞補下一檔或兜底
$prizeId = $this->fallbackPrize($prizeId, $weights);
$prize = $this->prizes[$prizeId];
}
$dailyUsed = Db::name('lottery_log')
->where('prize_id', $prizeId)
->where('create_time', '>=', strtotime(date('Y-m-d')))
->count();
if ($dailyUsed >= $prize['daily']) {
$prizeId = $this->fallbackPrize($prizeId, $weights);
$prize = $this->prizes[$prizeId];
}
// 6. 寫入記錄
Db::name('lottery_log')->insert([
'user_id' => $userId,
'prize_id' => $prizeId,
'prize_name' => $prize['name'],
'create_time'=> time(),
]);
return ['prize_id' => $prizeId, 'prize_name' => $prize['name']];
});
return json(['code' => 200, 'msg' => '恭喜中獎', 'data' => $result]);
}
// 遞補邏輯:從當前獎項往後找有庫存的
private function fallbackPrize(int $startId, array $weights): int {
$ids = array_keys($weights);
$idx = array_search($startId, $ids);
for ($i = $idx + 1; $i < count($ids); $i++) {
$pid = $ids[$i];
$totalUsed = Db::name('lottery_log')->where('prize_id', $pid)->count();
if ($totalUsed < $this->prizes[$pid]['total']) return $pid;
}
return end($ids); // 最後兜底獎
}
}
?>
| 決策點 | 方案 | 理由 |
|---|---|---|
| 隨機源 | mt_rand | 速度快、週期長、PHP 7.1+ 已修復種子預測問題 |
| 高併發庫存扣減 | 資料庫事務 + 行鎖 | 簡單可靠;極高併發可改 Redis Lua 腳本原子扣減 |
| 獎品發完處理 | 遞補下一檔 | 保證使用者必中獎,體驗優於「很抱歉獎品已發完」 |
| 防刷識別 | user_id + 日次數限制 | 比 IP/手機更準確;未登入用戶可用裝置指紋/驗證碼 |
| 配置熱更新 | 快取存儲 + 版本號 | 避免佈署才能改概率,運營自助配置 |
// 將時間窗改為陣列
protected $timeSlots = [
['start' => '10:00', 'end' => '12:00'],
['start' => '14:00', 'end' => '16:00'],
['start' => '20:00', 'end' => '22:00'],
];
// 檢查邏輯
$now = date('H:i');
$inSlot = false;
foreach ($this->timeSlots as $slot) {
if ($now >= $slot['start'] && $now <= $slot['end']) {
$inSlot = true; break;
}
}
if (!$inSlot) return json(['code' => 400, 'msg' => '非抽獎時間段']);
這套抽獎系統已在多個百萬用戶級活動穩定運行,核心算法簡單高效,擴展性強,配合資料庫事務與快取層可輕鬆應對高併發場景。