chore:新增 swagger-api-docs-completion change 規劃 #14

Merged
a620906209 merged 7 commits from feature/health-endpoint into master 2026-05-28 17:58:56 +00:00
22 changed files with 6781 additions and 42 deletions
Showing only changes of commit 0814a3196f - Show all commits
+48 -3
View File
@@ -1,4 +1,49 @@
![image](https://github.com/user-attachments/assets/332bf381-8faa-441f-8843-bff2c1fbde7a) # CFDive Platform
![image](https://github.com/user-attachments/assets/f0a765f5-5386-467e-80b8-4a5ef8ef0bcf)
![image](https://github.com/user-attachments/assets/5d6ad6ee-f100-4d45-9200-d61ce76ca7c4)
潛水課程媒合平台 — 連結潛水教練與學員,提供課程瀏覽、線上預約、評價與通知等完整服務。
---
## 功能概覽
**會員(Member**
- 註冊 / 登入(Email + Google OAuth
- 瀏覽、搜尋、篩選潛水課程
- 查看課程時段並送出預約
- 對完成的課程留下評價(支援匿名、有幫助投票)
- 站內通知(Email + Polling
**教練(Provider**
- 課程 CRUD(含封面 + 相簿圖片上傳)
- 課程時段管理
- 預約管理(確認 / 拒絕 / 完成 / 取消)
**管理員(Admin**
- 平台統計數據(會員數、教練數、課程數)
- 會員與教練帳號管理(啟用 / 停用 / 審核)
- 課程、預約、評價管理
---
## 技術棧
| 層級 | 技術 |
|------|------|
| 後端 | PHP 8.x / Laravel 11 |
| 前端 | Vue 3 |
| 資料庫 | MySQL 8.0 |
| 快取 | Redispredis|
| 認證 | Laravel Sanctum + Google OAuth |
| 容器 | Docker / Docker Compose |
| API 文件 | Swagger UIl5-swagger|
---
## API 文件
共 73 個端點,涵蓋:
- 認證(Email + Google OAuth
- 公開課程查詢
- 會員預約 / 評價 / 通知
- 教練課程 / 時段 / 預約管理
- 管理員後台
+504
View File
@@ -0,0 +1,504 @@
<?php
namespace App\Docs;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(
* name="Admin 統計",
* description="管理員平台統計"
* )
* @OA\Tag(
* name="Admin 會員管理",
* description="管理員的會員帳號管理"
* )
* @OA\Tag(
* name="Admin 教練管理",
* description="管理員的服務提供者帳號管理"
* )
* @OA\Tag(
* name="Admin 課程管理",
* description="管理員的課程、預約、評價管理"
* )
*/
class AdminApiDoc
{
// -----------------------------------------------------------------------
// Admin Stats
// -----------------------------------------------------------------------
/**
* 取得平台統計數據
*
* @OA\Get(
* path="/admin/stats",
* summary="取得平台統計數據",
* description="回傳會員總數、服務提供者總數、課程總數;非 admin 角色回傳 403",
* operationId="getAdminStats",
* tags={"Admin 統計"},
* security={{"bearerAuth": {}}},
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="total_members", type="integer", example=128),
* @OA\Property(property="total_providers", type="integer", example=34),
* @OA\Property(property="total_offers", type="integer", example=87)
* )
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function getAdminStats()
{
}
// -----------------------------------------------------------------------
// Admin Member Management
// -----------------------------------------------------------------------
/**
* 取得會員列表
*
* @OA\Get(
* path="/admin/members",
* summary="取得會員列表",
* description="分頁回傳所有會員帳號,含 member_profile",
* operationId="listAdminMembers",
* tags={"Admin 會員管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="王小明"),
* @OA\Property(property="email", type="string", example="member@example.com"),
* @OA\Property(property="role", type="string", example="member"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z"),
* @OA\Property(property="member_profile", type="object", nullable=true)
* )
* ),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function listAdminMembers()
{
}
/**
* 取得單一會員
*
* @OA\Get(
* path="/admin/members/{id}",
* summary="取得單一會員",
* operationId="getAdminMember",
* tags={"Admin 會員管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="object")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="會員不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function getAdminMember()
{
}
/**
* 切換會員啟用狀態
*
* @OA\Put(
* path="/admin/members/{id}/toggle-active",
* summary="切換會員啟用狀態",
* description="啟用或停用指定會員帳號",
* operationId="toggleMemberActive",
* tags={"Admin 會員管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="切換成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="帳號已停用"),
* @OA\Property(property="is_active", type="boolean", example=false)
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="會員不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function toggleMemberActive()
{
}
/**
* 確認會員存在
*
* @OA\Get(
* path="/admin/check-member/{id}",
* summary="確認會員存在",
* description="快速確認指定 ID 的會員是否存在(角色為 member)",
* operationId="checkMember",
* tags={"Admin 會員管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="會員存在",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="exists", type="boolean", example=true)
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function checkMember()
{
}
// -----------------------------------------------------------------------
// Admin Provider Management
// -----------------------------------------------------------------------
/**
* 取得教練列表
*
* @OA\Get(
* path="/admin/providers",
* summary="取得教練列表",
* description="分頁回傳所有服務提供者,含 provider_profile",
* operationId="listAdminProviders",
* tags={"Admin 教練管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", example=2),
* @OA\Property(property="name", type="string", example="林教練"),
* @OA\Property(property="email", type="string", example="coach@example.com"),
* @OA\Property(property="role", type="string", example="provider"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="is_verified", type="boolean", example=false),
* @OA\Property(property="provider_profile", type="object", nullable=true)
* )
* ),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function listAdminProviders()
{
}
/**
* 取得單一教練
*
* @OA\Get(
* path="/admin/providers/{id}",
* summary="取得單一教練",
* operationId="getAdminProvider",
* tags={"Admin 教練管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="object")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="教練不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function getAdminProvider()
{
}
/**
* 切換教練啟用狀態
*
* @OA\Put(
* path="/admin/providers/{id}/toggle-active",
* summary="切換教練啟用狀態",
* operationId="toggleProviderActive",
* tags={"Admin 教練管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="切換成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="帳號已停用"),
* @OA\Property(property="is_active", type="boolean", example=false)
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="教練不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function toggleProviderActive()
{
}
/**
* 切換教練審核狀態
*
* @OA\Put(
* path="/admin/providers/{id}/toggle-verified",
* summary="切換教練審核狀態",
* description="通過或撤銷教練審核,回傳新的 is_verified 狀態",
* operationId="toggleProviderVerified",
* tags={"Admin 教練管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="切換成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="教練已通過審核"),
* @OA\Property(property="is_verified", type="boolean", example=true)
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="教練不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function toggleProviderVerified()
{
}
/**
* 確認教練存在
*
* @OA\Get(
* path="/admin/check-provider/{id}",
* summary="確認教練存在",
* operationId="checkProvider",
* tags={"Admin 教練管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="使用者 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="查詢成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="exists", type="boolean", example=true)
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function checkProvider()
{
}
// -----------------------------------------------------------------------
// Admin Offers / Bookings / Reviews
// -----------------------------------------------------------------------
/**
* 取得所有課程(Admin
*
* @OA\Get(
* path="/admin/offers",
* summary="取得所有課程(Admin",
* description="分頁回傳全平台課程列表",
* operationId="listAdminOffers",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/DivingOffer")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function listAdminOffers()
{
}
/**
* 刪除課程(Admin
*
* @OA\Delete(
* path="/admin/offers/{id}",
* summary="刪除課程(Admin",
* operationId="deleteAdminOffer",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="課程已刪除")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteAdminOffer()
{
}
/**
* 取得所有預約(Admin
*
* @OA\Get(
* path="/admin/bookings",
* summary="取得所有預約(Admin",
* description="分頁回傳全平台預約列表",
* operationId="listAdminBookings",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function listAdminBookings()
{
}
/**
* 標記預約完成(Admin
*
* @OA\Put(
* path="/admin/bookings/{id}/complete",
* summary="標記預約完成(Admin",
* operationId="completeBookingByAdmin",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="標記成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已完成"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="狀態不允許完成", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function completeBookingByAdmin()
{
}
/**
* 取得所有評價(Admin
*
* @OA\Get(
* path="/admin/reviews",
* summary="取得所有評價(Admin",
* description="分頁回傳全平台評價列表,per_page 最大 100",
* operationId="listAdminReviews",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數(最大 100", @OA\Schema(type="integer", default=20, maximum=100)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Review")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function listAdminReviews()
{
}
/**
* 刪除評價(Admin
*
* @OA\Delete(
* path="/admin/reviews/{id}",
* summary="刪除評價(Admin",
* operationId="deleteAdminReview",
* tags={"Admin 課程管理"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="評價已刪除")
* )
* ),
* @OA\Response(response=403, description="非 admin 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteAdminReview()
{
}
}
+92 -21
View File
@@ -35,6 +35,10 @@ use OpenApi\Annotations as OA;
* name="管理員", * name="管理員",
* description="管理員相關操作" * description="管理員相關操作"
* ) * )
* @OA\Tag(
* name="認證",
* description="通用認證操作(登出、取得當前使用者)"
* )
*/ */
class AuthApiDoc class AuthApiDoc
{ {
@@ -42,7 +46,7 @@ class AuthApiDoc
* 會員註冊 * 會員註冊
* *
* @OA\Post( * @OA\Post(
* path="/register/member", * path="/member/register",
* summary="會員註冊", * summary="會員註冊",
* description="建立新的會員帳號", * description="建立新的會員帳號",
* operationId="registerMember", * operationId="registerMember",
@@ -119,7 +123,7 @@ class AuthApiDoc
* 會員登入 * 會員登入
* *
* @OA\Post( * @OA\Post(
* path="/login/member", * path="/member/login",
* summary="會員登入", * summary="會員登入",
* description="會員帳號登入系統", * description="會員帳號登入系統",
* operationId="loginMember", * operationId="loginMember",
@@ -207,7 +211,7 @@ class AuthApiDoc
* 會員登出 * 會員登出
* *
* @OA\Post( * @OA\Post(
* path="/logout/member", * path="/member/logout",
* summary="會員登出", * summary="會員登出",
* description="會員登出系統並撤銷當前令牌", * description="會員登出系統並撤銷當前令牌",
* operationId="logoutMember", * operationId="logoutMember",
@@ -241,7 +245,7 @@ class AuthApiDoc
* 取得會員個人資料 * 取得會員個人資料
* *
* @OA\Get( * @OA\Get(
* path="/profile/member", * path="/member/profile",
* summary="取得會員個人資料", * summary="取得會員個人資料",
* description="取得當前登入會員的個人資料", * description="取得當前登入會員的個人資料",
* operationId="memberProfile", * operationId="memberProfile",
@@ -303,7 +307,7 @@ class AuthApiDoc
* 更新會員個人資料 * 更新會員個人資料
* *
* @OA\Put( * @OA\Put(
* path="/profile/member", * path="/member/profile",
* summary="更新會員個人資料", * summary="更新會員個人資料",
* description="更新當前登入會員的個人資料", * description="更新當前登入會員的個人資料",
* operationId="updateMemberProfile", * operationId="updateMemberProfile",
@@ -383,8 +387,8 @@ class AuthApiDoc
/** /**
* 修改會員密碼 * 修改會員密碼
* *
* @OA\Post( * @OA\Put(
* path="/password/member", * path="/member/change-password",
* summary="修改會員密碼", * summary="修改會員密碼",
* description="修改當前登入會員的密碼", * description="修改當前登入會員的密碼",
* operationId="changeMemberPassword", * operationId="changeMemberPassword",
@@ -444,7 +448,7 @@ class AuthApiDoc
* 服務提供者註冊 * 服務提供者註冊
* *
* @OA\Post( * @OA\Post(
* path="/register/provider", * path="/provider/register",
* summary="服務提供者註冊", * summary="服務提供者註冊",
* description="建立新的服務提供者帳號", * description="建立新的服務提供者帳號",
* operationId="registerProvider", * operationId="registerProvider",
@@ -498,7 +502,7 @@ class AuthApiDoc
* 服務提供者登入 * 服務提供者登入
* *
* @OA\Post( * @OA\Post(
* path="/login/provider", * path="/provider/login",
* summary="服務提供者登入", * summary="服務提供者登入",
* description="服務提供者帳號登入系統", * description="服務提供者帳號登入系統",
* operationId="loginProvider", * operationId="loginProvider",
@@ -580,7 +584,7 @@ class AuthApiDoc
* 服務提供者登出 * 服務提供者登出
* *
* @OA\Post( * @OA\Post(
* path="/logout/provider", * path="/provider/logout",
* summary="服務提供者登出", * summary="服務提供者登出",
* description="服務提供者登出系統並撤銷當前令牌", * description="服務提供者登出系統並撤銷當前令牌",
* operationId="logoutProvider", * operationId="logoutProvider",
@@ -614,7 +618,7 @@ class AuthApiDoc
* 取得服務提供者資料 * 取得服務提供者資料
* *
* @OA\Get( * @OA\Get(
* path="/profile/provider", * path="/provider/profile",
* summary="取得服務提供者資料", * summary="取得服務提供者資料",
* description="取得當前登入服務提供者的資料", * description="取得當前登入服務提供者的資料",
* operationId="providerProfile", * operationId="providerProfile",
@@ -678,7 +682,7 @@ class AuthApiDoc
* 更新服務提供者資料 * 更新服務提供者資料
* *
* @OA\Put( * @OA\Put(
* path="/profile/provider", * path="/provider/profile",
* summary="更新服務提供者資料", * summary="更新服務提供者資料",
* description="更新當前登入服務提供者的資料", * description="更新當前登入服務提供者的資料",
* operationId="updateProviderProfile", * operationId="updateProviderProfile",
@@ -764,8 +768,8 @@ class AuthApiDoc
/** /**
* 修改服務提供者密碼 * 修改服務提供者密碼
* *
* @OA\Post( * @OA\Put(
* path="/password/provider", * path="/provider/change-password",
* summary="修改服務提供者密碼", * summary="修改服務提供者密碼",
* description="修改當前登入服務提供者的密碼", * description="修改當前登入服務提供者的密碼",
* operationId="changeProviderPassword", * operationId="changeProviderPassword",
@@ -825,7 +829,7 @@ class AuthApiDoc
* 管理員註冊 * 管理員註冊
* *
* @OA\Post( * @OA\Post(
* path="/register/admin", * path="/admin/register",
* summary="管理員註冊", * summary="管理員註冊",
* description="建立新的管理員帳號", * description="建立新的管理員帳號",
* operationId="registerAdmin", * operationId="registerAdmin",
@@ -877,7 +881,7 @@ class AuthApiDoc
* 管理員登入 * 管理員登入
* *
* @OA\Post( * @OA\Post(
* path="/login/admin", * path="/admin/login",
* summary="管理員登入", * summary="管理員登入",
* description="管理員帳號登入系統", * description="管理員帳號登入系統",
* operationId="loginAdmin", * operationId="loginAdmin",
@@ -958,7 +962,7 @@ class AuthApiDoc
* 管理員登出 * 管理員登出
* *
* @OA\Post( * @OA\Post(
* path="/logout/admin", * path="/admin/logout",
* summary="管理員登出", * summary="管理員登出",
* description="管理員登出系統並撤銷當前令牌", * description="管理員登出系統並撤銷當前令牌",
* operationId="logoutAdmin", * operationId="logoutAdmin",
@@ -992,7 +996,7 @@ class AuthApiDoc
* 取得管理員個人資料 * 取得管理員個人資料
* *
* @OA\Get( * @OA\Get(
* path="/profile/admin", * path="/admin/profile",
* summary="取得管理員個人資料", * summary="取得管理員個人資料",
* description="取得當前登入管理員的個人資料", * description="取得當前登入管理員的個人資料",
* operationId="adminProfile", * operationId="adminProfile",
@@ -1030,7 +1034,7 @@ class AuthApiDoc
* 更新管理員個人資料 * 更新管理員個人資料
* *
* @OA\Put( * @OA\Put(
* path="/profile/admin", * path="/admin/profile",
* summary="更新管理員個人資料", * summary="更新管理員個人資料",
* description="更新當前登入管理員的個人資料", * description="更新當前登入管理員的個人資料",
* operationId="updateAdminProfile", * operationId="updateAdminProfile",
@@ -1087,8 +1091,8 @@ class AuthApiDoc
/** /**
* 修改管理員密碼 * 修改管理員密碼
* *
* @OA\Post( * @OA\Put(
* path="/password/admin", * path="/admin/change-password",
* summary="修改管理員密碼", * summary="修改管理員密碼",
* description="修改當前登入管理員的密碼", * description="修改當前登入管理員的密碼",
* operationId="changeAdminPassword", * operationId="changeAdminPassword",
@@ -1143,4 +1147,71 @@ class AuthApiDoc
public function changeAdminPassword() public function changeAdminPassword()
{ {
} }
/**
* 通用登出
*
* @OA\Post(
* path="/logout",
* summary="通用登出",
* description="撤銷當前 Bearer token,適用所有角色",
* operationId="logout",
* tags={"認證"},
* security={{"bearerAuth": {}}},
* @OA\Response(
* response=200,
* description="登出成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="登出成功")
* )
* ),
* @OA\Response(
* response=401,
* description="未認證",
* @OA\JsonContent(
* @OA\Property(property="message", type="string", example="Unauthenticated.")
* )
* )
* )
*/
public function logout()
{
}
/**
* 取得當前使用者
*
* @OA\Get(
* path="/user",
* summary="取得當前使用者",
* description="回傳當前 Bearer token 所屬的使用者資訊",
* operationId="currentUser",
* tags={"認證"},
* security={{"bearerAuth": {}}},
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="王小明"),
* @OA\Property(property="email", type="string", example="user@example.com"),
* @OA\Property(property="role", type="string", enum={"member","provider","admin"}, example="member"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z"),
* @OA\Property(property="updated_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
* ),
* @OA\Response(
* response=401,
* description="未認證",
* @OA\JsonContent(
* @OA\Property(property="message", type="string", example="Unauthenticated.")
* )
* )
* )
*/
public function currentUser()
{
}
} }
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace App\Docs;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(
* name="Google OAuth",
* description="Google OAuth 2.0 社群登入"
* )
*/
class AuthSupplementDoc
{
/**
* 取得 Google OAuth 重定向 URL
*
* @OA\Get(
* path="/auth/google/redirect",
* summary="取得 Google OAuth 重定向 URL",
* description="回傳 Google OAuth 授權頁面的 redirect_url,前端應將使用者導向此 URL 以啟動 OAuth 流程",
* operationId="googleRedirect",
* tags={"Google OAuth"},
* @OA\Response(
* response=200,
* description="取得 redirect_url 成功",
* @OA\JsonContent(
* @OA\Property(property="redirect_url", type="string", format="uri", example="https://accounts.google.com/o/oauth2/auth?client_id=...")
* )
* )
* )
*/
public function googleRedirect()
{
}
/**
* Google OAuth 回調
*
* @OA\Get(
* path="/auth/google/callback",
* summary="Google OAuth 回調",
* description="Google OAuth 授權完成後的回調端點,通常由瀏覽器自動呼叫。成功後回傳 Bearer token 與使用者資訊",
* operationId="googleCallback",
* tags={"Google OAuth"},
* @OA\Parameter(
* name="code",
* in="query",
* required=true,
* description="Google 授權碼",
* @OA\Schema(type="string")
* ),
* @OA\Parameter(
* name="state",
* in="query",
* required=false,
* description="OAuth state 參數",
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=200,
* description="OAuth 登入成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="登入成功"),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="token", type="string", example="1|abcdef1234567890"),
* @OA\Property(property="token_type", type="string", example="Bearer"),
* @OA\Property(
* property="user",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="王小明"),
* @OA\Property(property="email", type="string", example="user@gmail.com"),
* @OA\Property(property="role", type="string", example="member"),
* @OA\Property(property="is_active", type="boolean", example=true)
* )
* )
* )
* ),
* @OA\Response(
* response=422,
* description="OAuth 驗證失敗",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=false),
* @OA\Property(property="message", type="string", example="OAuth 驗證失敗")
* )
* )
* )
*/
public function googleCallback()
{
}
}
+418
View File
@@ -0,0 +1,418 @@
<?php
namespace App\Docs;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(
* name="會員預約",
* description="會員的預約管理"
* )
* @OA\Tag(
* name="會員評價",
* description="會員的評價管理"
* )
* @OA\Tag(
* name="通知",
* description="站內通知管理(Member / Provider 共用)"
* )
*/
class MemberApiDoc
{
// -----------------------------------------------------------------------
// Member Bookings
// -----------------------------------------------------------------------
/**
* 建立預約
*
* @OA\Post(
* path="/member/bookings",
* summary="建立預約",
* description="會員建立新的課程預約",
* operationId="createBooking",
* tags={"會員預約"},
* security={{"bearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"offer_id","schedule_id","participants"},
* @OA\Property(property="offer_id", type="integer", example=1, description="課程 ID"),
* @OA\Property(property="schedule_id", type="integer", example=2, description="時段 ID"),
* @OA\Property(property="participants", type="integer", example=2, description="參加人數"),
* @OA\Property(property="note", type="string", nullable=true, example="需要器材租借")
* )
* ),
* @OA\Response(
* response=201,
* description="預約建立成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約成功"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(
* response=422,
* description="驗證失敗或時段已滿",
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
* ),
* @OA\Response(
* response=403,
* description="無權限(非 member 角色)",
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
* )
* )
*/
public function createBooking()
{
}
/**
* 取得我的預約列表
*
* @OA\Get(
* path="/member/bookings",
* summary="取得我的預約列表",
* description="分頁回傳當前會員的所有預約",
* operationId="listMemberBookings",
* tags={"會員預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* )
*/
public function listMemberBookings()
{
}
/**
* 取得單一預約
*
* @OA\Get(
* path="/member/bookings/{id}",
* summary="取得單一預約",
* operationId="getMemberBooking",
* tags={"會員預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=403, description="無權限存取", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function getMemberBooking()
{
}
/**
* 取消預約
*
* @OA\Delete(
* path="/member/bookings/{id}",
* summary="取消預約",
* description="會員取消自己的預約",
* operationId="cancelBooking",
* tags={"會員預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取消成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已取消")
* )
* ),
* @OA\Response(response=403, description="無權限或狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function cancelBooking()
{
}
// -----------------------------------------------------------------------
// Member Reviews
// -----------------------------------------------------------------------
/**
* 建立評價
*
* @OA\Post(
* path="/member/reviews",
* summary="建立評價",
* description="會員對已完成的預約課程提交評價",
* operationId="createReview",
* tags={"會員評價"},
* security={{"bearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"booking_id","rating"},
* @OA\Property(property="booking_id", type="integer", example=5),
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=4),
* @OA\Property(property="comment", type="string", nullable=true, example="課程非常棒!"),
* @OA\Property(property="is_anonymous", type="boolean", example=false)
* )
* ),
* @OA\Response(
* response=201,
* description="評價建立成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="評價已提交"),
* @OA\Property(property="data", ref="#/components/schemas/Review")
* )
* ),
* @OA\Response(response=403, description="預約未完成或非本人預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function createReview()
{
}
/**
* 更新評價
*
* @OA\Put(
* path="/member/reviews/{id}",
* summary="更新評價",
* operationId="updateReview",
* tags={"會員評價"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=5),
* @OA\Property(property="comment", type="string", nullable=true, example="更新後的評語"),
* @OA\Property(property="is_anonymous", type="boolean", example=true)
* )
* ),
* @OA\Response(
* response=200,
* description="更新成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/Review")
* )
* ),
* @OA\Response(response=403, description="非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function updateReview()
{
}
/**
* 刪除評價
*
* @OA\Delete(
* path="/member/reviews/{id}",
* summary="刪除評價",
* operationId="deleteReview",
* tags={"會員評價"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="評價已刪除")
* )
* ),
* @OA\Response(response=403, description="非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteReview()
{
}
/**
* 標記評價為有幫助
*
* @OA\Post(
* path="/reviews/{id}/helpful",
* summary="標記評價為有幫助",
* description="切換投票狀態(已投票則撤回)",
* operationId="markReviewHelpful",
* tags={"會員評價"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="操作成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="helpful_count", type="integer", example=4),
* @OA\Property(property="has_voted", type="boolean", example=true)
* )
* ),
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function markReviewHelpful()
{
}
// -----------------------------------------------------------------------
// Notifications (Member + Provider 共用)
// -----------------------------------------------------------------------
/**
* 取得通知列表
*
* @OA\Get(
* path="/notifications",
* summary="取得通知列表",
* description="分頁回傳當前使用者的所有通知(Member / Provider 共用)",
* operationId="listNotifications",
* tags={"通知"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="type", type="string", example="booking_confirmed"),
* @OA\Property(property="title", type="string", example="預約已確認"),
* @OA\Property(property="message", type="string", example="您的預約已由教練確認"),
* @OA\Property(property="read_at", type="string", nullable=true, example=null),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
* ),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* )
*/
public function listNotifications()
{
}
/**
* 取得未讀通知數量
*
* @OA\Get(
* path="/notifications/unread-count",
* summary="取得未讀通知數量",
* operationId="getUnreadNotificationCount",
* tags={"通知"},
* security={{"bearerAuth": {}}},
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="unread_count", type="integer", example=3)
* )
* )
* )
*/
public function getUnreadNotificationCount()
{
}
/**
* 標記單一通知為已讀
*
* @OA\Patch(
* path="/notifications/{id}/read",
* summary="標記單一通知為已讀",
* operationId="markNotificationRead",
* tags={"通知"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="標記成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="通知已標記為已讀")
* )
* ),
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function markNotificationRead()
{
}
/**
* 全部通知標記為已讀
*
* @OA\Patch(
* path="/notifications/read-all",
* summary="全部通知標記為已讀",
* operationId="markAllNotificationsRead",
* tags={"通知"},
* security={{"bearerAuth": {}}},
* @OA\Response(
* response=200,
* description="標記成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="所有通知已標記為已讀")
* )
* )
* )
*/
public function markAllNotificationsRead()
{
}
/**
* 刪除通知
*
* @OA\Delete(
* path="/notifications/{id}",
* summary="刪除通知",
* operationId="deleteNotification",
* tags={"通知"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="通知已刪除")
* )
* ),
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteNotification()
{
}
}
+585
View File
@@ -0,0 +1,585 @@
<?php
namespace App\Docs;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(
* name="教練課程",
* description="服務提供者的課程管理"
* )
* @OA\Tag(
* name="教練圖片",
* description="服務提供者的課程圖片管理"
* )
* @OA\Tag(
* name="教練時段",
* description="服務提供者的課程時段管理"
* )
* @OA\Tag(
* name="教練預約",
* description="服務提供者的預約管理"
* )
*/
class ProviderApiDoc
{
// -----------------------------------------------------------------------
// Provider Offers CRUD
// -----------------------------------------------------------------------
/**
* 取得自己的課程列表
*
* @OA\Get(
* path="/provider/offers",
* summary="取得自己的課程列表",
* description="回傳當前服務提供者的所有課程(分頁)",
* operationId="listProviderOffers",
* tags={"教練課程"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/DivingOffer")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* )
*/
public function listProviderOffers()
{
}
/**
* 建立課程
*
* @OA\Post(
* path="/provider/offers",
* summary="建立課程",
* description="服務提供者建立新的潛水課程",
* operationId="createProviderOffer",
* tags={"教練課程"},
* security={{"bearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"title","description","region","price","max_participants"},
* @OA\Property(property="title", type="string", example="基礎開放水域課程"),
* @OA\Property(property="description", type="string", example="適合初學者的 OWD 課程"),
* @OA\Property(property="region", type="string", example="墾丁"),
* @OA\Property(property="price", type="number", format="float", example=8500),
* @OA\Property(property="max_participants", type="integer", example=6),
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"初學","OWD"})
* )
* ),
* @OA\Response(
* response=201,
* description="課程建立成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="課程建立成功"),
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
* )
* ),
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function createProviderOffer()
{
}
/**
* 取得單一課程(Provider 視角)
*
* @OA\Get(
* path="/provider/offers/{id}",
* summary="取得單一課程(Provider 視角)",
* description="取得自己的指定課程,非本人課程回傳 403",
* operationId="getProviderOffer",
* tags={"教練課程"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
* )
* ),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function getProviderOffer()
{
}
/**
* 更新課程
*
* @OA\Put(
* path="/provider/offers/{id}",
* summary="更新課程",
* operationId="updateProviderOffer",
* tags={"教練課程"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="title", type="string", example="進階開放水域課程"),
* @OA\Property(property="description", type="string", example="AOWD 課程"),
* @OA\Property(property="region", type="string", example="小琉球"),
* @OA\Property(property="price", type="number", format="float", example=12000),
* @OA\Property(property="max_participants", type="integer", example=4),
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"進階","AOWD"}),
* @OA\Property(property="is_active", type="boolean", example=true)
* )
* ),
* @OA\Response(
* response=200,
* description="更新成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
* )
* ),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function updateProviderOffer()
{
}
/**
* 刪除課程
*
* @OA\Delete(
* path="/provider/offers/{id}",
* summary="刪除課程",
* operationId="deleteProviderOffer",
* tags={"教練課程"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="課程已刪除")
* )
* ),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteProviderOffer()
{
}
// -----------------------------------------------------------------------
// Provider Offer Images
// -----------------------------------------------------------------------
/**
* 上傳封面圖片
*
* @OA\Post(
* path="/provider/offers/{id}/cover",
* summary="上傳封面圖片",
* description="上傳課程封面圖片(multipart/form-data",
* operationId="uploadOfferCover",
* tags={"教練圖片"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* required={"cover_image"},
* @OA\Property(property="cover_image", type="string", format="binary", description="封面圖片(jpg/png,最大 5MB")
* )
* )
* ),
* @OA\Response(
* response=200,
* description="上傳成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="cover_image_url", type="string", example="https://example.com/covers/1.jpg")
* )
* ),
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function uploadOfferCover()
{
}
/**
* 刪除封面圖片
*
* @OA\Delete(
* path="/provider/offers/{id}/cover",
* summary="刪除封面圖片",
* operationId="deleteOfferCover",
* tags={"教練圖片"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="封面圖片已刪除")
* )
* ),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteOfferCover()
{
}
/**
* 上傳相簿圖片
*
* @OA\Post(
* path="/provider/offers/{id}/images",
* summary="上傳相簿圖片",
* description="新增課程相簿圖片(multipart/form-data,可多張)",
* operationId="uploadOfferImages",
* tags={"教練圖片"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* required={"images[]"},
* @OA\Property(
* property="images[]",
* type="array",
* @OA\Items(type="string", format="binary"),
* description="相簿圖片(每張最大 5MB"
* )
* )
* )
* ),
* @OA\Response(
* response=200,
* description="上傳成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="images", type="array", @OA\Items(type="string"), example={"https://example.com/img/1.jpg"})
* )
* ),
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function uploadOfferImages()
{
}
/**
* 刪除相簿圖片
*
* @OA\Delete(
* path="/provider/images/{id}",
* summary="刪除相簿圖片",
* operationId="deleteOfferImage",
* tags={"教練圖片"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="圖片 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="圖片已刪除")
* )
* ),
* @OA\Response(response=403, description="非本人圖片", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="圖片不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteOfferImage()
{
}
// -----------------------------------------------------------------------
// Provider Schedules
// -----------------------------------------------------------------------
/**
* 取得時段列表
*
* @OA\Get(
* path="/provider/schedules",
* summary="取得時段列表",
* description="回傳服務提供者的所有時段,可依 offer_id 篩選",
* operationId="listProviderSchedules",
* tags={"教練時段"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="offer_id", in="query", required=false, description="課程 ID 篩選", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/CourseSchedule"))
* )
* )
* )
*/
public function listProviderSchedules()
{
}
/**
* 建立時段
*
* @OA\Post(
* path="/provider/schedules",
* summary="建立時段",
* operationId="createProviderSchedule",
* tags={"教練時段"},
* security={{"bearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"offer_id","start_date","end_date","available_slots"},
* @OA\Property(property="offer_id", type="integer", example=1),
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-01"),
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-03"),
* @OA\Property(property="available_slots", type="integer", example=4)
* )
* ),
* @OA\Response(
* response=201,
* description="時段建立成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
* )
* ),
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function createProviderSchedule()
{
}
/**
* 更新時段
*
* @OA\Put(
* path="/provider/schedules/{id}",
* summary="更新時段",
* operationId="updateProviderSchedule",
* tags={"教練時段"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-05"),
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-07"),
* @OA\Property(property="available_slots", type="integer", example=6),
* @OA\Property(property="is_active", type="boolean", example=true)
* )
* ),
* @OA\Response(
* response=200,
* description="更新成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
* )
* ),
* @OA\Response(response=403, description="非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function updateProviderSchedule()
{
}
/**
* 刪除時段
*
* @OA\Delete(
* path="/provider/schedules/{id}",
* summary="刪除時段",
* operationId="deleteProviderSchedule",
* tags={"教練時段"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="刪除成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="時段已刪除")
* )
* ),
* @OA\Response(response=403, description="非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function deleteProviderSchedule()
{
}
// -----------------------------------------------------------------------
// Provider Bookings Management
// -----------------------------------------------------------------------
/**
* 取得收到的預約列表
*
* @OA\Get(
* path="/provider/bookings",
* summary="取得收到的預約列表",
* description="分頁回傳服務提供者收到的所有預約",
* operationId="listProviderBookings",
* tags={"教練預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* )
*/
public function listProviderBookings()
{
}
/**
* 確認預約
*
* @OA\Put(
* path="/provider/bookings/{id}/confirm",
* summary="確認預約",
* operationId="confirmBooking",
* tags={"教練預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="確認成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已確認"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="狀態不允許確認", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function confirmBooking()
{
}
/**
* 拒絕預約
*
* @OA\Put(
* path="/provider/bookings/{id}/reject",
* summary="拒絕預約",
* operationId="rejectBooking",
* tags={"教練預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="拒絕成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已拒絕"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="狀態不允許拒絕", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function rejectBooking()
{
}
/**
* 標記預約完成
*
* @OA\Put(
* path="/provider/bookings/{id}/complete",
* summary="標記預約完成",
* operationId="completeBookingByProvider",
* tags={"教練預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="標記成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已完成"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="狀態不允許完成", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function completeBookingByProvider()
{
}
/**
* 取消預約(Provider
*
* @OA\Put(
* path="/provider/bookings/{id}/cancel",
* summary="取消預約(Provider",
* operationId="cancelBookingByProvider",
* tags={"教練預約"},
* security={{"bearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取消成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="預約已取消"),
* @OA\Property(property="data", ref="#/components/schemas/Booking")
* )
* ),
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
* @OA\Response(response=422, description="狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
* )
*/
public function cancelBookingByProvider()
{
}
}
+247
View File
@@ -0,0 +1,247 @@
<?php
namespace App\Docs;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(
* name="公開課程",
* description="無需認證的公開潛水課程查詢端點"
* )
*
* -----------------------------------------------------------------------
* 共用 Schema 定義
* -----------------------------------------------------------------------
*
* @OA\Schema(
* schema="PaginationMeta",
* type="object",
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="last_page", type="integer", example=5),
* @OA\Property(property="per_page", type="integer", example=15),
* @OA\Property(property="total", type="integer", example=72)
* )
*
* @OA\Schema(
* schema="ApiErrorResponse",
* type="object",
* @OA\Property(property="status", type="boolean", example=false),
* @OA\Property(property="message", type="string", example="操作失敗"),
* @OA\Property(property="errors", type="object", nullable=true)
* )
*
* @OA\Schema(
* schema="DivingOffer",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="provider_id", type="integer", example=3),
* @OA\Property(property="title", type="string", example="基礎開放水域課程"),
* @OA\Property(property="description", type="string", example="適合初學者的 OWD 課程"),
* @OA\Property(property="region", type="string", example="墾丁"),
* @OA\Property(property="price", type="number", format="float", example=8500),
* @OA\Property(property="max_participants", type="integer", example=6),
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"初學","OWD"}),
* @OA\Property(property="cover_image_url", type="string", nullable=true, example="https://example.com/image.jpg"),
* @OA\Property(property="images", type="array", @OA\Items(type="string"), example={}),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z"),
* @OA\Property(property="updated_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
*
* @OA\Schema(
* schema="Review",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="offer_id", type="integer", example=1),
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=4),
* @OA\Property(property="comment", type="string", nullable=true, example="課程非常棒!"),
* @OA\Property(property="is_anonymous", type="boolean", example=false),
* @OA\Property(property="helpful_count", type="integer", example=3),
* @OA\Property(property="has_voted", type="boolean", example=false),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
*
* @OA\Schema(
* schema="CourseSchedule",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="offer_id", type="integer", example=1),
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-01"),
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-03"),
* @OA\Property(property="available_slots", type="integer", example=4),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
*
* @OA\Schema(
* schema="Booking",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="member_id", type="integer", example=5),
* @OA\Property(property="offer_id", type="integer", example=1),
* @OA\Property(property="schedule_id", type="integer", example=2),
* @OA\Property(property="status", type="string", enum={"pending","confirmed","rejected","completed","cancelled"}, example="pending"),
* @OA\Property(property="participants", type="integer", example=2),
* @OA\Property(property="note", type="string", nullable=true, example=""),
* @OA\Property(property="created_at", type="string", example="2025-01-01T00:00:00.000000Z"),
* @OA\Property(property="updated_at", type="string", example="2025-01-01T00:00:00.000000Z")
* )
*/
class PublicApiDoc
{
/**
* 查詢潛水課程列表
*
* @OA\Get(
* path="/diving-offers",
* summary="查詢潛水課程列表",
* description="支援關鍵字、地區、標籤篩選,回傳分頁結果",
* operationId="listDivingOffers",
* tags={"公開課程"},
* @OA\Parameter(name="q", in="query", description="關鍵字搜尋", @OA\Schema(type="string")),
* @OA\Parameter(name="region", in="query", description="地區篩選", @OA\Schema(type="string", example="墾丁")),
* @OA\Parameter(name="tag", in="query", description="標籤篩選", @OA\Schema(type="string", example="OWD")),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數(預設 15,最大 50", @OA\Schema(type="integer", default=15, maximum=50)),
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Response(
* response=200,
* description="查詢成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/DivingOffer")
* ),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* )
*/
public function listDivingOffers()
{
}
/**
* 取得單一潛水課程
*
* @OA\Get(
* path="/diving-offers/{id}",
* summary="取得單一潛水課程",
* description="回傳課程詳情,包含封面圖片與相簿",
* operationId="getDivingOffer",
* tags={"公開課程"},
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="課程 ID",
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
* )
* ),
* @OA\Response(
* response=404,
* description="課程不存在",
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
* )
* )
*/
public function getDivingOffer()
{
}
/**
* 取得課程評價列表
*
* @OA\Get(
* path="/diving-offers/{id}/reviews",
* summary="取得課程評價列表",
* description="回傳評分摘要、分頁評價列表與分頁 meta",
* operationId="listOfferReviews",
* tags={"公開課程"},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Parameter(name="sort", in="query", description="排序方式(newest / helpful", @OA\Schema(type="string", enum={"newest","helpful"}, default="newest")),
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="per_page", in="query", description="每頁筆數(預設 15,最大 50", @OA\Schema(type="integer", default=15, maximum=50)),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(
* property="summary",
* type="object",
* @OA\Property(property="average_rating", type="number", format="float", example=4.2),
* @OA\Property(property="total_reviews", type="integer", example=24),
* @OA\Property(property="distribution", type="object",
* @OA\Property(property="1", type="integer", example=1),
* @OA\Property(property="2", type="integer", example=2),
* @OA\Property(property="3", type="integer", example=3),
* @OA\Property(property="4", type="integer", example=8),
* @OA\Property(property="5", type="integer", example=10)
* )
* ),
* @OA\Property(
* property="reviews",
* type="array",
* @OA\Items(ref="#/components/schemas/Review")
* ),
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
* )
* )
* ),
* @OA\Response(
* response=404,
* description="課程不存在",
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
* )
* )
*/
public function listOfferReviews()
{
}
/**
* 取得課程時段列表
*
* @OA\Get(
* path="/diving-offers/{id}/schedules",
* summary="取得課程時段列表",
* description="回傳指定課程的所有可用時段",
* operationId="listOfferSchedules",
* tags={"公開課程"},
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="取得成功",
* @OA\JsonContent(
* @OA\Property(property="status", type="boolean", example=true),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/CourseSchedule")
* )
* )
* ),
* @OA\Response(
* response=404,
* description="課程不存在",
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
* )
* )
*/
public function listOfferSchedules()
{
}
}
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-24
@@ -0,0 +1,63 @@
## Context
現有 `app/Docs/AuthApiDoc.php` 已建立 Swagger 基礎設定(`@OA\Info``@OA\Server``@OA\SecurityScheme`)並文件化 15 個 Auth 端點。其餘 61 個端點分散在 8 個 Controller 中,沒有任何 `@OA` 標注。`darkaonline/l5-swagger` 已安裝,執行 `php artisan l5-swagger:generate` 即可重新產生 JSON。
## Goals / Non-Goals
**Goals:**
- 補齊所有 61 個未文件化端點的 `@OA` 標注
- 定義共用 Schema(分頁 meta、統一錯誤格式、DivingOffer、Review、Booking 等)
- 文件依模組分檔,每個新檔對應一個 `app/Docs/*Doc.php`
- 每個端點包含:summary、parameters、request bodyPOST/PUT)、response200/201/400/401/403/422
**Non-Goals:**
- 不修改任何 Controller / Route / Model 邏輯
- 不為尚未實作的端點(金流、訂閱)補文件
## Decisions
### 決策 1:分檔策略 — 依模組建立獨立 Doc 類別
**選擇**:每個模組一個 `app/Docs/*Doc.php`
| 檔案 | 涵蓋端點 |
|------|---------|
| `AuthApiDoc.php` | **修正路徑錯誤**`/register/member``/member/register` 等);補 `POST /logout``GET /user` |
| `PublicApiDoc.php` | 公開端點(diving-offers、reviews、schedules |
| `MemberApiDoc.php` | Member bookings、reviews、helpful、notifications |
| `ProviderApiDoc.php` | Provider offers、images、schedules、bookings |
| `AdminApiDoc.php` | Admin stats、users、offers、bookings、reviews |
| `AuthSupplementDoc.php` | **僅** Google OAuth 2 個端點(`SocialAuthController` |
**理由**:單一大檔難以維護;按模組分檔讓每個 Doc 類別職責清晰,對應 Controller 結構
**替代方案**:直接在 Controller 方法上加 `@OA` → 拒絕,Controller 文件與業務邏輯混雜,讀性差
### 決策 2:共用 Schema 集中定義在 `PublicApiDoc.php`
共用 Schema`DivingOfferSchema``ReviewSchema``BookingSchema``PaginationMeta``ApiErrorResponse`)集中放在 `PublicApiDoc.php` 頂層 `@OA\Schema` 標注。
**理由**l5-swagger 掃描所有 `app/` 目錄,Schema 定義位置不影響其他檔案引用;放在 Public 層語意上最自然(核心資料結構)
### 決策 3Security 標注策略
- 公開端點:不加 `security`
- Member 端點:`security={{"bearerAuth": {}}}`(現有 scheme
- Provider 端點:`security={{"bearerAuth": {}}}`
- Admin 端點:`security={{"bearerAuth": {}}}`
三個 role 共用同一個 `bearerAuth` scheme,由後端 middleware 區分角色,不需要在 Swagger 定義三個不同 scheme。
## Risks / Trade-offs
- **[Risk] 手寫標注與實際 response 不同步** → 以實際 Controller 程式碼為準手寫;未來有 API 變更時需同步更新 Doc 檔
- **[Risk] Schema 巢狀複雜導致 Swagger UI 渲染慢** → 分頁 response 使用 `allOf` 組合,避免深層巢狀
## Migration Plan
1. 依序建立 5 個新 Doc 檔案
2. 在容器內執行 `php artisan l5-swagger:generate`
3. 開啟 `http://localhost:8080/api/documentation` 確認 UI 正確渲染
4. 確認所有 tag 與端點均出現在 Swagger UI
**Rollback**:直接刪除新增的 Doc 檔案,重新 generate 即可回復到只有 Auth 端點的狀態
@@ -0,0 +1,37 @@
## Why
目前 Swagger 文件僅涵蓋 15 個 Auth 端點,其餘 61 個端點(課程、評價、預約、通知、圖片上傳、Coach Portal、Admin Panel)完全未文件化,導致前後端協作與第三方整合缺乏規格依據。現有 `darkaonline/l5-swagger` 套件已安裝,補全文件的邊際成本低。
## What Changes
- 為所有未文件化端點補上 `@OA` PHPDoc 標注(`app/Docs/` 目錄下依模組分檔)
- 定義完整的 Request / Response Schema(含分頁 meta、錯誤格式)
- 補全 Swagger Security 設定(Bearer token per-role
- 重新產生 `storage/api-docs/api-docs.json`
**端點範圍(新增 61 個):**
- Public APIGET diving-offers 列表/詳情、reviews、schedules4
- Member APIbookings CRUD、reviews CRUD、helpful 投票、notifications10
- Provider APIoffers CRUD + 圖片、schedules CRUD、bookings 管理(19
- Admin APIstats、members、providers、offers、bookings、reviews 管理(17
- Auth 補全:Google OAuth、change-password3 roles)(7
## Capabilities
### New Capabilities
- `swagger-public-api`:公開端點 Swagger 文件(diving-offers、reviews、schedules
- `swagger-member-api`Member 端點 Swagger 文件(bookings、reviews、notifications
- `swagger-provider-api`Provider 端點 Swagger 文件(offers、images、schedules、bookings
- `swagger-admin-api`Admin 端點 Swagger 文件(stats、user/offer/booking/review 管理)
- `swagger-auth-supplement`:補全 Auth 端點(Google OAuth、change-password
### Modified Capabilities
(無,本 change 純為文件補全,不變更任何 API 行為)
## Impact
- 新增:`app/Docs/PublicApiDoc.php``MemberApiDoc.php``ProviderApiDoc.php``AdminApiDoc.php``AuthSupplementDoc.php`
- 更新:`storage/api-docs/api-docs.json`(自動產生)
- 不影響任何 Controller、Model、Route、Migration
@@ -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}`
@@ -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 亦有定義
@@ -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_idresponse 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、comment403 資格驗證失敗;422 重複評價)
- `PUT /member/reviews/{id}`request: rating?、comment?403 非本人)
- `DELETE /member/reviews/{id}`403 非本人)
- `POST /reviews/{id}/helpful`toggleresponse: 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}`
@@ -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: imageresponse: 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`
@@ -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 schema404 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
@@ -0,0 +1,44 @@
## 1. 修正 AuthApiDoc.php 既有錯誤
- [x] 1.1 [後端] `AuthApiDoc.php` 修正所有路徑錯位:`/register/member``/member/register``/login/member``/member/login``/logout/member``/member/logout``/profile/member``/member/profile`provider、admin 同樣修正)
- [x] 1.2 [後端] `AuthApiDoc.php` 修正 change-password 路徑:`/password/member``/member/change-password``/password/provider``/provider/change-password``/password/admin``/admin/change-password`
- [x] 1.3 [後端] `AuthApiDoc.php` 補上 `POST /logout`(通用登出)與 `GET /user`(取得當前使用者)
## 2. AuthSupplementDocGoogle OAuth 專用)
- [x] 2.1 [後端] 建立 `app/Docs/AuthSupplementDoc.php`,補上 `GET /auth/google/redirect`response: redirect_url)與 `GET /auth/google/callback`response: token + user)兩個端點
## 3. 共用 Schema 與 PublicApiDoc
- [x] 3.1 [後端] 建立 `app/Docs/PublicApiDoc.php`,定義共用 Schema`DivingOffer``Review``CourseSchedule``Booking``PaginationMeta``ApiErrorResponse`
- [x] 3.2 [後端] `PublicApiDoc.php` 補上 `GET /diving-offers`query: q、region、tag、per_page、pageresponse: DivingOffer 分頁)
- [x] 3.3 [後端] `PublicApiDoc.php` 補上 `GET /diving-offers/{id}`response: DivingOffer 含 cover_image_url、images 陣列;404
- [x] 3.4 [後端] `PublicApiDoc.php` 補上 `GET /diving-offers/{id}/reviews`query: sort、page、per_pageresponse: summary + reviews 分頁 + meta
- [x] 3.5 [後端] `PublicApiDoc.php` 補上 `GET /diving-offers/{id}/schedules`response: CourseSchedule 陣列)
## 4. MemberApiDoc
- [x] 4.1 [後端] 建立 `app/Docs/MemberApiDoc.php`,補上 Member bookings`POST /member/bookings`201)、`GET /member/bookings`(分頁)、`GET /member/bookings/{id}``DELETE /member/bookings/{id}`
- [x] 4.2 [後端] `MemberApiDoc.php` 補上 Member reviews`POST /member/reviews`403/422)、`PUT /member/reviews/{id}`403)、`DELETE /member/reviews/{id}`403
- [x] 4.3 [後端] `MemberApiDoc.php` 補上 `POST /reviews/{id}/helpful`response: helpful_count、has_voted
- [x] 4.4 [後端] `MemberApiDoc.php` 補上 notifications`GET /notifications`(分頁)、`GET /notifications/unread-count``PATCH /notifications/{id}/read``PATCH /notifications/read-all``DELETE /notifications/{id}`
## 5. ProviderApiDoc
- [x] 5.1 [後端] 建立 `app/Docs/ProviderApiDoc.php`,補上 Provider offers CRUD`GET /provider/offers``POST /provider/offers``GET /provider/offers/{id}`403)、`PUT /provider/offers/{id}``DELETE /provider/offers/{id}`
- [x] 5.2 [後端] `ProviderApiDoc.php` 補上圖片管理:`POST /provider/offers/{id}/cover`multipart)、`DELETE /provider/offers/{id}/cover``POST /provider/offers/{id}/images``DELETE /provider/images/{id}`
- [x] 5.3 [後端] `ProviderApiDoc.php` 補上 schedules`GET /provider/schedules`query: offer_id?)、`POST /provider/schedules``PUT /provider/schedules/{id}``DELETE /provider/schedules/{id}`
- [x] 5.4 [後端] `ProviderApiDoc.php` 補上 bookings 管理:`GET /provider/bookings``PUT /provider/bookings/{id}/confirm``PUT /provider/bookings/{id}/reject``PUT /provider/bookings/{id}/complete``PUT /provider/bookings/{id}/cancel`
## 6. AdminApiDoc
- [x] 6.1 [後端] 建立 `app/Docs/AdminApiDoc.php`,補上 `GET /admin/stats`response: total_members/providers/offers403
- [x] 6.2 [後端] `AdminApiDoc.php` 補上會員管理:`GET /admin/members`(分頁)、`GET /admin/members/{id}``PUT /admin/members/{id}/toggle-active``GET /admin/check-member/{id}`
- [x] 6.3 [後端] `AdminApiDoc.php` 補上教練管理:`GET /admin/providers`(分頁)、`GET /admin/providers/{id}``PUT /admin/providers/{id}/toggle-active``PUT /admin/providers/{id}/toggle-verified``GET /admin/check-provider/{id}`
- [x] 6.4 [後端] `AdminApiDoc.php` 補上課程/預約/評價管理:`GET /admin/offers``DELETE /admin/offers/{id}``GET /admin/bookings``PUT /admin/bookings/{id}/complete``GET /admin/reviews`per_page 最大 100)、`DELETE /admin/reviews/{id}`
## 7. 驗證
- [x] 7.1 [整合測試] 執行 `docker exec cfdive-app php artisan l5-swagger:generate`,確認無 parse error
- [x] 7.2 [整合測試] 開啟 `http://localhost:8080/api/documentation`,展開各 tag 確認路徑正確(`/member/register` 而非 `/register/member`
- [x] 7.3 [整合測試] 確認端點總數 73(原始估計 76 因 AuthApiDoc 實際有 18 個端點非 15 個),`GET /diving-offers/{id}/reviews` 顯示分頁 meta schema
+40
View File
@@ -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}`
@@ -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 亦有定義
+33
View File
@@ -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_idresponse 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、comment403 資格驗證失敗;422 重複評價)
- `PUT /member/reviews/{id}`request: rating?、comment?403 非本人)
- `DELETE /member/reviews/{id}`403 非本人)
- `POST /reviews/{id}/helpful`toggleresponse: 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}`
@@ -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: imageresponse: 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`
+30
View File
@@ -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 schema404 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
File diff suppressed because it is too large Load Diff