Skip to content

feat(dispatch): 預約行程+常用地點——修掉停機後會把積壓的過期預約全部派車 - #70

Merged
thothawei merged 7 commits into
mainfrom
claude/scheduled-rides-saved-places
Jul 30, 2026
Merged

feat(dispatch): 預約行程+常用地點——修掉停機後會把積壓的過期預約全部派車#70
thothawei merged 7 commits into
mainfrom
claude/scheduled-rides-saved-places

Conversation

@thothawei

@thothawei thothawei commented Jul 30, 2026

Copy link
Copy Markdown
Owner

W. 兩張新表(migration 000025/000026)+9 支端點+一支到點轉單的背景排程器。
App 端對應 fleet-app #95

為什麼預約是獨立一張表,不是 rides 加一個狀態

rides 的查詢散布在派單池(status='requested')、乘客 active、歷史、報表、admin 訂單列表。
多塞一個「還不該被派單」的狀態進去,得逐一稽核每支查詢有沒有排除它——
漏一支就是「司機看到一張三天後才要出發的單」。獨立表對既有路徑是純新增、零風險。

到點轉單

每 30 秒掃一次,把約定時間前 15 分鐘內的 pending 轉成真訂單。
轉單走的是與手動叫車完全同一支 RideService.CreateByCustomer——派單、審計、車種驗證、
「同時只能有一張進行中訂單」全部自動沿用。另寫一份的話兩條路徑遲早長歪,
而且是預約那條先歪(沒人在旁邊看著)。

機制 防的是什麼
認領帶 attempt_count 樂觀鎖 多副本同時掃到同一筆 → 建出兩張要付錢的訂單
暫時性失敗維持 pending(上限 10 次) 乘客當下還在別的行程上,那是「等下一輪」不是「永久失敗」
永久性失敗立刻判死 座標/車種無效重試幾次都一樣,佔著額度只會讓乘客到約定時間才發現沒車
過期 30 分鐘作廢 停機後重啟會把積壓的過期預約全部派車(見下)

修掉的 bug:停機後會把積壓的過期預約全部派車

FindDue 原本只有上界沒有下界。部署/當機/DB 不可用停了一段時間,重啟時
昨天早上的預約會今天下午開一台車到乘客家樓下,而且他還得付那趟錢。
修法是超過約定時間 30 分鐘就標 failed 並寫原因——標 failed 而不是留在 pending,
是因為留著它會永遠掛在乘客的「即將到來」,而那台車永遠不會來。
先寫 TestDispatcherSkipsLongExpiredSchedules 取證看到它真的被轉單,才動手修。

常用地點

homework 每人各限一筆(部分唯一索引),服務層是 upsert 語意——
「設定住家」該把住家換成新地址,不是回一句「你已經有住家了」。
已達地點數上限時,覆蓋住家仍然成功(覆蓋不增加筆數,擋掉會變成連住家都不能改的死結)。

取消已轉單的回 409+現況

那張真訂單已經在派單池裡,司機可能正在開過來——App 要據此把畫面切成「已為你派車」
並引導去取消訂單,而不是「取消失敗,請稍後再試」(同一個病在 V 那章的 admin 端抓過一次)。

驗收

  • go buildgo vet 乾淨。
  • internal/service 完整套件 172 支全過、0 失敗ok ... 2911.952s,真 PostGIS 容器,
    本機跑了 48 分鐘),其中 17 支是這批新增的;internal/handler 完整套件也綠(136s)。
    ⚠️ CI 那支 build-and-unit-test 只花 1 分鐘,是因為 CI 環境跳過 testcontainers——
    它綠不代表整合測試綠,所以上面那份本機數字才是這批的證據。
  • 另加 scheduled_ride_route_shape_test.go——新路由與既有 /customer/rides/:id 同層混用
    靜態段與參數段,gin 的路由衝突是註冊當下 panic:服務起不來但單元測試照樣全綠。
  • 反向驗證三次,其中兩次推翻了自己的假設:拔掉 FindDue 的時間條件會紅;
    拔掉樂觀鎖 TestDispatcherIsIdempotent 仍然綠(它守的是循序重跑,沒走到認領那行);
    補的併發測試第一版也是假的(兩個 goroutine 沒真重疊,拔掉樂觀鎖連跑三次都綠),
    改成餵兩份 attempt_count=0 的相同快照直接進 dispatchOne 才造得出真重疊。
  • 真環境實跑:塞一筆 5 分鐘後到點的預約,前兩輪被「已有進行中的訂單」擋下並維持 pending,
    結掉擋路訂單後第三輪轉單成功(ride_id=39,起訖點與預約一致、已進派單池)。
    那張訂單也確認顯示在乘客 App 的「車已在路上」區(跨端閉環)。
  • scripts/seed_demo_data.sh 兩條分支(帶/不帶 PSQL_DSN)都實跑過。

🤖 Generated with Claude Code

thothawei and others added 5 commits July 31, 2026 01:25
W. 兩張新表(migration 000025/000026)+9 支端點+一支到點轉單的背景排程器。
App 端對應 fleet-app #95。

