Skip to content

feat(app): 聊天送出帶冪等鍵——逾時後用鍵比對補讀,重試不會多一則(第十八輪) - #90

Merged
thothawei merged 1 commit into
mainfrom
claude/chat-idempotency-app
Jul 30, 2026
Merged

thothawei merged 1 commit into
mainfrom
claude/chat-idempotency-app

Conversation

@thothawei

Copy link
Copy Markdown
Owner

一句話

聊天送出逾時後,App 現在會用冪等鍵去比對補讀:找到=上一次其實送出了(泡泡出現、不報錯);找不到就留著內容與同一個鍵讓使用者重試——重試因為後端去重而不會多一則。

需要後端一起上dispatch #68(migration 000024)。

為什麼這條不能照抄前幾輪的對帳

第十七輪把逾時對帳整族清完時,唯一剩下的就是這條,而且刻意沒做。理由:其他寫入(接單/取消/協尋/評分)在後端都有唯一狀態,查一次就知道生效沒;訊息沒有——「同內容再送一次」本來就是合法行為,所以 App 無法只靠 afterId 補讀分辨「上一次其實送出了」與「使用者真的想再說一次」。唯一的解法是由客戶端給一個鍵、後端據此去重。

App 端改了什麼

  • RideMessageclientMsgId;兩端 sendMessage 多具名參數 clientMsgId(用 null-aware element 'client_msg_id': ?clientMsgId,沒帶鍵時請求形狀完全不變)
  • ride_chat_screen
    • 這一則的鍵在第一次送出時產生,失敗時留著_pendingClientMsgId)——重試沿用同一個鍵才不會在後端變成兩則;確認落地才清掉,下一則拿新的鍵
    • 逾時(statusCode == null)→ 補讀 afterId 之後的訊息,用鍵比對;找到就當成成功
    • 補讀到的其他訊息(對方同時說的話)一併顯示——既然問了就別浪費
    • 明確拒絕(有狀態碼,如 400 訊息過長)不對帳

驗收

  • flutter analyze 無 issue、flutter test 361 passed(356 +新 5 案)
  • 反向確認兩層:關掉補讀對帳 → 2 案 FAIL(「其實送出了」「對方的話也要顯示」);重試不沿用同一個鍵 → 1 案 FAIL
  • 模擬器實跑(blackhole POST:/api/rides/\d+/messagesws_block,WS 全程被 503 擋):
    1. 代理 log 上游回 200,不交還 App = 後端真的寫入了
    2. 逾時 15 秒後出現對帳那次 GET /api/rides/26/messages
    3. 畫面出現自己的綠色泡泡、輸入框清空、沒有錯誤橫幅
    4. DB ride_messages 只有一則,client_msg_id = customer-1785385960604-0role-毫秒-序號
  • 冪等性在真後端上直接驗過(curl,dev DB 已跑 migration 24):
    • 同一個鍵重送 → 回 id=6,連 created_at 都是原本那筆,DB 沒有新增
    • 不同鍵、同樣內容 → 新的 id=7。「真的想再說一次」照樣成立

上線順序

後端先(App 不帶鍵時後端行為與現在相同),或兩支一起。App 端若先上而後端未部署,client_msg_id 會被舊後端忽略 → 退回「補讀比對不到鍵」=與現在行為相同,不會壞。

🤖 Generated with Claude Code

第十七輪把逾時對帳整族清完時,唯一剩下的就是聊天送出,並刻意沒做:
訊息在後端沒有唯一狀態,「同內容再送一次」本來就合法,所以 App 無法只靠
afterId 補讀分辨「上一次其實送出了」與「使用者真的想再說一次」。
這一輪把那個跨端決策做掉——後端收客戶端產生的冪等鍵(dispatch #68)。

App 端:
- RideMessage 多 clientMsgId;兩端 sendMessage 多具名參數 clientMsgId,
  用 null-aware element 帶欄位(沒帶鍵時請求形狀不變)。
- ride_chat_screen:這一則的鍵在第一次送出時產生,**失敗時留著**——重試沿用
  同一個鍵才不會在後端變成兩則;確認落地才清掉,下一則拿新的鍵。
- 逾時(statusCode == null)→ 補讀 afterId 之後的訊息並用鍵比對:找到=其實
  送出了(泡泡出現、輸入框清空、不報錯);找不到留著錯誤與內容讓他重試。
  補讀到的其他訊息(對方同時說的話)一併顯示。明確拒絕(有狀態碼)不對帳。

驗收:flutter analyze 無 issue、flutter test 361 passed(356 +新 5);
反向確認兩層——關掉補讀對帳 → 2 案 FAIL、重試不沿用同一個鍵 → 1 案 FAIL。
模擬器實跑(blackhole messages + ws_block、WS 全程被擋):代理 log 上游回 200
不交還 App → 逾時後出現對帳那次 GET messages → 綠色泡泡出現、輸入框清空、
沒有錯誤橫幅;DB 只有一則,client_msg_id = customer-1785385960604-0。
真後端 curl 另驗:同鍵重送回既有那筆(連 created_at 都是原本的)、DB 沒新增;
不同鍵的同內容仍是兩則。

需要後端 dispatch #68 一起上(dev DB 已跑 migration 24)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thothawei
thothawei merged commit 96b621b into main Jul 30, 2026
2 checks passed
@thothawei
thothawei deleted the claude/chat-idempotency-app branch July 30, 2026 04:42
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