Compare commits

..

18 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
a620906209 13b6b4145c Merge pull request 'feat(infra): 雲端就緒 P1 — Session/Log/Scheduler 改造' (#50) from feat/cloud-ready-p1 into master
Deploy Production / deploy (push) Successful in 12s
Run Tests / test (push) Successful in 31s
Reviewed-on: #50
2026-06-17 18:34:47 +00:00
a620906209 d423cf5079 feat(infra): 雲端就緒 P1 — Session/Log/Scheduler 改造
Run Tests / test (pull_request) Successful in 29s
- Dockerfile:移除 cron 安裝與 scheduler crontab 設定
- docker-entrypoint.sh:移除 service cron start
- docker-compose.yml:新增 scheduler service(php artisan schedule:work)
- .env.example:SESSION_DRIVER=redis、LOG_CHANNEL=stderr、移除 LOG_STACK/LOG_DAILY_DAYS
- docs/STARTUP.md:新增 Log 查看、VPS 維護窗口操作與 Rollback 說明

VPS 部署需手動更新 .env 並執行 docker compose up -d --build(詳見 STARTUP.md §11)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 02:34:01 +08:00
a620906209 55b2bff1b8 Merge pull request 'chore(openspec): 歸檔 cloud-ready-zero-impact + 同步主規格' (#49) from feat/cloud-ready-zero-impact into master
Deploy Production / deploy (push) Successful in 12s
Run Tests / test (push) Successful in 29s
Reviewed-on: #49
2026-06-17 18:02:18 +00:00
a620906209 e08c6652a5 chore(openspec): 歸檔 cloud-ready-zero-impact + 同步主規格
Run Tests / test (pull_request) Successful in 29s
- openspec/changes/archive/2026-06-18-cloud-ready-zero-impact/:change 歸檔
- openspec/specs/compose-cloud-baseline/:新增主規格(healthcheck + override 隔離)
- openspec/specs/env-cloud-annotations/:新增主規格(.env.example 雲端標示)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 02:01:12 +08:00
a620906209 2d14c058d1 Merge pull request 'feat(infra): 雲端就緒零影響改動 (compose overlay + healthcheck + env)' (#48) from feat/cloud-ready-zero-impact into master
Deploy Production / deploy (push) Successful in 14s
Run Tests / test (push) Successful in 32s
Reviewed-on: #48
2026-06-17 17:49:29 +00:00
a620906209 be629feef9 feat(infra): 雲端就緒零影響改動 (compose overlay + healthcheck + env)
Run Tests / test (pull_request) Successful in 30s
- docker-compose.yml:app healthcheck 改用 nc -z 127.0.0.1 9000 驗證 PHP-FPM port
- docker-compose.yml:移除 phpmyadmin、mailpit(開發工具不屬於正式服務)
- compose.override.yml:新增,含 phpmyadmin(8081)、mailpit(8025)port mapping
- .env.example:QUEUE_CONNECTION 預設改 redis;補充 FILESYSTEM_DISK、LOG_STACK、SESSION_DRIVER 雲端注意事項
- docs/STARTUP.md:新增「部署模式」章節,明確區分 VPS 與雲端 -f 指令

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 01:48:24 +08:00
a620906209 998f34139a Merge pull request 'fix(ci): 修正 Alpine 3.23 PHP 套件名稱為 php84' (#47) from fix/vps-stability into master
Deploy Production / deploy (push) Successful in 16s
Run Tests / test (push) Successful in 38s
Reviewed-on: #47
2026-06-17 16:38:17 +00:00
a620906209 0080e2da84 fix(ci): 修正 Alpine 3.23 PHP 套件名稱為 php84
Run Tests / test (pull_request) Successful in 37s
Alpine 3.23 已無 php82 套件,改用 php84。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 00:37:46 +08:00
a620906209 4720dc53f9 Merge pull request 'fix(ci): 替換 setup-php 為 Alpine apk 直接安裝 PHP 8.2' (#46) from fix/vps-stability into master
Deploy Production / deploy (push) Successful in 17s
Run Tests / test (push) Failing after 12s
Reviewed-on: #46
2026-06-17 16:34:07 +00:00
a620906209 95a93b2d72 fix(ci): 替換 setup-php 為 Alpine apk 直接安裝 PHP 8.2
Run Tests / test (pull_request) Failing after 7s
shivammathur/setup-php 在 Alpine runner 上需要 sudo 且編譯耗時。
改用 apk add php82 系列套件,速度更快且相容 Alpine 環境。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 00:32:44 +08:00
28 changed files with 1113 additions and 59 deletions
+10 -6
View File
@@ -15,9 +15,7 @@ APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stderr
LOG_STACK=daily
LOG_DAILY_DAYS=14
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
@@ -29,15 +27,15 @@ DB_CONNECTION=sqlite
# DB_PASSWORD= # DB_PASSWORD=
# MYSQL_ROOT_PASSWORD= # MYSQL_ROOT_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=redis
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null
BROADCAST_CONNECTION=log BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local FILESYSTEM_DISK=local # 雲端應改為 s3(local 在容器重啟後會遺失上傳檔案)
QUEUE_CONNECTION=database QUEUE_CONNECTION=redis # 雲端建議值;database 效能較差且不利擴容
CACHE_STORE=redis CACHE_STORE=redis
CACHE_PREFIX= CACHE_PREFIX=
@@ -80,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
+9 -5
View File
@@ -15,11 +15,15 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 run: |
with: apk add --no-cache \
php-version: '8.2' php84 php84-phar php84-mbstring php84-dom php84-fileinfo php84-gd \
extensions: mbstring, dom, fileinfo, gd, pdo_sqlite, sqlite3 php84-pdo php84-pdo_sqlite php84-sqlite3 php84-tokenizer \
coverage: none php84-xml php84-xmlwriter php84-ctype php84-curl php84-openssl \
php84-json php84-session php84-iconv \
curl unzip
ln -sf /usr/bin/php84 /usr/local/bin/php
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet
- name: Copy .env - name: Copy .env
run: cp .env.example .env run: cp .env.example .env
+1 -8
View File
@@ -17,8 +17,7 @@ RUN apt-get update && apt-get install -y \
libzip-dev \ libzip-dev \
default-mysql-client \ default-mysql-client \
netcat-openbsd \ netcat-openbsd \
grep \ grep
cron
# 清理 apt 快取以減小鏡像大小 # 清理 apt 快取以減小鏡像大小
RUN apt-get clean && rm -rf /var/lib/apt/lists/* RUN apt-get clean && rm -rf /var/lib/apt/lists/*
@@ -53,12 +52,6 @@ COPY docker/php/local.ini /usr/local/etc/php/conf.d/local.ini
COPY docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# 加入 Laravel Scheduler cron job
RUN echo "* * * * * www-data php /var/www/artisan schedule:run >> /var/log/laravel-scheduler.log 2>&1" \
> /etc/cron.d/laravel-scheduler \
&& chmod 0644 /etc/cron.d/laravel-scheduler \
&& crontab /etc/cron.d/laravel-scheduler
# 設置容器啟動時執行的入口點 # 設置容器啟動時執行的入口點
# 這將在 CMD 指令之前執行 # 這將在 CMD 指令之前執行
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+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
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
+27
View File
@@ -0,0 +1,27 @@
services:
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: cfdive-phpmyadmin
restart: unless-stopped
ports:
- "127.0.0.1:8081:80"
environment:
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: ${DB_USERNAME:-cfdiveuser}
PMA_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
networks:
- cfdive-network
depends_on:
db:
condition: service_healthy
mailpit:
image: axllent/mailpit
container_name: cfdive-mailpit
restart: unless-stopped
ports:
- "127.0.0.1:8025:8025"
networks:
- cfdive-network
+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' => [
+20 -20
View File
@@ -38,11 +38,11 @@ services:
# 健康檢查配置 # 健康檢查配置
healthcheck: healthcheck:
test: ["CMD", "php", "-v"] # 檢查 PHP 版本確認服務正常 test: ["CMD-SHELL", "nc -z 127.0.0.1 9000 || exit 1"]
interval: 30s # 每30秒檢查一次 interval: 30s
timeout: 10s # 超時時間10秒 timeout: 10s
retries: 3 # 重試3次 retries: 3
start_period: 40s # 容器啟動後40秒開始檢查 start_period: 40s
nginx: nginx:
image: nginx:alpine image: nginx:alpine
@@ -109,20 +109,26 @@ services:
ports: ports:
- "127.0.0.1:3306:3306" # 只綁 localhost,不對外 - "127.0.0.1:3306:3306" # 只綁 localhost,不對外
phpmyadmin:
image: phpmyadmin/phpmyadmin scheduler:
container_name: cfdive-phpmyadmin image: cfdive-platform
container_name: cfdive-scheduler
restart: unless-stopped restart: unless-stopped
working_dir: /var/www/
command: php artisan schedule:work
volumes:
- ./:/var/www
environment: environment:
PMA_HOST: db - DB_CONNECTION=mysql
PMA_PORT: 3306 - DB_HOST=db
PMA_USER: ${DB_USERNAME:-cfdiveuser} - DB_PORT=3306
PMA_PASSWORD: ${DB_PASSWORD} - DB_DATABASE=${DB_DATABASE:-CFDivePlatform}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - DB_USERNAME=${DB_USERNAME:-cfdiveuser}
- DB_PASSWORD=${DB_PASSWORD}
networks: networks:
- cfdive-network - cfdive-network
depends_on: depends_on:
db: app:
condition: service_healthy condition: service_healthy
queue-worker: queue-worker:
@@ -146,12 +152,6 @@ services:
app: app:
condition: service_healthy condition: service_healthy
mailpit:
image: axllent/mailpit
container_name: cfdive-mailpit
restart: unless-stopped
networks:
- cfdive-network
redis: redis:
image: redis:alpine image: redis:alpine
-3
View File
@@ -59,8 +59,5 @@ fi
echo "✅ [背景] 初始化完成" echo "✅ [背景] 初始化完成"
) & ) &
# 啟動 cronLaravel Scheduler
service cron start || cron || true
echo "🚀 啟動 php-fpm..." echo "🚀 啟動 php-fpm..."
exec "$@" exec "$@"
+79 -1
View File
@@ -1,5 +1,30 @@
# CFDivePlatform 啟動指令 # CFDivePlatform 啟動指令
## 部署模式
專案根目錄有兩個 compose 檔:
| 檔案 | 用途 |
|------|------|
| `docker-compose.yml` | 正式服務(app、nginx、frontend、db、redis、reverb、queue-worker、scheduler |
| `compose.override.yml` | 開發工具(phpmyadmin、mailpit),已進版控 |
**本機 / VPS(預設)**Docker Compose 自動合併兩檔,開發工具一同啟動:
```bash
docker compose up -d
```
**雲端正式環境**:明確指定單一檔案,排除開發工具:
```bash
docker compose -f docker-compose.yml up -d
```
> VPS 首次部署或更新後執行 `git pull` 即可取得最新的 `compose.override.yml`,不需手動建立。
---
## 專案位置 ## 專案位置
```powershell ```powershell
@@ -162,7 +187,60 @@ npm run dev
| Reverb WebSocket | <ws://localhost:8085> | | Reverb WebSocket | <ws://localhost:8085> |
| Health Check | <http://localhost:8080/health> | | Health Check | <http://localhost:8080/health> |
## 10. 停止服務 ## 10. Log 查看
Laravel 日誌走 stderr,透過 Docker 查看(不再有 `storage/logs/` 檔案):
```bash
# 即時串流
docker compose logs -f app
# 查看最後 50 行
docker compose logs app --tail=50
```
## 11. VPS 維護窗口操作(P1 部署)
PR merge 後 CI/CD 自動跑 composer/migrate/cache,完成後 SSH 進 VPS 執行:
```bash
cd /root/myproject/CFDivePlatform
# 1. 更新 .env(一次性)
sed -i 's/SESSION_DRIVER=database/SESSION_DRIVER=redis/' .env
sed -i 's/LOG_CHANNEL=stack/LOG_CHANNEL=stderr/' .env
sed -i '/^LOG_STACK=/d' .env
sed -i '/^LOG_DAILY_DAYS=/d' .env
# 2. Rebuild + 重啟(entrypoint 自動 config:clear
docker compose up -d --build
# 3. 驗證
docker compose ps # scheduler 應顯示 Up
docker compose logs app --tail=20 # 應有 stderr 日誌
docker compose exec redis redis-cli keys "laravel_session*" # 登入後應有 session key
docker compose exec app sh -c "which cron 2>/dev/null || echo 'cron binary not found'" # 應輸出 not found
```
**Rollback(若需還原):**
Session/Log 還原(不需 rebuild):
```bash
sed -i 's/SESSION_DRIVER=redis/SESSION_DRIVER=database/' .env
sed -i 's/LOG_CHANNEL=stderr/LOG_CHANNEL=stack/' .env
echo "LOG_STACK=daily" >> .env
echo "LOG_DAILY_DAYS=14" >> .env
docker compose exec app php artisan config:clear
docker compose restart app
```
Scheduler 還原(需 rebuild):
```bash
git revert <commit-hash>
docker compose up -d --build
```
## 12. 停止服務
停止容器,但保留資料庫 volume 停止容器,但保留資料庫 volume
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-17
@@ -0,0 +1,151 @@
## Context
P0 完成後 VPS 目前狀態:
- Session`SESSION_DRIVER=database`MySQL
- Log`LOG_CHANNEL=stack``LOG_STACK=daily`(寫檔案)
- Schedulercron daemon 跑在 app container 內,entrypoint 啟動
三項都是雲端不友善的設定:多實例時 cron 重複執行、檔案 log 無法被雲端 aggregator 收集、DB session 擴容效率低。
**Bind mount 暫時保留**`./:/var/www` 目前仍保留,移除 bind mount 屬於 P0 剩餘項目(需將 code bake 進 image),本 change 不處理。scheduler service 同樣沿用 bind mount,與 app/queue-worker 一致。
## Goals / Non-Goals
**Goals:**
- Session 存 Redis,擴容時不需共享 DB session table
- Log 走 stderr`docker compose logs` 及雲端 aggregator 皆可收
- Scheduler 獨立 container,水平擴容 app 時不會重複跑定時任務
- Dockerfile 移除 cron 依賴,image 更精簡
**Non-Goals:**
- 不處理 S3 檔案儲存(P0 待辦)
- 不移除 bind mountP0 待辦)
- 不動任何 PHP 業務邏輯、config/ 目錄下的 PHP 設定檔
## Pre-flight 確認(實作前已驗證)
| 項目 | 結果 |
|---|---|
| `config/logging.php``stderr` channel | ✅ 已存在(line 97),不需改 code |
| `config/session.php``driver``.env` | ✅ `env('SESSION_DRIVER', 'database')`line 21 |
| `SESSION_CONNECTION` | 預設 `null`(使用 default Redis connection),VPS Redis 已在跑,不需額外設定 |
## Decisions
### D1Scheduler 用 `php artisan schedule:work` 而非 crontab
`schedule:work` 是 Laravel 10+ 內建指令,在前台以無限迴圈每分鐘執行一次 `schedule:run`,不需要 cron daemon。符合 12-factor「一個 container 跑一個 process」原則。
**`depends_on` 取捨**scheduler 設 `depends_on: app: condition: service_healthy`,確保首次啟動時 PHP-FPM 就緒後 scheduler 才開始執行。注意:`depends_on` 只控制啟動順序,不控制運行時依賴——app container 重啟時 scheduler **不會**跟著重啟,會繼續跑;若 app 還沒 ready 而 scheduler 已觸發任務,任務本身可能失敗,但 scheduler 程序不會崩潰。這是可接受的,定時任務本身應有冪等與錯誤處理。
```yaml
scheduler:
image: cfdive-platform
container_name: cfdive-scheduler
restart: unless-stopped
working_dir: /var/www/
command: php artisan schedule:work
volumes:
- ./:/var/www
environment:
- DB_CONNECTION=mysql
- DB_HOST=db
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-CFDivePlatform}
- DB_USERNAME=${DB_USERNAME:-cfdiveuser}
- DB_PASSWORD=${DB_PASSWORD}
networks:
- cfdive-network
depends_on:
app:
condition: service_healthy
```
### D2Log 改 `LOG_CHANNEL=stderr`
`config/logging.php` 已內建 `stderr` channel`php://stderr`),Docker 自動對應到 container stderr stream`docker compose logs app` 即可看到。不需新增任何 PHP 設定。
`.env.example` 同步移除 `LOG_STACK``LOG_DAILY_DAYS`(改 stderr 後這兩個無作用,留著造成混淆)。
**Config cache 注意**VPS `.env` 改完後需清除 config cache 才能生效:
```bash
docker compose exec app php artisan config:clear
```
或直接 `docker compose up -d --build`entrypoint 會自動 `config:clear`)。
### D3Session 改 Redis,不需建新資料表
Redis 已在跑(`cfdive-redis`),`config/session.php``.env`,開箱即用。`sessions` table 保留不刪(避免 rollback 需要重建)。
### D4VPS `.env` 更新方式
選手動 SSH 更新(不進 deploy.yml 自動化),原因:`.env` 含機敏值,CI 自動覆寫風險高;此為一次性變更。
## Risks / Trade-offs
- **[Risk] Session 失效**:改 driver 後所有現有 session 立即失效,用戶需重新登入。
**Mitigation**:排低流量時段;VPS `.env` 改完後直接 rebuild,縮短空窗。
- **[Risk] Rebuild image 期間短暫停機**Dockerfile 移除 cron 需 rebuild。
**Mitigation**`docker compose up -d --build` 滾動更新,停機窗口約 30 秒。
- **[Risk] Config cache 未清除**`.env` 改了但 `bootstrap/cache/config.php` 仍是舊值,session/log 不生效。
**Mitigation**Migration Plan 步驟明確包含 `config:clear`entrypoint 在 rebuild 後也會自動清除。
- **[Risk] Log 遺失歷史**:改 stderr 後 `storage/logs/` 不再更新,但舊檔保留在磁碟。
**Mitigation**:Sentry 持續收錯誤;需要查歷史時直接看舊檔案。
## Acceptance Criteria
- [ ] `docker compose ps` 顯示 `cfdive-scheduler` 容器 Up
- [ ] `docker compose exec app sh -c "which cron 2>/dev/null || echo 'cron binary not found'"` 輸出 `cron binary not found`binary 已從 image 移除)
- [ ] 呼叫任意 API 後,`docker compose logs app` 有日誌輸出(stderr log 生效)
- [ ] `storage/logs/` 下無新增日誌檔(舊檔保留)
- [ ] 登入後 `docker compose exec redis redis-cli keys "laravel_session*"` 有輸出(session 走 Redis
## Migration Plan
**程式碼(PR 合併前):**
1. `Dockerfile`:移除 cron 安裝與 crontab 設定
2. `docker/php/docker-entrypoint.sh`:移除 `service cron start`
3. `docker-compose.yml`:新增 `scheduler` service
4. `.env.example``SESSION_DRIVER=redis``LOG_CHANNEL=stderr`、移除 `LOG_STACK`/`LOG_DAILY_DAYS`
5. `docs/STARTUP.md`:補充 scheduler、log 查看、VPS 操作說明
**VPS 維護窗口(PR merge + CI/CD 完成後):**
```bash
# SSH 進 VPS
cd /root/myproject/CFDivePlatform
# 1. 更新 .env
sed -i 's/SESSION_DRIVER=database/SESSION_DRIVER=redis/' .env
sed -i 's/LOG_CHANNEL=stack/LOG_CHANNEL=stderr/' .env
sed -i '/^LOG_STACK=/d' .env
sed -i '/^LOG_DAILY_DAYS=/d' .env
# 2. Rebuild + 重啟(entrypoint 自動 config:clear
docker compose up -d --build
# 3. 驗證
docker compose ps
docker compose logs app | tail -20
docker compose exec redis redis-cli keys "laravel_session*"
```
## Rollback Plan
Session/Log 還原(不需 rebuild):
```bash
sed -i 's/SESSION_DRIVER=redis/SESSION_DRIVER=database/' .env
sed -i 's/LOG_CHANNEL=stderr/LOG_CHANNEL=stack/' .env
echo "LOG_STACK=daily" >> .env
echo "LOG_DAILY_DAYS=14" >> .env
docker compose exec app php artisan config:clear
docker compose restart app
```
Scheduler 還原(需 rebuild):
```bash
git revert <commit-hash>
docker compose up -d --build
```
@@ -0,0 +1,31 @@
## Why
P0 零影響改動完成後,P1 處理三個需要維護窗口但改完後功能完全等效的雲端就緒項目:Session 存 Redis 取代 MySQL(擴容必要)、Log 走 stdout 取代檔案(雲端 logging 標準)、Scheduler 抽成獨立 container 取代 app 容器內 cron(水平擴容不重複執行)。
## What Changes
- **`.env.example`**`SESSION_DRIVER` 預設改 `redis``LOG_CHANNEL` 預設改 `stderr`
- **`Dockerfile`**:移除 `cron` 安裝、移除 scheduler cron job 寫入
- **`docker/php/docker-entrypoint.sh`**:移除 `service cron start`
- **`docker-compose.yml`**:新增 `scheduler` service`php artisan schedule:work`
- **`docs/STARTUP.md`**:更新服務清單,說明 scheduler container 與 log 查看方式
- **VPS `.env`**:需在維護窗口更新 `SESSION_DRIVER=redis``LOG_CHANNEL=stderr`(不進版控)
## Capabilities
### New Capabilities
- `scheduler-container`Scheduler 以獨立 container 方式執行,不依賴 app container 內的 cron daemon
### Modified Capabilities
- `env-cloud-annotations`:補充 `SESSION_DRIVER``LOG_CHANNEL` 的正確雲端預設值(原 spec 已標注說明,現在升級為實際預設值)
## Impact
- **`Dockerfile`**:移除 cron 相關 2 行,需 rebuild image
- **`docker/php/docker-entrypoint.sh`**:移除 1 行
- **`docker-compose.yml`**:新增 `scheduler` service
- **`.env.example`**2 個預設值變動
- **不影響**:所有 PHP 業務邏輯、API 行為、前端、資料庫 schema
- **部署影響**:Session 失效一次(用戶登出)、需 rebuild app image
@@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: QUEUE_CONNECTION 預設值為 redis
`.env.example``QUEUE_CONNECTION` SHALL 預設為 `redis`,與 VPS 實際運作設定一致,避免新環境按範本初始化後跑在效能較差的 database queue。
#### Scenario: 新環境照 .env.example 初始化
- **WHEN** 開發者或 CI 以 `.env.example` 為基礎建立 `.env`,未手動修改 `QUEUE_CONNECTION`
- **THEN** Laravel queue driver 使用 Redisqueue worker 正常消費 Redis 佇列中的 job
### Requirement: .env.example 標示雲端必要變數
`.env.example` SHALL 以行內註解或正確預設值標示以下雲端部署時必須明確設定的變數:
- `FILESYSTEM_DISK`:雲端應設為 `s3`(預設 `local` 在容器重啟後遺失上傳檔案)
- `LOG_CHANNEL`:預設改為 `stderr`(雲端 logging aggregator 標準;原 `stack/daily` 寫檔案不適合雲端)
- `QUEUE_CONNECTION`:預設為 `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 進行雲端部署
- **WHEN** 操作者參照 `.env.example` 設定雲端環境的 `.env`
- **THEN** 每個雲端關鍵變數旁有說明,提示預設值在雲端環境的限制與建議替代值
@@ -0,0 +1,19 @@
## ADDED 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
@@ -0,0 +1,45 @@
## 1. Dockerfile 移除 cron
- [x] 1.1 [後端] 從 `Dockerfile``apt-get install -y` 清單移除 `cron`
- [x] 1.2 [後端] 從 `Dockerfile` 移除寫入 `/etc/cron.d/laravel-scheduler``RUN echo ...` 指令
- [x] 1.3 [後端] 從 `Dockerfile` 移除 `crontab /etc/cron.d/laravel-scheduler` 指令
## 2. docker-entrypoint.sh 移除 cron 啟動
- [x] 2.1 [後端] 從 `docker/php/docker-entrypoint.sh` 移除 `service cron start || cron || true` 這一行
## 3. docker-compose.yml 新增 scheduler service
- [x] 3.1 [後端] 在 `docker-compose.yml` 新增 `scheduler` service,設定如下:
- `image: cfdive-platform``container_name: cfdive-scheduler``restart: unless-stopped`
- `working_dir: /var/www/``command: php artisan schedule:work`
- `volumes: - ./:/var/www`
- 環境變數:`DB_CONNECTION``DB_HOST=db``DB_PORT``DB_DATABASE``DB_USERNAME``DB_PASSWORD`(與 queue-worker 相同)
- `networks: - cfdive-network`
- `depends_on: app: condition: service_healthy`
## 4. .env.example 更新
- [x] 4.1 [後端] 將 `.env.example``SESSION_DRIVER=database # 雲端建議改為 redis` 改為 `SESSION_DRIVER=redis`(升級為預設值,移除說明)
- [x] 4.2 [後端] 將 `.env.example``LOG_CHANNEL=stack` 改為 `LOG_CHANNEL=stderr`
- [x] 4.3 [後端] 從 `.env.example` 移除 `LOG_STACK=daily``LOG_DAILY_DAYS=14` 兩行(改 stderr 後無作用)
## 5. 文件更新
- [x] 5.1 [後端] 更新 `docs/STARTUP.md`,補充以下內容:
- 服務清單新增 `scheduler``docker compose ps` 應見 `cfdive-scheduler`
- Log 查看方式:`docker compose logs -f app`(不再有 `storage/logs/` 檔案)
- **VPS 維護窗口操作**:說明 `sed` 更新 `.env` 的四個指令 + `docker compose up -d --build`
- **Config cache 注意**`.env` 改完須清 cacherebuild 時 entrypoint 自動處理)
- **Rollback 步驟**Session/Log 還原指令(不需 rebuild+ Scheduler 還原(需 rebuild
## 6. 手動驗證(本機,不需 VPS .env)
- [x] 6.1 [整合測試] 在本機 rebuild 後確認 `cfdive-scheduler` 正常啟動:`docker compose ps` 顯示 scheduler Up
- [x] 6.2 [整合測試] 確認 app container 無 cron`docker compose exec app sh -c "which cron 2>/dev/null || echo 'cron binary not found'"`cron binary 已從 image 移除,輸出 `cron binary not found`
## 7. 手動驗證(VPS,需更新 .env 後)
- [x] 7.1 [整合測試] 確認 log 走 stderr:呼叫任意 API 後 `docker compose logs app | tail -20` 有日誌輸出
- [x] 7.2 [整合測試] 確認 `storage/logs/` 無新增日誌檔(舊檔保留)
- [x] 7.3 [整合測試] 確認 session 走 Redis:登入後 `docker compose exec redis redis-cli keys "cfdiveplatform_database_*"` 有輸出(prefix 為 APP_NAME_database_,非 laravel_session*
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-17
@@ -0,0 +1,76 @@
## Context
CFDivePlatform 跑在單台 VPS 上,全部服務以 Docker Compose 管理。現有設定有三個雲端就緒問題:
1. `app` service healthcheck 用 `php -v`,只驗證 binary 存在,無法確認 PHP-FPM 是否監聽
2. `phpmyadmin``mailpit` 這兩個開發/管理工具寫死在 `docker-compose.yml`,雲端正式環境不應存在
3. `.env.example``QUEUE_CONNECTION=database` 與 VPS 實際使用 Redis 不一致,新環境按範本初始化會跑在效能較差的 DB queue
本次只改 compose 設定和 `.env.example`,不觸碰任何 PHP 程式碼。
## Goals / Non-Goals
**Goals:**
- healthcheck 改為確認 PHP-FPM FastCGI port 9000 正常監聽,比 `php -v` 更能反映 FPM 是否就緒
- 透過 `compose.override.yml` 機制隔離開發工具,VPS 行為不變,雲端部署只需 `-f docker-compose.yml`
- `.env.example` 的 queue 預設值與雲端慣例對齊,並補充雲端必要變數的說明
**Non-Goals:**
- 不修改 Session driver(留待 P1 change
- 不修改 Log 輸出方式(留待 P1 change
- 不修改 Scheduler 架構(留待 P1 change
- 不改任何 PHP 程式碼或 image 內容
- 不處理 S3 檔案儲存(P0 項目,需獨立 change)
## Decisions
### D1healthcheck 用 `nc -z 127.0.0.1 9000` 檢查 PHP-FPM port
**為什麼不用 `/health` HTTP 端點:**
app container 跑的是 PHP-FPMFastCGIport 9000),容器內**沒有 HTTP server**。
`/health` 路由必須經過 Nginx 才能到達 PHP-FPM,而 Nginx 是另一個 container。
所以 `curl``wget``http://127.0.0.1/health` 在 app container 內部完全打不到任何東西。
**為什麼不用 `wget`**
Dockerfile 安裝清單確認 `wget` 未安裝(僅裝 `curl`)。Nginx container 的 `wget` 是 busybox-wgetAlpine),兩者環境不同,不可混用。
**選定方案:**
```yaml
healthcheck:
test: ["CMD-SHELL", "nc -z 127.0.0.1 9000 || exit 1"]
```
`netcat-openbsd` 已在 Dockerfile 安裝。`nc -z` 只做 TCP connect,確認 PHP-FPM 的 FastCGI port 正在監聽,是 app container 內部能做到最準確的 FPM 就緒判斷。
**能力邊界:** 此 healthcheck 確認 PHP-FPM 程序已啟動並監聽 port,但不驗證 NginxNginx 有自己的獨立 healthcheck。
### D2`compose.override.yml` 放在 repo 根目錄並加進版控
**選項 A**:放進 `.gitignore`,每台機器手動建立
**選項 B**:放進 repo,所有環境共用
選 B。`compose.override.yml` 只定義開發工具(phpmyadmin、mailpit),不含任何 secret 或環境特定值,放進版控讓新成員 `git clone` 後直接可用。雲端部署 CI 指定 `-f docker-compose.yml` 排除即可。
### D3`mailpit` 同步移入 override
`mailpit``phpmyadmin` 性質相同(開發輔助工具),一起移入 override,保持 `docker-compose.yml` 只含正式服務。
## Risks / Trade-offs
- **[Risk] healthcheck 只驗證 TCP 連線,不驗證請求成功**:`nc -z` 確認 port 開著,但不確認 PHP-FPM 能正確處理 FastCGI 請求。
**Mitigation**:對 `depends_on` 的用途(等待 FPM 啟動再起 Nginx)已足夠;若要更深層驗證需用 `cgi-fcgi` 工具(過度工程,暫不引入)。
- **[Risk] VPS 若沒有 `compose.override.yml`**:移除後首次 `docker compose up` 會少 phpmyadmin/mailpit。
**Mitigation**`compose.override.yml` 已進版控,VPS 執行 `git pull` 即可取得,不需手動建立。STARTUP.md 同步更新說明此流程。
## Migration Plan
1. `docker-compose.yml`:改 healthcheck、移除 phpmyadmin service、移除 mailpit service
2. 新增 `compose.override.yml`:加入 phpmyadmin、mailpit
3. `.env.example`:改 QUEUE_CONNECTION 預設值、補說明
4. `STARTUP.md`:新增「部署模式」章節,明確區分:
- **VPS(預設)**`docker compose up -d`,自動合併 override,含 phpmyadmin + mailpit
- **雲端正式環境**`docker compose -f docker-compose.yml up -d`,排除開發工具
5. **VPS 部署**:執行 `git pull` 取得版控內的 `compose.override.yml`,不需 rebuild image`docker compose up -d` 即可
## Open Questions
無。本次改動範圍明確,無待決事項。
@@ -0,0 +1,27 @@
## Why
現有 Docker Compose 設定存在若干與雲端部署不相容的配置(開發工具混入正式服務、healthcheck 無法真實反映 app 狀態、queue 預設值不一致),在不動任何 PHP 程式碼、不重建 image、不影響 VPS 現有運作的前提下,先修正這四個零風險項目,作為雲端就緒路線圖的第一步。
## What Changes
- **`.env.example`**`QUEUE_CONNECTION` 預設值從 `database` 改為 `redis`;補充 `FILESYSTEM_DISK` 與 log 相關變數的說明與分組,標記哪些是雲端部署時必須設定的項目
- **`docker-compose.yml`**app service healthcheck 從 `php -v` 改為呼叫 `/health` 端點;移除 `phpmyadmin` service
- **新增 `compose.override.yml`**:將 `phpmyadmin``mailpit` 放入 override 檔,VPS/開發環境自動載入,正式雲端部署時只用 `-f docker-compose.yml` 即可排除
## Capabilities
### New Capabilities
- `compose-cloud-baseline`Docker Compose 雲端基準設定——healthcheck 使用真實端點、正式 compose 不含開發工具、dev-only 服務透過 override 隔離
- `env-cloud-annotations``.env.example` 補充雲端部署必要環境變數的說明與正確預設值
### Modified Capabilities
(本次不修改任何既有 spec 的需求行為)
## Impact
- **`docker-compose.yml`**healthcheck 指令、移除 phpmyadmin service
- **新增 `compose.override.yml`**phpmyadmin、mailpit service 定義搬移至此
- **`.env.example`**QUEUE_CONNECTION 預設值、新增說明行
- **不影響**:所有 PHP 程式碼、VPS `.env`、image 內容、Scheduler、Session、Log
@@ -0,0 +1,32 @@
## ADDED Requirements
### Requirement: App service healthcheck 驗證 PHP-FPM FastCGI port
`docker-compose.yml``app` service healthcheck SHALL 使用 `nc -z 127.0.0.1 9000` 確認 PHP-FPM FastCGI port 正在監聽,而非執行 `php -v`(只驗證 binary 存在)。
注意:app container 內無 HTTP server,無法透過 HTTP 打 `/health``netcat-openbsd` 已在 Dockerfile 安裝。
#### Scenario: PHP-FPM 啟動後 healthcheck 通過
- **WHEN** `app` container 啟動且 PHP-FPM 程序已監聽 port 9000
- **THEN** `nc -z 127.0.0.1 9000` 回傳 exit code 0container 標記為 healthy
#### Scenario: PHP-FPM 未啟動時 healthcheck 失敗
- **WHEN** `app` container 啟動但 PHP-FPM 程序尚未就緒(port 9000 未開)
- **THEN** `nc -z 127.0.0.1 9000` 回傳非 0 exit codecontainer 維持 unhealthy 狀態直到重試成功
### Requirement: 正式 compose 不含開發工具服務
`docker-compose.yml` SHALL 不包含 `phpmyadmin``mailpit` service 定義。這兩個服務屬開發/管理工具,不得出現在正式雲端部署的 compose 設定中。
#### Scenario: 只使用 docker-compose.yml 啟動
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`
- **THEN** 啟動的服務不包含 `phpmyadmin`port 8081)及 `mailpit`port 8025
### Requirement: 開發工具服務透過 compose.override.yml 提供
`compose.override.yml` SHALL 定義 `phpmyadmin``mailpit` service,供本機開發與 VPS 管理使用。
#### Scenario: 有 override 檔時開發工具正常啟動
- **WHEN** 專案根目錄存在 `compose.override.yml` 且執行 `docker compose up -d`(不指定 -f
- **THEN** Docker Compose 自動合併 override`phpmyadmin`port 8081)與 `mailpit`port 8025)一同啟動
#### Scenario: 雲端部署排除開發工具
- **WHEN** CI/CD 執行 `docker compose -f docker-compose.yml up -d`(明確指定單一檔案)
- **THEN** `phpmyadmin``mailpit` 不啟動,不佔用任何 port 或資源
@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: QUEUE_CONNECTION 預設值為 redis
`.env.example``QUEUE_CONNECTION` SHALL 預設為 `redis`,與 VPS 實際運作設定一致,避免新環境按範本初始化後跑在效能較差的 database queue。
#### Scenario: 新環境照 .env.example 初始化
- **WHEN** 開發者或 CI 以 `.env.example` 為基礎建立 `.env`,未手動修改 `QUEUE_CONNECTION`
- **THEN** Laravel queue driver 使用 Redisqueue worker 正常消費 Redis 佇列中的 job
### Requirement: .env.example 標示雲端必要變數
`.env.example` SHALL 以行內註解或分組說明標示以下雲端部署時必須明確設定的變數,協助操作者識別必填項目:
- `FILESYSTEM_DISK`:雲端應設為 `s3`(目前預設 `local`,雲端會導致檔案在容器重啟後遺失)
- `LOG_CHANNEL` / `LOG_STACK`:雲端建議改為 `stderr` 以接入雲端 logging aggregator
- `QUEUE_CONNECTION`:應為 `redis`
- `SESSION_DRIVER`:雲端建議改為 `redis`
#### Scenario: 操作者閱讀 .env.example 進行雲端部署
- **WHEN** 操作者參照 `.env.example` 設定雲端環境的 `.env`
- **THEN** 每個雲端關鍵變數旁有說明,提示預設值在雲端環境的限制與建議替代值
@@ -0,0 +1,31 @@
## 1. docker-compose.yml 調整
- [x] 1.1 [後端] 修改 `docker-compose.yml` app service healthcheck:將 `test: ["CMD", "php", "-v"]` 改為 `test: ["CMD-SHELL", "nc -z 127.0.0.1 9000 || exit 1"]`app container 無 HTTP server,用 nc 驗證 PHP-FPM FastCGI port 9000
- [x] 1.2 [後端] 從 `docker-compose.yml` 移除 `phpmyadmin` service 完整定義(包含 image、container_name、environment、networks、depends_on
- [x] 1.3 [後端] 從 `docker-compose.yml` 移除 `mailpit` service 完整定義
## 2. 新增 compose.override.yml
- [x] 2.1 [後端] 在專案根目錄新建 `compose.override.yml`,加入 `phpmyadmin` service(保留原有的 PMA_HOST、PMA_PORT、PMA_USER、PMA_PASSWORD、MYSQL_ROOT_PASSWORD 環境變數設定與 networks、depends_on
- [x] 2.2 [後端] 在 `compose.override.yml` 加入 `mailpit` service(保留原有設定與 networks
## 3. .env.example 更新
- [x] 3.1 [後端] 將 `.env.example``QUEUE_CONNECTION` 值從 `database` 改為 `redis`
- [x] 3.2 [後端] 在 `.env.example``FILESYSTEM_DISK=local` 旁加上行內說明,標示雲端應改為 `s3`
- [x] 3.3 [後端] 在 `.env.example``LOG_CHANNEL` / `LOG_STACK` 區段加上說明,標示雲端建議改為 `stderr`
- [x] 3.4 [後端] 在 `.env.example``SESSION_DRIVER=database` 旁加上說明,標示雲端建議改為 `redis`
- [x] 3.5 [後端] 在 `.env.example``QUEUE_CONNECTION=redis` 旁加上說明,確認此為雲端建議值
## 4. 文件更新
- [x] 4.1 [後端] 更新 `docs/STARTUP.md`,新增「部署模式」章節,明確列出兩組指令:
- **本機 / VPS**`docker compose up -d`(自動合併 override,含 phpmyadmin + mailpit
- **雲端正式環境**`docker compose -f docker-compose.yml up -d`(排除開發工具)
- 說明 `compose.override.yml` 已進版控,VPS `git pull` 後即可取得,不需手動建立
## 5. 手動驗證
- [x] 5.1 [整合測試] 在本機執行 `docker compose up -d`,確認 phpmyadmin8081)與 mailpit8025)仍可正常存取
- [x] 5.2 [整合測試] 執行 `docker compose -f docker-compose.yml up -d --remove-orphans`,確認 phpmyadmin 與 mailpit 不啟動
- [x] 5.3 [整合測試] 確認 `app` container 的 healthcheck 狀態在啟動後顯示 healthy`docker compose ps` 查看)
@@ -0,0 +1,36 @@
# compose-cloud-baseline
Docker Compose 雲端基準設定——healthcheck 使用真實端點、正式 compose 不含開發工具、dev-only 服務透過 override 隔離。
## Requirements
### Requirement: App service healthcheck 驗證 PHP-FPM FastCGI port
`docker-compose.yml``app` service healthcheck SHALL 使用 `nc -z 127.0.0.1 9000` 確認 PHP-FPM FastCGI port 正在監聽,而非執行 `php -v`(只驗證 binary 存在)。
注意:app container 內無 HTTP server,無法透過 HTTP 打 `/health``netcat-openbsd` 已在 Dockerfile 安裝。
#### Scenario: PHP-FPM 啟動後 healthcheck 通過
- **WHEN** `app` container 啟動且 PHP-FPM 程序已監聽 port 9000
- **THEN** `nc -z 127.0.0.1 9000` 回傳 exit code 0container 標記為 healthy
#### Scenario: PHP-FPM 未啟動時 healthcheck 失敗
- **WHEN** `app` container 啟動但 PHP-FPM 程序尚未就緒(port 9000 未開)
- **THEN** `nc -z 127.0.0.1 9000` 回傳非 0 exit codecontainer 維持 unhealthy 狀態直到重試成功
### Requirement: 正式 compose 不含開發工具服務
`docker-compose.yml` SHALL 不包含 `phpmyadmin``mailpit` service 定義。這兩個服務屬開發/管理工具,不得出現在正式雲端部署的 compose 設定中。
#### Scenario: 只使用 docker-compose.yml 啟動
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`
- **THEN** 啟動的服務不包含 `phpmyadmin`port 8081)及 `mailpit`port 8025
### Requirement: 開發工具服務透過 compose.override.yml 提供
`compose.override.yml` SHALL 定義 `phpmyadmin`port 8081)與 `mailpit`port 8025service,供本機開發與 VPS 管理使用。
#### Scenario: 有 override 檔時開發工具正常啟動
- **WHEN** 專案根目錄存在 `compose.override.yml` 且執行 `docker compose up -d`(不指定 -f
- **THEN** Docker Compose 自動合併 override`phpmyadmin`port 8081)與 `mailpit`port 8025)一同啟動
#### Scenario: 雲端部署排除開發工具
- **WHEN** CI/CD 執行 `docker compose -f docker-compose.yml up -d --remove-orphans`
- **THEN** `phpmyadmin``mailpit` 不啟動,不佔用任何 port 或資源
@@ -0,0 +1,31 @@
# env-cloud-annotations
`.env.example` 補充雲端部署必要環境變數的說明與正確預設值。
## Requirements
### Requirement: QUEUE_CONNECTION 預設值為 redis
`.env.example``QUEUE_CONNECTION` SHALL 預設為 `redis`,與 VPS 實際運作設定一致,避免新環境按範本初始化後跑在效能較差的 database queue。
#### Scenario: 新環境照 .env.example 初始化
- **WHEN** 開發者或 CI 以 `.env.example` 為基礎建立 `.env`,未手動修改 `QUEUE_CONNECTION`
- **THEN** Laravel queue driver 使用 Redisqueue worker 正常消費 Redis 佇列中的 job
### Requirement: .env.example 標示雲端必要變數
`.env.example` SHALL 以行內註解或正確預設值標示以下雲端部署時必須明確設定的變數:
- `FILESYSTEM_DISK`:雲端應設為 `s3`(預設 `local` 在容器重啟後遺失上傳檔案)
- `LOG_CHANNEL`:預設改為 `stderr`(雲端 logging aggregator 標準;原 `stack/daily` 寫檔案不適合雲端)
- `QUEUE_CONNECTION`:預設為 `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 進行雲端部署
- **WHEN** 操作者參照 `.env.example` 設定雲端環境的 `.env`
- **THEN** 每個雲端關鍵變數旁有說明,提示預設值在雲端環境的限制與建議替代值
@@ -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