Files
CFDivePlatform/openspec/changes/archive/2026-05-12-review-system/tasks.md
T
a620906209 81a9f84b26 feat:實作評價系統 — 匿名評價、有幫助投票、手動完成預約
後端:
- 新增 reviews / review_edits / review_votes migration(含索引)
- Review / ReviewEdit / ReviewVote Model
- ReviewController:評價 CRUD、資格驗證(completed booking)、rating 即時重算
- toggleHelpful:Member 限定、GREATEST 原子防負、DB transaction 同步
- AdminReviewController:全量列表、刪除(含重算)
- AdminBookingController:全量列表、手動標記 completed
- ProviderBookingController 新增 complete 方法(教練手動完成預約)
- DevelopmentSeeder:快速重建測試資料(admin/coach/member + offers + bookings)
- EnsureAdmin middleware 正式納入 bootstrap/app.php
- Nginx server_name 加入 cfdive.local

前端:
- 課程詳情頁加入評價區塊(星等分布、排序切換、撰寫/修改/刪除、有幫助 Toggle)
- Coach Portal 新增「課程評價」頁(只讀,依課程分組)
- Coach 預約管理加入「完成」按鈕
- Admin 新增「預約管理」頁(標記完成)、「評價管理」頁(刪除)
- Admin / Coach Navbar 新增對應連結

OpenSpec:
- review-system change 歸檔至 archive/2026-05-12-review-system
- 新增 specs/review-lifecycle 與 specs/review-voting 主規格
- review-voting spec 補充 Member 限定與 GREATEST 原子更新說明

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 02:46:54 +08:00

