Skip to content

fix(app): 「我的行程」只看得到最近 20 趟——舊行程的協尋與補評分入口整個消失(第二十四輪) - #97

Merged
thothawei merged 5 commits into
mainfrom
claude/next-todo-task-8b1933
Aug 1, 2026
Merged

fix(app): 「我的行程」只看得到最近 20 趟——舊行程的協尋與補評分入口整個消失(第二十四輪)#97
thothawei merged 5 commits into
mainfrom
claude/next-todo-task-8b1933

Conversation

@thothawei

Copy link
Copy Markdown
Owner

先講開工撞到的事

TODO 寫的下一輪首選(前景服務被系統收走)已經被 #94 認領,另一支 #95 在做預約司機+常用地點。所以本輪換族,盤點「清單的資料量上限」這個從沒碰過的角。

根因

fetchRideHistory() 固定要 20 筆、畫面沒有任何「載入更多」,而「我的行程」是事後聯絡司機/申報遺失物/補評分的唯一入口——第 21 趟以前的行程在乘客端永遠打不開。

盤點下來這一族只有這一個真洞:對話與協尋清單都不帶 limit(後端 MaxListRows=5000),收入頁是依月聚合。後端 GET /api/customer/rideslimit 照單全收,所以不用動 dispatch

修法

  • 捲到底自動再要一頁。沒有 cursor 就別假裝有:後端只有 limit、沒有 offset,所以「載入更多」是把 limit 加大重讀,不是接續抓下一段。代價是剛好整除時多要一次;好處是順帶更新已顯示那幾筆的評分/協尋狀態。
  • 失敗訊息分兩條historyMoreErrorhistoryError 分開——已經載進來的行程還在畫面上,錯誤只該長在清單尾巴。失敗時視窗不推進,重試要的是同一段。
  • 下拉刷新保持已展開的筆數(捲到第 60 筆的人不該被縮回 20 筆),登出才收回一頁。
  • 尾巴不是按鈕是自動載入——舊行程是「想起有東西掉在車上」時才會翻的,多一次點擊就多一個放棄點;只有失敗時才退化成重試按鈕。

驗收

情境 後端實際收到的查詢 畫面
修好的版本:開畫面、不捲動 只有 LIMIT 20 最新 #65 起 20 筆
修好的版本:捲到底 LIMIT 20LIMIT 40(rows 20 → 25) 最舊 #41pickup point 1 進得來
修改前的版本:同樣捲到底 14 次 只有一次 LIMIT 20 停在 #46pickup point 6,第 21~25 筆沒有任何入口

證據取自後端 GORM 的 SQL log,不是只看畫面。dev DB 的 25 筆全部是 status=9 終態。

過程中踩到的兩條(已寫進 TODO)

  1. 本機有兩個 redis 在搶 6379:主機的 redis-server127.0.0.1、docker 綁 *REDIS_ADDR=127.0.0.1:6379 會連到主機那個;在容器裡 redis-cli KEYS 查不到鍵,會誤以為限流沒生效。
  2. 造測試資料會撞叫車限流(5 次/分鐘,key 帶 line_user_id):要配速,而且每筆建完得立刻取消,否則 FindActiveByCustomer 擋掉下一筆。

🤖 Generated with Claude Code

thothawei and others added 2 commits July 31, 2026 07:54
`fetchRideHistory()` 固定要 20 筆、畫面沒有載入更多,而這個畫面是事後聯絡司機、
申報遺失物、補評分的唯一入口——第 21 趟以前的行程在乘客端永遠打不開。

後端 `limit` 照單全收(上限 5000),所以本輪不動 dispatch:改成捲到底自動再要一頁
(沒有 cursor,就是把 limit 加大重讀)。載入更多的錯誤與整頁錯誤分開、失敗時視窗
不推進;下拉刷新保持已展開的筆數,登出才收回一頁。

