所有促銷排成一條 pipeline,順序寫死在同一個檔案裡。
電商做久了,促銷一定會變多。滿額折扣、滿額贈品、折扣碼、加價購、點數折抵、點數回饋⋯⋯每一種都會動到金額。
多數人的寫法是在 controller 或某個 CartService 裡面,一段一段 if 疊上去。疊到第五種的時候,沒有人敢動那個檔案了,因為你不知道把某段往上移三行會發生什麼事。
我的做法是:把每一筆促銷設定攤平成一個 stage,串成一條 pipeline,依序跑完。
public static function process(CartState $state, PromotionCatalog $promos): CartState
{
return array_reduce(
self::stages($promos),
fn (CartState $carry, array $stage) => ($stage['apply'])($carry),
$state,
);
}
stages() 就是整條產線的排程表:
1. 滿額折扣階梯
2. 滿額禮(每一檔一個 stage)
3. 折扣碼
4. 加價購(每一檔一個 stage)
5. 點數回饋與折抵
注意兩件事。
第一,型別之間的順序是固定的、寫死的,就寫在這一個函式裡。行銷明天來問你「滿額折扣跟折扣碼是先折哪一個?兩個是相加還是連乘?」,你不用翻五個檔案,你打開這個檔案指給他看。這是我認為 pipeline 最大的價值——順序變成一份可以被閱讀、被討論的文件,而不是散落在 code 各處的隱性行為。
第二,同型別的多筆設定,各自是獨立的 stage。後台掛了三檔滿額禮,就攤平成三個 stage,每個 stage 只認自己那一筆設定。不需要在規則裡面寫迴圈,不需要判斷「我是第幾檔」。
每個 stage 我都帶一個 label:
$stages[] = [
'label' => "滿額禮|滿 {$promo->threshold}",
'apply' => fn (CartState $s) => PricingRules::thresholdGift($promo, $s),
];
這個 label 在開發階段非常好用。我可以做一個 debug 頁面,把每一個 stage 跑之前跟跑之後的狀態都印出來,一眼看出「金額是在哪一關變的、變了多少」。客服說「這張單為什麼只折 300?」,你把那筆購物車丟進去跑一次,答案就在畫面上。
促銷模組會一直長大,這是躲不掉的。你能做的,是讓它「長大的方式」很固定:新增一種促銷,就是新增一條規則、在 stages() 裡插一行。既有的規則一行都不用改。