feat(dispatch): 訊息送出的冪等鍵——App 逾時後才有辦法安全重試 - #68
Merged
Conversation
App 端聊天送出逾時後,「後端其實收到了、只是回應遺失」與「後端沒收到」在客戶端 看起來一樣。其他寫入路徑(接單/取消/協尋/評分)都能靠「查一次後端狀態」對帳, 因為那些狀態在後端是唯一的;訊息不是——「同內容再送一次」本來就是合法行為, 所以 App 無法只靠補讀分辨「上一次其實送出了」與「使用者真的想再說一次」。 由客戶端產生一個鍵、後端據此去重,是唯一能兩者兼顧的做法。 - migration 000024:ride_messages 加 client_msg_id VARCHAR(64) + partial unique index (ride_id, sender_role, sender_id, client_msg_id) WHERE NOT NULL。 去重範圍刻意是「同趟同發話者」而非全表:鍵由客戶端產生,跨使用者撞號不該 讓後方那位發不出訊息。既有訊息與非 App 來源(LINE webhook)維持 NULL、不受約束。 - ChatService.SendWithClientID:帶鍵時先查既有那筆 → 有就直接回傳, **不重複寫入也不重複推播**(WS 與 App 推播都不再發第二次,否則對方會看到同一句話兩次)。 查鍵排在授權之後——否則外人拿鍵去試就能反推出那趟有沒有這則訊息。 Create 撞唯一索引時(併發重送)改查既有那筆回傳,不把 DB 錯誤丟回 App。 - Send 保留舊簽名並委派,既有呼叫端與 LINE 來源不受影響。 - WS payload 帶上 client_msg_id,發話者其他裝置的回聲認得出「這是我剛送的那則」。 - handler 收 client_msg_id,過長回 400(ErrClientMsgIDTooLong)。 驗收:go build/go vet 乾淨、gofmt 無待整理;新增 6 案整合測試(testcontainers)本機 全綠;CI 的純單元測試集(含 handler 包)照過。 反向確認分兩層做:只關掉服務層預查 → 測試仍過(DB 唯一索引 + Create 後的 fallback 接手);預查與 fallback 都關掉 → 第一案 FAIL 於 `duplicate key value violates unique constraint "uq_ride_messages_client_msg_id"`—— 證明測試有效,也證明索引真的建起來了。 App 端(line-fleet-app)會在另一支 PR 帶上這個鍵。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
為什麼需要這個
App 聊天送出逾時後,「後端其實收到了、只是回應遺失」與「後端沒收到」在客戶端看起來完全一樣。
其他寫入路徑(接單/取消/協尋/評分)都能靠「查一次後端狀態」對帳,因為那些狀態在後端是唯一的。訊息不是——「同內容再送一次」本來就是合法行為,所以 App 無法只靠
afterId補讀分辨「上一次其實送出了」與「使用者真的想再說一次」。由客戶端產生一個鍵、後端據此去重,是唯一能兩者兼顧的做法。(來源:line-fleet-app 第十七輪把逾時對帳整族清完後,唯一剩下的就是這條,當時就記著「要嘛後端加冪等鍵,要嘛接受重複訊息」。)
改了什麼
ride_messages加client_msg_id VARCHAR(64)+ partial unique index(ride_id, sender_role, sender_id, client_msg_id) WHERE client_msg_id IS NOT NULL。ChatService.SendWithClientID:帶鍵時先查既有那筆 → 有就直接回傳,不重複寫入也不重複推播(WS 與 App 推播都不再發第二次,否則對方會看到同一句話兩次)。Create撞唯一索引時(併發重送)改查既有那筆回傳,不把 DB 錯誤丟回 App(App 會顯示成「送出失敗」,而訊息其實已經在了)。Send保留舊簽名並委派過去,既有呼叫端與 LINE 來源不受影響。client_msg_id,發話者其他裝置的回聲認得出「這是我剛送的那則」。client_msg_id,過長回 400。驗收
go build/go vet乾淨、gofmt -l無待整理./internal/handler/)照過Create後的 fallback 接手了duplicate key value violates unique constraint "uq_ride_messages_client_msg_id"部署注意
需要跑 migration(
server migrate)。App 端不帶鍵時行為與現在完全相同,所以後端可以先上。🤖 Generated with Claude Code