驗收:flutter analyze 無 issue、flutter test 386 passed;反向驗證兩半各紅一次;
模擬器實跑 25 筆行程,修好的版本捲到底發出 LIMIT 40 並撈到最舊那筆,
修改前的版本只發過一次 LIMIT 20、停在第 20 筆。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. 造大量測試行程改用 tool/seed_customer_rides.sh:叫車限流(5 次/分)、
   「已有進行中訂單」、建單回應是 ride_id 不是 id——三個都不會讓迴圈停下來,
   自己寫 for 迴圈會整批靜默失敗還印得像成功。腳本建單失敗直接 exit 1 並印回應。
   已用 seed_customer_rides.sh 3 實跑驗過(ride #66-#68,全部已取消)。

2. 本機後端跑法更正為 REDIS_ADDR=127.0.0.1:6380:6379 會靜默連到 Mac 本機的
   redis-server(已用 raw socket 對兩台寫 whoami 鍵驗死)。容器改發布 6380 見
   dispatch PR #72。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thothawei

Copy link
Copy Markdown
Owner Author

追加一支 commit:把這輪踩到的兩個環境坑修掉,不只是記下來

  • redis 撞埠 → dispatch PR #72:容器改發布 6380(與 postgis 讓開 5432 同一招)。本 PR 這邊把 TODO 的主機跑法更正為 REDIS_ADDR=127.0.0.1:6380。當初「6379 會連到本機那台」只是推論,事後對兩台各寫一個 whoami 鍵、用 raw socket 讀,才驗死。
  • 造測試資料 → 新增 tool/seed_customer_rides.sh:配速避開 5 次/分的叫車限流、每筆建完立刻取消(否則 FindActiveByCustomer 擋下一筆)、建單失敗直接 exit 1 印回應(第一版 25 次全跑空還印得像成功)。已用 seed_customer_rides.sh 3 實跑驗過。

改完的閉環證據:用 6380 跑一輪建單後,ratelimit:* 出現在容器那台,主機那台是空的。

thothawei and others added 3 commits July 31, 2026 08:21
「定位健康度」這一族的最後一個角落。TODO 原本寫「前景服務被系統收走時 App 端沒有
任何偵測」——查下去發現那句要更正:服務被收走而 App 還活著時串流會停,第二十一輪的
定期重評已經接住(第二十二輪實測 12~24 秒降級)。真正沒有網的是冷啟動:

init() 會用 _restoreActiveRide() 還原進行中行程(行程卡照樣顯示導航/已上車/完成),
但 _online 一律從 false 起、定位串流也沒起來 → 整趟零位置回報。後果不在司機端:
乘客端司機 marker 定格、後端抵達圍籬不觸發、F3 里程軌跡缺一整段。而 hero 只寫
「離線/目前不會收到派單」——在載客途中,那句話講的是接不接新單。

修法兩個:
1. 冷啟動還原到進行中行程時接回定位回報。三個限制寫在碼裡:只在冷啟動做(回前景也
   自動上線的話離線鈕按不掉)、只在已有權限時做(冷啟動彈權限視窗太侵入,沒權限就留
   一則「需要定位權限才能把位置回報給乘客」而不是「才能上線」)、沒有行程不自動上線。
2. refreshVehicle() 不再無條件 _setError(null)——它會把排在前面設好的訊息全洗掉,
   上面那句權限提示就是這樣消失的。改成只清自己造成的那則。

驗收:flutter analyze 無 issue、flutter test 383 passed(377+新 6)。
反向驗證兩半各一次,各 2 案 FAIL。模擬器實跑同一台裝置、同一張行程 #37 跑兩個版本:
force-stop 後修好的版本重開 8 秒內恢復回報(hero「上線中」),修改前 40 秒零筆
(行程卡完整、hero 卻寫「離線」)。負向對照:沒有行程時冷啟動零回報。

過程中踩到一條記進 TODO:接縫的預設實作會跑進測試環境——冷啟動直接 await 平台權限
確認,讓 driver_home_widget_test 從 4.8 秒變成 7 分鐘以上(測試環境沒有 platform
channel,那個 Future 永遠不會完成)。預設探針現在先看 FLUTTER_TEST 回「查不到」,
並用三態(null/denied/granted)區分「查不到」與「被拒絕」。

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PR #97 與 main 分岔:#94(第二十三輪)與 #95(預約司機+常用地點)先合併,
docs/TODO.md 的目錄、測試數字、章節順序與 customer_controller 的 _clearSession
三處衝突。解法都是兩邊並存,不是二選一:

- `_clearSession()`:分頁視窗(_historyWindow/_historyMoreError/_historyHasMore)
  與常用地點/預約的清除**都要留**——兩者都是「換人登入不能留上一位的資料」。
- TODO 目錄補上第二十四輪那行(原本兩邊各刪掉對方的)。
- 「現況」的測試數字先留 main 的 414,收尾重數後再更正(寫作規則 4)。
- 第二十四輪專章接在預約司機專章之後,「下次任務」以第二十四輪那段為最新在前。

驗收:flutter analyze 無 issue、flutter test **423 passed**(main 414 + 本 PR 9)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
回頭 debug 第二十四輪自己。`_HistoryFooter` 只在 initState 觸發一次補讀,而
`loadMoreRideHistory()` 有「已經有請求在飛就 return」的重入防護:尾巴被建出來的
那一刻若剛好有下拉刷新在飛,這唯一的一次觸發就被吞掉,之後沒有任何人再試一次
——尾巴永遠停在轉圈,舊行程的協尋與補評分入口又回到打不開的狀態。

原本 9 個案子測的是補讀本身對不對(視窗推進、錯誤分兩條、登出收回),沒有一個
案子讓兩個請求在時間上重疊,所以測不到。

修法兩個半邊,缺一不可:
- 尾巴加 didUpdateWidget,每次 rebuild 都補問一次(rebuild 由 notifyListeners
  帶動,刷新一結束就會走到);重入仍由 controller 擋,多問不會多打。
- 自動補讀失敗過就停手交給重試按鈕——didUpdateWidget 跟著每次 notifyListeners
  觸發,少了這道 guard 會在後端還沒恢復時變成連續打點。

順手補跨年行程的年份:`_dateLabel()` 只印「1月5日 08:00」。跨年的行程是分頁上線
後才翻得到的,而這頁是事後申報遺失物、補評分的入口,找錯年份就是找錯那一趟。

驗收:flutter analyze 無 issue、flutter test 425 passed(423+新 2)。
新測試在修改前就是紅的,紅的方式本身就是證據:pumpAndSettle timed out,因為那顆
CircularProgressIndicator 永遠不會停。反向驗證兩個半邊各一次——拿掉
didUpdateWidget 新案 FAIL;拿掉「失敗就停手」guard,既有的「載入更多失敗時尾巴
給重試」那案 FAIL(27 秒逾時,正是自動重試打成迴圈的樣子)。
flutter build apk --debug --flavor customer 成功(analyze 綠不代表包得出來)。

TODO 補上第二十五輪專章,含「掃過但沒有發現問題」的七項對照表(時區、預約排序、
載入路徑、error 顯示點、.first 空集合、dispose、其他一次性觸發),免得下輪重掃。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thothawei

Copy link
Copy Markdown
Owner Author

追加兩支 commit:解掉與 main 的衝突,並回頭 debug 這支 PR 自己(抓到 1 個永久卡死)

這支開出來之後 #94(第二十三輪)與 #95(預約司機+常用地點)先合併了,本 PR 變成 CONFLICTING。解完衝突之後沒有直接收工——把這支的新程式碼當成沒驗過的東西重查一遍,結果它有一個會永久卡死的狀態。

一、解衝突(三處,都是兩邊並存不是二選一)

衝突點 解法
customer_controller.dart_clearSession() 分頁視窗(_historyWindow_historyMoreError_historyHasMore)與 #95 的常用地點/預約清除都要留——兩者都是「換人登入不能留上一位的資料」
docs/TODO.md 目錄 補回第二十四輪那行(原本兩邊各刪掉對方的)
docs/TODO.md 現況的測試數字 先留 main 的,收尾重數後再更正(寫作規則 4)

驗收:flutter analyze 無 issue、flutter test 423 passed(main 414 + 本 PR 9)。

二、🐛 回頭 debug 抓到的真 bug:分頁的尾巴會卡死在轉圈

根因一句話_HistoryFooter 只在 initState 觸發一次補讀,而 loadMoreRideHistory() 有「已經有請求在飛就 return」的重入防護——尾巴被建出來的那一刻若剛好有下拉刷新在飛,這唯一的一次觸發就被吞掉,之後沒有任何人再試一次

症狀就是這支 PR 想修的那個病,在這條路徑上原封不動:尾巴永遠停在轉圈,第 21 筆之後的行程再也進不來,舊行程的協尋與補評分入口照樣打不開。

原本的 9 個測試為什麼抓不到:它們測的都是「補讀本身對不對」(視窗推進、錯誤分兩條、登出收回),沒有一個案子讓兩個請求在時間上重疊。這與預約那輪的教訓是同一條——沒有真的重疊的併發測試,測到的只是循序重跑。

修法兩個半邊,缺一不可

  1. 尾巴加 didUpdateWidget每次 rebuild 都補問一次(rebuild 由 controller 的 notifyListeners 帶動,刷新一結束就會走到)。重入仍由 controller 擋,多問不會多打。
  2. 但自動補讀失敗過就停手,交給既有的重試按鈕:didUpdateWidget 跟著每一次 notifyListeners 觸發,少了這道 guard 會在後端還沒恢復時變成連續打點

順手補上跨年行程的年份_dateLabel() 只印「1月5日 08:00」。跨年的行程是分頁上線後才翻得到的(在那之前只看得到最近 20 趟),而這一頁是事後申報遺失物、補評分的入口——找錯年份就是找錯那一趟。今年的不加年份,免得每張卡都變長。

三、驗收

  • flutter analyze 無 issue、flutter test 425 passed(423 +新 2)、54 個測試檔。
  • 抓 bug 的順序是先紅再修,而且紅的方式本身就是證據:pumpAndSettle timed out——因為尾巴那顆 CircularProgressIndicator 永遠不會停。這比任何斷言都直接。
  • 反向驗證兩個半邊各一次
    • 拿掉 didUpdateWidget → 新測試 FAIL(pumpAndSettle 逾時)。
    • 拿掉「失敗就停手」那道 guard → 既有的「載入更多失敗時清單還在,尾巴給重試」那案 FAIL,而且是 27 秒才逾時——正是自動重試打成迴圈的樣子。
  • flutter build apk --debug --flavor customer -t lib/main_customer.dart 成功app-customer-debug.apk)——analyze 綠不代表打得包出來。

