diff --git a/README.md b/README.md index 40d5fe5..3a9f240 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,49 @@ -![image](https://github.com/user-attachments/assets/332bf381-8faa-441f-8843-bff2c1fbde7a) -![image](https://github.com/user-attachments/assets/f0a765f5-5386-467e-80b8-4a5ef8ef0bcf) -![image](https://github.com/user-attachments/assets/5d6ad6ee-f100-4d45-9200-d61ce76ca7c4) +# CFDive Platform +潛水課程媒合平台 — 連結潛水教練與學員,提供課程瀏覽、線上預約、評價與通知等完整服務。 + +--- + +## 功能概覽 + +**會員(Member)** +- 註冊 / 登入(Email + Google OAuth) +- 瀏覽、搜尋、篩選潛水課程 +- 查看課程時段並送出預約 +- 對完成的課程留下評價(支援匿名、有幫助投票) +- 站內通知(Email + Polling) + +**教練(Provider)** +- 課程 CRUD(含封面 + 相簿圖片上傳) +- 課程時段管理 +- 預約管理(確認 / 拒絕 / 完成 / 取消) + +**管理員(Admin)** +- 平台統計數據(會員數、教練數、課程數) +- 會員與教練帳號管理(啟用 / 停用 / 審核) +- 課程、預約、評價管理 + +--- + +## 技術棧 + +| 層級 | 技術 | +|------|------| +| 後端 | PHP 8.x / Laravel 11 | +| 前端 | Vue 3 | +| 資料庫 | MySQL 8.0 | +| 快取 | Redis(predis)| +| 認證 | Laravel Sanctum + Google OAuth | +| 容器 | Docker / Docker Compose | +| API 文件 | Swagger UI(l5-swagger)| + +--- + +## API 文件 + +共 73 個端點,涵蓋: +- 認證(Email + Google OAuth) +- 公開課程查詢 +- 會員預約 / 評價 / 通知 +- 教練課程 / 時段 / 預約管理 +- 管理員後台 diff --git a/app/Docs/AdminApiDoc.php b/app/Docs/AdminApiDoc.php new file mode 100644 index 0000000..69ceeb4 --- /dev/null +++ b/app/Docs/AdminApiDoc.php @@ -0,0 +1,504 @@ +flush()` 清除所有課程列表快取,下次請求重新查詢 + +--- + +### Requirement: 評價分布快取 + +`GET /api/diving-offers/{id}/reviews` 的 `distribution`(1–5 星分布統計)SHALL 獨立快取,TTL 10 分鐘,Cache key 為 `offer_review_distribution_{id}`。 + +#### Scenario: 分布統計命中快取 + +- **WHEN** 同一課程 reviews 端點在 10 分鐘內再次請求 +- **THEN** `distribution` 欄位從 Redis 取得,不執行 `GROUP BY` SQL + +#### Scenario: 新增/修改/刪除評價時清除分布快取 + +- **WHEN** Member 成功新增、修改或刪除某課程的評價 +- **THEN** 系統呼叫 `Cache::forget("offer_review_distribution_{offerId}")` 清除對應快取 diff --git a/openspec/specs/db-index-optimization/spec.md b/openspec/specs/db-index-optimization/spec.md new file mode 100644 index 0000000..01f72e9 --- /dev/null +++ b/openspec/specs/db-index-optimization/spec.md @@ -0,0 +1,29 @@ +### Requirement: Notifications 表補複合索引 + +`notifications` 表 SHALL 新增 `[notifiable_type, notifiable_id, read_at]` 複合索引,以加速 `unreadNotifications()` 查詢。 + +#### Scenario: Migration 執行成功 + +- **WHEN** 執行 `php artisan migrate` +- **THEN** `notifications` 表上存在 `notifications_notifiable_read_at_index` 複合索引,`EXPLAIN` 結果不再為 full table scan + +#### Scenario: 未讀通知查詢走索引 + +- **WHEN** 系統執行 `Notification::where('notifiable_type', User::class)->where('notifiable_id', $userId)->whereNull('read_at')->get()` +- **THEN** MySQL `EXPLAIN` 顯示使用複合索引,`type` 為 `ref` 而非 `ALL` + +--- + +### Requirement: DivingOffers 表補 provider_id 索引 + +`diving_offers` 表 SHALL 新增 `provider_id` 單欄索引,以加速 Provider 課程列表查詢。 + +#### Scenario: Migration 執行成功 + +- **WHEN** 執行 `php artisan migrate` +- **THEN** `diving_offers` 表上存在 `provider_id` 索引 + +#### Scenario: Provider 課程列表查詢走索引 + +- **WHEN** 系統執行 `DivingOffer::where('provider_id', $providerId)->get()` +- **THEN** MySQL `EXPLAIN` 顯示使用 `provider_id` 索引,`type` 為 `ref` diff --git a/openspec/specs/review-lifecycle/spec.md b/openspec/specs/review-lifecycle/spec.md index e5f23fc..a0d4e15 100644 --- a/openspec/specs/review-lifecycle/spec.md +++ b/openspec/specs/review-lifecycle/spec.md @@ -55,11 +55,15 @@ Member SHALL 能刪除自己的評價,Admin SHALL 能刪除任何評價。 - **THEN** 系統回傳 403 ### Requirement: 評價公開顯示(匿名) -任何人(含未登入)SHALL 能查看課程評價列表,評價人統一顯示為「匿名潛水者」。Provider 在 Coach Portal 亦可查看自己課程的評價(只讀)。 +任何人(含未登入)SHALL 能查看課程評價列表,評價人統一顯示為「匿名潛水者」。Provider 在 Coach Portal 亦可查看自己課程的評價(只讀)。評價列表 SHALL 支援分頁,預設每頁 20 筆,最大 50 筆。 -#### Scenario: 取得評價列表(含 summary) -- **WHEN** 任何人送出 `GET /api/diving-offers/{id}/reviews?sort=helpful|rating|newest` -- **THEN** 系統回傳 `summary`(平均星等、總數、1–5 星分布)與 `reviews` 列表;`reviewer_name` 一律為「匿名潛水者」;已登入 Member 額外回傳 `is_mine`;未登入 `has_voted` 固定為 `false`、`is_mine` 欄位省略 +#### Scenario: 取得評價列表(含 summary,含分頁) +- **WHEN** 任何人送出 `GET /api/diving-offers/{id}/reviews?sort=helpful|rating|newest&page=1&per_page=20` +- **THEN** 系統回傳 `summary`(平均星等、總數、1–5 星分布)與分頁後的 `reviews` 列表;`reviewer_name` 一律為「匿名潛水者」;已登入 Member 額外回傳 `is_mine`;未登入 `has_voted` 固定為 `false`、`is_mine` 欄位省略;回傳包含分頁 meta(`current_page`、`last_page`、`per_page`、`total`) + +#### Scenario: per_page 超出上限時截斷 +- **WHEN** 請求帶有 `per_page=200` +- **THEN** 系統以 `per_page=50` 處理,不回傳錯誤 #### Scenario: 三種排序 - **WHEN** `sort=helpful`(預設) @@ -69,6 +73,10 @@ Member SHALL 能刪除自己的評價,Admin SHALL 能刪除任何評價。 - **WHEN** `sort=newest` - **THEN** 依 `created_at DESC` 排序 +#### Scenario: votes 透過 eager loading 查詢 +- **WHEN** 已登入 Member 送出評價列表請求 +- **THEN** `has_voted` 欄位透過 eager loaded `votes` collection 判斷,不額外發 SQL 查詢 + ### Requirement: 課程完成標記(評價資格觸發) Provider 或 Admin SHALL 能手動將 confirmed 預約標記為 completed,讓 Member 可立即評價,不需等待排程。 diff --git a/openspec/specs/swagger-admin-api/spec.md b/openspec/specs/swagger-admin-api/spec.md new file mode 100644 index 0000000..c5b68e3 --- /dev/null +++ b/openspec/specs/swagger-admin-api/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Admin 端點 Swagger 文件 + +`app/Docs/AdminApiDoc.php` SHALL 文件化所有需要 Admin Bearer token 的管理端點。 + +#### Scenario: Admin stats 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** `GET /admin/stats` 端點有文件,response 包含 `total_members`、`total_providers`、`total_offers`;403 非 admin 亦有定義 + +#### Scenario: Admin 會員管理端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件,標示 `security: bearerAuth`: + - `GET /admin/members`(分頁 response,含 member_profile) + - `GET /admin/members/{id}` + - `PUT /admin/members/{id}/toggle-active`(response: is_active 新狀態) + - `GET /admin/check-member/{id}` + +#### Scenario: Admin 教練管理端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /admin/providers`(分頁 response,含 provider_profile) + - `GET /admin/providers/{id}` + - `PUT /admin/providers/{id}/toggle-active` + - `PUT /admin/providers/{id}/toggle-verified`(response: is_verified 新狀態) + - `GET /admin/check-provider/{id}` + +#### Scenario: Admin 課程、預約、評價管理端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /admin/offers`(分頁 response) + - `DELETE /admin/offers/{id}` + - `GET /admin/bookings`(分頁 response) + - `PUT /admin/bookings/{id}/complete` + - `GET /admin/reviews`(分頁 response,含 per_page 參數,最大 100) + - `DELETE /admin/reviews/{id}` diff --git a/openspec/specs/swagger-auth-supplement/spec.md b/openspec/specs/swagger-auth-supplement/spec.md new file mode 100644 index 0000000..76771d8 --- /dev/null +++ b/openspec/specs/swagger-auth-supplement/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: Auth 補全端點 Swagger 文件 + +`app/Docs/AuthSupplementDoc.php` SHALL 補全未文件化的 Auth 相關端點(Google OAuth、change-password)。 + +#### Scenario: Google OAuth 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /auth/google/redirect`(response: redirect_url,說明用途為取得 Google OAuth redirect URL) + - `GET /auth/google/callback`(response: token + user,說明此為 OAuth callback,通常由瀏覽器自動呼叫) + +#### Scenario: change-password 端點文件化(三個 role) + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件,各自標示對應 role 的 `security: bearerAuth`: + - `PUT /member/change-password`(request: current_password、password、password_confirmation) + - `PUT /provider/change-password`(同上) + - `PUT /admin/change-password`(同上) + - 422 驗證失敗 response 亦有定義 diff --git a/openspec/specs/swagger-member-api/spec.md b/openspec/specs/swagger-member-api/spec.md new file mode 100644 index 0000000..be9acb0 --- /dev/null +++ b/openspec/specs/swagger-member-api/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Member 端點 Swagger 文件 + +`app/Docs/MemberApiDoc.php` SHALL 文件化所有需要 Member Bearer token 的端點(bookings、reviews、helpful 投票、notifications)。 + +#### Scenario: Member bookings 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件,並標示 `security: bearerAuth`: + - `POST /member/bookings`(request: schedule_id;response 201: Booking) + - `GET /member/bookings`(response: Booking 陣列 + 分頁 meta) + - `GET /member/bookings/{id}`(response: Booking 詳情) + - `DELETE /member/bookings/{id}`(response 200: message) + +#### Scenario: Member reviews 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `POST /member/reviews`(request: diving_offer_id、rating、comment;403 資格驗證失敗;422 重複評價) + - `PUT /member/reviews/{id}`(request: rating?、comment?;403 非本人) + - `DELETE /member/reviews/{id}`(403 非本人) + - `POST /reviews/{id}/helpful`(toggle,response: helpful_count、has_voted) + +#### Scenario: Member notifications 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /notifications`(response: 通知陣列 + 分頁 meta) + - `GET /notifications/unread-count`(response: count) + - `PATCH /notifications/{id}/read` + - `PATCH /notifications/read-all` + - `DELETE /notifications/{id}` diff --git a/openspec/specs/swagger-provider-api/spec.md b/openspec/specs/swagger-provider-api/spec.md new file mode 100644 index 0000000..ca52bb9 --- /dev/null +++ b/openspec/specs/swagger-provider-api/spec.md @@ -0,0 +1,43 @@ +## ADDED Requirements + +### Requirement: Provider 端點 Swagger 文件 + +`app/Docs/ProviderApiDoc.php` SHALL 文件化所有需要 Provider Bearer token 的端點(offers CRUD、圖片、schedules、bookings)。 + +#### Scenario: Provider offers CRUD 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件,標示 `security: bearerAuth`: + - `GET /provider/offers`(分頁 response) + - `POST /provider/offers`(request: title、location、spot?、price、region、tag?、badges?、description?) + - `GET /provider/offers/{id}`(403 非本人) + - `PUT /provider/offers/{id}`(request 同上,所有欄位 nullable) + - `DELETE /provider/offers/{id}` + +#### Scenario: Provider 圖片管理端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `POST /provider/offers/{id}/cover`(multipart/form-data: image;response: cover_image_url) + - `DELETE /provider/offers/{id}/cover` + - `POST /provider/offers/{id}/images`(multipart/form-data: images[];最多 10 張) + - `DELETE /provider/images/{id}` + +#### Scenario: Provider schedules 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /provider/schedules`(query: offer_id?;response: CourseSchedule 陣列) + - `POST /provider/schedules`(request: diving_offer_id、date、period、capacity) + - `PUT /provider/schedules/{id}` + - `DELETE /provider/schedules/{id}` + +#### Scenario: Provider bookings 端點文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 以下端點均有文件: + - `GET /provider/bookings`(response: Booking 陣列,含 member 資訊) + - `PUT /provider/bookings/{id}/confirm` + - `PUT /provider/bookings/{id}/reject` + - `PUT /provider/bookings/{id}/complete` + - `PUT /provider/bookings/{id}/cancel` diff --git a/openspec/specs/swagger-public-api/spec.md b/openspec/specs/swagger-public-api/spec.md new file mode 100644 index 0000000..1e00946 --- /dev/null +++ b/openspec/specs/swagger-public-api/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: 公開端點 Swagger 文件及共用 Schema + +`app/Docs/PublicApiDoc.php` SHALL 文件化所有無需認證的公開端點,並定義全域共用 Schema。 + +#### Scenario: 共用 Schema 定義完整 + +- **WHEN** 執行 `php artisan l5-swagger:generate` +- **THEN** 以下 Schema 出現在產生的 JSON:`DivingOffer`、`Review`、`CourseSchedule`、`PaginationMeta`、`ApiErrorResponse` + +#### Scenario: GET /api/diving-offers 文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** `GET /diving-offers` 端點顯示 query parameters(`q`、`region`、`tag`、`per_page`、`page`)及包含分頁 meta 的 response schema + +#### Scenario: GET /api/diving-offers/{id} 文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** `GET /diving-offers/{id}` 端點顯示 path parameter `id` 及包含 `cover_image_url`、`images` 陣列的 response schema;404 response 亦有定義 + +#### Scenario: GET /api/diving-offers/{id}/reviews 文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 端點顯示 `sort`(helpful/rating/newest)、`page`、`per_page` 參數;response 包含 `summary`(average、total、distribution)與分頁 `meta` + +#### Scenario: GET /api/diving-offers/{id}/schedules 文件化 + +- **WHEN** 開啟 Swagger UI +- **THEN** 端點顯示 response 包含 `CourseSchedule` 陣列(id、date、period、capacity、booked_count、status) diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index ee50ea4..71ff3ec 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -12,7 +12,1161 @@ } ], "paths": { - "/register/member": { + "/admin/stats": { + "get": { + "tags": [ + "Admin 統計" + ], + "summary": "取得平台統計數據", + "description": "回傳會員總數、服務提供者總數、課程總數;非 admin 角色回傳 403", + "operationId": "getAdminStats", + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "properties": { + "total_members": { + "type": "integer", + "example": 128 + }, + "total_providers": { + "type": "integer", + "example": 34 + }, + "total_offers": { + "type": "integer", + "example": 87 + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/members": { + "get": { + "tags": [ + "Admin 會員管理" + ], + "summary": "取得會員列表", + "description": "分頁回傳所有會員帳號,含 member_profile", + "operationId": "listAdminMembers", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "王小明" + }, + "email": { + "type": "string", + "example": "member@example.com" + }, + "role": { + "type": "string", + "example": "member" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + }, + "member_profile": { + "type": "object", + "nullable": true + } + }, + "type": "object" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/members/{id}": { + "get": { + "tags": [ + "Admin 會員管理" + ], + "summary": "取得單一會員", + "description": "取得單一會員", + "operationId": "getAdminMember", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "會員不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/members/{id}/toggle-active": { + "put": { + "tags": [ + "Admin 會員管理" + ], + "summary": "切換會員啟用狀態", + "description": "啟用或停用指定會員帳號", + "operationId": "toggleMemberActive", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "切換成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "帳號已停用" + }, + "is_active": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "會員不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/check-member/{id}": { + "get": { + "tags": [ + "Admin 會員管理" + ], + "summary": "確認會員存在", + "description": "快速確認指定 ID 的會員是否存在(角色為 member)", + "operationId": "checkMember", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "會員存在", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "exists": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/providers": { + "get": { + "tags": [ + "Admin 教練管理" + ], + "summary": "取得教練列表", + "description": "分頁回傳所有服務提供者,含 provider_profile", + "operationId": "listAdminProviders", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 2 + }, + "name": { + "type": "string", + "example": "林教練" + }, + "email": { + "type": "string", + "example": "coach@example.com" + }, + "role": { + "type": "string", + "example": "provider" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "is_verified": { + "type": "boolean", + "example": false + }, + "provider_profile": { + "type": "object", + "nullable": true + } + }, + "type": "object" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/providers/{id}": { + "get": { + "tags": [ + "Admin 教練管理" + ], + "summary": "取得單一教練", + "description": "取得單一教練", + "operationId": "getAdminProvider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "教練不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/providers/{id}/toggle-active": { + "put": { + "tags": [ + "Admin 教練管理" + ], + "summary": "切換教練啟用狀態", + "description": "切換教練啟用狀態", + "operationId": "toggleProviderActive", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "切換成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "帳號已停用" + }, + "is_active": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "教練不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/providers/{id}/toggle-verified": { + "put": { + "tags": [ + "Admin 教練管理" + ], + "summary": "切換教練審核狀態", + "description": "通過或撤銷教練審核,回傳新的 is_verified 狀態", + "operationId": "toggleProviderVerified", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "切換成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "教練已通過審核" + }, + "is_verified": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "教練不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/check-provider/{id}": { + "get": { + "tags": [ + "Admin 教練管理" + ], + "summary": "確認教練存在", + "description": "確認教練存在", + "operationId": "checkProvider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "使用者 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "查詢成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "exists": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/offers": { + "get": { + "tags": [ + "Admin 課程管理" + ], + "summary": "取得所有課程(Admin)", + "description": "分頁回傳全平台課程列表", + "operationId": "listAdminOffers", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/offers/{id}": { + "delete": { + "tags": [ + "Admin 課程管理" + ], + "summary": "刪除課程(Admin)", + "description": "刪除課程(Admin)", + "operationId": "deleteAdminOffer", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "課程已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/bookings": { + "get": { + "tags": [ + "Admin 課程管理" + ], + "summary": "取得所有預約(Admin)", + "description": "分頁回傳全平台預約列表", + "operationId": "listAdminBookings", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Booking" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/bookings/{id}/complete": { + "put": { + "tags": [ + "Admin 課程管理" + ], + "summary": "標記預約完成(Admin)", + "description": "標記預約完成(Admin)", + "operationId": "completeBookingByAdmin", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "標記成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已完成" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "狀態不允許完成", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/reviews": { + "get": { + "tags": [ + "Admin 課程管理" + ], + "summary": "取得所有評價(Admin)", + "description": "分頁回傳全平台評價列表,per_page 最大 100", + "operationId": "listAdminReviews", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數(最大 100)", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Review" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/admin/reviews/{id}": { + "delete": { + "tags": [ + "Admin 課程管理" + ], + "summary": "刪除評價(Admin)", + "description": "刪除評價(Admin)", + "operationId": "deleteAdminReview", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "評價 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "評價已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非 admin 角色", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "評價不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/member/register": { "post": { "tags": [ "會員" @@ -224,7 +1378,7 @@ } } }, - "/login/member": { + "/member/login": { "post": { "tags": [ "會員" @@ -446,7 +1600,7 @@ } } }, - "/logout/member": { + "/member/logout": { "post": { "tags": [ "會員" @@ -503,7 +1657,7 @@ ] } }, - "/profile/member": { + "/member/profile": { "get": { "tags": [ "會員" @@ -832,8 +1986,8 @@ ] } }, - "/password/member": { - "post": { + "/member/change-password": { + "put": { "tags": [ "會員" ], @@ -967,7 +2121,7 @@ ] } }, - "/register/provider": { + "/provider/register": { "post": { "tags": [ "服務提供者" @@ -1104,7 +2258,7 @@ } } }, - "/login/provider": { + "/provider/login": { "post": { "tags": [ "服務提供者" @@ -1310,7 +2464,7 @@ } } }, - "/logout/provider": { + "/provider/logout": { "post": { "tags": [ "服務提供者" @@ -1367,7 +2521,7 @@ ] } }, - "/profile/provider": { + "/provider/profile": { "get": { "tags": [ "服務提供者" @@ -1732,8 +2886,8 @@ ] } }, - "/password/provider": { - "post": { + "/provider/change-password": { + "put": { "tags": [ "服務提供者" ], @@ -1867,7 +3021,7 @@ ] } }, - "/register/admin": { + "/admin/register": { "post": { "tags": [ "管理員" @@ -1994,7 +3148,7 @@ } } }, - "/login/admin": { + "/admin/login": { "post": { "tags": [ "管理員" @@ -2199,7 +3353,7 @@ } } }, - "/logout/admin": { + "/admin/logout": { "post": { "tags": [ "管理員" @@ -2256,7 +3410,7 @@ ] } }, - "/profile/admin": { + "/admin/profile": { "get": { "tags": [ "管理員" @@ -2431,8 +3585,8 @@ ] } }, - "/password/admin": { - "post": { + "/admin/change-password": { + "put": { "tags": [ "管理員" ], @@ -2565,10 +3719,3093 @@ } ] } + }, + "/logout": { + "post": { + "tags": [ + "認證" + ], + "summary": "通用登出", + "description": "撤銷當前 Bearer token,適用所有角色", + "operationId": "logout", + "responses": { + "200": { + "description": "登出成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "登出成功" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未認證", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Unauthenticated." + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/user": { + "get": { + "tags": [ + "認證" + ], + "summary": "取得當前使用者", + "description": "回傳當前 Bearer token 所屬的使用者資訊", + "operationId": "currentUser", + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "王小明" + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "role": { + "type": "string", + "enum": [ + "member", + "provider", + "admin" + ], + "example": "member" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + }, + "updated_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未認證", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Unauthenticated." + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/auth/google/redirect": { + "get": { + "tags": [ + "Google OAuth" + ], + "summary": "取得 Google OAuth 重定向 URL", + "description": "回傳 Google OAuth 授權頁面的 redirect_url,前端應將使用者導向此 URL 以啟動 OAuth 流程", + "operationId": "googleRedirect", + "responses": { + "200": { + "description": "取得 redirect_url 成功", + "content": { + "application/json": { + "schema": { + "properties": { + "redirect_url": { + "type": "string", + "format": "uri", + "example": "https://accounts.google.com/o/oauth2/auth?client_id=..." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/auth/google/callback": { + "get": { + "tags": [ + "Google OAuth" + ], + "summary": "Google OAuth 回調", + "description": "Google OAuth 授權完成後的回調端點,通常由瀏覽器自動呼叫。成功後回傳 Bearer token 與使用者資訊", + "operationId": "googleCallback", + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Google 授權碼", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "description": "OAuth state 參數", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OAuth 登入成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "登入成功" + }, + "data": { + "properties": { + "token": { + "type": "string", + "example": "1|abcdef1234567890" + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "user": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "王小明" + }, + "email": { + "type": "string", + "example": "user@gmail.com" + }, + "role": { + "type": "string", + "example": "member" + }, + "is_active": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "OAuth 驗證失敗", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "OAuth 驗證失敗" + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/member/bookings": { + "get": { + "tags": [ + "會員預約" + ], + "summary": "取得我的預約列表", + "description": "分頁回傳當前會員的所有預約", + "operationId": "listMemberBookings", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Booking" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "會員預約" + ], + "summary": "建立預約", + "description": "會員建立新的課程預約", + "operationId": "createBooking", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "offer_id", + "schedule_id", + "participants" + ], + "properties": { + "offer_id": { + "description": "課程 ID", + "type": "integer", + "example": 1 + }, + "schedule_id": { + "description": "時段 ID", + "type": "integer", + "example": 2 + }, + "participants": { + "description": "參加人數", + "type": "integer", + "example": 2 + }, + "note": { + "type": "string", + "example": "需要器材租借", + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "預約建立成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約成功" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "驗證失敗或時段已滿", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "無權限(非 member 角色)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/member/bookings/{id}": { + "get": { + "tags": [ + "會員預約" + ], + "summary": "取得單一預約", + "description": "取得單一預約", + "operationId": "getMemberBooking", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "預約不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "無權限存取", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "會員預約" + ], + "summary": "取消預約", + "description": "會員取消自己的預約", + "operationId": "cancelBooking", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取消成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已取消" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "無權限或狀態不允許取消", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "預約不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/member/reviews": { + "post": { + "tags": [ + "會員評價" + ], + "summary": "建立評價", + "description": "會員對已完成的預約課程提交評價", + "operationId": "createReview", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "booking_id", + "rating" + ], + "properties": { + "booking_id": { + "type": "integer", + "example": 5 + }, + "rating": { + "type": "integer", + "maximum": 5, + "minimum": 1, + "example": 4 + }, + "comment": { + "type": "string", + "example": "課程非常棒!", + "nullable": true + }, + "is_anonymous": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "評價建立成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "評價已提交" + }, + "data": { + "$ref": "#/components/schemas/Review" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "預約未完成或非本人預約", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/member/reviews/{id}": { + "put": { + "tags": [ + "會員評價" + ], + "summary": "更新評價", + "description": "更新評價", + "operationId": "updateReview", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "評價 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "rating": { + "type": "integer", + "maximum": 5, + "minimum": 1, + "example": 5 + }, + "comment": { + "type": "string", + "example": "更新後的評語", + "nullable": true + }, + "is_anonymous": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/Review" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人評價", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "評價不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "會員評價" + ], + "summary": "刪除評價", + "description": "刪除評價", + "operationId": "deleteReview", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "評價 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "評價已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人評價", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "評價不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/reviews/{id}/helpful": { + "post": { + "tags": [ + "會員評價" + ], + "summary": "標記評價為有幫助", + "description": "切換投票狀態(已投票則撤回)", + "operationId": "markReviewHelpful", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "評價 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "helpful_count": { + "type": "integer", + "example": 4 + }, + "has_voted": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "評價不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications": { + "get": { + "tags": [ + "通知" + ], + "summary": "取得通知列表", + "description": "分頁回傳當前使用者的所有通知(Member / Provider 共用)", + "operationId": "listNotifications", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "example": "booking_confirmed" + }, + "title": { + "type": "string", + "example": "預約已確認" + }, + "message": { + "type": "string", + "example": "您的預約已由教練確認" + }, + "read_at": { + "type": "string", + "example": null, + "nullable": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/unread-count": { + "get": { + "tags": [ + "通知" + ], + "summary": "取得未讀通知數量", + "description": "取得未讀通知數量", + "operationId": "getUnreadNotificationCount", + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "unread_count": { + "type": "integer", + "example": 3 + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/{id}/read": { + "patch": { + "tags": [ + "通知" + ], + "summary": "標記單一通知為已讀", + "description": "標記單一通知為已讀", + "operationId": "markNotificationRead", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "通知 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "標記成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "通知已標記為已讀" + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "通知不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/read-all": { + "patch": { + "tags": [ + "通知" + ], + "summary": "全部通知標記為已讀", + "description": "全部通知標記為已讀", + "operationId": "markAllNotificationsRead", + "responses": { + "200": { + "description": "標記成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "所有通知已標記為已讀" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/{id}": { + "delete": { + "tags": [ + "通知" + ], + "summary": "刪除通知", + "description": "刪除通知", + "operationId": "deleteNotification", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "通知 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "通知已刪除" + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "通知不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/offers": { + "get": { + "tags": [ + "教練課程" + ], + "summary": "取得自己的課程列表", + "description": "回傳當前服務提供者的所有課程(分頁)", + "operationId": "listProviderOffers", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "教練課程" + ], + "summary": "建立課程", + "description": "服務提供者建立新的潛水課程", + "operationId": "createProviderOffer", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "description", + "region", + "price", + "max_participants" + ], + "properties": { + "title": { + "type": "string", + "example": "基礎開放水域課程" + }, + "description": { + "type": "string", + "example": "適合初學者的 OWD 課程" + }, + "region": { + "type": "string", + "example": "墾丁" + }, + "price": { + "type": "number", + "format": "float", + "example": 8500 + }, + "max_participants": { + "type": "integer", + "example": 6 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "初學", + "OWD" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "課程建立成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "課程建立成功" + }, + "data": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "無權限", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/offers/{id}": { + "get": { + "tags": [ + "教練課程" + ], + "summary": "取得單一課程(Provider 視角)", + "description": "取得自己的指定課程,非本人課程回傳 403", + "operationId": "getProviderOffer", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": [ + "教練課程" + ], + "summary": "更新課程", + "description": "更新課程", + "operationId": "updateProviderOffer", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string", + "example": "進階開放水域課程" + }, + "description": { + "type": "string", + "example": "AOWD 課程" + }, + "region": { + "type": "string", + "example": "小琉球" + }, + "price": { + "type": "number", + "format": "float", + "example": 12000 + }, + "max_participants": { + "type": "integer", + "example": 4 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "進階", + "AOWD" + ] + }, + "is_active": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "教練課程" + ], + "summary": "刪除課程", + "description": "刪除課程", + "operationId": "deleteProviderOffer", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "課程已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/offers/{id}/cover": { + "post": { + "tags": [ + "教練圖片" + ], + "summary": "上傳封面圖片", + "description": "上傳課程封面圖片(multipart/form-data)", + "operationId": "uploadOfferCover", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "cover_image" + ], + "properties": { + "cover_image": { + "description": "封面圖片(jpg/png,最大 5MB)", + "type": "string", + "format": "binary" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "上傳成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "cover_image_url": { + "type": "string", + "example": "https://example.com/covers/1.jpg" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "圖片驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "教練圖片" + ], + "summary": "刪除封面圖片", + "description": "刪除封面圖片", + "operationId": "deleteOfferCover", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "封面圖片已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/offers/{id}/images": { + "post": { + "tags": [ + "教練圖片" + ], + "summary": "上傳相簿圖片", + "description": "新增課程相簿圖片(multipart/form-data,可多張)", + "operationId": "uploadOfferImages", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "images[]" + ], + "properties": { + "images[]": { + "description": "相簿圖片(每張最大 5MB)", + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "上傳成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "images": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://example.com/img/1.jpg" + ] + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "圖片驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/images/{id}": { + "delete": { + "tags": [ + "教練圖片" + ], + "summary": "刪除相簿圖片", + "description": "刪除相簿圖片", + "operationId": "deleteOfferImage", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "圖片 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "圖片已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人圖片", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "圖片不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/schedules": { + "get": { + "tags": [ + "教練時段" + ], + "summary": "取得時段列表", + "description": "回傳服務提供者的所有時段,可依 offer_id 篩選", + "operationId": "listProviderSchedules", + "parameters": [ + { + "name": "offer_id", + "in": "query", + "description": "課程 ID 篩選", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CourseSchedule" + } + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "教練時段" + ], + "summary": "建立時段", + "description": "建立時段", + "operationId": "createProviderSchedule", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "offer_id", + "start_date", + "end_date", + "available_slots" + ], + "properties": { + "offer_id": { + "type": "integer", + "example": 1 + }, + "start_date": { + "type": "string", + "format": "date", + "example": "2025-07-01" + }, + "end_date": { + "type": "string", + "format": "date", + "example": "2025-07-03" + }, + "available_slots": { + "type": "integer", + "example": 4 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "時段建立成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/CourseSchedule" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "驗證失敗", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "非本人課程", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/schedules/{id}": { + "put": { + "tags": [ + "教練時段" + ], + "summary": "更新時段", + "description": "更新時段", + "operationId": "updateProviderSchedule", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "時段 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "start_date": { + "type": "string", + "format": "date", + "example": "2025-07-05" + }, + "end_date": { + "type": "string", + "format": "date", + "example": "2025-07-07" + }, + "available_slots": { + "type": "integer", + "example": 6 + }, + "is_active": { + "type": "boolean", + "example": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/CourseSchedule" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人時段", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "時段不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "教練時段" + ], + "summary": "刪除時段", + "description": "刪除時段", + "operationId": "deleteProviderSchedule", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "時段 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "刪除成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "時段已刪除" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人時段", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "時段不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/bookings": { + "get": { + "tags": [ + "教練預約" + ], + "summary": "取得收到的預約列表", + "description": "分頁回傳服務提供者收到的所有預約", + "operationId": "listProviderBookings", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Booking" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/bookings/{id}/confirm": { + "put": { + "tags": [ + "教練預約" + ], + "summary": "確認預約", + "description": "確認預約", + "operationId": "confirmBooking", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "確認成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已確認" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程的預約", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "狀態不允許確認", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/bookings/{id}/reject": { + "put": { + "tags": [ + "教練預約" + ], + "summary": "拒絕預約", + "description": "拒絕預約", + "operationId": "rejectBooking", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "拒絕成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已拒絕" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程的預約", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "狀態不允許拒絕", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/bookings/{id}/complete": { + "put": { + "tags": [ + "教練預約" + ], + "summary": "標記預約完成", + "description": "標記預約完成", + "operationId": "completeBookingByProvider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "標記成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已完成" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程的預約", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "狀態不允許完成", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/provider/bookings/{id}/cancel": { + "put": { + "tags": [ + "教練預約" + ], + "summary": "取消預約(Provider)", + "description": "取消預約(Provider)", + "operationId": "cancelBookingByProvider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "預約 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取消成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "預約已取消" + }, + "data": { + "$ref": "#/components/schemas/Booking" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "非本人課程的預約", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "狀態不允許取消", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/diving-offers": { + "get": { + "tags": [ + "公開課程" + ], + "summary": "查詢潛水課程列表", + "description": "支援關鍵字、地區、標籤篩選,回傳分頁結果", + "operationId": "listDivingOffers", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "關鍵字搜尋", + "schema": { + "type": "string" + } + }, + { + "name": "region", + "in": "query", + "description": "地區篩選", + "schema": { + "type": "string", + "example": "墾丁" + } + }, + { + "name": "tag", + "in": "query", + "description": "標籤篩選", + "schema": { + "type": "string", + "example": "OWD" + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數(預設 15,最大 50)", + "schema": { + "type": "integer", + "default": 15, + "maximum": 50 + } + }, + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "查詢成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/diving-offers/{id}": { + "get": { + "tags": [ + "公開課程" + ], + "summary": "取得單一潛水課程", + "description": "回傳課程詳情,包含封面圖片與相簿", + "operationId": "getDivingOffer", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/DivingOffer" + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + }, + "/diving-offers/{id}/reviews": { + "get": { + "tags": [ + "公開課程" + ], + "summary": "取得課程評價列表", + "description": "回傳評分摘要、分頁評價列表與分頁 meta", + "operationId": "listOfferReviews", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "sort", + "in": "query", + "description": "排序方式(newest / helpful)", + "schema": { + "type": "string", + "default": "newest", + "enum": [ + "newest", + "helpful" + ] + } + }, + { + "name": "page", + "in": "query", + "description": "頁碼", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "description": "每頁筆數(預設 15,最大 50)", + "schema": { + "type": "integer", + "default": 15, + "maximum": 50 + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "properties": { + "summary": { + "properties": { + "average_rating": { + "type": "number", + "format": "float", + "example": 4.2 + }, + "total_reviews": { + "type": "integer", + "example": 24 + }, + "distribution": { + "properties": { + "1": { + "type": "integer", + "example": 1 + }, + "2": { + "type": "integer", + "example": 2 + }, + "3": { + "type": "integer", + "example": 3 + }, + "4": { + "type": "integer", + "example": 8 + }, + "5": { + "type": "integer", + "example": 10 + } + }, + "type": "object" + } + }, + "type": "object" + }, + "reviews": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Review" + } + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + }, + "/diving-offers/{id}/schedules": { + "get": { + "tags": [ + "公開課程" + ], + "summary": "取得課程時段列表", + "description": "回傳指定課程的所有可用時段", + "operationId": "listOfferSchedules", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "課程 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "取得成功", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CourseSchedule" + } + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "課程不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } } }, "components": { "schemas": { + "PaginationMeta": { + "properties": { + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 5 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "total": { + "type": "integer", + "example": 72 + } + }, + "type": "object" + }, + "ApiErrorResponse": { + "properties": { + "status": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "操作失敗" + }, + "errors": { + "type": "object", + "nullable": true + } + }, + "type": "object" + }, + "DivingOffer": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "provider_id": { + "type": "integer", + "example": 3 + }, + "title": { + "type": "string", + "example": "基礎開放水域課程" + }, + "description": { + "type": "string", + "example": "適合初學者的 OWD 課程" + }, + "region": { + "type": "string", + "example": "墾丁" + }, + "price": { + "type": "number", + "format": "float", + "example": 8500 + }, + "max_participants": { + "type": "integer", + "example": 6 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "初學", + "OWD" + ] + }, + "cover_image_url": { + "type": "string", + "example": "https://example.com/image.jpg", + "nullable": true + }, + "images": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "is_active": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + }, + "updated_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + }, + "Review": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "offer_id": { + "type": "integer", + "example": 1 + }, + "rating": { + "type": "integer", + "maximum": 5, + "minimum": 1, + "example": 4 + }, + "comment": { + "type": "string", + "example": "課程非常棒!", + "nullable": true + }, + "is_anonymous": { + "type": "boolean", + "example": false + }, + "helpful_count": { + "type": "integer", + "example": 3 + }, + "has_voted": { + "type": "boolean", + "example": false + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + }, + "CourseSchedule": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "offer_id": { + "type": "integer", + "example": 1 + }, + "start_date": { + "type": "string", + "format": "date", + "example": "2025-07-01" + }, + "end_date": { + "type": "string", + "format": "date", + "example": "2025-07-03" + }, + "available_slots": { + "type": "integer", + "example": 4 + }, + "is_active": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + }, + "Booking": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "member_id": { + "type": "integer", + "example": 5 + }, + "offer_id": { + "type": "integer", + "example": 1 + }, + "schedule_id": { + "type": "integer", + "example": 2 + }, + "status": { + "type": "string", + "enum": [ + "pending", + "confirmed", + "rejected", + "completed", + "cancelled" + ], + "example": "pending" + }, + "participants": { + "type": "integer", + "example": 2 + }, + "note": { + "type": "string", + "example": "", + "nullable": true + }, + "created_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + }, + "updated_at": { + "type": "string", + "example": "2025-01-01T00:00:00.000000Z" + } + }, + "type": "object" + }, "AdminProfile": { "title": "管理員個人資料", "description": "管理員的詳細個人資料", @@ -2888,6 +7125,22 @@ } }, "tags": [ + { + "name": "Admin 統計", + "description": "管理員平台統計" + }, + { + "name": "Admin 會員管理", + "description": "管理員的會員帳號管理" + }, + { + "name": "Admin 教練管理", + "description": "管理員的服務提供者帳號管理" + }, + { + "name": "Admin 課程管理", + "description": "管理員的課程、預約、評價管理" + }, { "name": "會員", "description": "會員相關操作" @@ -2899,6 +7152,46 @@ { "name": "管理員", "description": "管理員相關操作" + }, + { + "name": "認證", + "description": "通用認證操作(登出、取得當前使用者)" + }, + { + "name": "Google OAuth", + "description": "Google OAuth 2.0 社群登入" + }, + { + "name": "會員預約", + "description": "會員的預約管理" + }, + { + "name": "會員評價", + "description": "會員的評價管理" + }, + { + "name": "通知", + "description": "站內通知管理(Member / Provider 共用)" + }, + { + "name": "教練課程", + "description": "服務提供者的課程管理" + }, + { + "name": "教練圖片", + "description": "服務提供者的課程圖片管理" + }, + { + "name": "教練時段", + "description": "服務提供者的課程時段管理" + }, + { + "name": "教練預約", + "description": "服務提供者的預約管理" + }, + { + "name": "公開課程", + "description": "無需認證的公開潛水課程查詢端點" } ] } \ No newline at end of file