在電商平臺中,促銷是必不可少的營銷手段,尤其在國內 各種玩法層出不窮,最開始的滿減/秒殺 到優惠卷 再到 拼團/砍價等等
一個良好的促銷系統應該具備易於擴充套件,易於統計促銷效果等特點,在遇到秒殺類促銷時還需要做到可擴容,抗併發(本次不考慮秒殺系統的設計)等等. 廢話說完了,進入正題吧
概覽
對各種促銷行為進行分析,會發現本質上是由兩個部分和一個作用域組成.
促銷的核心作用域既訂單.因此我在上一篇文章中介紹了電商中訂單系統的設計 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
<?php
namespace Promotion\Constructs;
interface Checker
{
public function isEligible(array $configuration): bool;
}
<?php
namespace Promotion\Constructs;
interface Action
{
public function execute(array $configuration);
}
每一條rule的設計都要實現上面的 Checker介面,每一條action都要實現上面的Action介面.
以上面的聖誕滿減促銷的rule和action為例子,來看看具體的實現
<?php
namespace Promotion\Checker;
/**
* 有很多的通用方法 如getOrder,getPromotionOrderItems等.
* 因此我建立了一個基類checker來實現interface和通用方法
*/
class PromotionItemsTotalChecker extends Checker
{
public function isEligible(array $configuration): bool
{
return $this->getPromotionOrderItemsTotal() >= $configuration['amount'];
}
}
需要注意一點,一筆訂單中可能存在許多變體,但通常情況是隻有部分變體參加了聖誕大促.因此我們計算購物總額時應該使用order中參與了聖誕促銷items
<?php
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中的amount
stock: 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,在後臺管理介面中都應該設定其相應的表單互動
結
如果你有疑惑或者更多的想法歡迎留言.