## 為什麼預約是獨立一張表,不是 rides 加一個狀態

rides 的查詢散布在派單池(status='requested')、乘客 active、歷史、報表、admin 訂單列表。
多塞一個「還不該被派單」的狀態進去,得逐一稽核每支查詢有沒有排除它——
漏一支就是「司機看到一張三天後才要出發的單」。獨立表對既有路徑是純新增、零風險。

## 到點轉單

每 30 秒掃一次,把約定時間前 15 分鐘內的 pending 轉成真訂單。
**轉單走的是與手動叫車完全同一支 RideService.CreateByCustomer**——派單、審計、
車種驗證、「同時只能有一張進行中訂單」全部自動沿用。另寫一份的話兩條路徑遲早長歪,
而且是預約那條先歪(沒人在旁邊看著)。

認領帶 attempt_count 樂觀鎖:多副本同時掃到同一筆會建出兩張要付錢的訂單。
暫時性失敗(乘客還在別的行程上)維持 pending 等下一輪;
永久性失敗(座標/車種無效)立刻判死,不佔著重試額度撐到約定時間才讓乘客發現沒車。

## 修掉的 bug:停機後會把積壓的過期預約全部派車

FindDue 原本只有上界沒有下界。部署/當機/DB 不可用停了一段時間,重啟時
昨天早上的預約會今天下午開一台車到乘客家樓下,而且他還得付那趟錢。
修法是超過約定時間 30 分鐘就標 failed 並寫原因——標 failed 而不是留在 pending,
是因為留著它會永遠掛在乘客的「即將到來」,而那台車永遠不會來。
先寫 TestDispatcherSkipsLongExpiredSchedules 取證看到它真的被轉單,才動手修。

## 常用地點

home/work 每人各限一筆(部分唯一索引),服務層是 upsert 語意——
「設定住家」該把住家換成新地址,不是回一句「你已經有住家了」。

## 驗收

go build/go vet 乾淨;internal/service 新增 15 支測試(真 PostGIS 容器)全綠。
另加 scheduled_ride_route_shape_test.go——新路由與既有 /customer/rides/:id 同層
混用靜態段與參數段,gin 的路由衝突是註冊當下 panic:服務起不來但單元測試照樣全綠。

反向驗證三次,其中兩次推翻了自己的假設:拔掉 FindDue 的時間條件會紅;
拔掉樂觀鎖 TestDispatcherIsIdempotent 仍然綠(它守的是循序重跑,沒走到認領那行);
補的併發測試第一版也是假的(兩個 goroutine 沒真重疊,拔掉樂觀鎖連跑三次都綠),
改成餵兩份 attempt_count=0 的相同快照直接進 dispatchOne 才造得出真重疊。

真環境實跑:塞一筆 5 分鐘後到點的預約,前兩輪被「已有進行中的訂單」擋下並維持 pending,
結掉擋路訂單後第三輪轉單成功(ride_id=39,起訖點與預約一致、已進派單池)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iso_in 的 BSD date 路徑在 macOS 上實跑過,GNU 那條是照文件寫的、未實測——
寫清楚比讓下一個人以為兩條都驗過好。順手清掉設了沒用到的 CANCEL_TARGET。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
轉單後那就是一張普通訂單,司機看不到約定上車時間。提前量 15 分鐘下多半剛好,
但司機若 5 分鐘就到,乘客可能還沒下樓,而司機不知道該等。

要做的話動的是 rides(帶 scheduled_at 或來源標記)+司機端顯示,
屬功能擴充不是 bug,所以沒有夾帶進這批——寫明條件放進待辦而不是靜靜做掉。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
第二條值得單獨記:本機 go test ./... 跑不完(service 有 172 支容器測試、約一小時),
要驗改動請跑受影響的 package。這一輪等了 90 分鐘才確認它只是慢不是卡。
而 CI 那支只花 1 分鐘是因為跳過 testcontainers——它綠不代表整合測試綠。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
常用地點是覆蓋(插槽語意),但預約會累積——每跑一次多四筆。
實跑兩條分支(帶/不帶 PSQL_DSN)時發現說明只講了前半,補上後半與清除指令。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
thothawei and others added 2 commits July 31, 2026 02:20
兩個上限本來就實作了但沒測到。常用地點那支特別驗一條容易做錯的:
**已達上限時覆蓋住家仍應成功**——覆蓋不增加筆數,
若把它也擋掉會變成「地點滿了就連住家都不能改」的死結。
預約那支則驗「取消一筆就讓出一個名額」——上限算的是還沒轉單的,不是這輩子建過的。

go test 兩支皆 PASS(真 PostGIS 容器)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
172 支全過、0 失敗(ok ... 2911.952s,本機真容器跑了 48 分鐘),其中 17 支是這批新增的。
先前只寫「新增 15 支全綠」,沒有回答「既有的有沒有被改壞」——現在有了。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thothawei
thothawei merged commit 368b565 into main Jul 30, 2026
1 check passed
@thothawei
thothawei deleted the claude/scheduled-rides-saved-places branch July 30, 2026 23:39
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