【專案實戰經驗】電商系統常用資料結構
一家糧油店
去家附近的一家糧油店買米, 店裡面的東西挺多的,各種品牌的柴米油鹽樣樣不少,轉了一圈,挑了一袋 50 斤 的洞庭湖1號大米, 告訴老闆自己的住址,給老闆付了錢,然後我就回家了,回到家不一會兒,老闆就把米送過來了, 這速度 還真快,給個贊。晚上用新買的米做了頓飯,吃起來鬆軟可口,再給個贊,並決定下次還去這家店買米。
這是人們日常生活中的一個很普通的場景,而正是通過這樣一個普通的場景,我們可以抽象出大多數電商系統使用的常用資料結構。
把這個糧油店搬到線上
糧油店的老闆是一個比較有想法的人,他想弄一個網店,這樣他的顧客們可以在這個網店上選購自己需要的大米和食油,在網上支付,並填好送貨地址,而他接到單後 會主動把顧客購買的貨物送到顧客家中, 於是他的顧客就不用自己到店裡去挑選貨物,並且更遠的顧客也可以在他的線上糧油店裡購買貨物。
一. 糧油店的產品
以我購買的洞庭湖1號大米為例,洞庭湖1號大米又分 10公斤,15公斤,25公斤以及30公斤4個不同級別的包裝型別。以糧油店這種規模,我們 只需要設計 products 和 variants 兩個表就能滿足相關的需求, 其中 variants 表用來儲存產品的涉及到具體型號的資料,比如重量,體積,大小,價格等。
表: products
Column | Type | 註釋 ----------------------+-----------------------------+---------------- id | integer | 產品主 ID name | character varying(255) | 產品名字 description | character varying(2048) | 產品的描述
products 這個表很簡單(目前很簡單,如果糧油店的規模擴大了,這個表也會變的複雜), 只有 name, description 等資訊,一些重要的資訊比如 價格,重點,sku 等我們都放在了 variants 表裡了。這裡提到了 sku, 那麼在瞭解 variants 表之前我們先了解下 sku 是什麼。
什麼是 sku?
SKU 是 Stock Keeping Unit 的簡稱, 即庫存進出計量的單元, 也可以說是最小庫存單元,不能再細分,用以區分物品, 例如洞庭湖1號大米下就有 4 個 sku,分別是:
- 洞庭湖1號大米 10 公斤
- 洞庭湖1號大米 15 公斤
- 洞庭湖1號大米 25 公斤
- 洞庭湖1號大米 30 公斤
在電商系統中我們會給 sku 一個合適的編碼, 這個編碼是唯一的, 因此我們也可以認為 sku 就是商品的編碼, 比如我們可以給洞庭湖1號大米的 4 個 sku 做如下編碼:
- 洞庭湖1號大米 10 公斤, sku 是 dongtignhu-1-10k1
- 洞庭湖1號大米 15 公斤, sku 是 dongtignhu-1-15kg
- 洞庭湖1號大米 25 公斤, sku 是 dongtignhu-1-25kg
- 洞庭湖1號大米 30 公斤, sku 是 dongtignhu-1-30kg
表: variants
Column | Type | 註釋
-------------------------+-----------------------------+--------------
id | integer | 主鍵
product_id | integer | 關聯產品
sku | character varying(255) | 產品編碼
price | numeric(12,2) | 零售價格
weight | numeric(12,2) | 重量(以克為單位)
height | numeric(12,2) | 高度
width | numeric(12,2) | 長度
cost_price | numeric(12,2) | 成本價
products 和 variants 關係圖
二. 糧油店的第一個線上訂單
如同在實體店裡,顧客通過線上商店挑選了一袋 25 公斤的洞庭湖1號大米,兩瓶 500ml 加加醬油, 然後顧客會去結算。 那麼線上糧油店的結算過程會是什麼樣的呢? 我們回到文章的開頭,我在糧油店挑了一袋 25 公斤的洞庭湖1號大米, 我指了指我的貨物, 然後我告訴老闆我的住址,老闆會根據我住址的遠近人肉計算是否需要收取一定的送貨服務費,最後我把總的費用支付給老闆,這樣就完成了一次 結算。 在這次結算中涉及到的物件有 顧客, 商品,地址,支付,訂單等。
訂單是電商系統裡的核心資料結構,顧客,商品,地址,支付,貨運都是圍繞訂單運轉的。
表: users
Column | Type | 註釋
------------------------+-----------------------------+-------------------
id | integer | 使用者主鍵
email | character varying(255) | 郵箱
login | character varying(255) | 登入時使用的帳戶名
在這裡 users 表只列出了可以表識出使用者的欄位,在實際的專案中, users 表擁有的欄位會很多。
表: addresses
首先對於每一個使用者我們需要維護一個收貨地址列表,這樣使用者在結算時可以從此地址列表中選擇一個合適的地址作為其 收貨地址,其次每一個訂單都會有一個收貨地址。對於這樣一個過程,我們可以做如下設計:
-
使用者從收貨地址列表裡選擇了一個合適的地址 A1 (如果沒有合適的地址,使用者會編輯現有的地址或者增加新的地址)作為收貨地址;
-
建立訂單後, 此訂單會和地址 A1 的一個拷貝關聯起來, 這樣即使使用者以後修改 A1 也不會影響到以完成訂單的收貨地址;
我們可以參考開源專案 spree 關於表 addresses 的設計,
Table "public.addresses"
Column | Type | 註釋
-------------------+-----------------------------+---------------------------------------------
id | integer | 主鍵
firstname | character varying | 地址主人的名(比如收貨人的名)
lastname | character varying | 地址主人的姓(比如收貨人的姓)
address1 | character varying | 詳細地址1, 這個地址可以精確到路名或者街道地址,名牌號等
address2 | character varying | 補充地址2,備用
city | character varying | 城市
zipcode | character varying | 郵編
phone | character varying | 聯絡電話
state_name | character varying | 省或者州
alternative_phone | character varying | 備用電話
company | character varying | 公司
state_id | integer | 省或者州的關聯 id
country_id | integer | 國家的關聯 id, 如果支援海外貨運的化需要這個
created_at | timestamp without time zone | 建立時間
updated_at | timestamp without time zone | 更新時間
使用者的收貨地址列表
我們需要一箇中間表來關聯 users 表和 addresses 表, 按 Rails 的約定可以將此中間表命名為 ship_addresses_users,
Column | Type | 註釋
------------+-----------------------------+------------
id | integer |
address_id | integer |
user_id | integer |
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
這樣我們可以通過 ship_addresses_users 中間表檢索出使用者的收貨地址列表:
select * from users u
join ship_addresses_users ship_addr_u on ship_addr_u.user_id = u.id
join addresses ship_addr on ship_addr.id = ship_addr_u.address_id
where u.id = xxx
使用者和收貨地址列表的關係圖
使用者的訂單
- 訂單屬於某一個使用者
- 訂單有一個收貨地址
- 訂單包含若干商品
- 訂單有一個總的金額(包括商品價格, 稅, 運費等其他費用)
- 訂單有一個商品的總金額(只含商品)
- 訂單有一個調整的總金額(除商品外,只含稅,運費等其他費用)
- 訂單有一個實際支付的總金額
- 訂單有一個自身的狀態
- 訂單有一個貨運狀態
- 訂單有一個支付狀態
- 訂單有一個訂單號
依據上面的 11 條邏輯,我們可以構造一個比較完整的訂單的資料結構, 同時我們也參考 spree table: orders 關於表 orders 的設計,
Table "public.orders"
Column | Type | 註釋
----------------------+-----------------------------+-------------------------
id | integer | 主鍵
number | character varying(15) | 訂單號
item_total | numeric(8,2) | 商品總金額
total | numeric(8,2) | 總金額(包括商品價格, 稅, 運費等其他費用)
state | character varying | 訂單自身的狀態
adjustment_total | numeric(8,2) | 調整的總金額(除商品外,只含稅,運費等其他費用)
user_id | integer | 使用者關聯 id
completed_at | timestamp without time zone | 訂單完成時間
ship_address_id | integer | 收貨地址關聯 id
payment_total | numeric(8,2) | 實際支付的總金額
shipment_state | character varying | 貨運狀態
payment_state | character varying | 支付狀態
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
訂單的狀態
訂單的狀態我們分三種情況來分析,
- 訂單自身的狀態, 對應欄位 state;
- 訂單的支付狀態, 對應欄位 payment_state;
- 訂單的貨運狀態, 對應欄位 shipment_state;
參考 spree 對訂單的設計, 一般來說訂單自身的狀態可設定為,
:cart, :address, :delivery, :payment, :confirm, :complete
支付狀態可以設定為,
:balance_due, :paid, :credit_owed, :failed, :void
貨運狀態可以設定為,
:ready, :pending, :partial, :shipped, :backorder, :canceled
當然這些狀態到底怎麼設定,不是絕對不變的,還是需要根據專案本身的實際情況去思考和設計, 根據實際情況設定符合專案要求的狀態值。
訂單包含的商品
怎樣將訂單和商品關聯起來呢? 為此我們需要建立一個 line_items 表作為訂單和商品之間的關聯表, 這裡我們可以參考 spree table: line_items 關於表 line_items 的設計,
spree_demo_development=# \d line_items;
Table "public.line_items"
Column | Type | 註釋
------------+-----------------------------+---------------------------------------------------------
id | integer | 主鍵
variant_id | integer | 商品關聯 id
order_id | integer | 訂單關聯 id
quantity | integer | 商品數量
price | numeric(8,2) | 商品單價(購買時的單價)
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
Order, Product, Variant, LineItem 之間的關係圖
三. 支付
在電商系統中支付是一個非常重要的環節,支付的過程的流暢和安全直接影響到客戶的購買體驗,很多時候客戶就是因為支付過程的不友好而放棄購買,同時支付也是一個比較複雜的過程。
支付開始的時候一般首先需要選擇支付方式, 由此我們需要為支付方式建立相關的表: payment_methods, 這個表的設計可以參考 spree table: payment_methods,
表: payment_methods
Table "public.payment_methods"
Column | Type | 註釋
-------------+-----------------------------+--------------------------------------------------------------
id | integer | 主鍵
type | character varying | 支付方法型別: 比如 cash, credit, transfer 等
name | character varying | 支付方法名,比如: 微信支付,財付通支付, 支付寶支付, 銀聯支付,銀行轉帳,貨到付款等
description | text | 支付方法的描述
active | boolean | 是否啟用此支付方法
environment | character varying | 支付方法所處的環境,比如 production, staging, development 等
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
使用者選擇好支付方法後,就會進入到一個實質的支付過程,如果我們使用外部的支付閘道器,比如微信支付,都會涉及到一個外部閘道器的請求,記錄和處理外部閘道器的響應,更新支付狀態等等環節, 為了處理好這些環節我們需要建立一個或多個模型,而這些模型背後的資料結構就是 payments, 對 payments 的設計我們同樣可以參考 spree table: payments,
表: payments
Table "public.payments"
Column | Type | 註釋
-------------------+-----------------------------+-------------------------------------------------------
id | integer | 主鍵
amount | numeric(8,2) | 支付金額
order_id | integer | 關聯訂單 id
payment_method_id | integer | 關聯支付方法 id
state | character varying | 支付狀態
response_code | character varying | 閘道器響應碼
avs_response | character varying | 閘道器響應體
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
payment 的狀態可以設定為,
:checkout, :pending, :processing, :failed, :completed
同樣這些狀態也不是絕對的,可能需要根據自身的專案要求做適當的裁減。
支付和訂單的關係圖
四. 發貨
雖然我們的糧油店可能只有一種物流方式: 店老闆自己人力送貨上門,但是說不定哪天糧油店規模擴大,會需要更多的物流方式,為此我們為了系統的可擴充套件性需要建立物流方法表: shpping_methods, 這個表我們可以參考 spree table: shpping_methods 來構建,
表: shipping_methods
Table "public.shipping_methods"
Column | Type | 註釋
----------------------+-----------------------------+---------------------------------------------------------------
id | integer | 主鍵
name | character varying | 物流方法名
zone_id | integer | 物流方法關聯的區域 id
deleted_at | timestamp without time zone | 假刪除,記錄刪除日期
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
同樣我們也需要某個模型來抽象發貨這一業務,發貨業務涉及到發貨的狀態, 訂單,倉庫,費用,貨運單號,物流方法, 收貨地址,發貨時間,快遞跟蹤單號等一系列資料。我們把這個模型叫作 Shipmment, 同樣需要為其建立 相關的表: shipments, 我們可以參考 spree talbe: shipments,
表: shipments
Table "public.shipments"
Column | Type | 註釋
--------------------+-----------------------------+-----------------------------------------
id | integer | 主鍵
tracking | character varying | 快遞跟蹤單號
number | character varying | 貨運單號
cost | numeric(8,2) | 實際運費
shipped_at | timestamp without time zone | 發貨時間
order_id | integer | 關聯訂單 id
shipping_method_id | integer | 關聯物流方法 id
address_id | integer | 收穫地址關聯 id
state | character varying | 物流狀態
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
shipment 的狀態可以設定為,
:ready, :pending, :assemble, :cancelled, :shipped
發貨和訂單的關係圖
五. 退貨退款
雖然我們不希望使用者退貨退款,但是如果使用者不能夠合理地退貨退款,那麼她永遠也不會再來我們的線上商店購買東西了,所以退貨退款的功能非常重要。
為了支援退貨,退款,首先我們需要建立模型: ReturnAuthorization, 這個模型的意思是退貨退款授權, 這個模型能處理下面的資料和業務,
- 是哪個訂單需要退款;
- 退款金額;
- 退款理由;
- 退款狀態;
- 操作員;
ReturnAuthorization 模型也需要相關的表: return_authorizations 做支撐,我們參考 spree table: return_authorizations 的實現,
Table "public.return_authorizations"
Column | Type | 註釋
------------+-----------------------------+------------------------------------------------
id | integer | 主鍵
number | character varying | 退款授權編號
state | character varying | 退款授權狀態: 'authorized', 'canceled'
amount | numeric(8,2) | 退款金額
order_id | integer | 退款關聯訂單 id
reason | text | 退款理由
enter_by | integer | 操作員
enter_at | timestamp without time zone | 操作時間
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
一個訂單可能包括多個商品,使用者退貨可能只退其中的一部分,所以我們還需要一個 inventory_units 的表來記錄使用者申請退貨的具體商品,我們可以參考 spree table: inventory_units,
表: inventory_units
Table "public.inventory_units"
Column | Type | 註釋
-------------------------+-----------------------------+--------------------------------------------------------------
id | integer | 主鍵
lock_version | integer | 鎖定版本號
state | character varying | 狀態: 'backordered', 'on_hand', 'shipped', 'returned'
variant_id | integer | 退款商品關聯 id
order_id | integer | 退款訂單關聯 id
shipment_id | integer | 退款貨運物流關聯 id
return_authorization_id | integer | 退款授權關聯 id
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
當商品退回時,即 inventory_unit 的 state 為 returned 時,我們應該把錢款退給使用者,為此我們可以建立一個 Chargeback 模型來處理這一事務,同樣 Chargeback 需要表 chargebacks 作支撐。 chargebacks 可以作如下設計,
表 chargebacks
Table "public.chargebacks"
Column | Type | 註釋
-------------+-----------------------------+-----------------------------------------------------------------
id | integer | 主鍵
state | character varying(20) | 狀態
order_id | integer | 關聯訂單 id
operator_id | integer | 操作員 id
amount | numeric(12,2) | 實際退款金額
created_at | timestamp without time zone |
updated_at | timestamp without time zone |
退貨退款和訂單的關係圖