Files
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

98 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### Requirement: Member 新增評價
已完成特定課程的 Member SHALL 能對該課程留下一次評價(星等 + 文字)。
#### Scenario: 成功新增評價
- **WHEN** 已登入 Member 送出 `POST /api/member/reviews`,包含 `diving_offer_id``rating`15 整數)、`comment`(非空字串),且系統查詢 `bookings JOIN course_schedules` 找到至少一筆 `member_id = X AND diving_offer_id = Y AND status = 'completed'`
- **THEN** 系統建立 Review,回傳 201
#### Scenario: 未完成課程不可評價(資格驗證)
- **WHEN** Member 送出評價,但 `bookings` 中不存在任何 `status = 'completed'` 且對應 `diving_offer_id` 的紀錄
- **THEN** 系統回傳 **403**,message:「須完成此課程後才能評價」
#### Scenario: 每門課只能評一次
- **WHEN** `reviews` 中已存在同一 `member_id` + `diving_offer_id` 的紀錄
- **THEN** 系統回傳 **422**(非 409),message:「已評價,如需修改請使用編輯功能」
#### Scenario: 星等範圍驗證
- **WHEN** `rating` 不在 15 之間
- **THEN** 系統回傳 422
### Requirement: 評價後即時更新課程統計
Member 新增、修改或刪除評價時,系統 SHALL 在同一 DB transaction 內重算 `diving_offers.rating``reviews`。Provider 或 Admin 手動標記 booking 為 completed 亦同樣觸發評價資格。
#### Scenario: 新增評價後重算
- **WHEN** Review 建立成功
- **THEN** `diving_offers.rating = ROUND(AVG(rating), 1)``diving_offers.reviews = COUNT(*)` 即時更新
#### Scenario: 刪除評價後重算
- **WHEN** Review 被 Member 或 Admin 刪除
- **THEN** `rating``reviews` 在同一 transaction 內重算;若剩餘 0 筆評價,`rating = 0``reviews = 0`
### Requirement: Member 修改評價
Member SHALL 能修改自己的評價,系統保留最近一次修改前的版本並標記已修改。
#### Scenario: 成功修改評價
- **WHEN** Member 送出 `PUT /api/member/reviews/{id}`,包含新的 `rating``comment`
- **THEN** 系統將舊版 `rating` / `comment` 寫入 `review_edits`(若已存在則覆蓋);更新 Review 內容;將 `is_edited = true`;重算課程統計
#### Scenario: 只能修改自己的評價
- **WHEN** Member 嘗試修改他人的評價
- **THEN** 系統回傳 403
### Requirement: Member 刪除評價
Member SHALL 能刪除自己的評價,Admin SHALL 能刪除任何評價。
#### Scenario: Member 刪除自己的評價
- **WHEN** Member 送出 `DELETE /api/member/reviews/{id}`
- **THEN** 系統刪除 Review 及對應的 review_edits / review_votes;重算課程統計
#### Scenario: Admin 刪除任意評價
- **WHEN** Admin 送出 `DELETE /api/admin/reviews/{id}`
- **THEN** 系統刪除 Review 及關聯資料;重算課程統計
#### Scenario: 只能刪除自己的評價(非 Admin)
- **WHEN** 非 Admin Member 嘗試刪除他人評價
- **THEN** 系統回傳 403
### Requirement: 評價公開顯示(匿名)
任何人(含未登入)SHALL 能查看課程評價列表,評價人統一顯示為「匿名潛水者」。Provider 在 Coach Portal 亦可查看自己課程的評價(只讀)。
#### Scenario: 取得評價列表(含 summary
- **WHEN** 任何人送出 `GET /api/diving-offers/{id}/reviews?sort=helpful|rating|newest`
- **THEN** 系統回傳 `summary`(平均星等、總數、15 星分布)與 `reviews` 列表;`reviewer_name` 一律為「匿名潛水者」;已登入 Member 額外回傳 `is_mine`;未登入 `has_voted` 固定為 `false``is_mine` 欄位省略
#### Scenario: 三種排序
- **WHEN** `sort=helpful`(預設)
- **THEN** 依 `helpful_count DESC, created_at DESC` 排序
- **WHEN** `sort=rating`
- **THEN** 依 `rating DESC, created_at DESC` 排序
- **WHEN** `sort=newest`
- **THEN** 依 `created_at DESC` 排序
### Requirement: 課程完成標記(評價資格觸發)
Provider 或 Admin SHALL 能手動將 confirmed 預約標記為 completed,讓 Member 可立即評價,不需等待排程。
#### Scenario: Provider 手動完成
- **WHEN** Provider 送出 `PUT /api/provider/bookings/{id}/complete`Booking status 為 `confirmed`
- **THEN** Booking status 改為 `completed`Member 即可對該課程送出評價
#### Scenario: Admin 手動完成
- **WHEN** Admin 送出 `PUT /api/admin/bookings/{id}/complete`Booking status 為 `confirmed`
- **THEN** Booking status 改為 `completed`
---
### Requirement: 評價建立後觸發 Provider 通知
評價系統 SHALL 在 Member 成功建立評價後,通知課程所屬 Provider(僅站內通知,不寄 Email)。`ReviewService::create()` MUST 在評價資料儲存成功後觸發通知,以 try/catch 包裹確保主業務不受影響。
#### Scenario: 評價成功送出
- **WHEN** `ReviewService::create()` 建立新評價,`reviews` 資料表寫入成功
- **THEN** `$provider->notify(new ReviewReceivedNotification($review))` 被呼叫,Provider 站內通知新增一筆
#### Scenario: 通知失敗不影響評價建立
- **WHEN** notify 呼叫失敗(例:DB 寫入通知失敗)
- **THEN** 評價資料已正確儲存,HTTP response 成功回傳,錯誤記錄至 log