專案

一般

配置概況

動作

Bug #1394

進行中

[HQ/Branch Bug] 同門牌成員停止充電遭拒但 LIFF 誤顯示成功

是由 陳國瑋6 天 前加入. 於 5 天 前更新.

狀態:
In Progress
優先權:
High
被分派者:
-
開始日期:
2026-07-28
完成日期:
預估工時:

概述

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.java
  • java/ems_hq/src/main/java/com/sylksoft/ems/hq/service/BranchApiService.java
  • react/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 撤回:

  1. react/web 的全部變更。
  2. user-scoped driverState 查詢與 WebSocket refetch 設計。
  3. shared MQ 不廣播 driverState 等與上述 FE 設計耦合的後端變更。

本次 Backend 僅保留:

  1. Branch:一般住戶只有啟動 transaction 的本人可以停止;同門牌 User 2 不可停止;保留 ADMIN、power_user、association、dealer 既有管理例外。
  2. HQ:保留 Branch business response 的 isSuccess、errorCode、message,不得因 HTTP 200 而覆寫授權失敗。

FE 待辦(本 branch 不修改):

  1. LIFF 只有在停止 API response.isSuccess === true 時顯示「充電已停止」。
  2. isSuccess=false 應在 mutation/API result handling 階段轉成 rejection,確保所有呼叫端一致進入錯誤流程,不依賴個別 onSuccess 再判斷。
  3. 非啟動者停止時顯示 Backend 回傳警告,不可顯示成功。
  4. shared WebSocket driverState 並非 user-scoped,不可把 stop.enabled 當成目前登入使用者已通過停止授權;若要預先隱藏/禁用按鈕,需另行確認 user-scoped API contract。
  5. 補驗 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 五項待辦對應:

  1. 停止/開始充電改為 fail-closed:判定由 isSuccess === false 改為 isSuccess !== true,欄位缺漏(舊版 HQ、非預期 payload)亦視為失敗,不再顯示「充電已停止」。
  2. 檢查由 mutation 的 onSuccess 移入 startCharging()/stopCharging() API 函式內,所有呼叫端一致進入錯誤流程,與同檔 switchOffPeak/dequeueConnector 既有模式一致;不再依賴 React Query 把 onSuccess 例外轉成 rejection 的內部行為。
  3. 錯誤訊息優先透傳 Branch 的 message,message 缺漏時顯示「錯誤代碼 {errorCode}」,最後才是泛用文字。
  4. 停止按鈕行為不變(不做預先隱藏/禁用)。已於 charge.ts 的 getDriverPresentation 加註:driverState 的 start/stop 為連接器層級的 WebSocket 共享狀態,非 user-scoped 授權,不可用於權限判斷。若日後要預先禁用,需另行定義 user-scoped API contract。
  5. 手動 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。

動作

匯出至 Atom PDF