四、這一輪掃過但沒有發現問題的(寫進 TODO,免得下一輪重掃)

掃的東西 結論
時區:全 repo 的 DateTime.parsetryParse 與顯示點 ✅ 送出 .toUtc()、顯示 .toLocal(),兩個方向都對
預約清單排序 ✅ 後端 scheduled_at ASC, id ASC、App 的 _mergeScheduledRide 也 sort
常用地點/預約的載入路徑 init()_authenticate() 都載,登出也清
每個 *Error 欄位有沒有顯示點 ✅ 6 個 getter 全部有對應畫面
.first.last 有沒有空集合保護 ✅ 7 處全部有 isNotEmpty 或三元退路
TimerStreamSubscriptionTextEditingController 的 dispose ✅ 有建就有 dispose
其他「一次性 initState 觸發+重入防護」的組合 ✅ 只有尾巴這一處

五、開工前的例行實查(本輪 = 第八次)

三個外部卡點依然都不在android/app/google-services.jsonios/Runner/GoogleService-Info.plist 不存在、xcrun devicectl list devices → No devices found。

功能清單目前沒有「不需前置條件就能做」的項目(唯一未勾的 [ ] 車種供給為零等產品拍板;司機端行程歷史後端也還沒有端點——cmd/server/main.go 的 driver 路由只有 /driver/rides/active),所以本輪照 2026-07-28 立的規矩改做 debug。

🤖 Generated with Claude Code

@thothawei
thothawei merged commit 1029ab5 into main Aug 1, 2026
2 checks passed
@thothawei
thothawei deleted the claude/next-todo-task-8b1933 branch August 1, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant