做電商項目時,經常要處理各種各樣的優惠活動:滿減、打折、VIP 專屬優惠、第二件特價、階梯優惠……
這些單獨實現起來都不復雜,但當你把它們放在一起,就變得混亂起來了。
我自己在工作裏寫過不少類似的邏輯,每次做法差不多:if/else、switch、各種判斷混在一起,過幾個月回頭看代碼,根本不想維護。
於是我乾脆寫了一個小庫,封裝了常見的優惠計算邏輯,讓這件事更清晰,也能隨時在別的項目裏用——php-promotion-engine 就是這樣來的。
為什麼要做這個東西?
一個簡單的例子:
- 購物車裏有滿 200 減 50 的活動
- VIP 用户可以再打 9 折
- 某些商品買三件還能再減 20
這三個優惠疊在一起,怎麼算?
- 是每條規則單獨算優惠,最後加在一起?
- 還是「滿減後再打折」的折上折?
- 或者一件商品只能享受其中一種優惠?
業務裏經常會遇到這些問題,而我不想再每個項目都重新造輪子,所以就抽了一個核心邏輯出來,做成 Composer 包。
我把優惠計算分成了三種模式
1. 獨立模式(independent)
每條優惠規則都基於商品原價獨立計算優惠金額,最後把這些金額加起來。
特點:
- 所有優惠平行計算,彼此之間沒有影響
- 優惠金額往往會比較大(因為每條規則都按原價算)
- 運營活動彼此獨立時,通常用這種模式
例子:
- 購物車 300 元,滿 200 減 50,VIP 9 折
- 「滿減」算 50 元優惠
- 「VIP」算 30 元優惠(300元 - 300 元 × 0.9)
- 最後優惠金額是 80 元,結果是 220 元
2. 折上折模式(sequential)
這裏的優惠是「順序計算」的,每條規則會根據上一條規則之後的價格來繼續打折或滿減。
很多電商活動是順序疊加的,比如「滿減後再打折」,這就是折上折模式的用武之地。
特點:
- 優惠金額會按比例分攤給參與優惠的商品,並實時更新商品價格
- 下一條規則拿到的是更新後的價格,真正意義上的“折上折”
-
可能出現這樣一種情況:
- 原價滿足滿減 → 滿減後價格降低 → 後面的優惠金額也變少
-
也可能出現:
- 某個商品滿減後價格降低,下一條滿減規則再也不滿足條件
例子:
- 購物車 300 元,滿 200 減 50,VIP 9 折
- 「滿減」算 50 元優惠
- 「VIP」算 25 元優惠(250 元 - 250 元 × 0.9)
- 最終優惠金額是 75 元(比獨立模式的 80 元少)
3. 鎖定模式(lock)
這個模式更“嚴格”——每件商品最多隻能享受一條優惠規則。
一旦被某個優惠“鎖定”,這件商品就不再參與其他規則的計算。
特點:
- 適用於「只能享受一次優惠」的場景(比如秒殺、專屬券)
- 優惠不會疊加,運營邏輯更容易控制
例子:
- A 商品被秒殺價鎖定,B 商品參與滿減
- 即使後面有 VIP 折扣,A 商品也不會再打折
這樣拆分之後,電商裏的幾乎所有優惠場景都能歸到這三類模式之一,只需要在引擎裏 setMode() 一下,就能決定計算方式。
怎麼用?
安裝方式很簡單:
composer require hejunjie/promotion-engine
然後可以直接寫:
use Hejunjie\PromotionEngine\PromotionEngine;
use Hejunjie\PromotionEngine\Rules\FullReductionRule;
use Hejunjie\PromotionEngine\Rules\VipDiscountRule;
use Hejunjie\PromotionEngine\Models\Cart;
use Hejunjie\PromotionEngine\Models\User;
// 創建一個購物車模型,實際場景中使用需要計算商品的名稱/價格/購買數量執行即可
// 可以通過第四個參數來設置標籤,執行規則時可以設置需要執行的標籤,標籤支持設置多個
$cart = new Cart();
$cart->addItem('T恤', 120, 1, ['tag']);
$cart->addItem('牛仔褲', 150, 1, ['tag','promo']);
// 創建一個用户模型,實際場景中僅是用來區分用户是否可以享受關於VIP折扣方面的規則
// 如果沒有設置VIP折扣方面的規則,則不會影響任何數據
$user = new User(vip: true);
$engine = new PromotionEngine();
$engine->setMode('sequential'); // 選擇折上折模式
$engine->addRule(new Rules\FullReductionRule(100, 30, ['promo'], 1)); // 滿 200 減 50, 僅適用於有 promo 標籤的商品, 執行順序 1(數字越小越先執行)
$engine->addRule(new Rules\VipDiscountRule(0.9, ['tag'], 2)); // VIP 9 折, 僅適用於有 tag 標籤的商品, 執行順序 2(數字越小越先執行)
$result = $engine->calculate($cart, $user);
print_r(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
你會得到類似這樣的結果:
{
"original": 270,
"discount": 54,
"final": 216,
"details": [
"指定商品滿100減30 (-¥30)",
"VIP 0.9 折 (-¥24)"
],
"items": [
{
"name": "T恤",
"price": 108,
"qty": 1,
"tags": [
"tag"
],
"original_price": 120,
"locked": false
},
{
"name": "牛仔褲",
"price": 108,
"qty": 1,
"tags": [
"tag",
"promo"
],
"original_price": 150,
"locked": false
}
]
}
這個庫能帶來什麼?
- 代碼更乾淨:不用每次都寫一堆
if/else,邏輯集中在「規則類」裏。 - 模式可切換:想獨立算就
independent,想折上折就sequential,換個模式就行。 - 擴展方便:有新活動?直接加個規則類,比如「第 N 件打折」「階梯滿減」,不用動核心邏輯。
最後
這個庫不是大而全的框架,就是一個我在工作中常用到的小工具,我把它整理出來放到 GitHub 上,希望以後別的項目也能用得上,也歡迎你們試試看。
GitHub 地址在這裏:https://github.com/zxc7563598/php-promotion-engine
如果你有建議、發現了 bug,或者有好玩的規則想加進來,歡迎 PR。