訂單系統的核心表天然是 orders系列表,laravel的遷移文件以下php
Schema::create('orders', function (Blueprint $table) {
$table->increments('id');
$table->string('number')->nullable()->comment('訂單號');
$table->unsignedInteger('address_id')->nullable()->comment('訂單地址');
$table->unsignedInteger('user_id')->index()->comment('用戶id');
$table->integer('items_total')->default(0)->comment('order每個item的total的和 unit/分');
$table->integer('adjustments_total')->default(0)->comment('調整金額 unit/分');
$table->integer('total')->default(0)->comment('需支付金額 unit/分');
$table->string('local_code')->comment('語言編號');
$table->string('currency_code')->comment('貨幣編號');
$table->string('state')->comment('主狀態 checkout/new/cancelled/fulfilled');
$table->string('payment_state')->comment('支付狀態 checkout/awaiting_payment/partially_paid/cancelled/paid/partially_refunded/refunded');
$table->string('shipment_state')->comment('運輸狀態 checkout/ready/cancelled/partially_shipped/shipped');
$table->ipAddress('user_ip')->comment('用戶ip ip2long後的結果');
$table->timestamp('paid_at')->nullable()->comment('支付時間');
$table->timestamp('confirmed_at')->nullable()->comment('確認訂單時間');
$table->timestamp('reviewed_at')->nullable()->comment('評論時間');
$table->timestamp('fulfilled_at')->nullable()->comment('訂單完成時間');
$table->json('rest')->nullable()->comment('非核心字段冗餘');
$table->timestamps();
});
複製代碼
接下來是order_items表,用於記錄order的itemlaravel
Schema::create('order_items', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('order_id')->index()->comment('外鍵');
$table->unsignedInteger('variant_id')->comment('variant是國外的稱呼,國內一般稱爲sku. 既庫存最小單位');
$table->unsignedInteger('product_id')->comment('冗餘字段');
$table->unsignedInteger('quantity')->comment('購買數量');
// adjustment calculate
$table->integer('units_total')->default(0)->comment('item中每個unit的和. 單位/分');
$table->integer('adjustments_total')->default(0);
$table->integer('total')->default(0)->comment('units_total + adjustments_total');
$table->integer('unit_price')->default(0)->comment('variant單價,冗餘字段');
$table->json('rest')->nullable()->comment('非核心字段冗餘');
$table->timestamps();
});
複製代碼
作過海外電商或者亞馬遜的朋友應該對variant(變體)不陌生. 國內稱爲sku. 每個商品都會有多個變體git
接下來是order_item_units 表github
Schema::create('order_item_units', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('item_id')->index();
$table->unsignedInteger('shipment_id')->comment();
$table->integer('adjustments_total')->default(0);
$table->timestamps();
});
複製代碼
對於用戶購買的每一件實體,咱們都須要謹慎的作一條記錄,其會涉及到運輸/促銷/退貨等問題, 例如variantA咱們購買了三件,那麼咱們就須要爲這三件相同的變體分別建立三條記錄.數據庫
上面三張表的關係從上往下 一個order會有多個item,一個item根據quantity的值,會有對應數量的unit.json
order和order_item表你們應該都知道.後端
order_item_units表可能有些同窗第一次知道,可是其是必要存在的api
tip: 全部的價格字段都使用分爲單位存儲,從而避免小數在計算機系統中存在的一些問題數據庫設計
能夠消化梳理一下上面的三張訂單系統核心表,而後再介紹一下其餘相關表的設計. 數據庫的設計應該是靈活的,能夠根據實際的需求任意添加和修改字段編碼
上面三張表都出現了adjustment_total字段,可能會有些疑惑.
若是咱們每一個變體的價格是10元,那我買三個這件變體則須要30元,可是實際支付的金額每每都不是30元.,會有各類各樣的狀況影響咱們最終支付的價格.
好比運費+5元,促銷折扣 -8元,稅收+3元,退還服務 +0.5元,最後實際須要支付 35.5元. 爲何30元的金額最後卻支付了35.5元?
咱們不能憑空蹦出個35.5元,影響商品實際支付金額的每個因素都是相當重要,咱們須要負責任的記錄下來.這即是adjustment表的來源.
首先看看遷移文件
Schema::create('adjustments', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('order_id')->nullable();
$table->unsignedInteger('order_item_id')->nullable();
$table->unsignedInteger('order_item_unit_id')->nullable();
$table->string('type')->comment('調整的類型 shipping/promotion/tax等等');
$table->string('label')->comment('結合type決定');
$table->string('origin_code')->comment('結合label決定');
$table->bool('included')->comment('是否會影響最終訂單須要支付的價格')
$table->integer('amount');
$table->timestamps();
$table->index('order_id');
$table->index('order_item_id');
$table->index('order_item_unit_id');
});
複製代碼
調整對訂單價格的影響分爲三種類型, 分別是 影響整個order, 影響order_item(較少預見),影響order_item_units.
included字段 用來判斷本條adjustment記錄,是否會影響消費者最終須要支付的金額
大部分的adjustment都會影響最終結算的價格, 小部分如商品稅,一般已經計算在了商品的單價中, 不會影響消費者最終須要支付的金額.可是在開具發票時 卻須要展現,由於咱們作必要的記錄
舉個例子, 假設咱們一筆訂單的運費是5元,那麼會有這樣一條adjustment記錄
{
id: 1,
order_id: 1,
order_item_id: null,
order_item_unit_id: null,
amount: 500,
type: 'shipping',
label: 'UPS',
origin_code: null,
included: 1,
}
複製代碼
假設咱們消費者在一個訂單中購買了三條1.5米數據線,並使用了一張8元的代金券,那麼會有這樣三條adjustment記錄
[
{
id: 2,
order_id: null,
order_item_id: null,
order_item_unit_id: 1,
amount: -267,
type: 'promotion',
label: '8元代金券',
origin_code: 'KSDI12K2', // 代金券code
included: 1
},
{
id: 2,
order_id: null,
order_item_id: null,
order_item_unit_id: 2,
amount: -267,
type: 'promotion',
label: '8元代金券',
origin_code: 'KSDI12K2', // 代金券code
included: 1
},
{
id: 2,
order_id: null,
order_item_id: null,
order_item_unit_id: 3,
amount: -266,
type: 'promotion',
label: '8元代金券',
origin_code: 'KSDI12K2', // 代金券code
included: 1
},
]
複製代碼
實際上對於大部分的促銷需求 咱們都應該將促銷的折扣金額均分到每個unit中.
這樣設計的一個好處是,當消費者退調用其中一根數據線時,咱們能夠很清楚的計算出應該退多少金額給消費者. 既 單價 + order_item_unit.adjustment
實際上清楚的記錄每一筆影響最終支付金額的adjustment,不管對消費者仍是對供應商來講都是負責的作法.
運費爲何不須要分攤到unit?
運費對於一筆訂單來講,是固定的外部消費(由快遞公司獲利),退款時商家並不須要爲運費負責, 只須要退還商品的等額價值便可
更加白話的說法就是 你在淘寶買了一個商品20元,運費10元, 你以爲商品很差想要退貨(不考慮寄回的運費), 商家須要退你30元嗎?
shipment爲訂單的運輸信息存儲,payment爲支付信息存儲.先來看看遷移文件
Schema::create('shipments', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('method_id')->comment('運輸方式 外鍵');
$table->unsignedInteger('order_id')->comment('訂單 外鍵');
$table->string('state')->comment('運輸狀態');
$table->string('tracking_number')->nullable()->comment('訂單號碼');
$table->timestamps();
$table->index('order_id');
});
複製代碼
Schema::create('payments', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('method_id')->comment('支付方式');
$table->unsignedInteger('order_id');7
$table->string('currency_code', 3)->comment('冗餘 貨幣編碼');
$table->unsignedInteger('amount')->default(0)->comment('支付金額');
$table->string('state');
$table->text('details')->nullable();
$table->timestamps();
$table->index('order_id');
});
複製代碼
上面在order_item_units表中存在一個shipment_id 就對應這裏的shipment表. shipment和order_item_units之間是一對多的關係,訂單中的每個實體均可以被分別運輸,例如京東購物時常常會見到這種狀況.
一條shipment/payment 會和一條實際存在的貨運記錄/支付記錄(退款記錄) 掛鉤.
上面就是訂單系統的核心表了,對於後端來講,數據庫就已經能夠反映出整個系統的設計了.
接下來抽出一些細節進行詳細的介紹
相信不少小夥伴在作訂單系統時會被各類狀態 待確認,待支付,待發貨,已發貨,關閉訂單 等等弄的暈頭轉向,今天咱們就來梳理一下訂單系統中的各類狀態
若是各類狀態只在order表使用一個state字段來記錄顯得有些力不從心,所以推薦使用三個字段,它們分別是 state,shipment_state,payment_state. 來分別記錄在訂單中咱們或者消費者最關心的三種狀態.
先來分別看看三個state的狀態轉移圖
order.state↓
這是一筆訂單的幾個最基本的幾個狀態.
先講一講初始狀態,既 checkout, 這與訂單在何時建立有關係,當消費者在購物車點擊結帳時,就建立了一個訂單,用於本次結帳, 所以訂單的初始狀態爲checkout
結帳也就是所謂的確認訂單頁,在該頁面中,消費者能夠選擇優惠券,選擇地址等操做
處於該狀態的訂單對於後臺管理系統/用戶我的中心都是不可見的,且checkout類型訂單的建立,也不會是庫存有任何的變化
當用戶在結帳界面操做完成後須要用戶點擊確認訂單. 既行爲 confirm的觸發,使訂單的狀態從checkout轉換成了new. 此時的訂單不管是對於消費者/運營人員/倉儲系統來講,都是真實存在且有效的. 且響應的購物車記錄也被清空. 對於一筆狀態爲new的訂單,消費者能夠對其行使付款的權利.
order.payment_state↓
payment的初始狀態爲checkout與上述一致.
當消費者觸發confirm後, 咱們就能夠觸發request_payment行爲,將訂單的付款狀態轉換爲 await_payment, 且將消費者引導到支付界面, 當消費者支付成功後,在支付成功的回調中,觸發pay行爲,將支付狀態轉換爲paid.
關於退款的狀態如上圖所示,須要注意的是,對於退款,會出現只須要退訂單中的部分商品的狀況,所以加入了 partially_refunded(部分退款的狀態).
order.shipment_state↓
當消費者confirm後, 咱們同時也須要調用響應的request_shipment,將咱們的運輸狀態設置爲一個ready狀態,此時庫存已經鎖定.
關於倉庫具體的備貨時機 是在用戶確認訂單以後,仍是等用戶支付完成以後,須要根據實際的產品需求肯定.
上面的狀態圖屬於前者,當消費者確認訂單後,便鎖定了庫存,並開始了備貨階段.若是是後一種狀況能夠將checkout修改成pending,等待消費者付款完成後再將狀態轉移到ready
對於上面繁雜的狀態轉換,能夠手動處理,也能夠選擇使用state-machine 進行處理
單價做爲一件商品的固有屬性,不會受到運輸/促銷折扣等等因素的影響. 當商家對一個價值100元商品進行一個30%的折扣時,消費者只須要用70元的價格買入, 但實際上商品的單價依舊是100元.
當一筆訂單不存在任何的adjustment時,咱們能夠很容易的計算出訂單的實際支付價格, 只須要把各個order_item的unit_price * quantity 相加起來便可
可是有了adjustment參與以後,咱們必須自下往上的計算. 下面的例子是在laravel項目且使用了上述的數據庫設計後的一個計算方法.
public function calculator(Order $order) {
$items = $order->items;
$items->load('adjustments', 'units.adjustments');
$order->load('adjustments');
$items->each(function ($item) {
$item->units->each(function ($unit) {
$unit->adjustments_total = $unit->adjustments->sum('amount');
});
$item->units()->saveMany($item->units);
$item->adjustments_total = $item->adjustments->sum('amount');
$item->units_total = $item->quantity * $item->unit_price + $item->units->sum('adjustments_total');
$item->total = $item->units_total + $item->adjustments_total;
});
$order->items()->saveMany($items);
$order->adjustments_total = $order->adjustments->sum('amount');
$order->items_total = $order->items->sum('total');
$order->total = $order->items_total + $order->adjustments_total;
$order->save();
}
複製代碼
當訂單建立的同時(結帳階段)就分別建立了一條payment/和shipment記錄.在payment和shipment中分別記錄了用戶選擇的支付方式與運輸方式.
在電商系統中,一般會有多種多樣的支付方式和運輸方式.
可是在實際的業務編寫時,業務層並不但願關心和處理繁雜的支付與運輸方式,此時支付網關和運輸網關便應運而生,其對業務層隱藏了繁雜的細節,而暴露出了統一的api接口.
支付網關如提供商業服務的 ping++,固然也有一些開源項目對這方面有所支持. 如 yansongda/pay , Payum/Payum等等
對於確認了但超過必定時間沒有付款的訂單,咱們能夠選擇主動關閉該訂單. 將order.state/order.payment_state/order.shipment_state 設置爲cancelled,並對庫存進行歸還等系列操做
下一篇將會介紹促銷系統的設計與實現,本篇的主要目的是介紹訂單系統的相關設計,爲下一篇作一個鋪墊.
因爲篇幅有限並無過多的細節,有疑問或者不妥的地方歡迎留言.