促销系统的设计
在電商平臺中,促銷是必不可少的營銷手段,尤其在國內 各種玩法層出不窮,最開始的滿減/秒殺 到優惠卷 再到 拼團/砍價等等
一個良好的促銷系統應該具備易于擴展,易于統計促銷效果等特點,在遇到秒殺類促銷時還需要做到可擴容,抗并發(本次不考慮秒殺系統的設計)等等. 廢話說完了,進入正題吧
概覽
對各種促銷行為進行分析,會發現本質上是由兩個部分和一個作用域組成.
促銷的核心作用域既訂單.因此我在上一篇文章中介紹了電商中訂單系統的設計 E-commerce 中訂單系統的設計
兩個部分既上圖中的rule和action部分.
rule描述了促銷限制,既訂單需要滿足那些條件才能參與某個促銷.常見的促銷限制有 訂單金額/購買時間/購買數量/收貨地址/支付方式/用戶類型/購買人數 等等.
action描述了給予訂單哪些優惠策略 如折扣/直減/免運費/返現/贈品 等等.
這樣設計最大好處是 rule與action相互獨立且高度抽象, 運營人員與開發人員可以自由組合rule和action來達到最大靈活性與可擴展性
數據庫設計
Promotion
Schema::create('promotions', function (Blueprint $table) {$table->increments('id');$table->string('code');$table->string('name')->nullable();$table->string('description')->nullable();$table->string('cover')->nullable()->comment('促銷封面');$table->string('asset_url')->nullable()->comment('促銷詳情鏈接')$table->integer('position')->default(0)->comment('權重');$table->string('type')->comment('優惠卷/滿減促銷/品牌促銷/秒殺/拼團/通用.');$table->json('config')->nullable()->comment('配置');$table->timestamp('began_at')->nullable()->comment('促銷開始時間');$table->timestamp('ended_at')->nullable()->comment('促銷結束時間');$table->timestamps();$table->softDeletes(); }); 復制代碼為了實現良好的促銷效果統計行為,所有的促銷行為都應該對應promotion表中的一條記錄.
Rule
Schema::create('promotion_rules', function (Blueprint $table) {$table->increments('id');$table->unsignedInteger('promotion_id');$table->string('type');$table->json('config')->nullable();$table->timestamps();$table->index('promotion_id'); }); 復制代碼常見的rule type有
- 訂單總額 order_total
- 訂單中促銷項目總額 promotion_items_total
- 第N筆訂單 nth_order
- 所屬分類 has_category
- 消費者用戶組 customer_group (白金會員組/鉆石會員組 等等)
- 購買數量 item_quantity
- 等等
Action
Schema::create('promotion_actions', function (Blueprint $table) {$table->increments('id');$table->unsignedInteger('promotion_id');$table->string('type');$table->json('config')->nullable();$table->timestamps();$table->index('promotion_id');}); 復制代碼常見的action type有
- 訂單固定折扣 order_fixed_discount
- 訂單百分比折扣 order_percentage_discount
- 訂單中促銷項目固定折扣 promotion_items_fixed_discount
- 訂單中促銷項目階梯式折扣 promotion_items_ladder_discount
- 贈送積分 present_integral
- 運費百分比折扣 shipping_percentage_discount
- 等等
json類型的config字段的靈活應用是促銷系統靈活的另一個主要原因
關于json字段的使用細項,及索引方式 可以參考 MySQL 中 JSON 字段的使用技巧
PromotionVariant
在常見的電商平臺中,一個促銷活動通常不會涉及所有的商品, 尤其是類似淘寶這種B2C模式的平臺,促銷通常是以商家報名的形式展開的. 因此我們會有一個表來記錄 有哪些變體(variant)參與了本次促銷.
變體(variant)即sku, 下文將統稱為變體.
另外不以product作為參與促銷的最小單位, 是為了進行更細顆粒度的控制.
一個促銷可以有多個變體參與,一個變體可以同時參與多個促銷. 因此 promotion_variants 實際上是promotions表和variants表中間的一張中間表, 并且這張中間表攜帶了其他信息, 來看看遷移文件
Schema::create('promotion_variants', function (Blueprint $table) {$table->increments('id');$table->unsignedInteger('variant_id')->index();$table->unsignedInteger('promotion_id')->index();$table->decimal('discount_rate')->nullable()->comment('折扣率, 值為0.3表示打7折');$table->unsignedInteger('stock')->nullable()->comment('促銷庫存');$table->unsignedInteger('sold')->default(0)->comment('銷售數量');$table->unsignedInteger('quantity_limit')->nullable()->comment('購買數量限制');$table->boolean('enabled')->default(1)->comment('啟用');// 冗余$table->unsignedInteger('product_id');$table->string('promotion_type')->comment('冗余promotion表type');$table->json('rest')->nullable()->comment('冗余');$table->timestamps(); }); 復制代碼上面便是促銷系統的核心表,數據庫字段可以按照實際需求進行增減和修改,特殊促銷可自行添加相關表, 如優惠卷促銷的coupons表, 拼團的groups表, 報名促銷的promotion_sign_up表等等
業務設計
流程設計
以一次圣誕節滿減促銷為例,第一步的工作是創建promotion和相應的rules和actions. 我們首先會有這樣3條記錄
// promotion {id: 1,code: '2018-christmas',name: '圣誕節滿減大促',type: 'full_discount',description: '促銷商品滿100減10元',cover: null,asset_url: null,rest: null,config: null,position: 0,began_at: '2018-12-25 00:00:00',ended_at: '2018-12-26 00:00:00' }// rule {'id': 1,'promotion_id': 1,'type': 'promotion_items_total', // 訂單中促銷項總額'config': {'amount' => 10000, // unit/分 } }// action {'id': 1,'promotion_id': 1,'type': 'promotion_items_fixed_discount', // 訂單中促銷項 固定折扣'config': {'amount' => 1000, // unit/分 } } 復制代碼當促銷創建完成后,下一步就是確定本次促銷的變體了.
對于自營網站,由網站運營創建促銷,挑選變體并添加到promotion_variants表中.對于B2C平臺,由網站運營創建促銷,商家選擇變體并報名參與本次促銷,運營審核后將其添加到相應的promotion_variants表中.
當促銷的變體確定后. 對于有需要的促銷,可以為促銷設計聚合頁面/詳情頁/宣傳頁/推廣頁,然后將相應的鏈接和封面添加到promotion.asset_url和promotion.cover中保存即可.
代碼邏輯
訂單對促銷的判斷的邏輯的laravel偽代碼
// 獲取平臺所有有效的促銷 $promotions = Promotion::active()->get();// 通過rule過濾promotion $promotions = $promotions->filter(function ($promotion) {$rules = $promotion->rules$order = $this->getOrder();// 判定訂單是否滿足所有rule,當存在一條rule不被訂單所滿足,應返回false,被過濾器過濾掉return true; });// 為訂單應用action. $promotion->each(function ($promotion) {$actions = $promotion->actions;$order = $this->getOrder();// 將actions逐條應用于訂單 })復制代碼特別注意: 對訂單應用actions并不意味著直接修改訂單中的商品單價或支付總額等. 而應有條理的記錄影響訂單支付金額的行為和原因. 既使用上一篇中提到的adjustment來記錄 E-commerce 中訂單系統的設計
關于action和rule的代碼邏輯可以先來看兩個interface
namespace Promotion\Constructs;interface Checker {public function isEligible(array $configuration): bool; } 復制代碼namespace Promotion\Constructs;interface Action {public function execute(array $configuration); }復制代碼每一條rule的設計都要實現上面的 Checker接口,每一條action都要實現上面的Action接口.
以上面的圣誕滿減促銷的rule和action為例子,來看看具體的實現
namespace Promotion\Checker;/*** 有很多的通用方法 如getOrder,getPromotionOrderItems等. * 因此我創建了一個基類checker來實現interface和通用方法*/ class PromotionItemsTotalChecker extends Checker {public function isEligible(array $configuration): bool{return $this->getPromotionOrderItemsTotal() >= $configuration['amount'];} } 復制代碼需要注意一點,一筆訂單中可能存在許多變體,但通常情況是只有部分變體參加了圣誕大促.因此我們計算購物總額時應該使用order中參與了圣誕促銷items
namespace Promotion\Actions;use Promotion\Helpers\CreateAdjustment; use Promotion\Helpers\Distribute;class PromotionItemsFixedDiscountAction extends Action {use Distribute, CreateAdjustment;public function execute(array $configuration){// 滿減的金額$amount = $configuration['amount'];if ($amount === 0) {return false;}// 格式校驗, amount如果小于訂單金額時,則使用訂單金額作為優惠amount$amount = -1 * min($this->getPromotionOrderItemsTotal(), $amount);if ($amount === 0) {return false;}$items = $this->getPromotionOrderItems();$itemsTotals = [];foreach ($items as $item) {$itemsTotals[] = $item->total;}// 促銷金額等比例分配.$splitAmount = $this->distributeAmountOfItem($itemsTotals, $reduceAmount);// 創建adjustments$this->createUnitsAdjustment($items, $this->getPromotion(), $splitAmount);} }復制代碼本文的主要目的是提供思路與想法, 因此沒有太過具體完整的代碼.
未來如果有機會的話會設計一些促銷系統擴展等提供參考.
上面便是一個促銷系統的流程思路,下面多提供一些demo供參考
優惠卷
已一張10元代金卷為例,我們會有這樣兩條記錄
// promotion {id: 1,code: '10-cash',name: '10元代金券',type: 'coupon',description: '全場可用',cover: null,asset_url: null,config: {type: 'cash',reduce_amount: 1000, // 冗余自下面action中的config中的amountstock: 10000, // 庫存數量sold: 0, // 已經領取的數量catch_limit: 1, // 領取限制date_type: 'fix_term', // 固定期限fix_term: 30, // 自領取日內30天有效,// date_type: 'fix_time_range', 固定時間段// began_at: '2018-12-23 00:00:00',// ended_at: '2018-12-25 00:00:00',},position: 0,began_at: '2018-12-25 00:00:00',ended_at: '2018-12-26 00:00:00' }// action {'id': 1,'promotion_id': 1,'type': 'order_fixed_discount', // 訂單中促銷項 固定折扣'config':{'amount' => 1000, // unit/分 } } 復制代碼代金券通常沒有使用限制,因此不需要rule.
代金券通常是全場可用, 因此action我們使用 order_fixed_discount,而不是promotion_items_fixed_discount.
對于config中的配置適用于各種優惠卷,如滿減卷,運費卷等等.
對于滿減卷的配置只要再為這筆促銷添加一個類型為promotion_items_total (部分變體滿減)或者order_total(全場滿減) 的rule即可
優惠卷促銷通常要創建一個 coupons表來存儲用戶領取的優惠卷及使用情況等
優惠卷促銷本質上是將傳統促銷以卷的形式體現了出來,既圣誕滿減促銷 => 圣誕滿減卷的轉換.
秒殺/直減/聚劃算
直減類型促銷通常是已變體為單位進行高折扣的促銷行為,秒殺具體要折扣多少通常不是統一設定的,不同的變體會有不同的折扣率,所以可能會有這樣兩條記錄
// promotion {id: 1,code: 'unit-discount-1290',name: '1290期直減',type: 'unit_discount',description: null,cover: null,asset_url: null,config: null,position: 0,began_at: '2018-12-25 00:00:00',ended_at: '2018-12-26 00:00:00' }// promotion_variant {'id': 1,'variant_id': 1,'prootion_id': 1,'discount_rate': 0.35,'stock': 100, // 秒殺庫存'sold': 0,'quantity_limit': 1, // 限購'enabled': 1,'product_id': 1,'promotion_type': 'unit_discount','rest': {variant_name: 'xxx', // 秒殺期間變體名稱image: 'xxx', // 秒殺期間變體圖片} } 復制代碼promotion_variant 由運營添加或者供應商報名得到.直減并沒有相應的rule/action組合而來, 屬于特殊促銷.
但是在代碼邏輯中依舊可以提現出這種特殊的rule和action
既UnitDiscountChecker來判定訂單是否可以參與本次秒殺促銷,
通過UnitDicountAction來記錄相應的PromotionOrderItems的折扣信息,既下面的偽代碼
// rule驗證階段 if ($promotion->type === 'unit_discount') {return (new UnitDiscountChecker)->isEligible() }// 應用action階段 if ($promotion->type === 'unit_discount') {(new UnitDiscountAction)->execute() } 復制代碼階梯式滿減
階梯式滿減屬于傳統滿減促銷的一個變種.下面是一個 滿100 - 10,滿150 - 20,滿200 - 30的階梯式滿減的action記錄.
// action {'id': 1,'promotion_id': 1,'type': 'promotion_items_ladder_discount','config': {"ladder": [{"least_amount": 10000,"reduce_amount": 1000}, {"least_amount": 15000, "reduce_amount": 2000}, {"least_amount": 20000, "reduce_amount": 3000}]} } 復制代碼具體的ladder應該由運營人員后臺設定,實際上對于每一種action和rule的type,在后臺管理界面中都應該設置其相應的表單交互
結
如果你有疑惑或者更多的想法歡迎留言.
總結
- 上一篇: C++:苹果促销
- 下一篇: java 策略模式 促销_设计模式之策略