壽星
生日這個月,多一份為自己準備的回饋
十月輪到小美當壽星。商店記得她的生日月份,也讓符合活動條件的購物多一份驚喜。
一筆壽星回饋,需要兩件事同時成立
會員的生日在這次計算的月份
訂單符合金額、商品與會員資格等規則
訂單進入回饋流程後,才可能取得點數或券
十月,小美的生日還有兩個星期。她照常買了幾樣東西,結帳後卻多拿到一筆點數。原來商店辦了壽星活動,生日在十月的會員,只要購物符合活動條件,就能拿到額外回饋。不是每個人都有,因為這個月剛好輪到她當壽星。
所謂壽星活動,就是商店在會員的生日月份,替符合條件的訂單加上一份專屬回饋,可能是點數,也可能是優惠券。它看的是整個生日月份,不必剛好在生日當天購物;但也不是一到生日月份就自動送禮,訂單仍可能需要達到指定金額、購買指定商品,或符合其他活動條件。
生日月份一年只有一次,這份回饋也因此多了點「這次是為我準備的」感覺。會員拿到的不只是點數或券,還有被記得的專屬感;商店則藉這個機會向熟客說聲生日快樂,讓彼此有個再碰面的理由。只是這份心意能不能真正入袋,還得看下單當時的生日資料和訂單是否符合規則。接下來就從活動規則開始,看看系統怎麼認出壽星,又怎麼決定這筆回饋該不該給。
建立壽星活動時的判斷與寫入順序
flowchart TB
request["建立回饋活動"] --> enabled{"開啟當月壽星?"}
enabled -->|"否"| regular["保存原本活動規則"]
enabled -->|"是"| type{"屬於支援的三種回饋類型?"}
type -->|"否"| regular
type -->|"是"| mapping{"商店已有月份對照?"}
mapping -->|"否"| create["建立十二個月份集合並寫入 DDB"]
mapping -->|"是"| reuse["沿用現有月份集合"]
create --> scope["追加 CurrentBirthdayMonth 條件"]
reuse --> scope
scope --> save["將 Rule JSON 存入活動"]
PromotionEngineService.Create;月份集合屬於商店共用資料。前言:壽星是活動上的一項條件
商店建立壽星活動時,並不是選一個獨立的「壽星活動」類型。做法是先建立一檔回饋活動,決定要送固定點數、比例點數或優惠券,再開啟「當月壽星」條件。活動仍有自己的期間、商品範圍和消費門檻,只是參加資格多了一關生日月份的檢查。
從程式碼看,支援這項條件的回饋活動只有 RewardReachPriceWithPoint2、RewardReachPriceWithRatePoint2 和 RewardReachPriceWithCoupon。建立活動的請求繼承 PromotionBaseEntity,其中的 IsBirthdayMonthEnabled 就是開關,預設為 false。
1 | // PromotionBaseEntity.cs |
建立活動時,後台做了什麼
PromotionEngineController.Create 收到 POST /api/promotion-rules/create 後,先驗證請求,再呼叫 PromotionEngineService.Create。Service 先建立活動主檔,設定原有的會員對象與其他活動條件,接著才處理壽星開關。以下是 PromotionEngineService.Create 中判斷壽星的程式碼:
1 | var enableBirthdayMonthPromotions = new HashSet<string> |
只有 IsBirthdayMonthEnabled 為 true,而且活動類型是上述三種回饋活動之一,才會進入這段壽星處理。進入後,後台會先確認商店的生日月份資料是否備妥,再把壽星資格寫進活動規則。
月份集合與活動 Rule 分開保存
flowchart LR shop["Shop 12"] --> mapping["DDB 月份對照"] mapping --> oct["10 月 CollectionId"] mapping --> other["其餘十一個月份 CollectionId"] activity["壽星活動 Rule"] --> tag["要求 CurrentBirthdayMonth"] oct -. "計算時比對會員" .-> tag
首次啟用時,建立月份集合與 DDB 對應
這裡有兩種不同的資料。Member Collection 負責判斷會員的生日屬於哪個月份;DDB 對應資料則記錄商店每個月份對應哪個 CollectionId。程式用 System_BirthdayMonth_{shopId} 查 DDB,若查不到對應資料,才呼叫 CreateMemberCollectionAsync(shopId) 準備這間商店的十二個月份集合。
CreateMemberCollectionAsync 會從 1 月跑到 12 月,逐月呼叫 CreateBirthdayMonthMemberCollectionAsync。後者送給 Member Collection API 的條件是 birthday.month 等於該月份,並將集合設為系統性、不可編輯且不在前台顯示。十二個集合都建立成功後,程式才把月份與 CollectionId 的清單寫入 DDB。
1 | // MemberCollectionService.CreateBirthdayMonthMemberCollectionAsync() |
這十二個集合屬於商店共用的判斷資料,並不是十二檔壽星活動。後續再建立壽星活動時,只要 DDB 已有對應資料,就會沿用,不會因為每新增一檔活動而重建一次。
把壽星資格寫進活動 Rule
接著,SetBirthdayMonthMatchedUserScopes() 會在這檔活動原有的會員對象條件後面,加上 CurrentBirthdayMonth:
1 | // PromotionEngineRuleBaseService.cs |
這裡設定的是活動要求會員具備的資格,並沒有在建立活動時替任何會員貼標籤。最後 GetRuleObject() 將會員資格與壽星開關寫入 Rule JSON,GetPromotionEngineRule() 產生的 JSON 再存進活動的 PromotionEngine_Rule。
1 | // PromotionEngineRuleBaseService.GetRuleObject() |
因此,一檔以全體會員為原始對象、開啟壽星條件的活動,其 Rule 會包含以下關鍵內容。這只是節錄,實際 Rule 還有活動期間、商品、通路與回饋門檻等欄位。
1 | { |
怎麼識別一檔壽星活動
先看活動類型是否為上述三種回饋活動,再看 Rule 中的 IsBirthdayMonthEnabled 是否為 true。正常新增流程也會在 MatchedUserScopes 留下 CurrentBirthdayMonth,可用來交叉核對。活動名稱不需要寫「壽星」,TargetMemberTypeDef 也不一定是 MemberCollection;後者設定的是原有會員對象,壽星條件則由程式另外加入。
到這裡,後台只完成了「這檔活動要求當月壽星」的設定。某位會員在購物當下是否符合資格,要等前台計算時才判斷,下一個分頁會接著說明。
原會員資格與壽星條件如何一起生效
flowchart TB
member["會員與訂單"] --> scopes["MatchedUserScopes:任一 Scope 符合"]
scopes -->|"都不符合"| no1["不命中"]
scopes -->|"至少一個符合"| birthday{"具有 CurrentBirthdayMonth?"}
birthday -->|"否"| no2["不命中"]
birthday -->|"是"| threshold{"找到原會員資格的門檻,且消費符合?"}
threshold -->|"否"| no3["不命中"]
threshold -->|"是"| yes["活動命中"]
MatchedUserScopes 本身使用 OR;壽星 Tag 與原會員門檻由後續檢查形成必要條件。假設一檔回饋活動原本只開放會員卡等級 CrmShopMemberCard:5,又希望限制為當月壽星。原本的會員資格不能被 CurrentBirthdayMonth 取代,因為它除了表示活動對象,也對應這檔活動的回饋門檻。後台因此先建立原有的 MatchedUserScopes,再用 SetBirthdayMonthMatchedUserScopes() 追加壽星 Tag,讓 Rule 保留兩種條件。
不過,在 MatchedUserScopes 多放一個 Tag,並不會讓這個清單本身變成 AND。引擎的 IsUserScopeMatched() 使用 Any,只要清單中的一個 Scope 符合就會回傳 true:
1 | // IBasicUserScope.IsUserScopeMatched() |
真正讓壽星成為額外必要條件的,是引擎在原本的 UserScope 比對之後,還會獨立檢查 IsBirthdayMonthEnabled 與會員的 CurrentBirthdayMonth Tag:
1 | // PromotionCommonRuleBase.IsMatch() |
對指定會員卡的回饋活動,還有一道門檻檢查。以固定給點活動為例,RewardReachPriceWithPoint2.IsMatch() 會依會員身上的 Tag 尋找對應的 Thresholds;找不到原本會員卡等級的門檻,就不會命中。因此,會員必須能取得原本會員資格對應的門檻,而且具有當月壽星 Tag,之後還要通過消費金額等其他條件。
| 會員卡符合原活動 | 具有壽星 Tag | 結果 |
|---|---|---|
| 是 | 是 | 壽星與會員資格皆通過,仍須檢查金額等其他條件 |
| 是 | 否 | 壽星檢查不通過 |
| 否 | 是 | 找不到對應會員卡門檻,不會命中 |
| 否 | 否 | 不會命中 |
所以,追加 Scope 是為了保留原本的會員對象設定;業務上需要的 AND,則由獨立的壽星檢查與原本的門檻檢查一起完成。不能只看 Rule 裡有兩個 MatchedUserScopes,就認定引擎會要求兩個 Scope 同時符合。
集合保存規則,會員名單在比對時才決定
sequenceDiagram
participant WebAPI as Promotion WebAPI
participant Member as MemberService
participant DDB as DDB 月份對照
participant Frontend as Promotion Frontend
WebAPI->>Member: 建立 Shop 12 的 10 月集合,規則 birthday.month = 10
Member-->>WebAPI: 回傳 CollectionId
WebAPI->>DDB: 保存 10 月與 CollectionId 對照
Frontend->>DDB: 取得 10 月 CollectionId
DDB-->>Frontend: 回傳 CollectionId
Frontend->>Member: 比對 MemberId#100001 與 CollectionId
Member->>Member: 讀取目前會員 Profile 並比對生日月份
alt 生日月份等於 10
Member-->>Frontend: 回傳命中的 CollectionId
else 生日月份不等於 10
Member-->>Frontend: 回傳空清單
end
活動規則可以寫「限當月壽星」,但光有這句規則,系統還不知道眼前這位會員的生日是幾月。壽星資格需要拿會員資料來判斷,而 Promotion Frontend 在這條計算鏈路中使用的是會員 ID,沒有自行讀取生日欄位。於是它把「這位會員是否屬於指定生日月份」交給 Member Collection API 回答。
Member Collection 本來就能用會員資料建立條件。壽星集合使用的條件很單純:birthday.month 等於某個月份。後台替每間商店建立一月至十二月的系統集合;前台計算時依月份取出對應的 CollectionId,將會員 ID 與 CollectionId 送去比對。下面是 MemberCollectionService.GetSpecialPriceMemberCollectionIdAsync() 實際送出的資料:
1 | var request = new GetMemberCollectionsMatchRequest |
用 Shop 12 的 10 月壽星集合走一次
以下的會員 ID 與生日資料是模擬值;10 月的 CollectionId 則沿用本文後面 DDB 範例中的 bcc62702-6408-4ef7-818b-44106a5b6378。JSON 保留這次交互需要看的欄位,方便對照實際請求與回應格式。
建立時,Promotion WebAPI 送的是規則。它以 Shop 12 的身分呼叫 POST /api/v2/member-collections,請 MemberService 建立一個「生日月份等於 10」的系統集合。10 是數值,請求內沒有會員 ID 清單。
1 | { |
MemberService 建立集合時產生 GUID,將名稱、Rule、類型與其他設定存到 member_collections。回應中的 id 就是後續查找這條規則用的 CollectionId。這裡用 DDB 範例的 ID 示意;並非在本次撰文時實際呼叫 API 所取得的回應。
1 | { |
同一輪初始化還會建立另外十一個月份集合。十二個都成功後,Promotion WebAPI 才將 { "Name": "Collection_System_BirthdayMonth_10_12", "Month": 10, "CollectionId": "bcc62702-6408-4ef7-818b-44106a5b6378" } 這樣的月份對照資料存入 DDB。這筆對照也沒有會員名單。
比對時,才讀指定會員的資料。假設計算月份是 10 月,系統從 DDB 找到上述 CollectionId,再以 Shop 12 的身分呼叫 POST /api/v2/member-collections/match。以下先查會員 100001:
1 | { |
假設 MemberService 此時讀到這位會員的 Profile 片段是 { "birthday": { "month": 10, "day": 15 } }。它取出 birthday.month,與集合規則的 equal: 10 比對,符合時回傳:
1 | { |
若改查會員 100002,而他當時的 Profile 是 { "birthday": { "month": 11, "day": 3 } },同一條規則會比對失敗,回應格式如下:
1 | { |
因此,CollectionId 指向的是可重複使用的判斷規則,不是建立當下凍結的會員清單。建立時寫入規則;每次比對時,MemberService 才讀取這次指定的會員,依當時讀到的生日資料回傳命中的 CollectionId。建立流程另有集合定義的版本快照,快照保存的仍是集合設定,並非會員名單。
這種分工帶來三個效果:
- 生日條件由會員系統判斷:Promotion 只提交會員 ID 與集合 ID,取得符合的集合,不需要在這條流程中自行查生日欄位、實作生日月份比對。
- 月份集合可供同一商店的活動共用:DDB 保存商店的十二個月份與 CollectionId 對應;新增下一檔壽星活動時,可以沿用這組集合。
- 活動 Rule 不必綁定某個月份的 CollectionId:前台把符合的生日月份 CollectionId 轉成
CurrentBirthdayMonth,Rule 固定要求這個 Tag。換月時改變的是計算所選的集合,不是活動 Rule 的內容。
可以把它想成三個角色:Rule 說明誰有資格,DDB 找出這個月要查哪個集合,Member Collection API 回答會員是否符合。這是從目前程式碼推得的資料分工與效果;原始設計當時為什麼選擇 Member Collection,仍須設計紀錄或當時的討論才能確認。
購物車試算與實際回饋,會各算一次
Shop 12 的會員 100001 生日在 10 月 15 日,商店有一檔「10 月壽星且消費達門檻」的回饋活動。10 月 20 日,他先在購物車看見活動,之後完成下單。購物車顯示符合活動,代表當下試算命中;真正的點數或優惠券,要等訂單進入 Promotion Worker 的回饋流程。
第一段:消費者在購物車,Shopping → Cart → Promotion Frontend
先看 Shopping 做什麼。消費者要求重新計算購物車時,Shopping.CartsController.Calculate(cartUniqueKey) 呼叫 Shopping.CartService.CalculateAsync。Service 建立 CartContext,執行 Calculate Processor 流程;其中的 Shopping.CalculateProcessor.ExecuteAsync 只帶 CartUniqueKey,以 POST /api/carts/calculate 請 Cart 重新計算。Cart 回應後,Shopping 流程還會跑 ArrangePromotionRuleInfoProcessor 整理活動資訊,接著產生商品卡與點數顯示資料,最後回傳購物車畫面需要的結果。
再看 Cart 做什麼。Cart.CartsController.Calculate 進入 Cart.CartService.CalculateAsync,由 CalculateInnerAsync 執行 CartCalculate Processor 流程:
LoadCacheCartDataProcessor先載入購物車。ExecuteLogisticCalculateProcessor<CartEntity>進入LogisticCalculate流程,並把這次計算標成IsCartCalculate = true。CalculatePromotionProcessor.ExecuteAsync整理促購請求,呼叫Cart.PromotionService.CalculateAsync;該 Service 透過PromotionHttpClient.Calculate呼叫 Promotion Frontend 的POST /api/cart-calculate。- 收到促購結果後,
CalculatePromotionProcessor.MappingPromotionToCartAsync把回饋活動紀錄整理進購物車的PromotionInfoList。Cart 流程隨後儲存、回傳購物車資料。若金物流交集要求重算,Cart.CartService.CalculateAsync還會再執行一次CalculateInnerAsync。
最後看 Promotion Frontend 怎麼判斷壽星。CalculateController.CartCalculateAsync 進入 CalculateService.CalculateShoppingCartAsync,在 BottomProcessGroup.UpdateProcessRuleListAsync 呼叫 PromotionRuleRepository.GetRuleInfoListAsync。後者檢查 Rule 的 IsBirthdayMonthEnabled,從 DDB 取得 Shop 12 於這次計算月份的 CollectionId,再用 SetMemberCollectionToUserTagAsync 呼叫 MemberService 比對 MemberId#100001。若 10 月集合命中,程式把回傳的 CollectionId 換成 CurrentBirthdayMonth;BaseProcessGroup.EngineCalculate 接著把會員 Tags 與商品交給 Promotion Engine,算出活動是否命中。
購物車試算的實際呼叫順序
sequenceDiagram
participant Consumer as 消費者
participant Shopping
participant Cart
participant Frontend as Promotion Frontend
participant Member as MemberService
participant Engine as Promotion Engine
Consumer->>Shopping: CartsController.Calculate
Shopping->>Shopping: CartService.CalculateAsync
Shopping->>Shopping: CalculateProcessor.ExecuteAsync
Shopping->>Cart: POST /api/carts/calculate
Cart->>Cart: CartsController.Calculate → CartService.CalculateAsync
Cart->>Cart: LoadCacheCartDataProcessor.ExecuteAsync
Cart->>Cart: ExecuteLogisticCalculateProcessor.ExecuteAsync
Cart->>Cart: CalculatePromotionProcessor.ExecuteAsync
Cart->>Frontend: PromotionService.CalculateAsync → POST /api/cart-calculate
Frontend->>Frontend: CartCalculateAsync → CalculateShoppingCartAsync
Frontend->>Frontend: BottomProcessGroup.UpdateProcessRuleListAsync
Frontend->>Frontend: GetRuleInfoListAsync → GetBirthdayCollectionIdByMonthAsync
Frontend->>Frontend: SetMemberCollectionToUserTagAsync
Frontend->>Member: GetSpecialPriceMemberCollectionIdAsync → POST /match
Member-->>Frontend: 命中的 CollectionId
Frontend->>Engine: EngineCalculate,帶入 CurrentBirthdayMonth
Engine-->>Frontend: 活動試算結果
Frontend-->>Cart: CalculateResponseEntity
Cart->>Cart: MappingPromotionToCartAsync
Cart-->>Shopping: CartEntity
Shopping->>Shopping: ArrangePromotionRuleInfoProcessor
Shopping-->>Consumer: 顯示購物車試算
這條路徑算的是購物車目前能否參加活動。即使畫面顯示壽星回饋,此時也尚未發放點數或優惠券。
第二段:Promotion Worker 依訂單重算並處理回饋
訂單事件進入 Worker 後,以給點為例,PromotionRewardLoyaltyPointsV2Job.DoJobAsync 解析任務資料,PromotionRewardProcess 再依事件來源進入 ProcessOccupyStateAsync(OrderPlaced 的佔額流程)或 ProcessRewardStateAsync(OrderCreated 等回饋流程)。給券對應的入口是 PromotionRewardCouponJob。
以首次建立給點回饋紀錄為主線,ProcessRewardStateAsync 先呼叫 GetAndCheckPromotionRuleAsync,用 PromotionEngineId、OrderDateTime 取得訂單當時的活動 rule-record。接著 PromotionRewardLoyaltyPointsCalculateAsync 取得訂單商品與會員資料,組成 BasketCalculateRequestEntity:其中有 PromotionRules.Rule = promotion.RuleJson、User.Id、SalepageSkuList、CalculateDateTime = entity.OrderDateTime。正常發放的 IsMemberCollectionRequired 為 true。
PromotionServiceClient.BasketCalculateAsync 呼叫 Promotion Frontend 的 POST /api/basket-calculate。CalculateController.BasketCalculate 進入 CalculateService.CalculateBasketAsync;因請求帶了 PromotionRules,會選 CustomRuleBasketGroup.UpdateProcessRuleListAsync,以傳入的 RuleJson 建立本次計算規則。該 Group 呼叫 PromotionRuleRepository.SetCustomRuleMemberCollectionToUserTagAsync,依訂單月份找壽星 CollectionId、請 MemberService 比對,命中時加入 CurrentBirthdayMonth,再執行 BaseProcessGroup.EngineCalculate。
Worker 首次建立回饋紀錄的呼叫順序
sequenceDiagram
participant Worker as Promotion Worker
participant Frontend as Promotion Frontend
participant Member as MemberService
participant Engine as Promotion Engine
participant Reward as 回饋服務
participant DDB as 回饋紀錄 DDB
Worker->>Worker: DoJobAsync → PromotionRewardProcess
Worker->>Worker: ProcessRewardStateAsync
Worker->>Worker: GetAndCheckPromotionRuleAsync
Worker->>Worker: PromotionRewardLoyaltyPointsCalculateAsync
Worker->>Frontend: BasketCalculateAsync → POST /api/basket-calculate
Frontend->>Frontend: BasketCalculate → CalculateBasketAsync
Frontend->>Frontend: CustomRuleBasketGroup.UpdateProcessRuleListAsync
Frontend->>Frontend: SetCustomRuleMemberCollectionToUserTagAsync
Frontend->>Member: GetSpecialPriceMemberCollectionIdAsync → POST /match
Member-->>Frontend: 命中的 CollectionId
Frontend->>Engine: EngineCalculate,帶入 CurrentBirthdayMonth
Engine-->>Frontend: IsMatch 與回饋試算
Frontend-->>Worker: CalculateResultEntity
Worker->>Reward: InsertLoyaltyPointRewardRecordAsync
Reward->>DDB: 寫入 RewardStatus 與 UserTag 字串
Worker->>Reward: 到達發放時間後 ProcessRewardPointAsync
Worker 用這次的 IsMatch 建立回饋紀錄:未命中記為 Unmatch;命中則依佔額條件進入 WaitToReward、Occupy 或額度不足等狀態。ProcessRewardStateAsync 只有在紀錄狀態為 WaitToReward、且已到 BookingTime 等條件通過後,才呼叫 LoyaltyPointService.ProcessRewardPointAsync。給券走 PromotionRewardCouponCalculateAsync、InsertCouponRewardRecordAsync,發放時呼叫 CouponService.ProcessRewardCouponAsync。
壽星 Tag 也會寫進回饋紀錄
PromotionRewardLoyaltyPointsCalculateAsync 會從 Frontend 的計算結果取出 calculateResult.User.Tags,放進 UserTagList。建立給點主紀錄時,LoyaltyPointService.InsertLoyaltyPointRewardRecordAsync 將這份清單序列化成 JSON 字串,寫入回饋紀錄 DDB 的 UserTag 欄位。給券的 CouponService.InsertCouponRewardRecordAsync 也做同樣的事。
延續 Shop 12、會員 100001 的模擬情境,回饋主紀錄的相關欄位可能長這樣。實際 Tag 清單仍取決於當次計算結果:
1 | { |
這裡真正儲存的欄位叫 UserTag,內容是字串化的陣列;UserTagList 是程式讀取這個字串後反序列化得到的清單。DDB 記的是這筆活動與訂單當次計算使用的會員 Tag,不是把會員加入前一段的月份 Collection,也不是修改會員本身的 Profile。
上面的 Worker 流程圖畫的是尚無回饋紀錄時的首次計算。如果同一活動與訂單已有回饋紀錄,Job 會讀既有紀錄與狀態,不會每次收到任務都重新呼叫 basket-calculate。OrderPlaced 的佔額分支也可能先計算並建立 Occupy 紀錄,之後回饋事件再接續處理。
| 時點 | 使用的資料 | 得到的結果 |
|---|---|---|
| 消費者看購物車 | 當次購物車、目前會員資料與可用活動 | 顯示活動試算結果 |
| Worker 處理訂單 | 訂單商品、訂單時間對應的活動 Rule,以及當次取得的會員資料 | 重新判斷 IsMatch,將 UserTag 寫入 DDB 回饋紀錄,符合發放時機才給點或給券 |
因此,購物車命中不是 Worker 的發放憑證。Worker 首次建立回饋紀錄時會根據訂單重新計算;活動規則取訂單時間的版本,壽星月份也取訂單月份。若是在回收或逆向重算,GetExistingUserTagAsync 查到原回饋紀錄的 UserTag 時,GetUserEntity 會將它與基本會員 Tag 合併,並設 IsMemberCollectionRequired = false,沿用原先記錄的壽星資格。
先下單、後補生日的時間軸
會員建立帳號
壽星資格須看當時可取得的會員資料
當次未命中壽星條件
事後比對可命中,但不能回推先前訂單的資料狀態
會員於 2025-10-20 23:04 註冊、23:13 訂單成立、23:14 執行回饋,直到 23:22 才填寫生日。事後查詢 Member Collection API,10 月壽星條件已可命中,但這不能代表訂單成立時已有生日資料。筆記的處理結論是:會員在下單後才填生日,該筆訂單維持不中。
這個案例提醒排查時必須還原訂單成立與計算當下的會員資料及時間順序。原筆記另外留下「稽核能否自行查生日欄位歷程」的疑問,尚未記錄明確答案。
沿著判斷資料往下追
先確認壽星開關與原本活動條件
找出購物車或訂單實際採用的月份
確認商店與月份對到的 CollectionId
確認當時指定會員是否命中集合
確認 CollectionId 是否轉成 CurrentBirthdayMonth
區分壽星失敗與金額、商品等其他條件未達
- 確認規則:活動有效期間、
IsBirthdayMonthEnabled、MatchedUserScopes中的CurrentBirthdayMonth,以及其他會員資格、商品、通路、金額條件。 - 確認計算月份:區分 cart calculate、basket calculate 與歷史訂單回饋所用的時間;檢查時區與訂單成立時間。
- 確認 DDB mapping:使用正確環境的
OSM_MemberCollectionMapping,以System_BirthdayMonth_{shopId}找到目標月份的 CollectionId。 - 確認會員比對:查 Member Collection API 在當時計算條件下是否 match,並留意會員生日資料的填寫或修改時間。
- 確認前台 Tag:查計算紀錄中的
context.Calculate.User.Tags是否出現CurrentBirthdayMonth;若沒有,往 CollectionId 查詢、API 回傳與 Tag 轉換步驟追查。 - 確認引擎結果:檢查
IsMatch、LackOfUserScope及其他 Lack 欄位,避免把其他條件未達成誤判為壽星判斷失敗。
Shop 12 的月份索引
bcc62702-6408-4ef7-818b-44106a5b6378。Table Name: HK_QA_OSM_MemberCollectionMapping
Key: System_BirthdayMonth_5
說明: 該 shop 的 12 個 Birthday CollectionIds
組成: Name / Month / CollectionId
HK_Prod_OSM_MemberCollectionMapping
key : System_BirthdayMonth_ShopId
查看 Shop 12 的十二筆月份對照 JSON
1 | [{"Name":"Collection_System_BirthdayMonth_1_12","Month":1,"CollectionId":"9c37727d-10af-48d9-9fc1-a99a1a4c9dc4"},{"Name":"Collection_System_BirthdayMonth_2_12","Month":2,"CollectionId":"0d849831-cb4b-46fc-b73c-84b87e8519d3"},{"Name":"Collection_System_BirthdayMonth_3_12","Month":3,"CollectionId":"5cf583b7-f470-4f65-879d-005445a15079"},{"Name":"Collection_System_BirthdayMonth_4_12","Month":4,"CollectionId":"3bccea69-6dda-4045-b650-a94a44811481"},{"Name":"Collection_System_BirthdayMonth_5_12","Month":5,"CollectionId":"074c32ca-d4f5-4eb3-a084-a20c6435b374"},{"Name":"Collection_System_BirthdayMonth_6_12","Month":6,"CollectionId":"153a18fb-1c72-4ee1-bb4b-255d156ee51c"},{"Name":"Collection_System_BirthdayMonth_7_12","Month":7,"CollectionId":"2733a896-82ec-4102-90b6-9c61675be410"},{"Name":"Collection_System_BirthdayMonth_8_12","Month":8,"CollectionId":"03324423-4c76-4a74-8dd0-ff0e0d747944"},{"Name":"Collection_System_BirthdayMonth_9_12","Month":9,"CollectionId":"69d8946c-86e3-4b81-bc97-a3cf59ba5092"},{"Name":"Collection_System_BirthdayMonth_10_12","Month":10,"CollectionId":"bcc62702-6408-4ef7-818b-44106a5b6378"},{"Name":"Collection_System_BirthdayMonth_11_12","Month":11,"CollectionId":"13a41a0d-21e3-417f-935d-ebdc83c07c22"},{"Name":"Collection_System_BirthdayMonth_12_12","Month":12,"CollectionId":"1976020e-9a6b-4d8f-a767-9306ac3cd5df"}] |


