Compare commits

...

8 Commits

Author SHA1 Message Date
a620906209 d1fe3d4138 Merge pull request 'docs(readme): 服務 URL 表格補上生產環境對應網址' (#54) from feat/swagger-coverage-gaps into master
Deploy Production / deploy (push) Successful in 13s
Run Tests / test (push) Successful in 29s
Reviewed-on: #54
2026-06-23 08:05:36 +00:00
a620906209 0bffa909e2 docs(readme): 服務 URL 表格補上生產環境對應網址
Run Tests / test (pull_request) Successful in 32s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 16:03:11 +08:00
a620906209 9222bf17fc Merge pull request 'fix(swagger): 修正 VPS 無法顯示 Swagger UI 的問題' (#53) from feat/swagger-coverage-gaps into master
Deploy Production / deploy (push) Successful in 17s
Run Tests / test (push) Successful in 33s
Reviewed-on: #53
2026-06-23 07:27:32 +00:00
a620906209 b218fa0964 fix(swagger): 修正 VPS 無法顯示 Swagger UI 的問題
Run Tests / test (pull_request) Successful in 33s
問題根因:
1. deploy.yml 未執行 l5-swagger:generate,VPS 上不存在 api-docs.json
2. @OA\Server 使用相對路徑 "/api",Swagger UI Try it out 無法正確打到 VPS API

修正:
- @OA\Server 改用 L5_SWAGGER_CONST_HOST 常數,透過 .env 各環境設定
- deploy.yml 在 config:cache 後補上 l5-swagger:generate 步驟
- .env.example 補上 L5_SWAGGER_CONST_HOST 與 L5_SWAGGER_GENERATE_ALWAYS 說明

VPS .env 需手動加入:
L5_SWAGGER_CONST_HOST=https://api.hank-space.com/api

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 15:23:18 +08:00
a620906209 a7e8468613 Merge pull request 'docs(swagger): 補全缺漏端點標註 + 更新 README 文件 URL' (#52) from feat/swagger-coverage-gaps into master
Deploy Production / deploy (push) Successful in 15s
Run Tests / test (push) Successful in 31s
Reviewed-on: #52
2026-06-23 07:09:07 +00:00
a620906209 60ff56a571 docs(swagger): 補全缺漏端點標註 + 更新 README 文件 URL
Run Tests / test (pull_request) Successful in 32s
新增 11 個先前缺漏的 @OA 標註:
- Token Refresh(Member / Provider / Admin)
- Provider 資格驗證申請(show / upload / delete cert / submit)
- 即時訊息聊天室(unread-counts / list / send / mark-read)

其他:
- l5-swagger.php 標題改為 CFDive Platform API
- README 補充 Swagger UI URL 與 JSON 端點位置

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 15:08:15 +08:00
a620906209 4afe7b5fd5 Merge pull request 'chore(openspec): archive cloud-ready-p1 + sync main specs' (#51) from chore/openspec-archive-cloud-ready-p1 into master
Deploy Production / deploy (push) Successful in 14s
Run Tests / test (push) Successful in 32s
Reviewed-on: #51
2026-06-17 19:11:04 +00:00
a620906209 c3572682ad chore(openspec): 歸檔 cloud-ready-p1 + 同步主規格
Run Tests / test (pull_request) Successful in 30s
- 所有 task 驗證完成(7.1 stderr log / 7.2 無新增日誌檔 / 7.3 session 走 Redis)
- 同步 env-cloud-annotations 主 spec(LOG_CHANNEL 取代 LOG_STACK + 新增 scenario)
- 新增 scheduler-container 主 spec(獨立 container 取代 cron daemon)
- 移至 archive/2026-06-18-cloud-ready-p1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 03:05:57 +08:00
15 changed files with 459 additions and 23 deletions
+6
View File
@@ -78,3 +78,9 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}" VITE_REVERB_SCHEME="${REVERB_SCHEME}"
# Swagger UI — API 伺服器 Base URL(結尾不加斜線)
# 本地:http://localhost:8080/api
# 生產:https://api.hank-space.com/api
L5_SWAGGER_CONST_HOST=http://localhost:8080/api
L5_SWAGGER_GENERATE_ALWAYS=false
+5
View File
@@ -33,6 +33,11 @@ jobs:
docker compose exec -T app php artisan view:cache docker compose exec -T app php artisan view:cache
docker compose exec -T app php artisan event:cache docker compose exec -T app php artisan event:cache
- name: Generate Swagger docs
run: |
cd /root/myproject/CFDivePlatform
docker compose exec -T app php artisan l5-swagger:generate
- name: Restart queue worker - name: Restart queue worker
run: | run: |
cd /root/myproject/CFDivePlatform cd /root/myproject/CFDivePlatform
+17 -10
View File
@@ -50,13 +50,18 @@
## API 文件 ## API 文件
Swagger 文件涵蓋公開、會員、教練、管理員與認證端點,包含: Swagger UI(由 l5-swagger 生成)涵蓋所有端點,包含:
- 認證(Email + Google OAuth - 認證(Email + Google OAuth、Token 刷新
- 公開課程查詢 - 公開課程查詢
- 會員預約 / 訊息 / 評價 / 通知 - 會員預約 / 即時訊息 / 評價 / 通知
- 教練課程 / 時段 / 預約管理 - 教練課程 / 時段 / 預約管理 / 資格驗證申請(證照送審)
- 管理員後台 - 管理員後台
| 環境 | Swagger UI |
|------|-----------|
| 本地 | http://localhost:8080/api/documentation |
| 生產 | https://api.hank-space.com/api/documentation |
--- ---
## 健康檢查 ## 健康檢查
@@ -82,9 +87,11 @@ GET /health
目前首頁尚未提供明顯導引到教練後台的入口。若要體驗教練功能,請直接開啟 `/coach/login`,使用上方教練試用帳號登入後會進入 `/coach/dashboard` 目前首頁尚未提供明顯導引到教練後台的入口。若要體驗教練功能,請直接開啟 `/coach/login`,使用上方教練試用帳號登入後會進入 `/coach/dashboard`
| 服務 | URL | | 服務 | 本地 | 生產 |
|------|-----| |------|------|------|
| API / 前端 | http://localhost:8080 | | 前端 | http://localhost:8080 | https://app.hank-space.com |
| phpMyAdmin | http://localhost:8081 | | API | http://localhost:8080/api | https://api.hank-space.com/api |
| Mailpit | http://localhost:8025 | | Swagger UI | http://localhost:8080/api/documentation | https://api.hank-space.com/api/documentation |
| Reverb WebSocket | ws://localhost:8085 | | phpMyAdmin | http://localhost:8081 | http://\<vps-ip\>:8081 |
| Mailpit | http://localhost:8025 | http://\<vps-ip\>:8025 |
| Reverb WebSocket | ws://localhost:8085 | wss://ws.hank-space.com |
+97 -1
View File
@@ -12,7 +12,7 @@ use OpenApi\Annotations as OA;
* ) * )
* *
* @OA\Server( * @OA\Server(
* url="/api", * url=L5_SWAGGER_CONST_HOST,
* description="API 伺服器" * description="API 伺服器"
* ) * )
* *
@@ -207,6 +207,38 @@ 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()
{
}
/** /**
* 會員登出 * 會員登出
* *
@@ -580,6 +612,38 @@ 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()
{
}
/** /**
* 服務提供者登出 * 服務提供者登出
* *
@@ -906,6 +970,38 @@ 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()
{
}
/** /**
* 管理員登出 * 管理員登出
* *
+150
View File
@@ -17,6 +17,10 @@ use OpenApi\Annotations as OA;
* name="通知", * name="通知",
* description="站內通知管理(Member / Provider 共用)" * description="站內通知管理(Member / Provider 共用)"
* ) * )
* @OA\Tag(
* name="即時訊息",
* description="預約聊天室訊息(Member / Provider 共用,僅限 confirmed / completed 預約的參與方)"
* )
*/ */
class MemberApiDoc 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_idstring),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/pngmessage 與 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 共用) // Notifications (Member + Provider 共用)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
+141
View File
@@ -21,9 +21,150 @@ use OpenApi\Annotations as OA;
* name="教練預約", * name="教練預約",
* description="服務提供者的預約管理" * description="服務提供者的預約管理"
* ) * )
* @OA\Tag(
* name="教練驗證",
* description="服務提供者的資格驗證申請(證照送審)"
* )
*/ */
class ProviderApiDoc 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 // Provider Offers CRUD
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
+1 -1
View File
@@ -5,7 +5,7 @@ return [
'documentations' => [ 'documentations' => [
'default' => [ 'default' => [
'api' => [ 'api' => [
'title' => 'L5 Swagger UI', 'title' => 'CFDive Platform API',
], ],
'routes' => [ 'routes' => [
@@ -40,6 +40,6 @@
## 7. 手動驗證(VPS,需更新 .env 後) ## 7. 手動驗證(VPS,需更新 .env 後)
- [ ] 7.1 [整合測試] 確認 log 走 stderr:呼叫任意 API 後 `docker compose logs app | tail -20` 有日誌輸出 - [x] 7.1 [整合測試] 確認 log 走 stderr:呼叫任意 API 後 `docker compose logs app | tail -20` 有日誌輸出
- [ ] 7.2 [整合測試] 確認 `storage/logs/` 無新增日誌檔(舊檔保留) - [x] 7.2 [整合測試] 確認 `storage/logs/` 無新增日誌檔(舊檔保留)
- [ ] 7.3 [整合測試] 確認 session 走 Redis:登入後 `docker compose exec redis redis-cli keys "laravel_session*"` 有輸出 - [x] 7.3 [整合測試] 確認 session 走 Redis:登入後 `docker compose exec redis redis-cli keys "cfdiveplatform_database_*"` 有輸出(prefix 為 APP_NAME_database_,非 laravel_session*
+12 -4
View File
@@ -12,11 +12,19 @@
- **THEN** Laravel queue driver 使用 Redisqueue worker 正常消費 Redis 佇列中的 job - **THEN** Laravel queue driver 使用 Redisqueue worker 正常消費 Redis 佇列中的 job
### Requirement: .env.example 標示雲端必要變數 ### Requirement: .env.example 標示雲端必要變數
`.env.example` SHALL 以行內註解標示以下雲端部署時必須明確設定的變數: `.env.example` SHALL 以行內註解或正確預設值標示以下雲端部署時必須明確設定的變數:
- `FILESYSTEM_DISK`:雲端應設為 `s3`(預設 `local` 在容器重啟後遺失上傳檔案) - `FILESYSTEM_DISK`:雲端應設為 `s3`(預設 `local` 在容器重啟後遺失上傳檔案)
- `LOG_STACK`:雲端建議改為 `stderr` 以接入雲端 logging aggregator - `LOG_CHANNEL`:預設改為 `stderr`雲端 logging aggregator 標準;原 `stack/daily` 寫檔案不適合雲端)
- `QUEUE_CONNECTION``redis` - `QUEUE_CONNECTION`預設`redis`
- `SESSION_DRIVER`雲端建議改為 `redis` - `SESSION_DRIVER`預設改為 `redis`(取代 `database`,擴容更高效)
#### Scenario: 新環境照 .env.example 初始化 session driver
- **WHEN** 開發者以 `.env.example` 為基礎建立 `.env`,未手動修改 `SESSION_DRIVER`
- **THEN** Laravel session driver 使用 Redis
#### Scenario: 新環境照 .env.example 初始化 log channel
- **WHEN** 開發者以 `.env.example` 為基礎建立 `.env`,未手動修改 `LOG_CHANNEL`
- **THEN** Laravel 日誌寫入 stderr,可透過 `docker compose logs app` 查看
#### Scenario: 操作者閱讀 .env.example 進行雲端部署 #### Scenario: 操作者閱讀 .env.example 進行雲端部署
- **WHEN** 操作者參照 `.env.example` 設定雲端環境的 `.env` - **WHEN** 操作者參照 `.env.example` 設定雲端環境的 `.env`
@@ -0,0 +1,23 @@
# scheduler-container
Laravel Scheduler 以獨立 container 執行,取代 app container 內的 cron daemon。
## Requirements
### Requirement: Scheduler 以獨立 container 執行
`docker-compose.yml` SHALL 定義 `scheduler` service,使用 `cfdive-platform` image,以 `php artisan schedule:work` 前台輪詢方式執行 Laravel Scheduler,取代原本在 app container 內的 cron daemon。
#### Scenario: Scheduler container 隨 app 啟動
- **WHEN** 執行 `docker compose up -d``app` container 已 healthy
- **THEN** `scheduler` container 啟動並持續執行 `php artisan schedule:work`
#### Scenario: App container 重啟時 scheduler 等待就緒
- **WHEN** `app` container 重啟中(unhealthy
- **THEN** `scheduler` container 依 `depends_on` 等待 `app` healthy 後才啟動,避免 scheduler 在 PHP-FPM 未就緒時執行任務
### Requirement: App container 不再包含 cron daemon
`Dockerfile` SHALL 不安裝 `cron` 套件,`docker-entrypoint.sh` SHALL 不執行 `service cron start`,確保 app container 職責單一(只跑 PHP-FPM)。
#### Scenario: App container 啟動不啟動 cron
- **WHEN** `app` container 啟動並執行 entrypoint
- **THEN** 不存在 cron daemon 程序,`pgrep cron` 回傳 exit code 1