diff --git a/README.md b/README.md index af5b7d2..fb40402 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,18 @@ ## API 文件 -Swagger 文件涵蓋公開、會員、教練、管理員與認證端點,包含: -- 認證(Email + Google OAuth) +Swagger UI(由 l5-swagger 生成)涵蓋所有端點,包含: +- 認證(Email + Google OAuth)、Token 刷新 - 公開課程查詢 -- 會員預約 / 訊息 / 評價 / 通知 -- 教練課程 / 時段 / 預約管理 +- 會員預約 / 即時訊息 / 評價 / 通知 +- 教練課程 / 時段 / 預約管理 / 資格驗證申請(證照送審) - 管理員後台 +| 環境 | Swagger UI URL | +|------|----------------| +| 本地 | http://localhost:8080/api/documentation | +| JSON | http://localhost:8080/docs/api-docs.json | + --- ## 健康檢查 @@ -85,6 +90,7 @@ GET /health | 服務 | URL | |------|-----| | API / 前端 | http://localhost:8080 | +| Swagger UI | http://localhost:8080/api/documentation | | phpMyAdmin | http://localhost:8081 | | Mailpit | http://localhost:8025 | | Reverb WebSocket | ws://localhost:8085 | diff --git a/app/Docs/AuthApiDoc.php b/app/Docs/AuthApiDoc.php index 341c921..7679349 100644 --- a/app/Docs/AuthApiDoc.php +++ b/app/Docs/AuthApiDoc.php @@ -207,9 +207,41 @@ class AuthApiDoc { } + /** + * 會員 Token 刷新 + * + * @OA\Post( + * path="/member/refresh", + * summary="會員 Token 刷新", + * description="撤銷當前 Token 並發放新的 Bearer token(不需要重新登入)", + * operationId="refreshMember", + * tags={"會員"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="刷新成功", + * @OA\JsonContent( + * @OA\Property(property="status", type="boolean", example=true), + * @OA\Property(property="token", type="string", example="24|newtoken..."), + * @OA\Property(property="token_type", type="string", example="Bearer") + * ) + * ), + * @OA\Response( + * response=401, + * description="未認證", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="Unauthenticated.") + * ) + * ) + * ) + */ + public function refreshMember() + { + } + /** * 會員登出 - * + * * @OA\Post( * path="/member/logout", * summary="會員登出", @@ -580,9 +612,41 @@ class AuthApiDoc { } + /** + * 服務提供者 Token 刷新 + * + * @OA\Post( + * path="/provider/refresh", + * summary="服務提供者 Token 刷新", + * description="撤銷當前 Token 並發放新的 Bearer token", + * operationId="refreshProvider", + * tags={"服務提供者"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="刷新成功", + * @OA\JsonContent( + * @OA\Property(property="status", type="boolean", example=true), + * @OA\Property(property="token", type="string", example="25|newtoken..."), + * @OA\Property(property="token_type", type="string", example="Bearer") + * ) + * ), + * @OA\Response( + * response=401, + * description="未認證", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="Unauthenticated.") + * ) + * ) + * ) + */ + public function refreshProvider() + { + } + /** * 服務提供者登出 - * + * * @OA\Post( * path="/provider/logout", * summary="服務提供者登出", @@ -906,9 +970,41 @@ class AuthApiDoc { } + /** + * 管理員 Token 刷新 + * + * @OA\Post( + * path="/admin/refresh", + * summary="管理員 Token 刷新", + * description="撤銷當前 Token 並發放新的 Bearer token", + * operationId="refreshAdmin", + * tags={"管理員"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="刷新成功", + * @OA\JsonContent( + * @OA\Property(property="status", type="boolean", example=true), + * @OA\Property(property="token", type="string", example="26|newtoken..."), + * @OA\Property(property="token_type", type="string", example="Bearer") + * ) + * ), + * @OA\Response( + * response=401, + * description="未認證", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="Unauthenticated.") + * ) + * ) + * ) + */ + public function refreshAdmin() + { + } + /** * 管理員登出 - * + * * @OA\Post( * path="/admin/logout", * summary="管理員登出", diff --git a/app/Docs/MemberApiDoc.php b/app/Docs/MemberApiDoc.php index 43a03ef..d310bd2 100644 --- a/app/Docs/MemberApiDoc.php +++ b/app/Docs/MemberApiDoc.php @@ -17,6 +17,10 @@ use OpenApi\Annotations as OA; * name="通知", * description="站內通知管理(Member / Provider 共用)" * ) + * @OA\Tag( + * name="即時訊息", + * description="預約聊天室訊息(Member / Provider 共用,僅限 confirmed / completed 預約的參與方)" + * ) */ class MemberApiDoc { @@ -277,6 +281,152 @@ class MemberApiDoc { } + // ----------------------------------------------------------------------- + // Booking Messages(即時訊息,Member + Provider 共用) + // ----------------------------------------------------------------------- + + /** + * 取得各預約未讀訊息數 + * + * @OA\Get( + * path="/bookings/messages/unread-counts", + * summary="取得各預約未讀訊息數", + * description="回傳當前使用者每個預約中對方發送的未讀訊息數量(Key 為 booking_id)", + * operationId="getMessageUnreadCounts", + * tags={"即時訊息"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="取得成功", + * @OA\JsonContent( + * @OA\Property(property="status", type="boolean", example=true), + * @OA\Property( + * property="data", + * type="object", + * description="Key 為 booking_id(string),value 為未讀數(integer)", + * example={"12": 3, "17": 1} + * ) + * ) + * ) + * ) + */ + public function getMessageUnreadCounts() + { + } + + /** + * 取得預約訊息列表 + * + * @OA\Get( + * path="/bookings/{booking}/messages", + * summary="取得預約訊息列表", + * description="回傳指定預約的所有訊息(依時間升冪排列),僅限預約參與方存取,且預約狀態需為 confirmed 或 completed", + * operationId="listBookingMessages", + * tags={"即時訊息"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter(name="booking", 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( + * type="object", + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="booking_id", type="integer", example=12), + * @OA\Property(property="sender_type", type="string", enum={"member","provider"}, example="member"), + * @OA\Property(property="message", type="string", nullable=true, example="請問需要自備潛水裝備嗎?"), + * @OA\Property(property="image_url", type="string", nullable=true, example=null), + * @OA\Property(property="read_at", type="string", nullable=true, example=null), + * @OA\Property(property="created_at", type="string", example="2025-07-01T10:00:00.000000Z") + * ) + * ) + * ) + * ), + * @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")), + * @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")) + * ) + */ + public function listBookingMessages() + { + } + + /** + * 發送預約訊息 + * + * @OA\Post( + * path="/bookings/{booking}/messages", + * summary="發送預約訊息", + * description="向指定預約的對方發送文字訊息或圖片(multipart/form-data)", + * operationId="sendBookingMessage", + * tags={"即時訊息"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")), + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="multipart/form-data", + * @OA\Schema( + * @OA\Property(property="message", type="string", nullable=true, example="請問需要自備潛水裝備嗎?"), + * @OA\Property(property="image", type="string", format="binary", nullable=true, description="圖片(jpg/png,message 與 image 至少擇一)") + * ) + * ) + * ), + * @OA\Response( + * response=201, + * description="訊息已送出", + * @OA\JsonContent( + * @OA\Property(property="status", type="boolean", example=true), + * @OA\Property( + * property="data", + * type="object", + * @OA\Property(property="id", type="integer", example=42), + * @OA\Property(property="booking_id", type="integer", example=12), + * @OA\Property(property="sender_type", type="string", example="member"), + * @OA\Property(property="message", type="string", nullable=true, example="請問需要自備潛水裝備嗎?"), + * @OA\Property(property="image_url", type="string", nullable=true, example=null), + * @OA\Property(property="read_at", type="string", nullable=true, example=null), + * @OA\Property(property="created_at", type="string", example="2025-07-01T10:05:00.000000Z") + * ) + * ) + * ), + * @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")), + * @OA\Response(response=422, description="message 與 image 皆為空", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")) + * ) + */ + public function sendBookingMessage() + { + } + + /** + * 標記訊息為已讀 + * + * @OA\Post( + * path="/bookings/{booking}/messages/read", + * summary="標記訊息為已讀", + * description="將指定預約中對方發送的所有未讀訊息批次標記為已讀", + * operationId="markMessagesRead", + * tags={"即時訊息"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter(name="booking", 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 markMessagesRead() + { + } + // ----------------------------------------------------------------------- // Notifications (Member + Provider 共用) // ----------------------------------------------------------------------- diff --git a/app/Docs/ProviderApiDoc.php b/app/Docs/ProviderApiDoc.php index 8ad6b66..1c7a09b 100644 --- a/app/Docs/ProviderApiDoc.php +++ b/app/Docs/ProviderApiDoc.php @@ -21,9 +21,150 @@ use OpenApi\Annotations as OA; * name="教練預約", * description="服務提供者的預約管理" * ) + * @OA\Tag( + * name="教練驗證", + * description="服務提供者的資格驗證申請(證照送審)" + * ) */ class ProviderApiDoc { + // ----------------------------------------------------------------------- + // Provider Verification (證照送審) + // ----------------------------------------------------------------------- + + /** + * 取得驗證申請狀態 + * + * @OA\Get( + * path="/provider/verification", + * summary="取得驗證申請狀態", + * description="回傳當前服務提供者的驗證狀態與已上傳的證照清單", + * operationId="getProviderVerification", + * tags={"教練驗證"}, + * 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="verification_status", type="string", enum={"unsubmitted","pending","approved","rejected"}, example="pending"), + * @OA\Property(property="rejection_reason", type="string", nullable=true, example=null), + * @OA\Property( + * property="certifications", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="url", type="string", example="http://localhost:8080/storage/providers/5/certifications/uuid.jpg") + * ) + * ) + * ) + * ) + * ), + * @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")) + * ) + */ + public function getProviderVerification() + { + } + + /** + * 上傳證照圖片 + * + * @OA\Post( + * path="/provider/verification/certifications", + * summary="上傳證照圖片", + * description="上傳一張證照圖片(multipart/form-data),未送審前可多次上傳", + * operationId="uploadCertification", + * tags={"教練驗證"}, + * security={{"bearerAuth": {}}}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="multipart/form-data", + * @OA\Schema( + * required={"certification"}, + * @OA\Property(property="certification", 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="data", + * type="object", + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="url", type="string", example="http://localhost:8080/storage/providers/5/certifications/uuid.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 uploadCertification() + { + } + + /** + * 刪除證照圖片 + * + * @OA\Delete( + * path="/provider/verification/certifications/{id}", + * summary="刪除證照圖片", + * description="刪除指定的已上傳證照,已送審(pending/approved)狀態不可刪除", + * operationId="deleteCertification", + * 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 deleteCertification() + { + } + + /** + * 送出驗證申請 + * + * @OA\Post( + * path="/provider/verification/submit", + * summary="送出驗證申請", + * description="將驗證狀態由 unsubmitted/rejected 轉為 pending,至少需有一張已上傳的證照", + * operationId="submitVerification", + * 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=422, description="尚未上傳證照或當前狀態不可送審", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")), + * @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")) + * ) + */ + public function submitVerification() + { + } + // ----------------------------------------------------------------------- // Provider Offers CRUD // ----------------------------------------------------------------------- diff --git a/config/l5-swagger.php b/config/l5-swagger.php index d7810ff..e01a4a9 100644 --- a/config/l5-swagger.php +++ b/config/l5-swagger.php @@ -5,7 +5,7 @@ return [ 'documentations' => [ 'default' => [ 'api' => [ - 'title' => 'L5 Swagger UI', + 'title' => 'CFDive Platform API', ], 'routes' => [