Stories

Detail Return Return

如何優雅地處理多種電商優惠規則?我用 PHP 封裝了一個 Promotion Engine - Stories Detail

做電商項目時,經常要處理各種各樣的優惠活動:滿減、打折、VIP 專屬優惠、第二件特價、階梯優惠……
這些單獨實現起來都不復雜,但當你把它們放在一起,就變得混亂起來了。

我自己在工作裏寫過不少類似的邏輯,每次做法差不多:if/elseswitch、各種判斷混在一起,過幾個月回頭看代碼,根本不想維護。
於是我乾脆寫了一個小庫,封裝了常見的優惠計算邏輯,讓這件事更清晰,也能隨時在別的項目裏用——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。

user avatar greatsql Avatar yuzhoustayhungry Avatar wobushiliaojian Avatar tencent_blueking Avatar _61e9689d548cc Avatar zhuyundataflux Avatar hunterxiong Avatar
Favorites 7 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.