CalculatePayDeliveryMappingProcessor
處理什麼問題?
多件商品放進同一台購物車,付款與配送方式必須符合每件商品的限制。
CalculatePayDeliveryMappingProcessor 找出共同支援的選項,再排除無法合法配對的金物流。
金流:每件商品都支援,才能留下
咖啡豆與餅乾一起結帳。✓ 代表支援,× 代表不支援。
| 商品 | 信用卡 | ATM | LINE Pay |
|---|---|---|---|
| 咖啡豆 | ✓ | ✓ | × |
| 餅乾 | ✓ | × | ✓ |
| 商品交集 | 保留 | 排除 | 排除 |
信用卡通過商品交集,仍須繼續套用購物車允許付款方式與配對限制。
物流:共同選宅配,各溫層保留自己的設定
假設同溫層商品已完成物流交集,且所有配送地區相同。
flowchart TB
Normal["常溫:宅配 #101、超取 #201"] -->|共同類型與地區| Shared["宅配"]
Frozen["冷凍:宅配 #301"] -->|共同類型與地區| Shared
Shared --> WarmDelivery["保留常溫宅配 #101"]
Shared --> ColdDelivery["保留冷凍宅配 #301"]
Normal -.冷凍不支援超取.-> Removed["排除超取 #201"]
classDef keep fill:#edf9f4,stroke:#32856b,color:#175440
classDef base fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef muted fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef stop fill:#fff1f2,stroke:#b42332,color:#852332
class Normal,Frozen base
class Shared,WarmDelivery,ColdDelivery keep
class Removed muted
跨溫層比較配送類型與地區,不要求物流 Id 相同,也不表示商品要裝在同一包裹。
配對:有物流,還要有能搭配的金流
本例商品交集留下信用卡,以及宅配、超取付款兩種物流。
flowchart LR
subgraph Payment["付款方式"]
Card["信用卡:本次可用"]
StorePay["超取付款金流:本次缺少"]
end
subgraph Shipping["配送方式"]
Home["宅配:保留"]
Store["超取付款物流:排除"]
end
Card -->|設定允許,本次成立| Home
StorePay -.設定允許,本次無法成立.-> Store
classDef keep fill:#edf9f4,stroke:#32856b,color:#175440
classDef base fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef muted fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef stop fill:#fff1f2,stroke:#b42332,color:#852332
class Card,Home keep
class StorePay,Store muted
內部資料處理
Processor 呼叫 PayShippingMappingService.CalculatePayDeliveryMapping,讀取資料、計算交集,再寫回購物車。
%%{init: {'flowchart': {'nodeSpacing': 18, 'rankSpacing': 30, 'curve': 'linear'}}}%%
flowchart TB
Mapping["全域 Mapping 配對資料"] --> Valid{"Mapping 有資料?"}
Valid -->|否| Error["拋出 CartCacheExpired 例外"]
Valid -->|是| Restore{"需要還原商品清單?"}
Dictionary["商品金流/物流 Dictionary"] -.依商品頁 Id 讀取.-> Assign["重新指派商品金物流清單"]
Restore -->|是| Assign
Restore -->|否:沿用商品清單| Delivery
Assign --> Delivery["同溫層與跨溫層物流交集"]
Delivery --> Pay["所有商品的金流交集"]
Limits["允許付款方式、優惠碼與黑名單"] -.物流或金流限制.-> Delivery
Limits -.金流允許清單與優惠碼.-> Pay
Pay --> Pair["Mapping 篩選 CheckoutType 選項"]
Mapping -.合法配對.-> Pair
Pair --> Union["整理海外配送物流聯集"]
Products["一般、贈品券與海外無交集商品物流"] -.聯集來源.-> Union
Union --> Available{"金流與物流皆有選項?"}
Available -->|是| Ready["保留可用選項,更新提示"]
Available -->|否| Move["商品移入無交集清單,更新提示"]
Move --> Cache["保存 Redis,設定中斷與重算旗標"]
classDef input fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef output fill:#edf9f4,stroke:#32856b,color:#175440
classDef failure fill:#fff1f2,stroke:#b42332,color:#852332
class Mapping,Dictionary,Limits,Products input
class Valid,Restore,Assign,Delivery,Pay,Pair,Union,Available work
class Ready output
class Error,Move,Cache failure
灰色為輸入、藍色為處理、綠色為可用結果、紅色為例外或無交集處理。實線表示順序,虛線表示資料供應。圖中省略排序與主子商品置換細節。
資料從哪裡來?查看來源與節點
下表購物車欄位位於 context.Data。
| 來源 | 讀取節點/用途 |
|---|---|
| 全域配對設定 | PayShippingMapping,定義金流與物流的合法搭配;查詢沒有以 ShopId 篩選。 |
| 商品金物流基準 | SalepageIdPayTypesDictionary[SalePageId]SalepageIdDeliveryTypesDictionary[SalePageId] |
| 參與交集的商品 | SalepageGroupList[…].SalepageList[…].PayTypeListSalepageGroupList[…].SalepageList[…].DeliveryTypeList;已存在的贈品券商品也參與。 |
| 本次允許付款 | EnablePayProfileTypeDef |
| 優惠碼與黑名單限制 | PromoCodeDispatch.PromoCodeInfo.PaymentTypesBlacklistMemberAllowedPayProfileList |
CartCreateProcessor.AssignSalePagePayShipping 先以 ShopId 與商品頁 Id 查詢 CartRepository.GetSalePagePayTypeAsync、GetSalePageDeliveryType,建立 Dictionary 並首次設定商品金物流。
兩支商品查詢分別呼叫 csp_GetSalePagePayTypeV2 與 csp_GetSalePageDeliveryTypeV2。全域 Mapping 則由 GetPayShippingMappingAsync 透過快取取得,未命中才查 Repository。
還原與交集:查看條件、方法與比較鍵
| 處理 | 規則 |
|---|---|
| 是否還原 | context.IsSkipSalepagePayDeliveryTypeInitial = false 才執行還原;為 true 時沿用目前商品清單。 |
| 商品還原 | ArrangePayDeliveryTypeToSalePage 在 Mapping Service 內從 Dictionary 重新指派商品的 PayTypeList、DeliveryTypeList,不重新查商品設定。 |
| 同溫層物流 | GetSalePageGroupDeliveryType 先處理合併物流,再比對 Id + ShippingAreaId。 |
| 跨溫層物流 | CalculateDeliveryTypeAsync 比對 ShippingProfileTypeDef + ShippingAreaId,保留各溫層明細。 |
| 商品金流 | CalculatePayType 依 PayProfileTypeDef 找共同付款類型,再套用允許清單與優惠碼規則。 |
| 合法配對 | CalculatePayShippingMapping 依全域設定刪減 CheckoutType 的兩份選項清單。 |
海外配送:聯集與交集的差別
CalculateOverseaDeliveryTypeAsync 讀取一般商品、已有贈品券商品,以及無交集清單中標記 OverseaNoIntersection 的商品物流,整理合併物流並去重、排序。
結果寫入 DeliveryTypeUnionList,供後續海外配送使用;它不是本輪共同可結帳的物流清單。
流程位置與原始碼對照
在 InitailPayShippingCalculate 中為第 4 步,在 RePayShippingCalculate 中為第 3 步。實際執行仍受入口條件與中斷影響。
主要來源:CalculatePayDeliveryMappingProcessor.ExecuteAsync、PayShippingMappingService.CalculatePayDeliveryMapping、ProcessorDefinitionCenter。
全域 Mapping:金流與物流的合法配對設定
PayShippingMapping 定義付款類型與配送類型之間允許的搭配。這份設定供購物車計算使用,查詢沒有依本次商店的 ShopId 篩選。
一筆資料代表一組允許的搭配
| 付款類型 | 配送類型 | 這筆設定的意思 |
|---|---|---|
| 信用卡 | 宅配 | 允許信用卡搭配宅配。 |
| 信用卡 | 海外配送 | 允許同一付款類型搭配另一配送類型。 |
資料取得:先讀快取,未命中才查資料庫
CalculatePayDeliveryMappingProcessor.ExecuteAsync 呼叫 PayShippingMappingService.CalculatePayDeliveryMapping;Service 一開始便呼叫自己的 GetPayShippingMappingAsync 取得設定。
flowchart TB
Start["GetPayShippingMappingAsync"] --> Memory{"Memory 有資料?"}
Memory -->|命中| Result["回傳全域配對清單"]
Memory -->|未命中| Redis{"Redis 有資料?"}
Redis -->|命中| Fill["回填 Memory"]
Redis -->|未命中| DB["Repository 查詢兩表有效設定"]
DB --> Save["快取結果至 Redis"]
Save --> Fill
Fill --> Result
classDef input fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef output fill:#edf9f4,stroke:#32856b,color:#175440
class Start,Memory,Redis,Save,Fill work
class DB input
class Result output
正常讀取路徑,cleanCache 預設為 false。快取由 IDataCacheService.GetMemoryOrRedisCacheDataAsync 統一處理。
| 來源節點 | 用途與範圍 |
|---|---|
DataCacheService.GlobalSettingShopId | 使用全域設定識別值 -735268,不使用本次購物車的商店 Id。 |
| Memory/Redis | 此查詢分別傳入 600 秒、3600 秒的快取期限。 |
PayShippingMappingRepository.GetPayShippingMappingAsync | 快取未命中時執行的資料來源查詢。 |
WebstoreReadOnlyDbContext | 讀取 PayShippingMapping 與 PayProfile。 |
快取識別:ServiceName、TypeName 與 Key
PayShippingMappingService.GetCacheKey 建立 ServiceName = PayShippingMapping、TypeName = GetPayShippingMappingAsync-2016122117、Key = All。
這些是傳給快取服務的組成值。DataCacheService 會再加入全域設定 Id 與 locale,因此 All 不是完整實體快取鍵;全域也不表示跨所有 locale 共用同一鍵。
資料庫如何組成回傳欄位?
Repository 以 PayShippingMapping_PayProfileTypeDef = PayProfile_TypeDef JOIN 兩表,且 PayShippingMapping_ValidFlag、PayProfile_ValidFlag 都必須為 true。
| 資料庫來源欄位 | PayShippingMappingEntity 欄位 | 資料意義 |
|---|---|---|
PayShippingMapping.PayShippingMapping_PayProfileTypeDef | PayProfileTypeDef | 配對的付款類型。 |
PayShippingMapping.PayShippingMapping_ShippingProfileTypeDef | ShippingProfileTypeDef | 配對的配送類型。 |
PayProfile.PayProfile_StatisticsTypeDef | StatisticsTypeDef | 付款統計類型;本輪配對判斷未使用此欄。 |
取得後存在哪裡、交給誰?
| 節點/條件 | 處理 |
|---|---|
CalculatePayDeliveryMapping 的區域變數 payShippingMappingList | 接收 List<PayShippingMappingEntity>,並未另外寫入 context.Data 的某個 Mapping 欄位。 |
| 清單為空 | 立即拋出 CalculateException(CartCacheExpired),尚未進入商品交集計算。 |
| 清單有資料 | Service 保留此變數,待商品交集計算後,傳給 CalculatePayShippingMapping(cartEntity, payShippingMappingList) 使用。 |
商品頁本身支援哪些金流與物流?
查詢範圍:商店與商品頁 Id
CartCreateProcessor.ExecuteAsync 整理可結帳商品後,呼叫 AssignSalePagePayShipping。方法取出 salePageList 的 CartSalePageId,以 Distinct 去重,再連同 context.ShopId 傳給 Repository。
| 資料 | CartRepository 方法 | Stored Procedure |
|---|---|---|
| 商品金流 | GetSalePagePayTypeAsync | csp_GetSalePagePayTypeV2 |
| 商品物流 | GetSalePageDeliveryType | csp_GetSalePageDeliveryTypeV2 |
兩支查詢都透過唯讀 DbContext 執行,參數是 @shopId 與逗號串接的 @salePageIds,查詢結果再以 SalePageId 分組。此處未套用前章全域 Mapping 的兩層快取。
資料如何進入購物車?
flowchart TB
Query["商店 Id + 商品頁 Id 清單"] --> SP["金流、物流兩支 SP"]
SP --> Group["各自按 SalePageId 分組"]
Group --> Prepare["CartCreate 整理資料"]
Prepare --> Dict["保存兩份 Dictionary"]
Dict --> First["首次指派商品金物流"]
First --> Cart["依溫層分組並建立 CartEntity"]
Cart --> Later["Mapping Service 依條件重新指派"]
classDef input fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef output fill:#edf9f4,stroke:#32856b,color:#175440
class Query,SP input
class Group,Prepare,First,Cart work
class Dict,Later output
Dictionary 留在購物車內供後續依商品頁 Id 取用;重新指派時不在該方法重查 SP。
| 資料節點 | 誰寫入、誰讀取 |
|---|---|
context.Data.SalepageIdPayTypesDictionary[SalePageId] | AssignSalePagePayShipping 寫入金流基準清單;ArrangePayDeliveryTypeToSalePage 讀取。 |
context.Data.SalepageIdDeliveryTypesDictionary[SalePageId] | 同一寫入與讀取流程,保存物流明細。 |
salePageList[…].PayTypeList、DeliveryTypeList | AssignSalePagePayShipping 依 CartSalePageId 首次指派。 |
context.Data.SalepageGroupList[…].SalepageList[…].PayTypeList、DeliveryTypeList | GroupCartSalePage 帶入商品群組;之後由交集計算方法直接讀取。 |
GroupCartSalePage 將 CartSalePageId 轉成商品的 Id,並帶入兩份清單。建立新的 CartEntity 時,也會將先前的兩份 Dictionary 一併帶入。
例子:同一商品頁的不同 SKU 使用同一個查詢鍵
| 購物車商品 | 商品頁 Id | 金流來源 |
|---|---|---|
| 咖啡豆,小包 SKU | 1001 | SalepageIdPayTypesDictionary[1001] |
| 咖啡豆,大包 SKU | 1001 | SalepageIdPayTypesDictionary[1001] |
| 馬克杯 | 2002 | SalepageIdPayTypesDictionary[2002] |
回傳欄位:金流類型與物流明細
| Entity | 欄位/轉換 |
|---|---|
ShoppingCartClientPayTypeEntity | PayProfileTypeDef、StatisticsTypeDef 由 SP 結果同名欄位帶入。 |
ShoppingCartClientDeliveryTypeEntity 的識別與分類 | Id = ShopShippingTypeId;另含 ShippingProfileTypeDef、ShippingAreaId、TemperatureTypeDef、MergeShippingTypeId、TypeName。 |
| 物流費用與限制 | Fee、OriginalFee 均來自 Fee;FeeTypeDef、OriginalFeeTypeDef 均來自 FeeTypeDef。另有 FeeTypeDefDesc、OverPrice、MaxPriceLimit、WeightLimit,以及 Rule = FeeRule。 |
| 配送日期與排序 | IsEnableBookingPickupDate、ExcludeOfWeek;Sort 取 ShippingAreaSort,無值時為 0。 |
本次核對到 Cart Repository 的 SP 呼叫與結果模型,尚未展開兩支 SP 內部的資料表 JOIN,因此不將未查證的資料表列為底層來源。
Dictionary 是否就是未加工的資料庫原始設定?
AssignSalePagePayShipping 在寫入 Dictionary 前,依序執行自訂包裝顯示更新、全家點加金設定處理、代客下單金物流調整,以及物流多語系替換。因此它是 CartCreate 準備好的基準資料,可能已經過情境加工。
| 代客下單條件 | 資料調整 |
|---|---|
IsAssistMode = false | 不進入代客下單替換分支。 |
FreeOfChargeAssistOrder | 付款清單替換為 FreeOfCharge,物流依零元訂單排除清單過濾。 |
| 其他代客下單 | 付款清單只保留 CustomOfflinePayment。 |
Mapping Service 如何讀取?缺 Key 與子商品如何處理?
IsSkipSalepagePayDeliveryTypeInitial = false 時,CalculatePayDeliveryMapping 呼叫自己的 ArrangePayDeliveryTypeToSalePage。這個方法以商品 Id 對兩份 Dictionary 執行 TryGetValue,成功才以 ToList() 重新指派商品清單;缺 Key 時沒有在這裡查 SP,也不在該分支清空既有清單。
旗標為 true 時跳過重新指派。重新指派使用的 ToList() 只建立新的清單容器,不代表其中每個金物流物件都被深層複製。
一般商品來自 SalepageGroupList,已有贈品券商品則從 SalePageGiftCouponGroupList 讀取。當 IsEnabledAddOnsPayDelivery 未啟用時,一般商品還原範圍會排除加購子商品;最後的 ReplaceSubSalePagePayShipping 再依設定,讓符合條件的加購或組合子商品沿用主商品清單。
後續 CalculatePayType 與 CalculateDeliveryTypeAsync 讀取的是商品上的兩份清單。這些清單描述商品在目前處理階段的支援資料,尚不能直接當成整台購物車的最終選項。
所有參與商品共同接受哪些付款方式?
商品各自有付款清單,CalculatePayType 從中找出所有參與商品共同支援的付款類型,再套用本次購物車的限制。共同的範圍是商品之間,金流不按溫層分開計算。
案例:每件商品都支援,才是共同金流
| 商品頁 | 信用卡 | ATM | LINE Pay |
|---|---|---|---|
| 咖啡豆 A | ✓ | ✓ | ✓ |
| 馬克杯 B | ✓ | × | ✓ |
| 商品交集 | 保留 | 移除 | 保留 |
flowchart TB
A["讀取所有參與商品付款清單"] --> B["按付款類型找共同選項"]
B --> C["排序,套用允許付款清單"]
C --> D["依條件套用優惠碼指定金流"]
D --> E["寫入 CheckoutType.PayTypeList"]
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef result fill:#edf9f4,stroke:#32856b,color:#175440
class A,B,C,D work
class E result
若本次 EnablePayProfileTypeDef 只允許信用卡與 ATM,上例的 LINE Pay 仍會被移除,輸出只剩信用卡。商品共同支援,是進入選項清單的第一道條件。
讀取來源與比較鍵
| 節點/方法 | 用途 |
|---|---|
context.Data.SalepageGroupList[…].SalepageList[…].PayTypeList | 一般商品目前的付款清單;方法也追加 SalePageGiftCouponGroupList 中已有贈品券商品的付款資料。 |
PayProfileTypeDef | 依付款類型分組;保留分組筆數等於參與商品清單筆數的類型。 |
context.Data.EnablePayProfileTypeDef | 由前序 GetPayTypeIsAvaliableProcessor 準備,本方法用它再次過濾候選金流。 |
context.Data.PromoCodeDispatch.PromoCodeInfo.PaymentTypes | 優惠碼指定付款方式,有符合條件才縮減清單。 |
context.Data.CheckoutType.PayTypeList | 本方法最終寫入的付款候選清單,接著仍需經過全域 Mapping 配對。 |
優惠碼指定金流:有交集才縮減,完全不符交由後續處理
| 條件 | 本方法行為 |
|---|---|
| 候選金流為空,或無優惠碼資料/未指定付款方式 | 跳過優惠碼金流過濾。 |
| 指定付款方式與候選金流至少有一種相同 | 只保留候選中符合指定的付款方式。 |
| 指定付款方式與候選金流完全不符 | 保留這一步之前的候選清單;註解指定交由 CalculatePromoCodeProcessor 處理優惠碼無交集錯誤。 |
例如候選為信用卡、LINE Pay:優惠碼指定信用卡時留下信用卡;若只指定 ATM,此方法不因該條件直接清空候選金流。這不表示優惠碼已通過驗證。
程式中的計數、贈品券與排序細節
此實作使用 SelectMany → GroupBy(PayProfileTypeDef) → Count == salePageCount,未先依商品去重。將它理解為集合交集,前提是各商品清單中的同類型沒有重複;若資料重複,單純比較總筆數可能偏離集合交集語意。
前一個物流方法會把贈品券商品併入一般商品群組,而本方法又追加贈品券清單。因此計數基準是方法實際組出的 salePageList.Count,不能直接當作不重複商品頁 Id 數;這裡沿用程式行為,不另外宣稱已去重。
每個保留類型建立一筆 ShoppingCartClientPayTypeEntity,StatisticsTypeDef 取該組第一筆。排序使用 PayProfileTypeDefComparer 與全域/商店 EventSort 設定。
各溫層都能使用哪些配送類型與地區?
CalculateDeliveryTypeAsync 先找同溫層商品的共同物流,再找各溫層共同具有的配送類型與地區。跨溫層通過後,仍保留各溫層自己的物流明細。
flowchart TB
A["依溫層併入已有贈品券商品"] --> B["各溫層先整理合併物流"]
B --> C["溫層內:物流 Id + 地區"]
C --> D["跨溫層:配送類型 + 地區"]
D --> E["保留各溫層明細,再套用限制"]
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef result fill:#edf9f4,stroke:#32856b,color:#175440
class A,B,C,D work
class E result
第一層:同溫層商品要有相同物流 Id 與地區
| 常溫商品/地區 1 | 宅配 #101 | 超取付款 #201 |
|---|---|---|
| 咖啡豆 A | ✓ | ✓ |
| 馬克杯 B | ✓ | × |
| 常溫交集 | 保留 #101 | 移除 #201 |
GetSalePageGroupDeliveryType 在整理合併物流後,以 Id + ShippingAreaId 分組,保留筆數等於該溫層商品筆數的組合,並取各組第一筆明細。即使配送名稱都叫宅配,Id 或地區不同也不直接視為相同。
第二層:跨溫層改比配送類型與地區
| 溫層內交集結果 | 物流 Id | 配送類型 | 地區 | 跨溫層結果 |
|---|---|---|---|---|
| 常溫 | #101 | 宅配 | 1 | 保留 |
| 冷凍 | #301 | 宅配 | 1 | 保留 |
| 冷凍 | #302 | 宅配 | 2 | 移除,常溫沒有地區 2 |
兩個溫層都具有「宅配+地區 1」,所以 #101、#301 同時保留。這一步比較 ShippingProfileTypeDef + ShippingAreaId,不要求物流 Id 相同,也不表示常溫與冷凍商品裝在同一包裹。
資料讀寫節點
| 節點/方法 | 用途 |
|---|---|
context.Data.SalepageGroupList | 主要商品群組;按溫層合併已有的 SalePageGiftCouponGroupList 後參與計算。 |
SalepageList[…].DeliveryTypeList | 每件商品目前的物流明細,是 GetSalePageGroupDeliveryType 的讀取來源。 |
salePageGroupDeliveryTypeList | 區域變數,保存各溫層的物流交集結果。 |
distinctSalePageGroupShippingTypeDefList | 區域變數,保存跨溫層共同的配送類型與地區組合。 |
context.Data.CheckoutType.DeliveryTypeList | 展開各溫層明細,只保留共同組合,排序後寫入,再依優惠碼與黑名單條件刪減。 |
合併物流:比較 Id 前會先整理
方法先收集具有 MergeShippingTypeId,且其指向 Id 也存在於該溫層物流中的設定,再由 MergeDeliveryType 整理每件商品清單。
| 假設設定 | 商品原清單 | 整理後 |
|---|---|---|
物流 #3 的 MergeShippingTypeId = 1 | #1 | #3 |
| 同一設定 | #1、#3 | #3 |
| 同一設定 | #1、#2 | #3、#2 |
以上條件成立時,#1 或 #3 都會被整理成合併設定 #3,再進行同溫層交集。這是本方法的比較用清單,不是單純忽略物流 Id。
交集後的限制:優惠碼與黑名單
交集後的物流限制:優惠碼與黑名單
| 來源/條件 | 處理 |
|---|---|
PromoCodeDispatch.PromoCodeInfo.PaymentTypes 指定 StoreCredit、GiftCard 或 HamiPoint | 從該付款方式對應的 DeferredPaymentShippingProfileTypeDefList 找出不適用配送類型並移除;其他付款類型跳過此分支。 |
BlacklistMemberAllowedPayProfileList 非 null,且包含上述折抵方式 | 從對應的不適用配送類型清單,扣除允許清單中也存在的值,再移除剩下的配送類型。不是把所有後付款物流一律移除。 |
對應限制清單分別定義於 CalculateCheckoutStoreCreditProcessor、GiftCardSelectionHandler 與 CheckoutHamiPointCalculator。判斷逐項執行,符合多項時會連續過濾。配送類型為空白的項目在這兩段過濾中保留。
群組、計數與輸出邊界
salePageGroupList 直接引用 context.Data.SalepageGroupList。贈品券依溫層新增群組或追加商品,會修改 Context 中的群組,不是獨立複本。
同溫層使用分組總筆數與商品清單筆數比較,未全面先按商品去重。跨溫層則先在各組內對「配送類型+地區」執行 Distinct,再保留出現次數等於溫層群組數的組合。
保留明細依 ShippingProfileTypeDefComparer 排序,再依 ShippingAreaId 排序;Comparer 使用全域與商店 EventSort 設定。
輸出為 CheckoutType.DeliveryTypeList,後續全域 Mapping 與其他 Processor 仍可能刪減。海外聯集另由 CalculateOverseaDeliveryTypeAsync 寫入 DeliveryTypeUnionList,不屬於本章的物流交集輸出。
共同支援的選項,還要有合法搭配
商品共同接受某種付款、某種配送,不代表兩者可以搭配。CalculatePayShippingMapping 將交集後的選項套入全域 Mapping,留下有搭配對象的付款與配送選項。
三份輸入,各自回答不同問題
| 輸入節點 | 前序來源 | 讀取欄位 |
|---|---|---|
payShippingMappingList | 外層 CalculatePayDeliveryMapping 一開始取得的全域配對清單。 | PayProfileTypeDef、ShippingProfileTypeDef |
context.Data.CheckoutType.PayTypeList | CalculatePayType 完成商品交集與付款限制後的候選金流。 | PayProfileTypeDef |
context.Data.CheckoutType.DeliveryTypeList | CalculateDeliveryTypeAsync 完成交集與物流限制後的候選配送明細。 | ShippingProfileTypeDef |
上表以 Processor 的 context.Data 表示購物車根節點;進入私有配對方法後,其 context 參數本身就是 CartEntity。
案例:兩端都存在,這筆配對才有效
假設候選金流為「信用卡、ATM」,候選物流為「宅配、超取付款」。下表為教學假設設定,名稱是類型代稱。
| 全域 Mapping 配對 | 付款端存在? | 配送端存在? | 本輪配對 |
|---|---|---|---|
| 信用卡 → 宅配 | ✓ | ✓ | 保留 |
| ATM → 宅配 | ✓ | ✓ | 保留 |
| 超取付款金流 → 超取付款物流 | × | ✓ | 排除 |
| 信用卡 → 海外配送 | ✓ | × | 排除 |
flowchart LR
Card["信用卡:保留"] --> Home["宅配:保留"]
ATM["ATM:保留"] --> Home
Missing["超取付款金流:候選中不存在"] -.設定有配對,本輪無效.-> CVS["超取付款物流:移除"]
classDef keep fill:#edf9f4,stroke:#32856b,color:#175440
classDef muted fill:#f3f4f6,stroke:#7d8998,color:#334155
class Card,ATM,Home keep
class Missing,CVS muted
實線為本輪有效配對,虛線為設定存在但本輪無效的配對。圖中聚焦候選物流的去留,省略候選中不存在的海外配送。
反向套回選項後,信用卡與 ATM 都有宅配可搭配,兩者保留;超取付款物流沒有有效連線,因此即使通過商品交集,仍會被移除。
實作:先篩配對,再依序刪減兩份清單
| 步驟 | 操作 | 結果位置 |
|---|---|---|
| 1.篩選配對 | 兩個 Where 分別確認付款類型、配送類型存在於目前候選。 | 區域變數 avaliblePayShippingMappingList,拼字沿用原始碼。 |
| 2.刪減金流 | RemoveAll 移除沒有出現在有效配對付款端的類型。 | 直接修改 CheckoutType.PayTypeList。 |
| 3.刪減物流 | RemoveAll 移除沒有出現在有效配對配送端的類型。 | 直接修改 CheckoutType.DeliveryTypeList。 |
比對鍵:為什麼不再比較物流 Id、溫層與地區?
這一步只使用 PayProfileTypeDef 與 ShippingProfileTypeDef。物流 Id、溫層、地區已在前段處理,本方法不以它們重新查詢 Mapping。
若宅配類型有有效配對,前段保留的常溫宅配 #101、冷凍宅配 #301 都能留下;此處不把兩筆明細合成一筆。StatisticsTypeDef 也不是本方法的配對鍵。
方法不重新查商品設定、不新增選項,也不把篩選後的配對表另外存入購物車。Where 結果是延遲列舉,未先以 ToList() 建立獨立配對快照。
每個選項有配對,不代表可以任意組合
另一個假設:只允許「信用卡 → 宅配」與「超取付款金流 → 超取付款物流」,而四個端點都在候選清單。此方法會保留全部四個選項,但不會因此允許「信用卡 → 超取付款物流」。
輸出清單表示每個選項至少存在一個合法搭配,不能將兩份清單直接解讀為所有組合都合法。這一步也沒有替消費者選定付款與配送方式。
配對後往哪裡走?
外層 Service 接著整理海外物流聯集,再檢查付款與配送清單是否皆非空。任一為空,就進入下一章的無交集商品搬移與中斷處理;皆有選項則更新提示並繼續後續計算。
「全域 Mapping 本身為空」在 Service 入口就會拋出 CartCacheExpired;「有全域設定,但本輪沒有有效配對」則會讓兩份候選清單被清空。這是兩個不同的處理情境。
DeliveryTypeUnionList:保留供後續查找的物流資料
CalculateOverseaDeliveryTypeAsync 整理商品物流聯集,寫入 context.Data.DeliveryTypeUnionList,供後續海外配送流程使用。方法名稱描述後續用途;此方法本身沒有只挑海外物流,也沒有判斷配送國家或地區。
為什麼交集之外,還需要聯集?
| 商品/資料集合 | 宅配 #101 | 海外配送 #401 |
|---|---|---|
| 商品 A | ✓ | ✓ |
| 商品 B | ✓ | × |
| 商品共同物流 | 保留 | 不在共同選項中 |
| 商品物流聯集 | 保留 | 保留,商品 A 有此設定 |
| 節點 | 用途 |
|---|---|
CheckoutType.DeliveryTypeList | 商品交集、限制與 Mapping 配對後的本輪配送候選明細。 |
DeliveryTypeUnionList | 從商品物流重新收集並整理的聯集,保留供後續查找的物流設定,可能同時包含本地與海外物流。 |
聯集中存在 #401,只表示來源商品具有這筆物流資料,不表示商品 B 可以海外配送,也不表示整台購物車已通過海外配送檢查。
建立聯集與挑選海外物流,是兩個階段
flowchart TB
subgraph Prepare["目前的 Mapping Service"]
A["讀取商品自己的物流清單"] --> B["整理合併物流、去重與排序"]
B --> C["寫入 DeliveryTypeUnionList"]
end
subgraph Later["後續海外配送流程"]
D["GetOverseaShippingProcessor"] --> E["從聯集挑出 Oversea 類型"]
E --> F["依配送設定取得地區"]
F --> G["設定海外可用狀態與地區清單"]
end
C -.提供物流資料.-> D
classDef work fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef result fill:#edf9f4,stroke:#32856b,color:#175440
class A,B,D,E,F work
class C,G result
實線表示階段內的處理順序,虛線表示資料供應。只有後續 GetOverseaDeliveryTypeList 才以 ShippingProfileTypeDef = Oversea 篩選。
聯集從哪些商品節點收集?
下表節點皆位於 context.Data。收集的是商品自己的 DeliveryTypeList,不是把 CheckoutType.DeliveryTypeList 複製成另一份清單。
| 來源節點 | 納入條件 |
|---|---|
SalepageGroupList[…].SalepageList[…] | 目前一般商品群組內的商品。 |
SalePageGiftCouponGroupList[…].SalepageList[…] | 清單中已存在的贈品券商品。 |
UnMappingCheckoutSalepageList | 只納入 PayShippingIntersectionStatus = OverseaNoIntersection 的商品;不把所有無交集商品一律加回。 |
不只是塞值:合併、去重與排序規則
方法先展開來源商品的物流明細。若物流有 MergeShippingTypeId,且指向的 Id 也在收集清單內,便交給 MergeDeliveryType 整理比較用物流。
接著依 Id 分組,每組取第一筆,再以 ShippingProfileTypeDefComparer 與 ShippingAreaId 排序,最後用 ToList() 寫入 DeliveryTypeUnionList。此處去重鍵只有物流 Id,不是「類型+地區」的交集鍵。
ToList() 建立新的清單容器,不代表物流明細物件都被深層複製。這個方法也沒有用全域 Mapping 或付款清單重新篩選聯集。
後續誰讀取?寫出哪些結果?
GetOverseaShippingProcessor.ExecuteAsync 呼叫 ShippingAreaService.ArrangeOverseaShippingAreasAsync。其中的 GetOverseaDeliveryTypeList 從 DeliveryTypeUnionList 挑出 Oversea 類型。
取得海外物流後,Service 依物流 Id 查找配送運費表與地區資料,整理配送地區聯集,再更新 CanOverseaShipping 與 ShippingAreaList。沒有海外物流時,會將海外可用狀態設為 false、清空地區清單並返回。
這是聯集資料的後續用途,不是 CalculateOverseaDeliveryTypeAsync 已經完成的工作;是否所有商品能送到所選地區仍須後續流程判斷。
在目前 Processor 中的執行位置
CalculatePayDeliveryMapping 先完成商品交集與 CalculatePayShippingMapping,再呼叫此方法建立聯集,之後才檢查候選金流、物流是否為空。因此即使建立了聯集,本輪仍可能在隨後的無交集判斷中要求中斷;不能假設後續海外 Processor 每次都一定執行。
結果與例外
先區分「配對設定缺失」與「商品計算後沒有共同選項」,兩者走不同處理分支。
flowchart TB
Start{"全域 Mapping 有資料?"}
Start -->|否| Error["拋出 CartCacheExpired"]
Start -->|是| Calculate["完成交集、配對與海外物流聯集"]
Calculate --> Available{"金流與物流皆非空?"}
Available -->|是| Ready["更新提示,繼續後續計算"]
Available -->|否| Move["商品移入無交集清單,清空原群組"]
Move --> Mark["標記 NoIntersection,更新提示"]
Mark --> Save["保存 CartEntity 至 Redis"]
Save -->|保存成功| Flags["設定中斷與重算旗標"]
classDef keep fill:#edf9f4,stroke:#32856b,color:#175440
classDef base fill:#edf4ff,stroke:#517ab0,color:#203c60
classDef muted fill:#f3f4f6,stroke:#7d8998,color:#334155
classDef stop fill:#fff1f2,stroke:#b42332,color:#852332
class Start,Calculate,Available base
class Ready keep
class Error,Move,Mark,Save,Flags stop
CheckoutType.PayTypeList
CheckoutType.DeliveryTypeList
這兩份結果供後續運費、重量、金額門檻與預選計算使用,仍可能被調整。
無交集時,哪些商品與旗標會改變?
例如咖啡豆只支援信用卡、餅乾只支援 ATM,即使都能宅配,付款交集仍為空。
| 節點 | 變化 |
|---|---|
UnMappingCheckoutSalepageList | 合併目前一般商品與贈品券商品,不只挑其中一件移除。 |
SalepageGroupList、SalePageGiftCouponGroupList | 搬移商品後清空。 |
PayShippingIntersectionStatus | 無交集清單商品標記為 NoIntersection。 |
context.IsInterrupted、context.NeedReCalculate | Service 回傳無交集結果;Processor 保存 Redis 成功後,兩者設為 true。 |
這裡只提出重算需求,不直接重跑自己,後續由外層流程接手。
選項減少時,提示如何更新?
CheckPayShippingMapping 更新 CheckoutType.DisplayMessage。商品原有的金流種類數或配送類型與地區組合數,若與最後選項不同,就設定 OnlyFollowingPaymentAndShipping 對應提示。
出現提示可能只是選項減少,不表示一定無法結帳。無交集時商品已先搬移,也不能假設必定出現同一提示。




