購物車資料結構解密:ShoppingCart 與 ShoppingCartExtendInfo
ShoppingCart 天生是一列只記一個 SKU 的單品項明細表,兩者產生了結構矛盾。這篇文章將拆解系統如何透過輔助表 ShoppingCartExtendInfo 補足這層分組關聯。業務需求與資料庫設計的矛盾:
- 業務共同特徵:購物車內必須明確表達「一個主商品搭配多個子商品」的主從關係,且子商品不可脫離主商品獨立存在。
- 底層結構限制:
ShoppingCart資料表設計為單列單品項明細,每一列只有自身欄位,完全沒有空間記錄群組關係。 - 解法核心:引入周邊輔助表
ShoppingCartExtendInfo作為分組貼紙,透過共用ItemGroup串接主子列。
ShoppingCart 表的主鍵是單一自增序號 ShoppingCart_Id,不是複合鍵,代表每一列本身就是一筆可以獨立定址的商品明細,而不是某種容器或群組的代表列。| 欄位 | 用途 |
|---|---|
ShoppingCart_Id | 主鍵,單一自增序號,一列代表一個商品項目 |
ShoppingCart_SalePageId | 商品頁序號 |
ShoppingCart_SaleProductId | 商品序號 |
ShoppingCart_SaleProductSKUId | 商品 SKU 序號 |
ShoppingCart_Qty | 購買數量,單一整數欄位 |
ShoppingCart_IsMajor | 是否為主件 |
ShoppingCart_IsExtra | 是否為加購商品 |
ShoppingCart_IsGift | 是否為買就送的贈品 |
ShoppingCart_OptionalTypeId | 選項類型序號,組合商品用來對應選配區塊 |
ShoppingCart_OptionalTypeDef | 選項類型定義,例如 SalepageBundle、AddOnsSalepageExtraPurchase |
判斷依據:從欄位設計可以看出這張表是典型的單列單品項明細表,理由如下:
Qty是單一整數:如果一列能代表多個商品,數量欄位就無法用一個整數表示,因為不同商品的數量可能不一樣。IsMajor、IsExtra、IsGift是自身角色旗標:這些欄位都是描述這一列自己在購物車裡扮演的角色,而不是描述一整組商品的狀態。
這些欄位設計方式共同證實了這張表類似訂單明細常見的單列單品項設計模式。
購物車與資料列的對應關係:一台購物車對應的是一批列,不是一列。ShoppingCart 表裡沒有任何欄位叫做購物車序號,查詢購物車靠的是以下條件與行為:
- 查詢鍵:用
ShoppingCart_MemberOrUnloginId加上商店序號去篩選,而不是靠某一個固定 ID。 - 查詢條件:
CartRepository.GetCartAsync的WHERE條件只鎖定會員或未登入序號、商店、門市這三個維度。 - 回傳型別:回傳的是一個清單,而不是單一物件。
也就是說,同一位會員的購物車,通常會對應到好幾筆各自獨立的 ShoppingCart_Id,每加入一種商品就多一列,購物車本身是這一批列在程式裡被撈出來後組成的邏輯集合,而不是資料庫裡真實存在的某一列或某一張主表。
ShoppingCart 沒辦法表達這幾列是同一組,系統另外開了一張輔助表 ShoppingCartExtendInfo,專門記錄誰跟誰是一組、遵循什麼規則、誰是主誰是從。它本身不存商品內容,商品的價格、名稱、庫存都還是查 ShoppingCart 或商品頁資料,這張表純粹扮演分組索引的角色。| 欄位 | 用途 |
|---|---|
ShoppingCartExtendInfo_Id | 主鍵,單一自增序號 |
ShoppingCartExtendInfo_ShoppingCartId | 外鍵,對應 ShoppingCart_Id,標記這筆延伸資料屬於哪一個商品項目 |
ShoppingCartExtendInfo_ItemGroup | 分組識別碼,同一組主子商品共用同一個值 |
ShoppingCartExtendInfo_ItemType | 1 代表主商品 Major,2 代表子商品 Sub |
ShoppingCartExtendInfo_RuleTypeDef | 規則類型,SalepageBundle 為組合商品,AddOnsSalepageExtraPurchase 為加價購 |
ShoppingCartExtendInfo_ValidFlag | 生效標誌,軟刪除機制 |
貼紙比喻:可以把這張表想像成一疊分組貼紙。ShoppingCart 裡原本互不相干的幾筆獨立商品列,被貼上同一個 ItemGroup 編號後,就變成程式眼中同一組,再搭配 ItemType 標明貼紙上寫的是老大還是跟班。
為什麼不是直接在 ShoppingCart 加欄位:分組資訊描述的是這一列跟別的列有什麼關係,而不是這一列自己的屬性,拆成獨立表有以下幾個理由:
- 關聯而非屬性:主商品那一列需要表達的其實是有哪些子商品跟著我,這是一份長度不固定的清單,無法用單一欄位存下來,只能靠
ItemGroup這種共用值去關聯多筆資料。 - 避免稀疏資料:只有少數商品項目會用到分組,大多數還是一般商品,如果把
ItemGroup、ItemType、RuleTypeDef硬塞進ShoppingCart,會讓九成以上的列白白多出幾乎用不到的欄位,而ShoppingCart又是加入購物車、結帳都會查詢的高頻表,欄位越寬對效能越不利。 - 因應規則演進:拆成獨立表之後,未來如果要新增第三種、第四種分組規則,只需要擴充這張周邊表,完全不影響
ShoppingCart原本的結構與既有查詢邏輯。
WHERE 條件比對達成。| ShoppingCart 端 | 對應方向 | ShoppingCartExtendInfo 端 |
|---|---|---|
ShoppingCart_Id(主鍵) | 比對 | ShoppingCartExtendInfo_ShoppingCartId(外鍵) |
商品明細:SalePageId/SKU/Qty | 不重複儲存 | 分組資訊:ItemGroup/ItemType/RuleTypeDef |
沒有記錄就是一般商品:如果某個 ShoppingCart_Id 在 ShoppingCartExtendInfo 裡完全查不到對應記錄,代表這個商品項目是單純的一般商品,不屬於任何組合商品或加價購分組。這也是程式判斷要不要進入分組邏輯的依據。
為什麼一個 ShoppingCartId 對應到 0 或 1 筆延伸記錄:每個商品項目在同一時間只會屬於一組關聯,要嘛是某個組合商品的一員,要嘛是某個加價購的一員,要嘛完全不屬於任何分組,所以對應關係最多只有一筆。真正把多筆商品串成一組的關鍵,是多個不同 ShoppingCartId 的延伸記錄共用同一個 ItemGroup 值。
T-shirt,一組相機加電池加腳架的組合商品,以及洗髮精加購潤髮乳的加價購商品。以下用實際欄位示範這六筆商品在兩張表裡分別長什麼樣子。| CartId | 商品 | Qty | IsMajor | IsExtra | OptionalTypeId | OptionalTypeDef |
|---|---|---|---|---|---|---|
1001 | T-shirt | 2 | true | false | 0 | (空白) |
1002 | 相機(主商品) | 1 | true | false | 0 | (空白) |
1003 | 電池(子商品) | 2 | false | false | 10(電池選配區) | SalepageBundle |
1004 | 腳架(子商品) | 1 | false | false | 11(腳架選配區) | SalepageBundle |
1005 | 洗髮精(主商品) | 1 | true | false | 0 | (空白) |
1006 | 潤髮乳(子商品) | 1 | false | true | 0 | AddOnsSalepageExtraPurchase |
| ShoppingCartId | ItemGroup | ItemType | RuleTypeDef |
|---|---|---|---|
1002(相機) | 9001 | Major | SalepageBundle |
1003(電池) | 9001 | Sub | SalepageBundle |
1004(腳架) | 9001 | Sub | SalepageBundle |
1005(洗髮精) | 9002 | Major | AddOnsSalepageExtraPurchase |
1006(潤髮乳) | 9002 | Sub | AddOnsSalepageExtraPurchase |
注意:1001(T-shirt)完全沒有出現在延伸表裡,這正是一般商品的特徵。ItemGroup 為 9001 的一組把相機、電池、腳架三筆原本互不相干的獨立列串成組合商品,ItemGroup 為 9002 的一組則把洗髮精、潤髮乳串成加價購,兩組互不干擾。
ShoppingCart,沒有任何延伸記錄,程式讀到後直接當獨立商品處理,不會進入分組邏輯。ItemGroup 9001,子商品各自的 OptionalTypeId 對應不同選配區塊,用來套用各自的購買數量規則。ItemGroup 9002,子商品的 IsExtra 標記為 true,代表這是靠加購資格才能買到的商品。ItemGroup 把資料重新拼接回一個帶有主子關係的物件結構。| 階段 | ShoppingCart 查詢 | ShoppingCartExtendInfo 查詢 | 記憶體物件 |
|---|---|---|---|
| 1 | 依會員或未登入序號查詢,回傳六筆商品明細 | 尚未執行 | 尚未處理 |
| 2 | 已完成 | 依 CartId 清單查出分組記錄 | 尚未處理 |
| 3 | 已完成 | 已完成 | 補上 ItemGroup/ItemType/CartExtendInfos |
一般商品的處理方式:一般商品直接沿用 ShoppingCart 查詢結果,不需要額外處理,因為在 ShoppingCartExtendInfo 裡完全查不到對應的分組記錄。
展開:程式怎麼判斷一組資料有沒有問題
- 把
ShoppingCartExtendInfo的查詢結果依ItemGroup分組,逐一比對每一組裡有沒有主商品。 - 如果一組裡找不到主商品,代表資料異常,整組會直接從購物車移除。
- 如果分組規則對應到已關閉的功能開關,例如組合商品開關被關閉,也會把整組從購物車移除。
- 確認分組有效後,把
CartExtendInfoItemGroup、CartExtendInfoItemTypeDef這些欄位寫回對應的商品物件,並且在主商品身上記錄一份子商品的CartId清單,方便後續步驟快速找到同一組裡的其他成員。
- 組合商品的購買數量上限計算,需要依
ItemGroup找出同一組的所有子商品,取各子商品可賣量除以必購量的最小值,決定主商品最多能賣幾組。 - 加價購活動有效性驗證,需要先知道哪個商品是加購子商品,才能查詢對應的加購檔期是否還在有效期間內,沒中活動就整組拆解。
- 金物流繼承,子商品的付款方式與配送方式不會各自查詢,而是直接複製主商品的結果,避免同一組商品出現金物流互相矛盾的情況。
| 概念 | 定位 |
|---|---|
ShoppingCart | 單列單品項明細表 |
ShoppingCartExtendInfo | 分組貼紙表 |
ItemGroup | 串接分組的關鍵值 |
核心結論:
ShoppingCart存的是每個商品項目各自獨立的明細,價格、數量、選項都齊全,但完全不知道商品之間的主從關係。ShoppingCartExtendInfo不存商品內容,純粹記錄哪幾筆商品共用同一個ItemGroup、誰是主誰是從、遵循哪一種業務規則。
程式在建立購物車時必須把兩張表的查詢結果重新拼接,才能重建出相機加電池加腳架是一組、洗髮精加潤髮乳是一組、T-shirt 自己一國的真實購物車樣貌,這也是後續組合商品數量計算、加購活動驗證、金物流繼承等規則的共同基礎。


