Bookingtime Job 跨日誤判
訂單在 11/05 23:54 成立,NMQ Job 到午夜後才寫入回饋紀錄,UpdateTime 落在 11/06 00:02。前後只隔幾分鐘,稽核卻說 BookingTime 差了 24 小時。Job 沒有跑上一整天;問題是正流程拿訂單日期排程,舊稽核卻可能拿 Job 寫入紀錄的日期重算。同一筆回饋,兩邊翻了不同頁的日曆。
NMQ Job 晚幾分鐘寫入,稽核為何多算 24 小時?
flowchart TB
Order["11/05 23:54 訂單成立"] --> Booking["正流程:以 OrderTime 日期排程"]
Order --> Job["午夜後:NMQ Job 處理回饋"]
Job --> Update["11/06 00:02 寫入紀錄 UpdateTime"]
Update --> Audit["舊稽核:以 UpdateTime 日期重算"]
Booking --> Gap["兩個日期基準差一天"]
Audit --> Gap
Gap --> Alarm["稽核報差 24 小時"]
classDef event fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef boundary fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef danger fill:#f9e9e3,stroke:#bb765f,color:#704737
class Order,Booking event
class Job,Update,Audit,Gap boundary
class Alarm danger
UpdateTime,不是 NMQ Job 的精確啟動時間。圖要指出的是:Job 處理與寫入落在訂單成立的隔日,而日期基準選錯,才讓稽核重算結果可能差一天。先認識這場戲的主角:BookingTime 是回饋的預約號碼牌。Dispatcher 看號碼牌,決定何時輪到給點或給券;稽核器則翻出規則,檢查號碼牌當初有沒有印錯。同一筆時間被兩個單位拿去問不同的問題,難怪只看欄位名稱,案情還是霧煞煞。
同一張號碼牌,兩個單位來查
flowchart TB
Calculate["正流程計算 BookingTime"] --> Stored["DDB 儲存 BookingTimeUTC"]
Stored -->|依日期與時間窗口查詢| Dispatcher["Dispatcher:輪到這筆了嗎?"]
Dispatcher --> Reward["建立給點或給券 Task"]
Stored -->|讀取實際排程時間| Auditor["稽核器:這筆約對時間了嗎?"]
Expected["依回饋條件重算預期時間"] --> Auditor
classDef service fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef waiting fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef result fill:#fbf7ef,stroke:#b49b78,color:#493b2d
class Calculate,Stored,Expected service
class Dispatcher,Auditor waiting
class Reward result
BookingDateUTC-BookingTimeUTC-index 找出時間窗口內的紀錄。稽核像事後對帳:重算預期時間,再比對 BookingTimeUTC。一個問「輪到誰」,一個問「號碼對不對」。先看正流程怎麼排時間。案發當時,線上訂單若走「加 N 天」分支,程式從訂單成立的 OrderTime 取日期,加上回饋天數,排在目標日午夜。NMQ Job 晚幾分鐘才寫入紀錄,不會讓這筆預約自動順延一天;這是第一張對照用的日曆。
正流程先抽哪張時間規則?
flowchart TB
Start["線上訂單計算 BookingTime"] --> Fixed{"有指定回饋日期?"}
Fixed -->|有| ByDate["使用 RewardDateTime"]
Fixed -->|沒有| Status{"訂單狀態是 WaitingToShipping?"}
Status -->|是| Immediate["保留 OrderTime"]
Status -->|否| Days{"RewardDays 大於 0?"}
Days -->|否| Zero["OrderTime 加上 RewardDays"]
Days -->|是| Delayed["取 OrderTime 的日期,再加 N 天"]
classDef service fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef decision fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef result fill:#fbf7ef,stroke:#b49b78,color:#493b2d
class Start,Delayed service
class Fixed,Status,Days decision
class ByDate,Immediate,Zero result
EComOrderPromotionRewardCalculateService.GetRewardConditionSetting() 的線上訂單分流。先看有沒有指定日期,再看訂單狀態與 RewardDays。規則有先後,不能一見「加 N 天」就直接跳到結局。OrderTime.Date.AddDays(N),結果落在目標日的 00:00。UpdateTime 記錄的是 Job 何時寫入這筆回饋,不能代替訂單成立時間。下一幕要看稽核器是否拿同一個起點來對答案。輪到稽核器出場。已出貨、指定日期、券不足、未出貨,各有各的比對規則。這次的未出貨回饋卡在一個選擇題:用訂單成立的 OrderTime 起算,還是用 NMQ Job 寫入紀錄時留下的 UpdateTime 起算?兩者剛好分居午夜兩側,選錯起點,預期日期就跟著換頁。
稽核先分流,才輪到對時間
| 回饋類型 | 判斷順序 | 本文關注的入口 |
|---|---|---|
| 給點 | 略過指定狀態 → 已出貨且非指定日 → 指定日期 → 其餘 | 未出貨時進入 VerifyBookingTimeForUnshippedOrder |
| 給券 | 略過指定狀態 → 近期券不足 → 已出貨 → 指定日期 → 其餘 | 沒有先命中券不足、已出貨或指定日時,才進入未出貨稽核 |
MatchWithoutQuota、Unmatch、Cancel、Reward 會直接略過。給點與給券的關卡順序也不同,給券會先查券不足。先看入場券,再談時間算式。未出貨這題,誰提供起跑線?
flowchart TB
Start["未出貨 BookingTime 稽核"] --> Offline{"線下訂單?"}
Offline -->|是| Order["以 OrderTime 加 RewardDays"]
Offline -->|否| Initial{"未記錄延遲,且 RewardDays 大於 0?"}
Initial -->|是| Order
Initial -->|否| Update["以 UpdateTime 加 RewardDays"]
Order --> Days{"RewardDays 大於 0?"}
Update --> Days
Days -->|是| Date["取目標日期"]
Days -->|否| Time["保留算出的時分秒"]
Date --> Compare["與 DDB BookingTime 比對"]
Time --> Compare
classDef service fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef decision fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef result fill:#fbf7ef,stroke:#b49b78,color:#493b2d
class Start,Order,Update service
class Offline,Initial,Days decision
class Date,Time,Compare result
OrderTime;線上訂單若是加 N 天、又沒有改期記號,也走 OrderTime。其餘未出貨情境才輪到 UpdateTime。這張圖聚焦在稽核選擇哪個時間基準。真正的轉折不是「Job 晚了幾分鐘」本身,而是這幾分鐘剛好跨過午夜。正流程用 11/05 的訂單日期排 BookingTime;舊稽核若用 NMQ Job 寫入紀錄後留下的 11/06 UpdateTime 重算,再把時分秒歸零,兩張日曆就差了一天。Job 沒遲到 24 小時,稽核的答案卻可能晚了 24 小時。
Job 跨日後寫入,為什麼讓稽核多算一天?
flowchart TB
Order["訂單成立:11/05 23:54"] --> OrderDate["正流程:用 11/05 加 7 天"]
OrderDate --> OrderResult["得到 11/12 00:00"]
Update["NMQ Job 寫入紀錄:11/06 00:02"] --> UpdateDate["舊稽核:用 11/06 加 7 天"]
UpdateDate --> UpdateResult["得到 11/13 00:00"]
classDef service fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef waiting fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef danger fill:#f9e9e3,stroke:#bb765f,color:#704737
class Order,OrderDate,OrderResult service
class Update,UpdateDate waiting
class UpdateResult danger
RewardDays,所以這是機制示範,不能當成原案的實際算式。真正的案卷,留下了哪些時間?
| DDB 欄位 | 原始 UTC 值 | 換算為 UTC+8 |
|---|---|---|
OrderTimeUTC |
2025-11-05 15:54:52Z | 11/05 23:54:52 |
UpdateTimeUTC |
2025-11-05 16:02:14Z | 11/06 00:02:14 |
BookingTimeUTC |
2025-11-12 16:00:00Z | 11/13 00:00:00 |
OrderTimeUTC 是訂單成立時間;UpdateTimeUTC 是 NMQ Job 處理回饋時寫下的紀錄更新時間,不是 Job 的精確啟動時間。兩者換成 UTC+8 後確實跨過午夜。不過案卷沒有列出這筆的 RewardDays;若硬套上圖的「加 7 天」,算出的 UTC 值也對不上實際 BookingTimeUTC。機制能解釋誤判方向,不能把示意圖冒充現場計算結果。UpdateTime 變晚了,不代表預約真的改期。這點很像群組訊息:有人半夜補一句話,不等於整場活動改到明天。稽核得翻 RewardHistoryList,找程式留下的指定改期記號,再決定要不要改用更新時間起算。沒有記號,就不能自行幫預約辦一場「改期發布會」。
有更新紀錄,就算真的改期嗎?
flowchart TB
History["檢查 RewardHistoryList"] --> Keyword{"找到改期記號?"}
Keyword -->|有| Delayed["HasDelayBookingTime = true"]
Keyword -->|沒有| Initial["HasDelayBookingTime = false"]
Delayed --> Update["線上未出貨:可走 UpdateTime 基準"]
Initial --> Order["線上加 N 天:走 OrderTime 基準"]
classDef service fill:#edf3e4,stroke:#7a9a5e,color:#3f542d
classDef decision fill:#f5e8d3,stroke:#a06a41,color:#65442c
classDef result fill:#fbf7ef,stroke:#b49b78,color:#493b2d
class History service
class Keyword decision
class Delayed,Initial,Update,Order result
HasDelayBookingTime() 只認既有程式寫入的固定字串 設定BookingDataTime。找到記號也還沒直接大結局;實際時間基準仍要依線上或線下,以及 RewardDays 的分支順序決定。分支一多,規則就像口袋裡的耳機線,拿出來自動打結。拆成三個問題就好:這筆要不要稽核?拿哪個時間起算?最後怎麼比?表格是攻略地圖,但關卡有順序,不能直接傳送到喜歡的結局。
八種情境,別拿錯攻略
| 情境 | 預期時間的來源 | 稽核如何比 |
|---|---|---|
MatchWithoutQuota、Unmatch、Cancel、Reward | 不計算 | 直接略過 |
| 線上未出貨、加 N 天、沒有改期記號 | OrderTime + RewardDays | 取訂單日期,加 N 天後與 BookingTime 比 |
| 線上未出貨、加 N 天、有改期記號 | UpdateTime + RewardDays | 取更新日期,加 N 天後與 BookingTime 比 |
| 線下未出貨 | OrderTime + RewardDays | 加 N 天時取日期並比對 BookingTime |
未出貨、RewardDays = 0 | 線上以 UpdateTime;線下以 OrderTime | 保留時分秒,比對 BookingTime |
| 指定日期回饋 | 活動設定的 RewardDateTime | 與 BookingTime 精確比對 |
| 已出貨回饋 | 最後出貨時間加回饋天數,且不早於活動有效日 | 比對 BookingDate 的日期 |
| 券不足重試 | 依 UpdateTime 排到當天 22:00、當天或隔天 10:00 | 與 BookingTime 精確比對 |
不同關卡,不能共用同一把尺
時間差必須小於 1 小時。剛好差 1 小時也不算過關。
照活動設定的日期時間比對 BookingTime,不靠未出貨分支的時間基準。
依重試規則排出時間,再與 BookingTime 精確比對。
最後把鏡頭拉遠:訂單在午夜前成立,NMQ Job 在午夜後處理並寫入回饋紀錄。真正讓稽核報出 24 小時差異的,是正流程與舊稽核可能採用了不同的起算日期。後來的修正提交把未延遲情境改回 OrderTime;但提交不等於已在正式環境上線,這段不能擅自替劇情加彩蛋。
HK-Prod 訂單先成立,NMQ Job 跨日後寫入回饋紀錄。
BookingTime 與預期不符,差異為 24.00 小時。
未延遲的線上加 N 天回饋,稽核改用 OrderTime 起算。
案發到修正,時間線說了什麼?
訂單成立,午夜倒數中。
NMQ Job 處理後寫入回饋紀錄,UpdateTime 落在隔日;這不是 Job 啟動時間。
工作項目記錄 BookingTime 與預期相差 24 小時。警報先響,原因還得查。
未延遲的線上加 N 天回饋,稽核改用 OrderTime 起算。
0dadda61。提交日期是程式碼履歷,不是正式環境的上線打卡紀錄。OrderTime 起算。Job 何時寫入紀錄,不能悄悄把原本的訂單日期換掉。至少這次,查票與排班終於拿同一份時刻表。要完整復盤,案卷還缺哪些拼圖?
- 該筆回饋實際設定的
RewardDays,以及適用的回饋條件。 - 應用程式讀入三個 UTC 欄位時的
DateTime時區轉換與日期基準。 - 修正版本在 HK-Prod 的實際上線時間與上線後的稽核結果。
- 該筆 NMQ Job 的啟動時間與完整執行日誌;目前只有紀錄寫入時留下的
UpdateTime。
在這些資料補齊之前,能確定的是「24 小時警報」和「修正程式選擇的時間基準」。加 7 天只是示意算式,不能假扮成這筆訂單的實測結果。故事可以說得好懂,證據邊界還是要畫清楚。
2026/08/31 的提交 38b01eb9 才加入 RewardBookingTimeAllocator,將加 N 天的目標日分到 00:00、01:00 或 02:00。現行未出貨稽核對此使用 Math.Max(bucketCount, 3) 小時的容忍值。這是後續版本的規則,不用來重算 2025 年 11 月的警報。


