情境導覽
購物車裡的商品,為什麼有些會綁在一起
電商購物車常有「主商品綁定子商品」的業務玩法,子商品的金流、物流與活動資格都必須跟著主商品連動。然而負責記錄購物車的 ShoppingCart 天生是一列只記一個 SKU 的單品項明細表,兩者產生了結構矛盾。這篇文章將拆解系統如何透過輔助表 ShoppingCartExtendInfo 補足這層分組關聯。
資料表關聯 組合商品 加價購 ShoppingCartExtendInfo
玩法一:組合商品
例如相機加電池加腳架以套裝價一起銷售,缺一不可,整組享有統一的套裝優惠與規則。
玩法二:加價購
例如購買洗髮精(主商品)後,獲得資格以優惠折扣加購潤髮乳(子商品)。

業務需求與資料庫設計的矛盾:

  • 業務共同特徵:購物車內必須明確表達「一個主商品搭配多個子商品」的主從關係,且子商品不可脫離主商品獨立存在。
  • 底層結構限制:ShoppingCart 資料表設計為單列單品項明細,每一列只有自身欄位,完全沒有空間記錄群組關係。
  • 解法核心:引入周邊輔助表 ShoppingCartExtendInfo 作為分組貼紙,透過共用 ItemGroup 串接主子列。
資料表結構
ShoppingCart:每一列就是一個 SKU 項目
ShoppingCart 表的主鍵是單一自增序號 ShoppingCart_Id,不是複合鍵,代表每一列本身就是一筆可以獨立定址的商品明細,而不是某種容器或群組的代表列。
欄位用途
ShoppingCart_Id主鍵,單一自增序號,一列代表一個商品項目
ShoppingCart_SalePageId商品頁序號
ShoppingCart_SaleProductId商品序號
ShoppingCart_SaleProductSKUId商品 SKU 序號
ShoppingCart_Qty購買數量,單一整數欄位
ShoppingCart_IsMajor是否為主件
ShoppingCart_IsExtra是否為加購商品
ShoppingCart_IsGift是否為買就送的贈品
ShoppingCart_OptionalTypeId選項類型序號,組合商品用來對應選配區塊
ShoppingCart_OptionalTypeDef選項類型定義,例如 SalepageBundleAddOnsSalepageExtraPurchase
🔍

判斷依據:從欄位設計可以看出這張表是典型的單列單品項明細表,理由如下:

  • Qty 是單一整數:如果一列能代表多個商品,數量欄位就無法用一個整數表示,因為不同商品的數量可能不一樣。
  • IsMajorIsExtraIsGift 是自身角色旗標:這些欄位都是描述這一列自己在購物車裡扮演的角色,而不是描述一整組商品的狀態。

這些欄位設計方式共同證實了這張表類似訂單明細常見的單列單品項設計模式。

🛒

購物車與資料列的對應關係:一台購物車對應的是一批列,不是一列。ShoppingCart 表裡沒有任何欄位叫做購物車序號,查詢購物車靠的是以下條件與行為:

  • 查詢鍵:用 ShoppingCart_MemberOrUnloginId 加上商店序號去篩選,而不是靠某一個固定 ID。
  • 查詢條件:CartRepository.GetCartAsyncWHERE 條件只鎖定會員或未登入序號、商店、門市這三個維度。
  • 回傳型別:回傳的是一個清單,而不是單一物件。

也就是說,同一位會員的購物車,通常會對應到好幾筆各自獨立的 ShoppingCart_Id,每加入一種商品就多一列,購物車本身是這一批列在程式裡被撈出來後組成的邏輯集合,而不是資料庫裡真實存在的某一列或某一張主表。

