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>
5.3 KiB
5.3 KiB
Requirement: Member 送出預約
Member SHALL 能選擇一個開放時段送出預約,系統記錄價格快照。pending 狀態不佔用時段名額。
Scenario: 成功建立預約
- WHEN 已登入 Member 送出
POST /api/member/bookings,指定schedule_id與participants(≥1) - THEN 系統建立 Booking,status 為
pending,total_price快照為diving_offer.price × participants,回傳 201
Scenario: 時段已滿無法預約
- WHEN 指定時段 status 為
full或cancelled - THEN 系統回傳 422,告知時段不可用
Scenario: 超過剩餘名額(API 層快速驗證)
- WHEN
participants大於時段當前剩餘名額(max_participants - current_participants),在進入 DB transaction 前 - THEN 系統回傳 422,告知人數超過上限,不進入 lockForUpdate 流程
Scenario: 超過剩餘名額(DB 層二次驗證)
- WHEN API 層通過但 lockForUpdate 後重新計算剩餘名額仍不足(race condition 情境)
- THEN 系統 rollback transaction,回傳 422,告知名額不足
Scenario: 不可重複預約同一時段
- WHEN Member 對同一
schedule_id已有pending或confirmed狀態的 Booking - THEN 系統回傳 422,告知已有預約(取消後可重新預約)
Requirement: 預約狀態機
系統 SHALL 維護七個合法狀態,且只允許以下轉換:
pending→confirmed(Provider 確認)pending→rejected(Provider 拒絕)pending→member_cancelled(Member 取消)pending→expired(Scheduler 超時)confirmed→completed(Scheduler 課程後自動)confirmed→member_cancelled(Member 取消)confirmed→provider_cancelled(Provider 取消)
Scenario: 非法狀態轉換被拒絕
- WHEN 任何角色嘗試執行上述以外的狀態轉換
- THEN 系統回傳 422,說明當前狀態不允許此操作
Requirement: Provider 確認或拒絕預約
Provider SHALL 能對自己課程的 pending 預約執行確認或拒絕。確認時才真正佔用時段名額。
Scenario: 確認預約
- WHEN Provider 送出
PUT /api/provider/bookings/{id}/confirm - THEN Booking status 改為
confirmed,時段current_participants增加對應人數
Scenario: 拒絕預約
- WHEN Provider 送出
PUT /api/provider/bookings/{id}/reject - THEN Booking status 改為
rejected,current_participants不變(pending 未佔位)
Scenario: 只能操作自己課程的預約
- WHEN Provider 嘗試操作不屬於自己課程的 Booking
- THEN 系統回傳 403 Forbidden
Requirement: Provider 取消已確認預約
Provider SHALL 能取消 confirmed 狀態的預約(例如天氣因素)。
Scenario: Provider 取消確認中預約
- WHEN Provider 送出
PUT /api/provider/bookings/{id}/cancel - THEN Booking status 改為
provider_cancelled,時段名額釋放
Requirement: Member 取消預約
Member SHALL 能取消自己的 pending 或 confirmed 預約,但須在課程開始前 24 小時之前提出。
Scenario: 取消 pending 預約(期限內)
- WHEN Member 送出
DELETE /api/member/bookings/{id},Booking status 為pending,且當前時間早於scheduled_date + start_time - 24h - THEN Booking status 改為
member_cancelled,current_participants不變
Scenario: 取消 confirmed 預約(期限內)
- WHEN Member 送出
DELETE /api/member/bookings/{id},Booking status 為confirmed,且當前時間早於scheduled_date + start_time - 24h - THEN Booking status 改為
member_cancelled,時段名額釋放
Scenario: 課程開始前 24h 內不可取消
- WHEN Member 送出
DELETE /api/member/bookings/{id},但當前時間距scheduled_date + start_time不足 24 小時 - THEN 系統回傳 422,告知「距課程開始不足 24 小時,無法取消,請聯繫教練」;Booking 狀態不變
Scenario: 不可取消已終態預約
- WHEN Booking status 為
completed、rejected、expired、provider_cancelled - THEN 系統回傳 422,告知無法取消
Requirement: 系統自動過期 pending 預約
Scheduler SHALL 每小時掃描 pending 超過 48 小時的 Booking 並標記為 expired。
Scenario: 過期觸發
- WHEN Booking status 為
pending且created_at早於 48 小時前 - THEN Scheduler 將 status 改為
expired,current_participants不變(pending 未佔位)
Requirement: 系統自動完成 confirmed 預約
Scheduler SHALL 每日掃描課程日期已過的 confirmed Booking 並標記為 completed。
Scenario: 自動完成
- WHEN Booking status 為
confirmed,對應course_schedule.scheduled_date早於今天 - THEN Scheduler 將 status 改為
completed
Requirement: Member 查看自己的預約列表
Member SHALL 能查詢自己所有預約的列表及詳情,含課程連結與完整課程資訊。
Scenario: 取得預約列表
- WHEN 已登入 Member 送出
GET /api/member/bookings - THEN 系統回傳該 Member 所有 Booking,含 offer_id、課程名稱、地點、時段日期、狀態、金額
Scenario: 取得單一預約詳情
- WHEN 已登入 Member 送出
GET /api/member/bookings/{id} - THEN 系統回傳該 Booking 詳情;若非本人預約則回傳 403