iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

Re:從零開始做直播代購電商平台系列 第 9 篇

Day 9|購物車到訂單(上):兩種購物車,與三層結構

  • 分享至 

  • xImage
  •  

交易主線的最後一站:單怎麼從購物車走到訂單。留言進了購物車、身分掛好了單、庫存卡住了不變量——這章把它們聚合成一筆可以付錢、可以出貨、可以開發票的東西。明天的標題不是比喻:這個系統裡真的有一台狀態機,被主播們用行動拔掉了。

兩種購物車,一張表

全景說過購物車有兩種:直播下單的佔庫存、商城自己加的不佔。資料模型的答案在身分章亮過相:一張 cart item 表,用 content type + object id(泛型外鍵)標記來源。佔不佔、算直播價還是商城價,由來源決定;數量調整、結帳、清除,全走同一套邏輯。

調整數量的邊界也因此很乾淨:佔庫存的單要加量,就是再打一次庫存章那句條件更新——搶得到才加;減量就是釋放,把差額還給計數。留言的 LWW 改單、客人自己調、客服代調,三條路徑走的是同一組動作,只是觸發者不同。

重來要補的一張表:購物車的操作帳

上面數過購物車的寫入者:LWW 改單、客人自調、客服代調,加上庫存章的檔期結算清除與重喊重置——一張表,五路寫入。但五路的「留痕」嚴重不對稱:留言來的變更有完整溯源(msg → cart item 那條鏈,結構化、可 join);人來的變更,當年不是沒記——我們會手動把操作寫進 Django 內建的那張 log 表——問題是那張表是全系統的大雜燴:購物車調整、商品改動、各種雜項操作通通混在同一張表,欄位泛用到只剩「誰、動了哪個東西、一段文字」。要回答「這筆單怎麼變成這樣」,得在大雜燴裡撈文字:撈得到靠運氣,撈到了也沒有前後數量、沒有差額、跟重算對不起來。客服接到「我的購物車怎麼變這樣」,留言那半查得到,人那半只能考古。這件事的教訓很具體:「有記錄」和「查得動的記錄」是兩回事——泛用 log 表寫的時候人人安心,查的時候形同沒有。

重來我會為 cart item 專門開一張操作紀錄表——不是「開始記錄」,是把記錄從大雜燴裡搬出來、給它結構:每次變更,在同一筆交易內 append 一筆,觸發者(哪則留言、客人、哪位客服、檔期結算、重喊)、前後數量、差額,欄位打死、可以 join。三個講究:

  • 它是變更帳,不是事實來源。 cart 表照舊是交易事實、照舊在同步交易裡守超賣——這不是把購物車改成 event sourcing。超賣是硬不變量,檢查必須發生在寫入之前;把「先查再扣」搬進 event store、再靠投影派生狀態,最難的序列化決策一點沒省,只是換了身衣服。
  • 同一筆交易 append,成本趨近零,買到三件事:客訴可重建、客服操作有 audit(後台章的重來清單本來就有這條)、每小時重算抓到差異時終於有地方查案——差異不為零代表某條增量路徑有 bug,這張表就是查案的卷宗。
  • 這不是新發明,是把「貨」的紀律補給「單」。 配貨系統當年就有配貨紀錄表、庫存變動可用事件全量重建(出貨章會講)——貨有的待遇,單也該有。哪天催付、風控、營運分析想吃購物車的變更流,這張表升級成 outbox 就行:是演進,不是重寫。

合併結帳:三層,各自對應一個現實

結帳有個當年就支援的狠需求:跨檔期合併結帳——不同檔期、不同實況主買的東西,一次付清。這逼出了三層結構:

三層不是架構潔癖:付錢、履約、會計是三個節奏不同的現實,各要一層。

三層各自的存在理由,比「正規化」更務實:

  • payment 是付錢的單位。 客人不在乎你的檔期,他要一次付清——所以聚合層必須存在。它的狀態由第三方付款事實聚合而來(部分付款、多渠道,精彩留給下一章)。
  • order 是履約的單位,按檔期切。 代購的貨跟著檔期到、出貨跟著檔期走,售後的節奏天生以檔期為界;檔期限定的優惠券也記在這層。
  • order item 是會計的單位。 成交那一刻定格金額——發票、退款、對帳,全部站在這個不再變動的數字上。

而「購物車到訂單」這個動作本身的形狀,已經預告了明天的主題:結帳不是去改 cart item 的狀態——付款當下新增一筆 order item,和庫存帳本上「購物車數量轉訂單數量」在同一筆交易內完成。狀態轉移用新增記錄表達,溯源鏈因此自然多接一段:msg → cart item → order item,任何一筆成交都能一路回溯到當初那則留言。

價格的生命週期:承諾之前查事實,承諾之後不再變

價格在這個系統裡有一條清楚的生命線:

  • 購物車階段:不存價,永遠查現價。 主播現場喊價、營運事後才補登金額——如果加入購物車時就把價格拷貝進 cart item,每次補登都要跑大量回填。查現價,零回填。正規化的優勢第三次出現(前兩次:身分章的反正規化判準、庫存章的檔期大掃除)。cart item 因為帶著來源,直播價和商城價自然分流——同一件商品,兩個通路兩個價,不用任何特殊處理。
  • 成交那一刻:定格。 order item 把金額存死——不只是發票和退款需要一個不再變動的數字,更因為客人的心理契約:付過錢的東西,數字不能變。優惠也在這一刻算清:優惠券、免運,有的記在 order item、有的記在檔期層(檔期限定券),各自留下事實。

一句話收:snapshot 只發生在承諾點——承諾之前永遠查事實,承諾之後永遠不再變。 cart 是意向,order 是承諾,兩邊的價格策略相反,而且都是對的。

反思

三層結構不是潔癖,是三個現實各要一層

payment、order、order item 這三層,如果當年是為了「架構好看」而分,大概撐不過第一次需求變更。它們撐住了,是因為每層背後有一個獨立變動的現實:客人要一次付清(付錢的節奏)、貨按檔期到(履約的節奏)、帳要經得起發票與退款(會計的節奏)。後來跨檔期合併、部分付款、檔期限定券這些複雜需求一個個冒出來,結構都接得住——複雜需求殺不死職責單純的結構,殺得死聰明但混雜的結構。 判斷要不要多一層的標準,從來不是「教科書說要」,是「這層對應的現實,會不會獨立於其他層變動」。

這一章之所以「無聊」,是前面的章在還債

寫到這章我發現一件事:它沒有事故可講。沒有超賣等級的爆炸、沒有被打爆的 API——結帳這段當年就是穩穩地跑。回頭看不是運氣:購物車一張表多型,所以合併結帳不用縫合兩套邏輯;單掛在身分上,所以誰來結帳都不用搬資料;帳本雙計數,所以 cart→order 的轉移一筆交易就完成。結帳只是把前面章節做對的事實聚合起來而已。 系統設計裡最被低估的讚美就是「無聊」——會爆炸的章節好寫,無聊的章節難得。願你的結帳流程也無聊。

但這個系統裡,有一台真的被拔掉的狀態機。明天講它的故事。


本文改寫自我的部落格系列《Re:從零開始做直播代購電商平台》,本篇完整版:https://blog.aidan.tw/blog/rezero-cart-order/


上一篇
Day 8|庫存(下):那次真的超賣,兇手是一次 migration
下一篇
Day 10|購物車到訂單(下):被主播拔掉的狀態機
系列文
Re:從零開始做直播代購電商平台 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言