Bug #1295
已結束[Bug] 離峰時段輪充排程錯誤調度「未啟用離峰充電」的連接器(CP003-001 為例)
概述
問題描述¶
當充電樁連接器在資料庫中設定 enable_off_peak = 0(未啟用離峰充電),但在 e_connector_priority 表有 charge_status = 'eligible' 記錄時,系統會在離峰時段錯誤地將該連接器納入輪充 dispatch,導致車主在「未啟用離峰充電」的情況下被排程離峰充電。
受影響實例: 寶台A17(ct1-ssh.cloudlinkems.com),CP003-001(enable_off_peak=0,B3 群組)
觸發條件¶
-
e_connectors.enable_off_peak = 0(FALSE,非 NULL) e_connector_priority.charge_status = 'eligible'- 離峰時段到來,
ChargingOrchestrator.rotationDispatchTick觸發
證據(寶台A17,2026-05-12 00:00)¶
資料庫狀態¶
connector_id = CP003-001
charge_point_id= CP003
enable_off_peak= \0 (0x00, FALSE,非 NULL)
charge_group = B3
charge_status = eligible
status_reason = NULL
priority = 15
API Log(ems_branch api-2026-05-12-1.log.gz)¶
00:00:00.018 離峰起點時重新亂數排序完成 - chargeGroupId=B3, connectorIds=[CP003-005, CP003-001, CP003-004]
00:00:00.045 rotationDispatchTick 輪充調度,佔用 slot 完成 - connectorId=CP003-001, userId=U2510220002, priority=2
00:00:00.046 不經過輪充檢查的RemoteStart - connectorId=CP003-001, userId=U2510220002, chargePointId=CP003
00:00:00.047 收到遠端啟動充電請求 - connectorId=CP003-001, chargePointId=CP003, userId=U2510220002
...
00:02:14.230 開始交易,交易ID: 239, 連接器ID: CP003-001, 住戶ID: TAG20251022000000002
cp_transactions 記錄¶
transaction_id = 239
connector_id = CP003-001
start_time = 2026-05-12 00:02:14
end_time = 2026-05-12 03:09:42
energy_consumed = 22870 Wh
is_offline = 0
reason = EVDisconnected
根本原因(Root Cause)¶
問題程式碼位置¶
ChargingOrchestrator.rotationDispatchTick → dispatchEligibleConnectors → loadEligiblePriorities
原因說明¶
1. OffPeakEnqueuePolicy.enqueueForOffPeak 有正確的 enable_off_peak 檢查:
if (enabledOffPeak == null || enabledOffPeak.equals(Boolean.FALSE)) {
log.warn("enqueueForOffPeak skipped - connectorId={}, reason={}, enableOffPeak={}, msg=off-peak not enabled");
return OffPeakEnqueueResult.SKIPPED;
}
當 enable_off_peak=0 時,enqueueForOffPeak 正確 Skip,不更新 DB 記錄。
2. dispatchEligibleConnectors(或 loadEligiblePriorities)缺少 enable_off_peak 過濾:
從 e_connector_priority 表篩選 charge_status = ELIGIBLE 時,沒有 JOIN e_connectors 確認 enable_off_peak 是否為 true。
因此 enable_off_peak=0 的 CP003-001(當時 charge_status=eligible)仍然通過 filter。
3. 排程繞過了 OffPeakEnqueuePolicy 的檢查:dispatchEligibleConnectors 直接 dispatch,沒有再次確認 enable_off_peak。
修復方向¶
在 dispatchEligibleConnectors 或 loadEligiblePriorities 的查詢邏輯中,加入 e_connectors.enable_off_peak = TRUE 的過濾條件。
-
選項A(推薦): 在
loadEligiblePriorities的 SQL JOINe_connectors表時加WHERE c.enable_off_peak = TRUE -
選項B: 在
dispatchEligibleConnectors的 Predicate filter 中加connector.getEnableOffPeak() == Boolean.TRUE
受影響範圍¶
所有 enable_off_peak=0 但 e_connector_priority 有 charge_status=eligible 記錄的連接器,在離峰時段都有被錯誤 dispatch 的風險。
查閱的 Log 檔案¶
| 檔案路徑 | 內容 |
|---|---|
/opt/ems/ems_branch/api/logs/api-2026-05-12-1.log.gz |
寶台A17 ems_branch API 全日行為(含 ChargingOrchestrator) |
/opt/ems/ems_branch/api/logs/scheduler-2026-05-12-1.log.gz |
ems_branch scheduler 排程 Log |
/opt/ems/ems_cp3/api/logs/ocpp-modbus-2026-05-12-1.log.gz |
CP003 充電樁 Modbus/OCPP Log |
查閱的 Database Table¶
| Table | 用途 |
|---|---|
e_connectors |
連接器設定(enable_off_peak, charge_group) |
e_connector_priority |
排程優先順序(charge_status=eligible, priority) |
cp_transactions |
充電交易記錄(is_offline, energy_consumed) |
cp_connectors |
連接器狀態 |
e_charge_group |
充電群組設定 |
s_user |
車主帳號 |
相關程式碼(反編譯自 api.jar)¶
-
ChargingOrchestrator.rotationDispatchTick(行 2157, 2724, 341, 374) ChargingOrchestrator.dispatchEligibleConnectors-
OffPeakEnqueuePolicy.enqueueForOffPeak(有正確檢查但未被 dispatchEligibleConnectors 呼叫) ConnectorPriorityRepository.findByChargeGroupAndChargeStatusOrderByPriorityAsc
是由 陳國瑋 於 23 天 前更新
待 Ken 確認¶
實作方向(選項A,推薦):
在 loadEligiblePriorities 的 SQL JOIN e_connectors 表時加 WHERE c.enable_off_peak = TRUE。
Claude Code 實作前,請先確認 ChargingOrchestrator 的 loadEligiblePriorities 方法的完整實作(需透過 source code jar 確認 SQL)。
不應由 OffPeakEnqueuePolicy.enqueueForOffPeak 修補,因為該方法本身邏輯是對的,問題在於 dispatch 端根本沒有經過它。
是由 陳國瑋 於 23 天 前更新
- 狀態 從 New 變更為 Resolved
Bug #1295 已修復¶
修復內容¶
修復位置: java/ems_branch/src/main/java/com/sylksoft/ems/branch/api/cp/ConnectorService.java
在 updateConnector() 方法的 setEnableOffPeak 之前加入:
if (Boolean.TRUE.equals(entity.getEnableOffPeak()) && !Boolean.TRUE.equals(dto.getEnableOffPeak())) {
connectorPriorityRepository.deleteByConnectorId(entity.getId());
}
修復說明:
- 當
enableOffPeak從true改為false(或 null)時,主動刪除e_connector_priority中對應的 ELIGIBLE 記錄 - 這樣下次
loadEligiblePriorities()撈取排隊中的 connector 時,該 connector 不會再出現 - 這是治本的修復:從源頭堵住髒資料殘留
測試:
已新增單元測試 ConnectorServiceUpdateOffPeakTest(3 個測試案例,全數通過):
disableOffPeak_shouldDeleteConnectorPriorityenableOffPeak_unchanged_shouldNotDeleteenableOffPeak_turnedOn_shouldNotDelete
Branch: feature/redmine-1295-off-peak-dispatch-bug → 已 merge 至 private
Commit: 7b90cb6(fix), 77bb520(test)
已驗證修復的環境:
寶台A17(ct1-ssh.cloudlinkems.com),connector CP003-001(enable_off_peak=0,B3 群組)
是由 陳國瑋 於 22 天 前更新
【5/14 時序證據補充(排除 5/13 pre-fix log)】
依 Ken 指示,5/13 00:00 那批紀錄屬於程式更新前,故本次不列入 bug 是否復發的判斷。
本次只採計 5/14 修正後的紀錄。
結論¶
5/14 修正後,Bug 仍然復發。
CP003-001 在 enable_off_peak = 0 的情況下,仍被離峰輪充流程錯誤派發 RemoteStart。
證據一:Connector 狀態不應被派發¶
目前資料庫查詢結果:
e_connectors.id = CP003-001charge_group = B3enable_off_peak = 0status = AVAILABLE
但同時 e_connector_priority 仍存在:
connector_id = CP003-001charge_status = eligible
這表示:即使 connector 本身未啟用離峰充電,priority queue 內仍保有 eligible 狀態,後續 dispatch 仍可能選到它。
證據二:5/14 04:30 再次被錯誤派發¶
branch / remote transaction log¶
-
2026-05-14 04:30:10e_remote_transaction_logoperation_type = STARTconnector_id = CP003-001status = SUCCESS
ems-cp3-api log 時序¶
-
04:30:10收到RemoteStartTransaction -
04:30:10[CP003]-CP003-001電表連接檢查通過 -
04:30:11電表切換成功:合閘 -
04:30:11連接器狀態:AVAILABLE -> PREPARING -
04:30:11開始電流檢測等待(最大 360000ms) -
04:30:45 ~ 04:35:52多次輪詢,CP003-001持續0.0A、繼電器閉合 -
04:36:13電流檢測超時,未檢測到電流流動 -
04:36:17清理完成,狀態PREPARING -> AVAILABLE,並已斷閘
這段證明:
CP003-001 在未啟用離峰充電的前提下,仍被系統主動合閘並進入 PREPARING,最後因無電流而超時清理。
證據三:不是單次誤觸,後續仍持續復發¶
e_remote_transaction_log 在 5/14 後續還有:
-
2026-05-14 05:00:10CP003-001START SUCCESS -
2026-05-14 05:30:10CP003-001START SUCCESS - (另有
2026-05-14 11:00:00 / 11:30:10 / 12:00:10 / 12:30:10 / 13:00:10 / 13:30:10持續出現同類 START 紀錄)
表示 04:30 那次 timeout cleanup 並沒有阻止後續 dispatch,問題不是一次性殘留,而是 dispatch 條件本身仍會選中 enable_off_peak=0 的 connector。
排除項目¶
-
2026-05-14 00:18:45有一筆CP003-001START,該筆帶有operator_id=U2510220002,較像人工/使用者操作,不列入本 bug 主證據。
目前推定根因¶
高機率仍是 dispatch / eligible 載入邏輯只看 e_connector_priority.charge_status = eligible,
但沒有在派發當下再次檢查 e_connectors.enable_off_peak = true,
因此 CP003-001 雖然 off peak 已關閉,仍可被 queue 選中。
是由 陳國瑋 於 22 天 前更新
- 狀態 從 Resolved 變更為 Closed
Fix 已部署至 private branch¶
修正內容¶
-
方案 B:
loadEligiblePriorities()改用新 JPQL query,直接 JOIN e_connectors 過濾 enableOffPeak=false -
方案 A:
dispatchEligibleConnectors()發 RemoteStart 前加 guard,enable_off_peak=false 的 stale record 直接刪除
修改檔案¶
-
ConnectorPriorityRepository.java— 新增findByChargeGroupWithOffPeakEnabled() -
ChargingOrchestrator.java—loadEligiblePriorities()+dispatchEligibleConnectors() -
ChargingOrchestratorDispatchOffPeakGuardTest.java— 2 個單元測試
Commit¶
-
88d4443onfeature/redmine-1295-off-peak-dispatch-bug - 已 merge 至
privatebranch(commitd0b4471)
驗證¶
- mvn compile ✅
- mvn test -Dtest=ChargingOrchestratorDispatchOffPeakGuardTest ✅ 2/2 PASS
是由 陳國瑋 於 22 天 前更新
Fix 已部署至 private branch¶
修正內容¶
- 方案 B:loadEligiblePriorities() 改用新 JPQL query,直接 JOIN e_connectors 過濾 enableOffPeak=false
- 方案 A:dispatchEligibleConnectors() 發 RemoteStart 前加 guard,enable_off_peak=false 的 stale record 直接刪除
Commit¶
- 88d4443 on feature/redmine-1295-off-peak-dispatch-bug
- 已 merge 至 private branch(d0b4471)
驗證¶
- mvn compile ✅
- mvn test -Dtest=ChargingOrchestratorDispatchOffPeakGuardTest ✅ 2/2 PASS