Bug #1521
是由 陳國瑋 於 2 天 前更新
## 背景與目的 背景
本票是 #1518 的 R3 子票。修正兩個相連的問題:補送舊 ENERGY 讀值後,即時預估費用可能停在舊值;停充後若完整計價尚無法成立,充電紀錄不應把部分金額或 0 元顯示成已確認的費用。 的子票,來源為 2026-09-28 對 MeterValues 第二~第五階段實作的 Astra 程式審查。該實作目前只存在主要 workspace,尚未部署至 A17;A17 已套用的第一階段複合索引不受本票影響。
工作位置:主 workspace `/Users/ken/Documents/workspace/workspace_cloudlink/ems`、現有 工作位置:
- EMS workspace:`/Users/ken/Documents/workspace/workspace_cloudlink/ems`
- EMS branch:`feature/redmine-1517-ocpp-liveness`
- `java/charge_point` 為獨立 repository,使用相同 branch `feature/redmine-1517-ocpp-liveness`。不建立 worktree;未經 名稱
- 直接使用主要 workspace,不建立 worktree
- 未經 Ken 指示不部署或重啟案場服務。#1518 的程式尚未部署 A17;已套用的第一階段索引不受本票影響。 指示不得部署或重啟案場服務
## 問題一:補送舊讀值未更新即時預估 問題
`LiveTransactionCostService` 目前記住最新 即時計費 cache 只比較最新 ENERGY 讀值與累計費用。若 MeterValue。若 CP 補送較早 timestamp 的資料,而最新讀值本身未變,下一次 estimate 會沿用舊結果,也不會重新檢查游標以前的資料。 的資料,最新一筆的 ID/timestamp/value 不變,cache 會直接回傳舊金額,也不會查詢 cursor 以前新增的資料。
例:原本只有 重現情境:
```text
原始資料:
07:59:30 10000 Wh、08:01:00 Wh
08:01:00 10090 Wh,跨越 08:00 費率邊界;之後補送 Wh
後續補送:
08:00:00 10030 Wh、08:00:30 Wh
08:00:30 10060 Wh。補送前資料不足,補送後依完整歷史可算 Wh
```
補送前跨過 08:00 費率邊界且資料不足,無法安全計價。補送後完整歷史計費可算出 0.45 元,但舊 元,但新 cache 仍回傳 因最新一筆未變仍回傳 0.00 元。
## 問題二:停充紀錄可能把未完成的計算當作確定金額 影響
Branch 管理端與車主端的充電紀錄都讀取完整歷史計價結果,但現行程式未在展示前確認 `calculation.status`。引擎在資料不足或錯誤時仍可能帶有可計價區段的部分合計;管理端甚至在例外時填 0 元。因此「停充後的金額」可能被誤讀為已確認。即時費用本來就是預估;車主充電中畫面已改標示「目前預估費用」。 - WebSocket 重連後補送 backlog 時,即時費用可能長期停留在舊值。
- 同 timestamp 修正或歷史資料補正不會反映。
- 即使後續又收到較新資料,cursor 以前的補送仍可能永遠被略過。
## 修正方式 相關程式
1. 保留現有 process-local cache,不增加資料表、Redis 或另一份需維護的持久快取。每個 PDU 保存時,只記錄成功保存的 - `java/ems_branch/.../LiveTransactionCostService.java`
- `java/ems_branch/.../MeterValueService.java`
- `java/ems_branch/.../MeterValueConnectorEventDispatcher.java`
## 建議修正
- ENERGY 中最早 timestamp;非 ENERGY 不觸發費用失效。
2. 交易成功 MeterValue 成功 commit 後,若該 timestamp 早於或等於 後,主動通知 cache 目前最新 ENERGY,就把該交易摘要標記 dirty;下一次 estimate 從資料庫完整歷史重建一次。正常較新讀值仍走既有尾端增量路徑。Rollback 不得失效;同 transactionId、timestamp、MeterValue ID 或資料版本。
- 新資料 timestamp 新讀值與舊新混合 PDU 都要涵蓋。已有其他人工補正入口時,亦應共用同一失效方法。 大於 cursor 才走正常增量。
3. 充電紀錄沿用共用 `MeterValueTariffCalculationService` 的 `COMPLETE / PENDING_DATA / ERROR` 判定,不以「中途漏一筆」直接視為資料不足。只有整段計價需要的起訖讀值、費率及跨邊界資料足以讓引擎回傳 `COMPLETE`,才顯示費用。短暫漏點但可完整、安全計價仍是 `COMPLETE`。 - timestamp 小於或等於 cursor,或同 timestamp 出現新 ID/修正值時,將 transaction cache 標記 dirty。
4. 停充後結果為 `PENDING_DATA`(例如缺起訖錨點或跨費率邊界的長缺口)時,API 金額為 null,畫面顯示「計費待確認」;`ERROR`(例如衝突讀值、無法處理的負增量、缺費率或計算例外)時,API 金額為 null,畫面顯示「計費異常」。不得將部分金額或 0 元當成確定金額。真正完整計算出 0 元時仍可顯示 0.00 元。 - 下一次 estimate 對 dirty cache 執行完整重建或能證明正確的局部重算。
5. 管理端的總額與平均費用若包含未能完整計價的紀錄,也不得以已算出的部分合計冒充完整總額;API 要提供可區分狀態,畫面要對應呈現。車主端透過 HQ 轉送的紀錄也要帶出同樣狀態。進行中的交易仍為預估,不宣稱已結算。 - Invalidation 必須在 commit 後發生;rollback 不得污染 cache。
6. 本票不改月結/請款規則,不新增 schema,不在案場有車充電時重啟服務。 - 人工補正或其他 MeterValue 寫入入口也要使用同一套 invalidation。
## 驗收條件
- 補送上述兩筆 ENERGY 並成功 commit 後,即使最新讀值不變,下一次即時估算得到 1. 上述 late-reading 案例補送後,費用由 0.00 元更新為 0.45 元;正常遞增仍走增量路徑。 元。
- 較早 2. 即使最新 MeterValue 完全不變,也能偵測 cursor 以前的新資料。
3. 補送較早 timestamp、同 timestamp、新舊混合 timestamp 修正、舊新資料混合 PDU 都正確失效;非 ENERGY 與 rollback 不誤觸發。失效與 estimate 並發時不丟失 dirty 訊號。 均會正確失效或重算。
- 缺中間少量讀值但完整計價狀態為 COMPLETE 時,停充紀錄仍顯示正確金額;PENDING_DATA/ERROR 或計算例外時顯示對應文字且不顯示部分費用。 4. 補送後沒有更晚資料到達時,下一次 snapshot 仍能得到正確費用。
- Branch 管理端單筆、彙總與車主充電紀錄的金額語意一致;API 可區分完整、待確認、異常。跨月查詢維持既有區段歸屬規則。
- 不新增持久快取或資料庫 migration。補送及停充狀態案例加入 E2E catalog,後續於 ken environment 驗證;部署、E2E 另由 Ken 指示。 5. Rollback 不得發布 invalidation;commit 後 cache 與資料庫最終一致。
## 重現與驗證 重現
現有單案例重現:`python3 ```text
python3 test_report/evidence/1518-review/run_one.py late-reading`(修正前預期 0.45、實際 0.00)。本地可執行隔離單一 Unit Test 與跳過測試的編譯;跨服務/E2E 測試需依 SOP 另列計畫,由 Ken 啟動。 late-reading
```
審查時預期 0.45 元、實際 0.00 元,案例以 AssertionError 結束。
返回