Files
CFDivePlatform/openspec/specs/notification-triggers/spec.md
T
a620906209 03f8caf3e9 feat:實作通知系統 — 站內通知、Email 通知、Polling 機制
後端
- 新增 6 個 Notification class(預約建立/確認/拒絕/取消/完成、收到評價),database + mail 雙 channel
- 新增 NotificationController(list / unread-count / markRead / markAllRead / destroy)
- 整合通知觸發至 MemberBookingController、ProviderBookingController、CompleteFinishedBookings、ReviewController
- 新增 notifications / jobs / failed_jobs migration
- Docker Compose 加入 queue-worker、mailpit service
- DivingOffer 補上 provider() 關聯

前端
- 新增 notificationStore(Polling 30s/60s 自適應 + Page Visibility API)
- 新增 NotificationBell(未讀 Badge)、NotificationDrawer(側邊通知中心)
- main.js:auth store init 前置於 router.use(),修正 beforeEach guard 時序問題
- notificationAxios:依路徑動態選擇 member/coach token
- NotificationDrawer:改用 new URL().pathname 提取 action_url 路徑

OpenSpec
- 歸檔 notification-system change
- 同步 notification-core / notification-email / notification-triggers specs 至主規格
- 更新 booking-lifecycle / review-lifecycle spec(補充通知觸發 requirement)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-17 22:26:14 +08:00

5.7 KiB
Raw Blame History

ADDED Requirements

Requirement: 預約建立觸發通知

系統 SHALL 在預約成功建立(status = pending)時,通知課程所屬 Provider(站內 + Email)。觸發點在 MemberBookingController::store() 的 DB transaction commit 之後。

Scenario: Member 建立預約

  • WHEN MemberBookingController::store() 成功建立預約並回傳 201
  • THEN 取得 $booking->schedule->divingOffer->providerProvider),呼叫 $provider->notify(new BookingCreatedNotification($booking)),以 try/catch 包裹

Requirement: 預約確認觸發通知

系統 SHALL 在 Provider 確認預約(status pendingconfirmed)時,通知 Member(站內 + Email)。觸發點在 ProviderBookingController::confirm() 的 DB transaction commit 之後。

Scenario: Provider 確認預約

  • WHEN ProviderBookingController::confirm() 執行,狀態更新為 confirmed
  • THEN 取得 $booking->member,呼叫 $member->notify(new BookingConfirmedNotification($booking))

Requirement: 預約拒絕觸發通知

系統 SHALL 在 Provider 拒絕預約(status → rejected)時,通知 Member(站內 + Email)。觸發點在 ProviderBookingController::reject()$booking->update() 之後。

Scenario: Provider 拒絕預約

  • WHEN ProviderBookingController::reject() 執行
  • THEN 取得 $booking->member,呼叫 $member->notify(new BookingRejectedNotification($booking))

Requirement: BookingCancelledNotification 文案區分

BookingCancelledNotification SHALL 依建構子參數 cancelledBy: 'member' | 'provider' 產生不同文案:

cancelledBy 通知對象 title body
'member' Provider 學員取消了預約 學員已取消《{課程名稱}》的預約(時段:{日期})
'provider' Member 教練取消了你的預約 教練已取消你的《{課程名稱}》預約(時段:{日期}),如有疑問請聯繫教練

toArray()action_url

  • cancelledBy: 'member'{FRONTEND_URL}/coach/bookings
  • cancelledBy: 'provider'{FRONTEND_URL}/my-bookings/{booking.id}

Scenario: 文案依角色區分

  • WHEN new BookingCancelledNotification($booking, cancelledBy: 'member')toArray() 被呼叫
  • THEN title 為「學員取消了預約」,action_url 指向 /coach/bookings

Scenario: Provider 取消文案

  • WHEN new BookingCancelledNotification($booking, cancelledBy: 'provider')toArray() 被呼叫
  • THEN title 為「教練取消了你的預約」,action_url 指向 /my-bookings/{id}

Requirement: 預約取消觸發通知(Member 發起)

系統 SHALL 在 Member 取消預約(status → member_cancelled)時,通知 Provider(站內 + Email)。觸發點在 MemberBookingController::cancel() 的 DB transaction commit 之後。

Scenario: Member 取消預約

  • WHEN MemberBookingController::cancel() 執行,$booking->update(['status' => BookingStatus::MemberCancelled])
  • THEN 取得 $booking->schedule->divingOffer->providerProvider),呼叫 $provider->notify(new BookingCancelledNotification($booking, cancelledBy: 'member'))

Requirement: 預約取消觸發通知(Provider 發起)

系統 SHALL 在 Provider 取消預約(status → provider_cancelled)時,通知 Member(站內 + Email)。觸發點在 ProviderBookingController::cancel() 的 DB transaction commit 之後。

Scenario: Provider 取消預約

  • WHEN ProviderBookingController::cancel() 執行,$booking->update(['status' => BookingStatus::ProviderCancelled])
  • THEN 取得 $booking->member,呼叫 $member->notify(new BookingCancelledNotification($booking, cancelledBy: 'provider'))

Requirement: 預約完成觸發通知

系統 SHALL 在預約標記為完成(status → completed)時,通知 Member 可前往評價(站內 + Email)。觸發點包含:ProviderBookingController::complete()(手動)與 CompleteFinishedBookings Command(排程自動完成)。

Scenario: 手動完成

  • WHEN ProviderBookingController::complete() 執行
  • THEN 取得 $booking->member,呼叫 $member->notify(new BookingCompletedNotification($booking))

Scenario: 排程自動完成(含 N+1 防護)

  • WHEN CompleteFinishedBookings::handle() 執行
  • THEN 使用 ->with(['member', 'schedule.divingOffer'])->get() 取得 booking 集合(禁止 bulk ->update()),loop 內逐筆 $booking->update(status: Completed) + try/catch notify;單筆 notify 失敗不中斷整個批次

Requirement: 收到評價觸發通知

系統 SHALL 在 Member 成功提交評價後,通知被評價課程的 Provider(僅站內通知,無 Email)。觸發點在 ReviewController::store() 的 DB transaction commit 之後。

取得 Provider 的方式:DivingOffer::with('provider')->findOrFail($offerId)->providerDivingOffer belongsTo User)。

Scenario: Member 提交評價

  • WHEN ReviewController::store() 的 DB transaction 成功,$review 建立完成
  • THEN 取得 $offer->providerProvider),呼叫 $provider->notify(new ReviewReceivedNotification($review))(僅 ['database']

Requirement: 通知觸發為原子操作,不影響主業務

所有 notify 呼叫 SHALL 以 try/catch (\Throwable $e) 包裹,若失敗僅寫入 Laravel log,不得造成主業務回傳錯誤或 rollback。

Scenario: notify 失敗不影響主業務

  • WHEN $user->notify(...) 拋出任何例外
  • THEN 預約/評價主業務資料已正確儲存,HTTP response 正常回傳,\Log::error(...) 記錄錯誤