975b56ca54
後端: - 新增 course_schedules / bookings migration(含索引) - BookingStatus / ScheduleStatus PHP BackedEnum - CourseSchedule / Booking Model(七狀態機 VALID_TRANSITIONS) - ScheduleController、ProviderBookingController、MemberBookingController - 雙層名額驗證(API 層快速失敗 + DB lockForUpdate 防超賣) - 24h 取消截止、pending 不佔位設計 - ExpirePendingBookings(每小時)/ CompleteFinishedBookings(每日)Scheduler - Docker cron 配置、CACHE_STORE 改為 file 修正 502 前端: - 課程詳情頁加入時段選擇與預約流程 - 我的預約頁(展開式卡片、狀態說明、連結課程詳情) - Coach 時段管理(上午/下午時間選擇器、新課程引導) - Coach 預約管理(依課程分組、待確認徽章) - Navbar 新增「我的預約」與「時段/預約管理」入口 - 密碼格式提示與即時比對 OpenSpec: - booking-system change 歸檔至 archive/2026-05-12-booking-system - 新增 specs/course-scheduling 與 specs/booking-lifecycle 主規格 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
8.5 KiB
8.5 KiB
1. 資料庫層
- 1.1 [後端] 建立 Migration
create_course_schedules_table:欄位含diving_offer_id、provider_id、scheduled_date、start_time、max_participants、current_participants、statusstring(非 enum);加索引(diving_offer_id, status, scheduled_date)與(provider_id) - 1.2 [後端] 建立 Migration
create_bookings_table:欄位含schedule_id、member_id、participants、total_price、statusstring(非 enum)、notes;加索引(member_id, status)、(schedule_id, status)、(status, created_at) - 1.3 [後端] 執行 Migration,確認 DB schema 與索引正確(
SHOW INDEX FROM course_schedules)
2. Enum 與 Model 層
- 2.1 [後端] 建立
app/Enums/BookingStatus.php:PHP BackedEnum,七個 case(pending / confirmed / completed / rejected / expired / member_cancelled / provider_cancelled) - 2.2 [後端] 建立
app/Enums/ScheduleStatus.php:PHP BackedEnum,三個 case(open / full / cancelled) - 2.3 [後端] 建立
app/Models/CourseSchedule.php:fillable、casts = ['status' => ScheduleStatus::class]、關聯(belongsTo DivingOffer / belongsTo User as provider、hasMany Booking) - 2.4 [後端] 建立
app/Models/Booking.php:fillable、casts = ['status' => BookingStatus::class]、VALID_TRANSITIONS常數(pending→{confirmed,rejected,expired,member_cancelled};confirmed→{completed,member_cancelled,provider_cancelled};其餘終態→[])、canTransitionTo()驗證方法、關聯(belongsTo CourseSchedule / belongsTo User as member) - 2.5 [後端] 在
DivingOfferModel 新增hasMany CourseSchedule關聯
3. Provider 時段管理 API
- 3.1 [後端] 建立
app/Http/Controllers/API/ScheduleController.php:index、store(含所有權驗證、日期驗證)、update、destroy(DB transaction:時段 → cancelled + 批次 cascade pending/confirmed Booking → provider_cancelled) - 3.2 [後端] 在
routes/api.php新增/provider/schedules路由群組(CRUD) - 3.3 [後端] 在
routes/api.php新增公開路由GET /diving-offers/{id}/schedules;Controller 查詢條件:status = 'open' AND scheduled_date >= CURDATE()ORDER BYscheduled_date ASC, start_time ASC;回傳每筆含remaining_spots = max_participants - current_participants
4. Provider 預約管理 API
- 4.1 [後端] 建立
app/Http/Controllers/API/ProviderBookingController.php:confirm(階段 B 佔位):DB transaction + lockForUpdate 取得 schedule → 重新計算剩餘名額(max - current_participants)→ 不足則 422 → 更新 Booking status=confirmed +increment('current_participants')→ 若達滿則 schedule status=fullreject:Booking status → rejected,不動 current_participants(pending 本來就未佔位)cancel:Booking status → provider_cancelled +decrement('current_participants')+ 若原為 full 則 schedule status 改回 open(僅 confirmed 才需釋放)index:列出自己課程的預約
- 4.2 [後端] 在
routes/api.php新增/provider/bookings路由群組(index / confirm / reject / cancel)
5. Member 預約 API
- 5.1 [後端] 建立
app/Http/Controllers/API/MemberBookingController.php:store(階段 A,pending 不佔位):Layer 1 快速名額檢查(max - current_participants,422 early return)→ DB transaction 內:lockForUpdate 取得 schedule + Layer 2 名額再次驗證 + 重複預約檢查(同一 member_id + schedule_id 是否已有 pending/confirmed)→ 建立 Booking + 價格快照;不 incrementcurrent_participantsdestroy:24h 截止驗證(Carbon datetime 比較)→ 合法則改 member_cancelled;僅 confirmed 狀態需 decrementcurrent_participants+ 若原為 full 改回 open;pending 取消不動人數index、show:一般查詢
- 5.2 [後端] 在
routes/api.php新增/member/bookings路由群組(CRUD)
6. Scheduler 自動任務
- 6.1 [後端] 建立
app/Console/Commands/ExpirePendingBookings.php:查詢status=pending且created_at < now()-48h(利用索引status, created_at),批次更新為expired;執行結尾Log::info("ExpirePendingBookings: {$count} expired") - 6.2 [後端] 建立
app/Console/Commands/CompleteFinishedBookings.php:查詢status=confirmed且 join schedule 的scheduled_date < today(),批次更新為completed;執行結尾Log::info("CompleteFinishedBookings: {$count} completed") - 6.3 [後端] 在
routes/console.php註冊:ExpirePendingBookings每小時(->hourly())、CompleteFinishedBookings每日凌晨(->dailyAt('00:05')) - 6.4 [後端] 在 Docker
cfdive-app容器的 Dockerfile / entrypoint 加入 cron job:* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1;確認 cron daemon 已啟動 - 6.5 [後端] 手動驗證兩支 Command 可獨立執行:
php artisan app:expire-pending-bookings、php artisan app:complete-finished-bookings不報錯,且storage/logs/laravel.log有對應紀錄
7. 前端 API 封裝
- 7.1 [前端] 建立
frontend/src/api/scheduleApi.js:封裝getSchedulesByOffer(offerId) - 7.2 [前端] 建立
frontend/src/api/bookingApi.js(member):getMyBookings()、getBooking(id)、createBooking(payload)、cancelBooking(id) - 7.3 [前端] 建立
frontend/src/api/coachBookingApi.js(provider):getProviderBookings()、confirmBooking(id)、rejectBooking(id)、cancelBooking(id) - 7.4 [前端] 建立
frontend/src/api/coachScheduleApi.js:getSchedules()、createSchedule(payload)、updateSchedule(id, payload)、deleteSchedule(id)
8. Member 前端頁面
- 8.1 [前端] 更新課程詳情頁(
frontend/src/views/CourseDetailView.vue):新增「可用時段」區塊,顯示日期、時間、剩餘名額,含「立即預約」按鈕(呼叫 createBooking) - 8.2 [前端] 新增
frontend/src/views/MyBookingsView.vue:列出 Member 所有預約,顯示狀態 Badge(七種狀態對應不同顏色),含取消按鈕(pending/confirmed) - 8.3 [前端] 在 Member Navbar 加入「我的預約」連結,路由
/my-bookings - 8.4 [前端] 在
frontend/src/router/index.js新增/my-bookings路由(requiresAuth)
9. Coach 前端頁面
- 9.1 [前端] 新增
frontend/src/views/coach/ScheduleManagerView.vue:時段列表(含狀態、剩餘名額)、建立時段表單(日期選擇、時間、人數上限)、取消時段按鈕 - 9.2 [前端] 新增
frontend/src/views/coach/BookingManagerView.vue:預約列表(依課程分組或全部列出)、顯示 Member 姓名、人數、金額、狀態、確認/拒絕/取消按鈕 - 9.3 [前端] 在 Coach Navbar(
CoachNavBar.vue)加入「時段管理」與「預約管理」連結 - 9.4 [前端] 在
frontend/src/router/index.js新增/coach/schedules、/coach/bookings路由(requiresCoach)
10. 整合驗證
- 10.1 [整合測試] 完整流程測試:Provider 建立時段 → Member 預約 → Provider 確認 → 驗證人數扣減
- 10.2 [整合測試] 超賣防護測試:最後一個名額同時送出兩筆預約,驗證只有一筆成功(Layer 2 lockForUpdate 生效)
- 10.3 [整合測試] 取消流程測試:①Member 取消 confirmed 預約 → current_participants 減少、schedule 若原為 full 改回 open;②Member 取消 pending 預約 → current_participants 不變(pending 本來就未佔位)
- 10.4 [整合測試] Scheduler 測試:手動執行
php artisan app:expire-pending-bookings、php artisan app:complete-finished-bookings,驗證狀態正確更新 - 10.5 [整合測試] Cascade 測試:Provider 取消時段後驗證 pending/confirmed Booking 全部變 provider_cancelled;completed/rejected Booking 狀態不變
- 10.6 [整合測試] 取消截止測試:建立一筆課程開始前 12h 的 confirmed 預約,Member 嘗試取消應回傳 422;課程前 36h 的預約應可取消
- 10.7 [整合測試] Participants 雙層驗證測試:超過剩餘名額的預約在 Layer 1 被攔截,回傳 422 且不進入 DB transaction
- 10.8 [整合測試] 重複預約防護測試:Member 對同一時段送出第二筆 pending 預約應回傳 422;第一筆取消後再送出第三筆應成功
- 10.9 [整合測試] 公開 API 過濾測試:
GET /api/diving-offers/{id}/schedules不回傳 status=full、cancelled 及過去日期時段;回傳時段含正確的 remaining_spots