74 lines
5.9 KiB
Markdown
Raw 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.
## 1. 資料庫層
- [x] 1.1 [後端] 建立 Migration `create_reviews_table`:欄位含 `diving_offer_id``member_id``rating` (tinyint)、`comment` (text)、`helpful_count` (int)、`is_edited` (boolean)UNIQUE(member_id, diving_offer_id);加索引 `(diving_offer_id, helpful_count)``(diving_offer_id, rating)``(diving_offer_id, created_at)`
- [x] 1.2 [後端] 建立 Migration `create_review_edits_table`:欄位含 `review_id` (UNIQUE FK)、`old_rating``old_comment``edited_at`
- [x] 1.3 [後端] 建立 Migration `create_review_votes_table`:欄位含 `review_id``member_id`UNIQUE(review_id, member_id)
- [x] 1.4 [後端] 執行 Migration,確認三張資料表與索引正確
## 2. Model 層
- [x] 2.1 [後端] 建立 `app/Models/Review.php`fillable、casts、關聯(belongsTo DivingOffer / belongsTo User as member、hasOne ReviewEdit、hasMany ReviewVote
- [x] 2.2 [後端] 建立 `app/Models/ReviewEdit.php`fillable、belongsTo Review
- [x] 2.3 [後端] 建立 `app/Models/ReviewVote.php`fillable、belongsTo Review / belongsTo User as member
- [x] 2.4 [後端] 在 `DivingOffer` Model 新增 `hasMany Review` 關聯
## 3. Member 評價 API
- [x] 3.1 [後端] 建立 `app/Http/Controllers/API/ReviewController.php`
- 私有方法 `recalculateOfferRating(int $offerId)`:重算 AVG(rating) 與 COUNT(*),並 UPDATE diving_offers**必須在 DB transaction 內被呼叫**
- `store`:資格驗證(`bookings JOIN course_schedules WHERE status=completed AND diving_offer_id=X`,否則 403)→ 重複評價檢查(422)→ DB transaction 建立 Review + 呼叫 recalculate
- `update`:所有權驗證(他人 403)→ DB transactionupdateOrCreate review_edits(覆蓋舊版)→ 更新 Review 內容 + is_edited=true → 呼叫 recalculate
- `destroy`:所有權驗證(他人 403)→ DB transaction:刪除 Reviewcascade edits/votes)→ 呼叫 recalculate
- [x] 3.2 [後端] 在 `routes/api.php` 新增 `/member/reviews` 路由群組(POST / PUT /{id} / DELETE /{id}
## 4. 有幫助投票 API
- [x] 4.1 [後端] 在 `ReviewController` 新增 `toggleHelpful` 方法:不可投自己(422)→ **整個 toggle 在 DB transaction 內**:查 ReviewVote → 有則 delete + `DB::raw('GREATEST(helpful_count - 1, 0)')` 原子更新(禁止兩段式 decrement+check);無則 create + increment
- [x] 4.2 [後端] 在 `routes/api.php` 新增 `POST /reviews/{id}/helpful` 路由(auth:sanctum
## 5. 公開評價列表 API
- [x] 5.1 [後端] 在 `ReviewController` 新增 `publicList` 方法:
- 回傳 `summary`AVG、COUNT、1–5 星分布):分布用 `GROUP BY rating COUNT(*)` 動態查詢並補齊 key 1–5(含零值),不另存欄位
- 依 sort 參數排序:helpful→`(helpful_count DESC, created_at DESC)`rating→`(rating DESC, created_at DESC)`newest→`(created_at DESC)`
- 批次查詢 has_voted(已登入:`ReviewVote::whereIn('review_id', ...)->pluck('review_id')`;未登入:全 false
- is_mine:已登入才加此欄位(未登入省略)
- reviewer_name 固定為「匿名潛水者」
- [x] 5.2 [後端] 在 `routes/api.php` 新增 `GET /diving-offers/{id}/reviews` 公開路由
## 6. Admin 評價管理 API
- [x] 6.1 [後端] 建立 `app/Http/Controllers/API/AdminReviewController.php`
- `index`:全量列出(`created_at DESC`)含課程名、member email、rating、comment 前 50 字
- `destroy`DB transaction 刪除 Reviewcascade)→ 呼叫 `recalculateOfferRating`(**Admin 刪除也必須重算**,與 Member destroy 共用同一邏輯)
- [x] 6.2 [後端] 在 `routes/api.php` Admin 群組新增 `/admin/reviews` 路由(GET / DELETE /{id}
## 7. 前端 API 封裝
- [x] 7.1 [前端] 建立 `frontend/src/api/reviewApi.js``getReviews(offerId, sort)``createReview(payload)``updateReview(id, payload)``deleteReview(id)``toggleHelpful(reviewId)`
## 8. 課程詳情頁評價區塊
- [x] 8.1 [前端] 更新 `frontend/src/views/CourseDetailView.vue`:新增評價區塊,顯示整體星等、評分分布條、排序切換按鈕(最多幫助 / 最高分 / 最新)
- [x] 8.2 [前端] 評價列表元件:顯示星等、「匿名潛水者」、日期、「已修改」標記、「有幫助 N 人」按鈕(登入後可點擊 Toggle)
- [x] 8.3 [前端] 評價表單:已登入 Member 且有 completed booking 才顯示;已評過則顯示「我的評價」含修改/刪除按鈕
## 9. Admin 評價管理頁
- [x] 9.1 [前端] 新增 `frontend/src/views/admin/ReviewsView.vue`:列出所有評價(課程名、內容、星等、刪除按鈕)
- [x] 9.2 [前端] 在 Admin Navbar 加入「評價管理」連結,路由 `/admin/reviews`
- [x] 9.3 [前端] 在 `frontend/src/router/index.js` 新增 `/admin/reviews` 路由(requiresAdmin
## 10. 整合驗證
- [x] 10.1 [整合測試] 完整流程:Member 完成課程 → 新增評價 → 確認 diving_offers.rating / reviews 更新
- [x] 10.2 [整合測試] 修改評價:確認 is_edited=true、review_edits 有舊版、rating 重算正確
- [x] 10.3 [整合測試] 刪除評價:rating/reviews 歸零或重算正確
- [x] 10.4 [整合測試] 投票 Toggle:連點兩次確認 helpful_count 正確增減、第三次確認不低於 0
- [x] 10.5 [整合測試] 不可投自己:Member 對自己評價投票應回傳 422
- [x] 10.6 [整合測試] 匿名確認:API 回傳的 reviewer_name 一律為「匿名潛水者」,不含真實姓名
- [x] 10.7 [整合測試] 排序確認:三種 sort 參數回傳順序正確
- [x] 10.8 [整合測試] Admin 刪除重算:Admin 刪除評價後確認 diving_offers.rating / reviews 同步更新
- [x] 10.9 [整合測試] is_mine / has_voted 欄位規則:未登入不含 is_mine 欄位;登入後自己的評價 is_mine=truehas_voted 正確反映投票狀態
- [x] 10.10 [整合測試] 資格驗證:無 completed booking 的 Member 嘗試評價應回傳 403;有 completed booking 才能成功