在開發抽獎、轉盤、隨機廣告等功能時,常需要根據權重機率來決定中獎結果。本文將詳細介紹如何使用 PHP 實作一個靈活、可擴展的加權隨機抽獎系統,包含核心邏輯解析、物件導向設計與完整可執行代碼。
加權隨機的基本原理是將各獎品的權重累加形成總權重 N,然後在 1 ~ N 之間取一個隨機數 M,最後判斷 M 落在哪個獎品的權重區間內,該獎品即為中獎結果。
總權重 N = 5 + 6 + 7 + 2 = 20。隨機數範圍為 1~20。區間劃分如下:
若隨機數為 14,則落在獎品 3 的區間,中獎結果為獎品 3。
為了方便擴展與維護,我們將抽獎邏輯封裝為三個類別:
<?php
/**
* 獎品實體類別
*/
class Prize {
public $id = null;
public $weight = null;
public $name = null;
protected $start = 0;
protected $end = 0;
public function __construct($id, $weight, $name) {
if (!$id) {
throw new Exception('獎品ID不可為空。');
}
$this->id = $id;
$this->weight = $weight ?: 0;
$this->name = $name ?: '隨機獎品' . $id;
}
public function getId() { return $this->id; }
public function getWeight() { return $this->weight; }
public function setRange($start, $end) {
$this->start = $start;
$this->end = $end;
}
public function inRange($num) {
return ($num >= $this->start) && ($num <= $this->end);
}
}
/**
* 獎品池類別,實作 IteratorAggregate 與 Countable 介面
*/
class PrizePoll implements IteratorAggregate, Countable {
protected $items = [];
public function addItem(Prize $item) {
$this->items[$item->getId()] = $item;
return $this;
}
public function removeItem($itemId) {
if (isset($this->items[$itemId])) {
unset($this->items[$itemId]);
}
return $this;
}
public function updateItem(Prize $item) {
if (isset($this->items[$item->getId()])) {
$this->items[$item->getId()] = $item;
}
return $this;
}
public function getItems() { return $this->items; }
public function getVisibleItems() {
$items = [];
foreach ($this->items as $item) {
if ($item->getWeight()) {
$items[$item->getId()] = $item;
}
}
return $items;
}
public function count() { return count($this->items); }
public function getIterator() { return new ArrayIterator($this->items); }
}
/**
* 簡易抽獎執行類別
*/
class SimpleTurn {
protected $poll = null;
public function __construct(PrizePoll $poll = null) {
if ($poll) $this->setPoll($poll);
}
public function run(PrizePoll $poll = null) {
$poll = $poll ?: $this->poll;
if (!$poll) throw new Exception('獎池未初始化');
if ($poll->count() <= 0) throw new Exception('獎池為空');
$items = $poll->getVisibleItems();
if (count($items) <= 0) throw new Exception('無可用獎品(權重皆為 0)');
$sum = 0;
foreach ($items as $item) {
$start = $sum + 1;
$sum += $item->getWeight();
$end = $sum;
$item->setRange($start, $end);
}
$rand = $this->getRandNum(1, $sum);
foreach ($items as $item) {
if ($item->inRange($rand)) {
return $item;
}
}
return null;
}
protected function getRandNum($min, $max) {
return mt_rand($min ?: 1, $max);
}
public function setPoll(PrizePoll $poll) {
$this->poll = $poll;
}
}
// 使用範例
try {
$prizePoll = new PrizePoll();
$prizePoll->addItem(new Prize(1, 5, '一等獎'))
->addItem(new Prize(2, 6, '二等獎'))
->addItem(new Prize(3, 7, '三等獎'))
->addItem(new Prize(4, 2, '安慰獎'));
$turn = new SimpleTurn($prizePoll);
$prize = $turn->run();
if ($prize) {
echo "恭喜中獎:{$prize->name} (ID: {$prize->id}, 權重: {$prize->weight})";
} else {
echo "未中獎";
}
} catch (Exception $e) {
echo "錯誤:" . $e->getMessage();
}
?>
Q: 為何使用 mt_rand 而非 rand?
A: mt_rand 基於 Mersenne Twister 演算法,速度更快且隨機品質更好,適合高併發場景。
Q: 權重為 0 的獎品會參與抽獎嗎?
A: 不會。getVisibleItems() 會自動過濾權重為 0 的獎品,確保其永不中獎。
這套架構清晰、擴展性強,可直接應用於電商促銷、遊戲掉寶、隨機推薦等業務場景。根據實際需求調整權重配置即可靈活控制中獎機率。