資料表結構
ShoppingCartExtendInfo:分組貼紙表
既然 ShoppingCart 沒辦法表達這幾列是同一組,系統另外開了一張輔助表 ShoppingCartExtendInfo,專門記錄誰跟誰是一組、遵循什麼規則、誰是主誰是從。它本身不存商品內容,商品的價格、名稱、庫存都還是查 ShoppingCart 或商品頁資料,這張表純粹扮演分組索引的角色。
欄位用途
ShoppingCartExtendInfo_Id主鍵,單一自增序號
ShoppingCartExtendInfo_ShoppingCartId外鍵,對應 ShoppingCart_Id,標記這筆延伸資料屬於哪一個商品項目
ShoppingCartExtendInfo_ItemGroup分組識別碼,同一組主子商品共用同一個值
ShoppingCartExtendInfo_ItemType1 代表主商品 Major,2 代表子商品 Sub
ShoppingCartExtendInfo_RuleTypeDef規則類型,SalepageBundle 為組合商品,AddOnsSalepageExtraPurchase 為加價購
ShoppingCartExtendInfo_ValidFlag生效標誌,軟刪除機制
🏷️

貼紙比喻:可以把這張表想像成一疊分組貼紙。ShoppingCart 裡原本互不相干的幾筆獨立商品列,被貼上同一個 ItemGroup 編號後,就變成程式眼中同一組,再搭配 ItemType 標明貼紙上寫的是老大還是跟班。

🧷

為什麼不是直接在 ShoppingCart 加欄位:分組資訊描述的是這一列跟別的列有什麼關係,而不是這一列自己的屬性,拆成獨立表有以下幾個理由:

  • 關聯而非屬性:主商品那一列需要表達的其實是有哪些子商品跟著我,這是一份長度不固定的清單,無法用單一欄位存下來,只能靠 ItemGroup 這種共用值去關聯多筆資料。
  • 避免稀疏資料:只有少數商品項目會用到分組,大多數還是一般商品,如果把 ItemGroupItemTypeRuleTypeDef 硬塞進 ShoppingCart,會讓九成以上的列白白多出幾乎用不到的欄位,而 ShoppingCart 又是加入購物車、結帳都會查詢的高頻表,欄位越寬對效能越不利。
  • 因應規則演進:拆成獨立表之後,未來如果要新增第三種、第四種分組規則,只需要擴充這張周邊表,完全不影響 ShoppingCart 原本的結構與既有查詢邏輯。
關聯方式
兩表如何用外鍵串起來
兩張表之間沒有正式的資料庫外鍵約束,關聯完全靠程式查詢時的 WHERE 條件比對達成。
商品明細ShoppingCart
1 對 0~1
分組貼紙ShoppingCartExtendInfo
ShoppingCart 端對應方向ShoppingCartExtendInfo 端
ShoppingCart_Id(主鍵)比對ShoppingCartExtendInfo_ShoppingCartId(外鍵)
商品明細:SalePageIdSKUQty不重複儲存分組資訊:ItemGroupItemTypeRuleTypeDef
🧩

沒有記錄就是一般商品:如果某個 ShoppingCart_IdShoppingCartExtendInfo 裡完全查不到對應記錄,代表這個商品項目是單純的一般商品,不屬於任何組合商品或加價購分組。這也是程式判斷要不要進入分組邏輯的依據。

⚙️

為什麼一個 ShoppingCartId 對應到 0 或 1 筆延伸記錄:每個商品項目在同一時間只會屬於一組關聯,要嘛是某個組合商品的一員,要嘛是某個加價購的一員,要嘛完全不屬於任何分組,所以對應關係最多只有一筆。真正把多筆商品串成一組的關鍵,是多個不同 ShoppingCartId 的延伸記錄共用同一個 ItemGroup 值。

實際案例
一般商品、組合商品、加購品同時出現在購物車
假設某位會員的購物車裡同時放了三種型態的商品:一件單純的 T-shirt,一組相機加電池加腳架的組合商品,以及洗髮精加購潤髮乳的加價購商品。以下用實際欄位示範這六筆商品在兩張表裡分別長什麼樣子。
CartId商品QtyIsMajorIsExtraOptionalTypeIdOptionalTypeDef
1001T-shirt2truefalse0(空白)
1002相機(主商品)1truefalse0(空白)
1003電池(子商品)2falsefalse10(電池選配區)SalepageBundle
1004腳架(子商品)1falsefalse11(腳架選配區)SalepageBundle
1005洗髮精(主商品)1truefalse0(空白)
1006潤髮乳(子商品)1falsetrue0AddOnsSalepageExtraPurchase
ShoppingCartIdItemGroupItemTypeRuleTypeDef
1002(相機)9001MajorSalepageBundle
1003(電池)9001SubSalepageBundle
1004(腳架)9001SubSalepageBundle
1005(洗髮精)9002MajorAddOnsSalepageExtraPurchase
1006(潤髮乳)9002SubAddOnsSalepageExtraPurchase
🔗

