Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ REST 一定完整,而推播要傳達的資訊只有「有事發生了」。
至此三端齊備:**乘客評 → 司機看得到自己的平均分 → 營運看得出誰評價低**。
詳見 [`docs/TODO.md`](docs/TODO.md)「⭐ 乘客評分司機」。

**目前**:`flutter analyze` 無 issue、`flutter test` **444 passed**(57 個測試檔,2026-08-01 實跑)。
~~440 passed~~/~~434 passed~~/~~425 passed~~/~~414 passed~~/~~383 passed~~/~~377 passed~~/~~361 passed~~/~~356 passed~~/~~351 passed~~/~~339 passed~~ 是漏更新的舊數字——**這一行請跟著最後一次實跑一起改**。
**目前**:`flutter analyze` 無 issue、`flutter test` **448 passed**(58 個測試檔,2026-08-01 實跑)。
~~444 passed~~/~~440 passed~~/~~434 passed~~/~~425 passed~~/~~414 passed~~/~~383 passed~~/~~377 passed~~/~~361 passed~~/~~356 passed~~/~~351 passed~~/~~339 passed~~ 是漏更新的舊數字——**這一行請跟著最後一次實跑一起改**。

**2026-07-30 弱網逾時對帳的實跑收尾**(詳見 [`docs/TODO.md`](docs/TODO.md) 第十四~十五輪):
先做了一支「請求照送、回應吃掉」的代理 [`tool/lossy_proxy.py`](tool/lossy_proxy.py)——
Expand Down
101 changes: 93 additions & 8 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
- [🔤 第二十六輪:App 的字數上限與後端數的不是同一種單位](#-2026-08-01-第二十六輪app-的字數上限與後端數的不是同一種單位本批修掉-6-個欄位)
- [🚦 第二十七輪:後端比 App 新時,進行中的訂單會從畫面上消失](#-2026-08-01-第二十七輪後端比-app-新時進行中的訂單會從畫面上消失本批修掉-1-個補完上輪缺口)
- [📱 第二十八輪:同一帳號的第二台裝置永遠不知道有進行中的行程](#-2026-08-01-第二十八輪同一帳號的第二台裝置永遠不知道有進行中的行程本批修掉-1-個)
- [🔌 第二十九輪:WS 重連後沒有人對帳——司機繼續開往已取消的行程](#-2026-08-01-第二十九輪ws-重連後沒有人對帳司機繼續開往已取消的行程本批修掉-2-個)

**四、維護、決策與待辦**
- [🧹 清開發殘留 worktree/舊分支(維護項 5)](#-清開發殘留-worktree舊分支維護項-52026-07-28-完成)
Expand All @@ -96,7 +97,7 @@
- **UI/UX 翻新(2026-07-10)**:LINE 綠亮暗雙主題;司機駕駛情境 UI;乘客地圖為底+卡片降級。靜態驗收 49 tests 通過;模擬器主鏈路待後端 docker 可起後補跑。
**登入/註冊頁 2026-07-23 補齊翻新**(先前是唯一漏網畫面),詳見下方「🔐 登入頁 UI/UX 翻新+驗證」。
- **座標導航(2026-07-10)**:司機端目的地導航改吃後端 `dropoff_point` 座標,地址僅供顯示與退路。
- 單元測試:**57 個測試檔、`flutter test` 444 passed**(2026-08-01 第二十八輪後實測
- 單元測試:**58 個測試檔、`flutter test` 448 passed**(2026-08-01 第二十九輪後實測
`flutter analyze` 無 issue、`flutter build apk --debug --flavor customer` 成功)。
~~(54 項)~~ 是 2026-07-10 的數字,長期沒更新,已更正——**本節的數字請跟著最後盤點日一起改**。
- 遠端:`github.com/thothawei/fleet-app`。**2026-07-29 實查**:`git ls-remote --heads origin` 只有 `main`、
Expand Down Expand Up @@ -2655,18 +2656,102 @@ WS 那條的 `default: break;` 是忽略未知事件型別,這是對的(事

### 同族還沒碰的角落

- **WS 斷線期間發生的事**:B 台若在事件送出的當下沒有連線,這次對帳就不會發生。
目前靠 `onAppResumed` 的無條件 `refreshActive` 自癒——**但那需要 App 進過背景再回來**,
一台一直開在前景的裝置不會走到。**條件**:等真的有人回報再處理;
要修的話是「WS 重連成功後補一次對帳」,動的是 `onConnectionChanged`。
- **司機端沒有這條對帳**:司機的 `_handleWsEvent` 對不認得的單也是丟掉,
但司機端在待命時**有**輪詢(hero 上線後持續回報位置並對帳),情境不同。**未查證,下輪可看。**
- ~~**WS 斷線期間發生的事**~~ ✅ **第二十九輪修掉**(見下一段)。
- ~~**司機端沒有這條對帳**~~ ✅ **第二十九輪查證了,而且比預期嚴重**——
**上面那句「司機端待命時有輪詢」是錯的,已更正**:司機端**完全沒有行程輪詢**。

---

## 🔌 2026-08-01 第二十九輪:WS 重連後沒有人對帳——司機繼續開往已取消的行程(本批修掉 2 個)

> 上一輪列了兩個候選,本輪查下去發現**它們是同一個洞的兩半**,而且
> **我上一輪寫的那句「司機端待命時有輪詢」是錯的**(下面已更正)。

### 先更正一個錯誤敘述

第二十八輪的「同族還沒碰的角落」寫著「司機端在待命時**有**輪詢」。
**實查是錯的**:`grep Timer lib/driver/driver_controller.dart` 只有
`_locationHealthTimer`(定位健康度重評),**沒有任何行程輪詢**。
司機端的 `_api.activeRide()` 只在三種時機被呼叫:`init()`、自己動作之後
(接單/上車/完成)、`onAppResumed`。

### 根因一句話

`onConnectionChanged` **只記旗標、notifyListeners**,沒有人去問後端;
而 WS 重連**不補送**斷線期間的事件(第四輪就記過)。
兩件事湊起來,斷線視窗裡發生的每一件事都永遠送不到。

### 為什麼司機端最嚴重

| 端 | 斷線視窗的補救 | 結果 |
|---|---|---|
| 乘客端 | 有進行中訂單時每 15 秒輪詢 | 有訂單時會自己校正;**閒置時輪詢是停著的**(第二十八輪那個情境剛好落在這裡) |
| **司機端** | **沒有任何行程輪詢** | 只剩 `onAppResumed`——而那需要 App **真的進過背景**。開車時 App 全程在前景,**走不到** |

最惡劣的具體後果:斷線視窗裡乘客取消 → `ride.cancelled` 送不到 →
**司機繼續開往一個已經取消的上車點**,而畫面上那張行程卡看起來完全正常。

### 修法(兩端同一招)

`onConnectionChanged` 在**重連**時補一次對帳:司機端 `_restoreActiveRide(silent: true)`、
乘客端 `refreshActive(silent: true)`。

- **只在「重連」時做,第一次連上不做**(新增 `_everConnected` 旗標):
冷啟動時 `init()` 才剛問過,再問只是多打一支 API。
- **斷線本身不觸發**(問了也問不到)。
- 兩端都走 `silent`:這不是使用者按出來的,失敗不該冒錯誤橫幅。

### 驗收

- `flutter analyze` 無 issue、`flutter test` **448 passed**(444 +新 4)、58 個測試檔。
- **先紅再修**:司機端主案在修改前就是紅的(`activeCalls` 停在 1)。
- **反向驗證三處**:
- 拿掉司機端的重連對帳 → 主案 FAIL;
- 拿掉 `_everConnected` 守衛 → 「第一次連上不重複對帳」FAIL(變成 2 次);
- 拿掉乘客端的重連對帳 → 乘客端那案 FAIL。
- `flutter build apk --debug --flavor customer` 成功。

### 過程中修正的一個測試錯誤

第一版司機端測試**跳過了「第一次連上」**,於是 `_everConnected` 永遠是 false、
重連判定不成立,測試紅得像是修法沒生效。**錯的是測試不是程式**——
production 一定會先連上一次。已補上 `ws.setConnected(true)` 當作初次連線。
這與上一輪那條是同一個教訓:**測試紅了要先問「是我的假設錯了嗎」**。

### 同族還沒碰的角落

- **`onConnectionChanged` 收不到「連線其實已經死了」的情況**(弱網:連得上但通不了,
凍結的後端不會送 FIN)。那一族在第六輪處理過一半(司機端有 `locationStale` 偵測),
但**重連對帳幫不上忙——因為根本沒有「重連」事件**。
**條件**:要修得靠心跳逾時主動斷線重連,動的是 `FleetWsClient`。
- **乘客端閒置時仍然沒有輪詢**:本輪讓「重連」與「收到不認得的事件」兩條路補上了,
但「WS 一直連著、事件也真的沒送到」這種情況還是沒人管。
**條件**:那要嘛信任後端一定會送、要嘛開閒置輪詢(每台裝置每 15 秒一支 API)——
現在的證據不足以支持後者的成本。

---

## 下次任務

> **🎯 2026-08-01 第二十八輪之後的狀態(開工先看這段)**
> **🎯 2026-08-01 第二十九輪之後的狀態(開工先看這段)**
>
> 1. **`gh pr list`(三個 repo)照樣是第一件事**——本輪開工時皆為 0。
> 2. **第十二次實查三個外部卡點:依然都不在**。
> 3. **這一輪學到的一條(最重要)**:
> **上一輪寫進「還沒碰的角落」的敘述,下一輪要當成假設去查,不能當成事實。**
> 第二十八輪我寫「司機端待命時有輪詢,情境不同」——**那句是錯的**,
> 司機端根本沒有行程輪詢,而且因此是兩端裡最嚴重的一邊。
> 寫「未查證」三個字有用,但**只有真的去查才算數**。
> 4. **下一輪候選**(依價值排序):
> - **弱網下沒有「重連」事件**:凍結的後端不會送 FIN,`onConnectionChanged`
> 永遠不會 false→true,本輪的對帳幫不上忙。要修得靠心跳逾時主動斷線重連,
> 動的是 `FleetWsClient`。**這是本輪對帳唯一補不到的缺口,最具體。**
> - **App 版本比後端新**:App 送出後端不認得的欄位/值時,後端是忽略還是 400?
> 三端都沒寫明這個契約。
> - **登出/換帳號時的殘留**(從沒系統掃過):第二十四/二十八輪各修過一個
> 「上一位使用者的資料留著」,但沒有人把 `_clearSession` 的欄位與
> controller 的所有狀態欄位對過一次帳。

>
> 1. **`gh pr list`(三個 repo)照樣是第一件事**——本輪開工時皆為 0。
> 2. **第十一次實查三個外部卡點:依然都不在**。
Expand Down
12 changes: 12 additions & 0 deletions lib/customer/customer_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class CustomerController extends ChangeNotifier {
String? _error;
bool _busy = false;
bool _wsConnected = false;

/// 這條 session 曾經連上過 WS。用來分辨「第一次連上」與「重連」——
/// 只有後者需要對帳(第一次連上時 init/登入路徑才剛問過)。
bool _everConnected = false;
Position? _lastPosition;
CustomerRide? _activeRide;
// 最近一筆進行中訂單的鏡像:即使輪詢對帳先把 _activeRide 清成 null,仍能在稍後才到的
Expand Down Expand Up @@ -792,8 +796,16 @@ class CustomerController extends ChangeNotifier {
_ws = _wsFactory(
onEvent: _handleWsEvent,
onConnectionChanged: (connected) {
// 與司機端同一個理由:WS 重連不補送斷線期間的事件。乘客端有輪詢,
// 但**輪詢只在有進行中訂單時才跑**(`_applyActiveRide` 沒有單就 `_stopPolling`)——
// 所以「閒置時另一台裝置叫了車」這條(第二十八輪修的那個情境)
// 若剛好卡在斷線視窗裡,事件收不到、輪詢也沒開,就沒有任何人會發現。
final reconnected = connected && !_wsConnected && _everConnected;
_wsConnected = connected;
if (connected) _everConnected = true;
notifyListeners();
// 第一次連上不對帳——`init()`/登入路徑才剛 `refreshActive` 過。
if (reconnected) unawaited(refreshActive(silent: true));
},
);
final saved = await _storage.read();
Expand Down
Loading
Loading