Bug #1394
進行中[HQ/Branch Bug] 同門牌成員停止充電遭拒但 LIFF 誤顯示成功
概述
h2. 問題現象
CT3 現場 CP001-002 發生以下情況:
- User 1 開始充電。
- 同門牌的 User 2 從 LINE LIFF 點擊停止充電。
- LIFF 顯示停止成功,但設備未斷電,充電持續進行。
h2. 現場調查結果(2026-07-28)
- User 1 與 User 2 的
unit_no相同。 - 兩人使用不同 IdTag,但 IdTag 的
parent_id_tag相同。 - 兩人都已經由
e_connector_user綁定 CP001-002。 - User 1 啟動的 transaction 17 於 15:28:27 進入 ACTIVE。
- User 2 在 15:29:05、15:29:29、15:29:49、15:29:56、15:33:51 共發出五次停止請求。
- Branch 五次皆記錄
transaction ownership mismatch。 - 失敗請求沒有建立 STOP 類型的
e_remote_transaction_log,charge_point 端也完全沒有收到 transaction 17 的RemoteStopTransaction。 - 15:38:18 檢查時 transaction 17 仍為 ACTIVE、CP001-002 仍為 CHARGING。
- User 1 在 15:20:54 曾成功停止上一筆 transaction 16,設備正常斷閘並送出 StopTransaction,因此可排除 CP 停止功能或 OCPP 通訊故障。
h2. 根因一:Branch 目前只允許交易本人停止
RemoteChargingSupport.remoteStopWithTransaction() 會把 transaction IdTag 解析成 owner user ID,並與請求的 user ID 做 exact match。
目前不會因為以下條件而允許停止:
- 同一
unit_no - 同一
parent_id_tag - 兩人皆綁定同一 Connector
特殊系統 operator 除外。
相關程式:
java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/charging/RemoteChargingSupport.java
h2. 根因二:HQ 把 Branch 業務失敗覆寫成成功
Branch 停止 API 以 HTTP 200 回傳 business response;授權失敗時 response 內容為 isSuccess=false。
HQ 的 ConnectorBranchApiHandler.handleStopCharging():
先以 BranchApiResponse.isSuccess() 判斷 HTTP 呼叫是否成功。¶
HTTP 200 時取得 Branch response。¶
隨後無條件呼叫 resp.markSuccess("停止充電(Branch API)成功")。¶
原本的 isSuccess=false、errorCode 與 message 因此被清除。¶
LIFF 本身已會在 isSuccess=false 時顯示錯誤;但收到 HQ 覆寫後的 true,所以顯示「充電已停止」。
相關程式:
java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/ConnectorBranchApiHandler.javajava/ems_hq/src/main/java/com/sylksoft/ems/hq/service/BranchApiService.javareact/web/app/charger/hooks/use-connectors.ts
h2. 版本沿革
- 2025-08 初版只檢查 transaction IdTag 是否屬於任一住戶,未核對請求者;行為過度寬鬆,並非明確的同門牌授權。
- 2026-01-08 充電流程重構後改成 exact transaction owner 才能停止。
- 2026-01-23 HQ
RespBase重構加入上述markSuccess(),形成 HTTP 成功覆寫業務失敗的問題。
h2. 必須先確認的業務規則
請確認同門牌/同一 IdTag family 的成員是否應可互相停止充電:
- 若「可以」:Branch 應驗證同門牌或同一 IdTag family,並同時確認請求者具有該 Connector 操作權。
- 若「不可以」:LIFF 必須針對非 transaction owner 停用停止按鈕,並顯示「僅啟動充電者可停止」等明確訊息。
無論採哪一種規則,HQ 覆寫 Branch 業務失敗的問題都必須修正。
h2. 驗收條件
- Branch 回傳 HTTP 200 +
isSuccess=false時,HQ 與 LIFF 必須保留失敗狀態、errorCode 與 message。 - 沒有實際 dispatch
RemoteStopTransaction時,LIFF 不得顯示停止成功。 - User 1 啟動後,User 2(同門牌)的停止結果符合已確認的業務規則。
- 不同門牌使用者不得停止他人交易。
- transaction owner 可以正常停止。
- LIFF 停止按鈕的 enabled 狀態與後端授權規則一致。
- E2E 驗證 Branch remote transaction log、OCPP
RemoteStopTransaction、charge_point 斷閘、StopTransaction 與 transaction COMPLETED 狀態。
是由 陳國瑋 於 6 天 前更新
- 狀態 從 New 變更為 In Progress
Stage 3 實作與單元測試完成,等待 CT3 手動部署後執行 E2E。
Branch: feature/redmine-1394-remote-stop-owner-feedback
Commit: c76232f
修正摘要:
- Branch 集中交易啟動者停止授權;同門牌非啟動者拒絕,既有 ADMIN/power_user/association/dealer 例外保留。
- HQ 不再以 HTTP 200 覆寫 Branch isSuccess=false/errorCode/message。
- Connector driverState 改為使用者專屬停止權限;共享 MQ 事件不廣播個人授權。
- LIFF 收到 WebSocket 更新後刷新個人 driverState,並在 mutation 階段 fail-closed 顯示 Branch 錯誤。
驗證:
- ems_branch mvn test:141 tests passed。
- ems_hq mvn test:10 tests passed。
- react/web yarn exec tsc --noEmit:passed。
- react/web yarn build:passed(僅既有 eslint-config-next CommonJS warning)。
建議部署順序:ems_hq → ems_branch → react/web。
是由 陳國瑋 於 6 天 前更新
【範圍修正,取代前一則實作說明】
Ken 已明確規定:本 Issue 的 backend agent 不得修改 react/web;FE 調整只能寫在 Redmine,由 FE 負責實作。
已自目前 branch 撤回:
- react/web 的全部變更。
- user-scoped driverState 查詢與 WebSocket refetch 設計。
- shared MQ 不廣播 driverState 等與上述 FE 設計耦合的後端變更。
本次 Backend 僅保留:
- Branch:一般住戶只有啟動 transaction 的本人可以停止;同門牌 User 2 不可停止;保留 ADMIN、power_user、association、dealer 既有管理例外。
- HQ:保留 Branch business response 的 isSuccess、errorCode、message,不得因 HTTP 200 而覆寫授權失敗。
FE 待辦(本 branch 不修改):
- LIFF 只有在停止 API response.isSuccess === true 時顯示「充電已停止」。
- isSuccess=false 應在 mutation/API result handling 階段轉成 rejection,確保所有呼叫端一致進入錯誤流程,不依賴個別 onSuccess 再判斷。
- 非啟動者停止時顯示 Backend 回傳警告,不可顯示成功。
- shared WebSocket driverState 並非 user-scoped,不可把 stop.enabled 當成目前登入使用者已通過停止授權;若要預先隱藏/禁用按鈕,需另行確認 user-scoped API contract。
- 補驗 User 1 啟動後由同門牌 User 2 停止的 LIFF 回歸案例。
修正 commit 與測試結果會在完成後另行補充。
是由 陳國瑋 於 6 天 前更新
【修正完成】
Corrective commit:217ee97(fix(#1394): 撤回超出範圍的前端變更)
Branch:feature/redmine-1394-remote-stop-owner-feedback
確認結果:
- 相對本 Issue 實作前基準,react/web 淨差異為 0;FE 程式碼已完整撤回。
- user-scoped driverState、WebSocket refetch、shared MQ payload 調整等延伸變更已撤回。
- 本 branch 淨保留 Backend:Branch transaction owner 停止授權,以及 HQ Branch business failure 透傳。
測試:
- ems_branch:139 tests,0 failures,0 errors。
- ems_hq:7 tests,0 failures,0 errors。
FE 所需調整請依上一則 Redmine 待辦由 FE 處理;Backend branch 不修改 react/web。尚未建立 MR、尚未 merge。
是由 鍾正剛 於 5 天 前更新
【FE 實作完成】依 note #4904 的 FE 待辦實作,未修改 backend。
Branch: fix/issue-1394-liff-stop-charging-fail-closed
Commit: 9a27ab4
FE 五項待辦對應:
- 停止/開始充電改為 fail-closed:判定由 isSuccess === false 改為 isSuccess !== true,欄位缺漏(舊版 HQ、非預期 payload)亦視為失敗,不再顯示「充電已停止」。
- 檢查由 mutation 的 onSuccess 移入 startCharging()/stopCharging() API 函式內,所有呼叫端一致進入錯誤流程,與同檔 switchOffPeak/dequeueConnector 既有模式一致;不再依賴 React Query 把 onSuccess 例外轉成 rejection 的內部行為。
- 錯誤訊息優先透傳 Branch 的 message,message 缺漏時顯示「錯誤代碼 {errorCode}」,最後才是泛用文字。
- 停止按鈕行為不變(不做預先隱藏/禁用)。已於 charge.ts 的 getDriverPresentation 加註:driverState 的 start/stop 為連接器層級的 WebSocket 共享狀態,非 user-scoped 授權,不可用於權限判斷。若日後要預先禁用,需另行定義 user-scoped API contract。
- 手動 E2E 驗證步驟已寫入 documents/redmine-1394-liff-stop-e2e.md,涵蓋:同門牌 User2 停止(含 branch log / e_remote_transaction_log / OCPP RemoteStopTransaction 比對點)、owner 正向停止、不同門牌使用者、以及回應缺漏時的 fail-closed 情境。
驗證:
- react/web npx tsc --noEmit:passed。
- react/web yarn build:passed。
- yarn lint 無法執行(本專案既有的 eslint-config-next CommonJS named export 問題,與本次修改無關)。
- E2E 尚未執行:需 backend branch 部署至 CT3 後依上述文件執行。
尚未建立 MR、尚未 merge。