注意:1001(T-shirt)完全沒有出現在延伸表裡,這正是一般商品的特徵。ItemGroup 為 9001 的一組把相機、電池、腳架三筆原本互不相干的獨立列串成組合商品,ItemGroup 為 9002 的一組則把洗髮精、潤髮乳串成加價購,兩組互不干擾。

一般商品:T-shirt
只出現在 ShoppingCart,沒有任何延伸記錄,程式讀到後直接當獨立商品處理,不會進入分組邏輯。
組合商品:相機組
相機是主商品,電池與腳架是子商品,三者共用 ItemGroup 9001,子商品各自的 OptionalTypeId 對應不同選配區塊,用來套用各自的購買數量規則。
加價購:洗髮精組
洗髮精是主商品,潤髮乳是加購子商品,共用 ItemGroup 9002,子商品的 IsExtra 標記為 true,代表這是靠加購資格才能買到的商品。
讀取流程
建立購物車時,程式怎麼把兩張表拼回關聯
建立購物車時,程式會分兩步驟先各自查詢兩張表,再用 ItemGroup 把資料重新拼接回一個帶有主子關係的物件結構。
查詢 ShoppingCart 取得商品明細清單
查詢 ShoppingCartExtendInfo 取得分組記錄
依 ItemGroup 把分組資訊補回商品物件
後續金物流、活動、數量規則才能運作
階段ShoppingCart 查詢ShoppingCartExtendInfo 查詢記憶體物件
1依會員或未登入序號查詢,回傳六筆商品明細尚未執行尚未處理
2已完成依 CartId 清單查出分組記錄尚未處理
3已完成已完成補上 ItemGroup/ItemType/CartExtendInfos
🧭

一般商品的處理方式:一般商品直接沿用 ShoppingCart 查詢結果,不需要額外處理,因為在 ShoppingCartExtendInfo 裡完全查不到對應的分組記錄。

展開:程式怎麼判斷一組資料有沒有問題
  1. ShoppingCartExtendInfo 的查詢結果依 ItemGroup 分組,逐一比對每一組裡有沒有主商品。
  2. 如果一組裡找不到主商品,代表資料異常,整組會直接從購物車移除。
  3. 如果分組規則對應到已關閉的功能開關,例如組合商品開關被關閉,也會把整組從購物車移除。
  4. 確認分組有效後,把 CartExtendInfoItemGroupCartExtendInfoItemTypeDef 這些欄位寫回對應的商品物件,並且在主商品身上記錄一份子商品的 CartId 清單,方便後續步驟快速找到同一組裡的其他成員。
  • 組合商品的購買數量上限計算,需要依 ItemGroup 找出同一組的所有子商品,取各子商品可賣量除以必購量的最小值,決定主商品最多能賣幾組。
  • 加價購活動有效性驗證,需要先知道哪個商品是加購子商品,才能查詢對應的加購檔期是否還在有效期間內,沒中活動就整組拆解。
  • 金物流繼承,子商品的付款方式與配送方式不會各自查詢,而是直接複製主商品的結果,避免同一組商品出現金物流互相矛盾的情況。
總結
重點回顧
概念定位
ShoppingCart單列單品項明細表
ShoppingCartExtendInfo分組貼紙表
ItemGroup串接分組的關鍵值
🧾

核心結論:

  • ShoppingCart 存的是每個商品項目各自獨立的明細,價格、數量、選項都齊全,但完全不知道商品之間的主從關係。
  • ShoppingCartExtendInfo 不存商品內容,純粹記錄哪幾筆商品共用同一個 ItemGroup、誰是主誰是從、遵循哪一種業務規則。

程式在建立購物車時必須把兩張表的查詢結果重新拼接,才能重建出相機加電池加腳架是一組、洗髮精加潤髮乳是一組、T-shirt 自己一國的真實購物車樣貌,這也是後續組合商品數量計算、加購活動驗證、金物流繼承等規則的共同基礎。