Merge pull request 'chore:新增 swagger-api-docs-completion change 規劃' (#3) from feature/swagger-api-docs into master
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -1,4 +1,49 @@
|
||||

|
||||

|
||||

|
||||
# 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)
|
||||
- 公開課程查詢
|
||||
- 會員預約 / 評價 / 通知
|
||||
- 教練課程 / 時段 / 預約管理
|
||||
- 管理員後台
|
||||
|
||||
@@ -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
@@ -35,6 +35,10 @@ use OpenApi\Annotations as OA;
|
||||
* name="管理員",
|
||||
* description="管理員相關操作"
|
||||
* )
|
||||
* @OA\Tag(
|
||||
* name="認證",
|
||||
* description="通用認證操作(登出、取得當前使用者)"
|
||||
* )
|
||||
*/
|
||||
class AuthApiDoc
|
||||
{
|
||||
@@ -42,7 +46,7 @@ class AuthApiDoc
|
||||
* 會員註冊
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/register/member",
|
||||
* path="/member/register",
|
||||
* summary="會員註冊",
|
||||
* description="建立新的會員帳號",
|
||||
* operationId="registerMember",
|
||||
@@ -119,7 +123,7 @@ class AuthApiDoc
|
||||
* 會員登入
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/login/member",
|
||||
* path="/member/login",
|
||||
* summary="會員登入",
|
||||
* description="會員帳號登入系統",
|
||||
* operationId="loginMember",
|
||||
@@ -207,7 +211,7 @@ class AuthApiDoc
|
||||
* 會員登出
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/logout/member",
|
||||
* path="/member/logout",
|
||||
* summary="會員登出",
|
||||
* description="會員登出系統並撤銷當前令牌",
|
||||
* operationId="logoutMember",
|
||||
@@ -241,7 +245,7 @@ class AuthApiDoc
|
||||
* 取得會員個人資料
|
||||
*
|
||||
* @OA\Get(
|
||||
* path="/profile/member",
|
||||
* path="/member/profile",
|
||||
* summary="取得會員個人資料",
|
||||
* description="取得當前登入會員的個人資料",
|
||||
* operationId="memberProfile",
|
||||
@@ -303,7 +307,7 @@ class AuthApiDoc
|
||||
* 更新會員個人資料
|
||||
*
|
||||
* @OA\Put(
|
||||
* path="/profile/member",
|
||||
* path="/member/profile",
|
||||
* summary="更新會員個人資料",
|
||||
* description="更新當前登入會員的個人資料",
|
||||
* operationId="updateMemberProfile",
|
||||
@@ -383,8 +387,8 @@ class AuthApiDoc
|
||||
/**
|
||||
* 修改會員密碼
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/password/member",
|
||||
* @OA\Put(
|
||||
* path="/member/change-password",
|
||||
* summary="修改會員密碼",
|
||||
* description="修改當前登入會員的密碼",
|
||||
* operationId="changeMemberPassword",
|
||||
@@ -444,7 +448,7 @@ class AuthApiDoc
|
||||
* 服務提供者註冊
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/register/provider",
|
||||
* path="/provider/register",
|
||||
* summary="服務提供者註冊",
|
||||
* description="建立新的服務提供者帳號",
|
||||
* operationId="registerProvider",
|
||||
@@ -498,7 +502,7 @@ class AuthApiDoc
|
||||
* 服務提供者登入
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/login/provider",
|
||||
* path="/provider/login",
|
||||
* summary="服務提供者登入",
|
||||
* description="服務提供者帳號登入系統",
|
||||
* operationId="loginProvider",
|
||||
@@ -580,7 +584,7 @@ class AuthApiDoc
|
||||
* 服務提供者登出
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/logout/provider",
|
||||
* path="/provider/logout",
|
||||
* summary="服務提供者登出",
|
||||
* description="服務提供者登出系統並撤銷當前令牌",
|
||||
* operationId="logoutProvider",
|
||||
@@ -614,7 +618,7 @@ class AuthApiDoc
|
||||
* 取得服務提供者資料
|
||||
*
|
||||
* @OA\Get(
|
||||
* path="/profile/provider",
|
||||
* path="/provider/profile",
|
||||
* summary="取得服務提供者資料",
|
||||
* description="取得當前登入服務提供者的資料",
|
||||
* operationId="providerProfile",
|
||||
@@ -678,7 +682,7 @@ class AuthApiDoc
|
||||
* 更新服務提供者資料
|
||||
*
|
||||
* @OA\Put(
|
||||
* path="/profile/provider",
|
||||
* path="/provider/profile",
|
||||
* summary="更新服務提供者資料",
|
||||
* description="更新當前登入服務提供者的資料",
|
||||
* operationId="updateProviderProfile",
|
||||
@@ -764,8 +768,8 @@ class AuthApiDoc
|
||||
/**
|
||||
* 修改服務提供者密碼
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/password/provider",
|
||||
* @OA\Put(
|
||||
* path="/provider/change-password",
|
||||
* summary="修改服務提供者密碼",
|
||||
* description="修改當前登入服務提供者的密碼",
|
||||
* operationId="changeProviderPassword",
|
||||
@@ -825,7 +829,7 @@ class AuthApiDoc
|
||||
* 管理員註冊
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/register/admin",
|
||||
* path="/admin/register",
|
||||
* summary="管理員註冊",
|
||||
* description="建立新的管理員帳號",
|
||||
* operationId="registerAdmin",
|
||||
@@ -877,7 +881,7 @@ class AuthApiDoc
|
||||
* 管理員登入
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/login/admin",
|
||||
* path="/admin/login",
|
||||
* summary="管理員登入",
|
||||
* description="管理員帳號登入系統",
|
||||
* operationId="loginAdmin",
|
||||
@@ -958,7 +962,7 @@ class AuthApiDoc
|
||||
* 管理員登出
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/logout/admin",
|
||||
* path="/admin/logout",
|
||||
* summary="管理員登出",
|
||||
* description="管理員登出系統並撤銷當前令牌",
|
||||
* operationId="logoutAdmin",
|
||||
@@ -992,7 +996,7 @@ class AuthApiDoc
|
||||
* 取得管理員個人資料
|
||||
*
|
||||
* @OA\Get(
|
||||
* path="/profile/admin",
|
||||
* path="/admin/profile",
|
||||
* summary="取得管理員個人資料",
|
||||
* description="取得當前登入管理員的個人資料",
|
||||
* operationId="adminProfile",
|
||||
@@ -1030,7 +1034,7 @@ class AuthApiDoc
|
||||
* 更新管理員個人資料
|
||||
*
|
||||
* @OA\Put(
|
||||
* path="/profile/admin",
|
||||
* path="/admin/profile",
|
||||
* summary="更新管理員個人資料",
|
||||
* description="更新當前登入管理員的個人資料",
|
||||
* operationId="updateAdminProfile",
|
||||
@@ -1087,8 +1091,8 @@ class AuthApiDoc
|
||||
/**
|
||||
* 修改管理員密碼
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/password/admin",
|
||||
* @OA\Put(
|
||||
* path="/admin/change-password",
|
||||
* summary="修改管理員密碼",
|
||||
* description="修改當前登入管理員的密碼",
|
||||
* operationId="changeAdminPassword",
|
||||
@@ -1143,4 +1147,71 @@ class AuthApiDoc
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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 body(POST/PUT)、response(200/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 層語意上最自然(核心資料結構)
|
||||
|
||||
### 決策 3:Security 標注策略
|
||||
|
||||
- 公開端點:不加 `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 API:GET diving-offers 列表/詳情、reviews、schedules(4)
|
||||
- Member API:bookings CRUD、reviews CRUD、helpful 投票、notifications(10)
|
||||
- Provider API:offers CRUD + 圖片、schedules CRUD、bookings 管理(19)
|
||||
- Admin API:stats、members、providers、offers、bookings、reviews 管理(17)
|
||||
- Auth 補全:Google OAuth、change-password(3 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
|
||||
+40
@@ -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}`
|
||||
+21
@@ -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
@@ -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}`
|
||||
+43
@@ -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`
|
||||
+30
@@ -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)
|
||||
@@ -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. AuthSupplementDoc(Google 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、page;response: 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_page;response: 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/offers;403)
|
||||
- [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
|
||||
@@ -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_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}`
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
+4311
-18
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user