Compare commits
32 Commits
4383239bab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d1fe3d4138 | |||
| 0bffa909e2 | |||
| 9222bf17fc | |||
| b218fa0964 | |||
| a7e8468613 | |||
| 60ff56a571 | |||
| 4afe7b5fd5 | |||
| c3572682ad | |||
| 13b6b4145c | |||
| d423cf5079 | |||
| 55b2bff1b8 | |||
| e08c6652a5 | |||
| 2d14c058d1 | |||
| be629feef9 | |||
| 998f34139a | |||
| 0080e2da84 | |||
| 4720dc53f9 | |||
| 95a93b2d72 | |||
| c0616b17df | |||
| 6a74f766db | |||
| d8340de322 | |||
| aebce80152 | |||
| 5fdd5b3329 | |||
| daf72711e8 | |||
| 2fbcd0c50d | |||
| 979ea4df75 | |||
| 6b1e31c4af | |||
| 64874e1c12 | |||
| 19705fc8d7 | |||
| 80f5cf179f | |||
| d99ffb0fd6 | |||
| afdca18c67 |
+22
-5
@@ -15,8 +15,7 @@ APP_MAINTENANCE_STORE=database
|
|||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stderr
|
||||||
LOG_STACK=single
|
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
@@ -28,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=
|
||||||
@@ -67,3 +66,21 @@ VITE_APP_NAME="${APP_NAME}"
|
|||||||
|
|
||||||
SENTRY_LARAVEL_DSN=
|
SENTRY_LARAVEL_DSN=
|
||||||
SENTRY_ENVIRONMENT="${APP_ENV}"
|
SENTRY_ENVIRONMENT="${APP_ENV}"
|
||||||
|
|
||||||
|
REVERB_APP_ID=
|
||||||
|
REVERB_APP_KEY=
|
||||||
|
REVERB_APP_SECRET=
|
||||||
|
REVERB_HOST=localhost
|
||||||
|
REVERB_PORT=8085
|
||||||
|
REVERB_SCHEME=http
|
||||||
|
|
||||||
|
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||||
|
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||||
|
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||||
|
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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -8,18 +8,22 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: self-hosted
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
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
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ yarn-error.log
|
|||||||
/frontend/dist
|
/frontend/dist
|
||||||
/frontend/.env
|
/frontend/.env
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Ponytail, lazy senior dev mode
|
||||||
|
|
||||||
|
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
||||||
|
|
||||||
|
## Before writing any code
|
||||||
|
|
||||||
|
Stop at the first rung that holds:
|
||||||
|
|
||||||
|
- Does this need to be built at all? (YAGNI)
|
||||||
|
- Does the standard library already do this? Use it.
|
||||||
|
- Does a native platform feature cover it? Use it.
|
||||||
|
- Does an already-installed dependency solve it? Use it.
|
||||||
|
- Can this be one line? Make it one line.
|
||||||
|
- Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- No abstractions that weren't explicitly requested.
|
||||||
|
- No new dependency if it can be avoided.
|
||||||
|
- No boilerplate nobody asked for.
|
||||||
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||||
|
- Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm.
|
||||||
|
- Mark intentional simplifications with a ponytail: comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||||
|
|
||||||
|
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||||
+1
-8
@@ -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"]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
**會員(Member)**
|
**會員(Member)**
|
||||||
- 註冊 / 登入(Email + Google OAuth)
|
- 註冊 / 登入(Email + Google OAuth)
|
||||||
|
- Token 自動續期(401 refresh-then-retry,sessionStorage 儲存)
|
||||||
- 瀏覽、搜尋、篩選潛水課程
|
- 瀏覽、搜尋、篩選潛水課程
|
||||||
- 查看課程時段並送出預約
|
- 查看課程時段並送出預約
|
||||||
- 與教練即時訊息(文字 + 圖片,含已讀回執)
|
- 與教練即時訊息(文字 + 圖片,含已讀回執)
|
||||||
@@ -15,14 +16,16 @@
|
|||||||
- 站內通知(Bell Icon 即時更新 + 瀏覽器推播)
|
- 站內通知(Bell Icon 即時更新 + 瀏覽器推播)
|
||||||
|
|
||||||
**教練(Provider)**
|
**教練(Provider)**
|
||||||
- 課程 CRUD(含封面 + 相簿圖片上傳)
|
- 課程 CRUD(含封面 + 相簿圖片上傳,伺服器端壓縮)
|
||||||
- 課程時段管理
|
- 課程時段管理
|
||||||
- 預約管理(確認 / 拒絕 / 完成 / 取消)
|
- 預約管理(確認 / 拒絕 / 完成 / 取消)
|
||||||
- 與學員即時訊息
|
- 與學員即時訊息
|
||||||
|
- 證照上傳與教練資格送審
|
||||||
|
|
||||||
**管理員(Admin)**
|
**管理員(Admin)**
|
||||||
- 平台統計數據(會員數、教練數、課程數)
|
- 平台統計數據(會員數、教練數、課程數)
|
||||||
- 會員與教練帳號管理(啟用 / 停用 / 審核)
|
- 會員與教練帳號管理(啟用 / 停用)
|
||||||
|
- 教練資格審核(送審 / 通過 / 駁回 / 撤銷)
|
||||||
- 課程、預約、評價管理
|
- 課程、預約、評價管理
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -36,23 +39,29 @@
|
|||||||
| 資料庫 | MySQL 8.0 |
|
| 資料庫 | MySQL 8.0 |
|
||||||
| 快取 | Redis |
|
| 快取 | Redis |
|
||||||
| 即時通訊 | Laravel Reverb(WebSocket,`wss://ws.hank-space.com`)|
|
| 即時通訊 | Laravel Reverb(WebSocket,`wss://ws.hank-space.com`)|
|
||||||
| 認證 | Laravel Sanctum + Google OAuth |
|
| 認證 | Laravel Sanctum + Google OAuth + Token Refresh |
|
||||||
| 容器 | Docker / Docker Compose |
|
| 容器 | Docker / Docker Compose |
|
||||||
| API 文件 | Swagger UI(l5-swagger)|
|
| API 文件 | Swagger UI(l5-swagger)|
|
||||||
| 錯誤監控 | Sentry |
|
| 錯誤監控 | Sentry |
|
||||||
| CI/CD | Gitea Actions(自動部署至 VPS)|
|
| CI/CD | Gitea Actions(自動部署至 VPS)|
|
||||||
|
| 測試 | PHPUnit / Laravel Feature & Unit Tests |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API 文件
|
## API 文件
|
||||||
|
|
||||||
共 73 個端點,涵蓋:
|
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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 健康檢查
|
## 健康檢查
|
||||||
@@ -67,25 +76,22 @@ GET /health
|
|||||||
|
|
||||||
## 本地開發
|
## 本地開發
|
||||||
|
|
||||||
**啟動**
|
**測試用帳號**
|
||||||
|
|
||||||
```bash
|
| 角色 | 帳號 | 密碼 | 用途 |
|
||||||
docker compose up -d
|
|------|------|------|------|
|
||||||
```
|
| 會員 | Guest@cfdive.com | guestpassword | 體驗課程瀏覽、預約、會員預約紀錄 |
|
||||||
|
| 教練 | Guest_Coach@cfdive.com | coachpassword | 體驗課程管理、時段管理、預約管理 |
|
||||||
|
|
||||||
| 服務 | URL |
|
**教練頁入口**
|
||||||
|------|-----|
|
|
||||||
| API / 前端 | http://localhost:8080 |
|
|
||||||
| phpMyAdmin | http://localhost:8081 |
|
|
||||||
| Mailpit | http://localhost:8025 |
|
|
||||||
| Reverb WebSocket | ws://localhost:8085 |
|
|
||||||
|
|
||||||
**環境設定**
|
目前首頁尚未提供明顯導引到教練後台的入口。若要體驗教練功能,請直接開啟 `/coach/login`,使用上方教練試用帳號登入後會進入 `/coach/dashboard`。
|
||||||
|
|
||||||
複製 `.env.example` 為 `.env`,填入以下必要值:
|
| 服務 | 本地 | 生產 |
|
||||||
|
|------|------|------|
|
||||||
```env
|
| 前端 | http://localhost:8080 | https://app.hank-space.com |
|
||||||
REVERB_APP_KEY= # 32 字元隨機字串
|
| API | http://localhost:8080/api | https://api.hank-space.com/api |
|
||||||
REVERB_APP_SECRET= # 32 字元隨機字串
|
| Swagger UI | http://localhost:8080/api/documentation | https://api.hank-space.com/api/documentation |
|
||||||
SENTRY_LARAVEL_DSN= # Sentry DSN(留空則停用)
|
| 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
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理員登出
|
* 管理員登出
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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_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 共用)
|
// Notifications (Member + Provider 共用)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->trustProxies(at: ['*']);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'admin' => \App\Http\Middleware\EnsureAdmin::class,
|
'admin' => \App\Http\Middleware\EnsureAdmin::class,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -5,7 +5,7 @@ return [
|
|||||||
'documentations' => [
|
'documentations' => [
|
||||||
'default' => [
|
'default' => [
|
||||||
'api' => [
|
'api' => [
|
||||||
'title' => 'L5 Swagger UI',
|
'title' => 'CFDive Platform API',
|
||||||
],
|
],
|
||||||
|
|
||||||
'routes' => [
|
'routes' => [
|
||||||
|
|||||||
+24
-20
@@ -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
|
||||||
@@ -159,6 +159,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network
|
- cfdive-network
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -205,3 +207,5 @@ networks:
|
|||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -59,8 +59,5 @@ fi
|
|||||||
echo "✅ [背景] 初始化完成"
|
echo "✅ [背景] 初始化完成"
|
||||||
) &
|
) &
|
||||||
|
|
||||||
# 啟動 cron(Laravel Scheduler)
|
|
||||||
service cron start || cron || true
|
|
||||||
|
|
||||||
echo "🚀 啟動 php-fpm..."
|
echo "🚀 啟動 php-fpm..."
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
# 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
|
||||||
|
C:\laragon\www\CFDivePlatform
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. 進入專案目錄
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\laragon\www\CFDivePlatform
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 準備 `.env`
|
||||||
|
|
||||||
|
如果尚未建立 `.env`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
copy .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
確認 `.env` 至少設定以下項目:
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_KEY=
|
||||||
|
DB_DATABASE=CFDivePlatform
|
||||||
|
DB_USERNAME=cfdiveuser
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
MYSQL_ROOT_PASSWORD=your_root_password
|
||||||
|
|
||||||
|
REVERB_APP_ID=your_reverb_app_id
|
||||||
|
REVERB_APP_KEY=your_reverb_app_key
|
||||||
|
REVERB_APP_SECRET=your_reverb_app_secret
|
||||||
|
VITE_REVERB_APP_KEY=your_reverb_app_key
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `APP_KEY` 是空的,可在容器啟動後執行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose exec app php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 啟動 Docker 服務
|
||||||
|
|
||||||
|
第一次啟動,或 Dockerfile / compose 設定有變更時:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
平常啟動:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 查看服務狀態
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
查看所有服務 log:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
只查看 Laravel App log:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose logs -f app
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 初始化與維護指令
|
||||||
|
|
||||||
|
容器啟動時會自動執行部分初始化流程:
|
||||||
|
|
||||||
|
- 安裝 Composer 依賴
|
||||||
|
- 等待 MySQL 啟動
|
||||||
|
- 執行 migration
|
||||||
|
- 清除 Laravel cache
|
||||||
|
- 建立 storage link
|
||||||
|
- 產生 Swagger 文件
|
||||||
|
|
||||||
|
如需手動執行,可使用:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose exec app composer install
|
||||||
|
docker compose exec app php artisan migrate
|
||||||
|
docker compose exec app php artisan storage:link
|
||||||
|
docker compose exec app php artisan l5-swagger:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
清除 Laravel 快取:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose exec app php artisan config:clear
|
||||||
|
docker compose exec app php artisan cache:clear
|
||||||
|
docker compose exec app php artisan route:clear
|
||||||
|
docker compose exec app php artisan view:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 建立管理員帳號
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose exec app php artisan app:create-admin "Admin" "admin@example.com" --password="your_password"
|
||||||
|
```
|
||||||
|
|
||||||
|
密碼至少需要 8 碼。
|
||||||
|
|
||||||
|
## 7. 前端 Vite 開發模式
|
||||||
|
|
||||||
|
如果要使用本機 Vite 開發模式:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
正式建置:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker Compose 內也有 `frontend` 服務,預設網址為:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 重啟 frontend
|
||||||
|
|
||||||
|
如果 frontend 是透過 Docker Compose 啟動:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose restart frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
如果有修改 frontend 的 Dockerfile、環境變數或 build 內容,建議重建:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose up -d --build frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
如果是使用本機 Vite 開發模式,先在原本終端機按 `Ctrl + C` 停止,再重新執行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 服務網址
|
||||||
|
|
||||||
|
| 服務 | URL |
|
||||||
|
| --- | --- |
|
||||||
|
| API / Laravel + Nginx | <http://localhost:8080> |
|
||||||
|
| Frontend Docker | <http://localhost:5173> |
|
||||||
|
| phpMyAdmin | <http://localhost:8081> |
|
||||||
|
| Mailpit | <http://localhost:8025> |
|
||||||
|
| Reverb WebSocket | <ws://localhost:8085> |
|
||||||
|
| Health Check | <http://localhost:8080/health> |
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
停止容器並移除 volume,會清除 MySQL 資料:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最短啟動流程
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\laragon\www\CFDivePlatform
|
||||||
|
copy .env.example .env
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose exec app php artisan key:generate
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
啟動完成後開啟:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
@@ -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`(寫檔案)
|
||||||
|
- Scheduler:cron 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 mount(P0 待辦)
|
||||||
|
- 不動任何 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
|
||||||
|
|
||||||
|
### D1:Scheduler 用 `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
|
||||||
|
```
|
||||||
|
|
||||||
|
### D2:Log 改 `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`)。
|
||||||
|
|
||||||
|
### D3:Session 改 Redis,不需建新資料表
|
||||||
|
|
||||||
|
Redis 已在跑(`cfdive-redis`),`config/session.php` 讀 `.env`,開箱即用。`sessions` table 保留不刪(避免 rollback 需要重建)。
|
||||||
|
|
||||||
|
### D4:VPS `.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
|
||||||
+27
@@ -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 使用 Redis,queue 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` 改完須清 cache(rebuild 時 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
|
||||||
|
|
||||||
|
### D1:healthcheck 用 `nc -z 127.0.0.1 9000` 檢查 PHP-FPM port
|
||||||
|
|
||||||
|
**為什麼不用 `/health` HTTP 端點:**
|
||||||
|
app container 跑的是 PHP-FPM(FastCGI,port 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-wget(Alpine),兩者環境不同,不可混用。
|
||||||
|
|
||||||
|
**選定方案:**
|
||||||
|
```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,但不驗證 Nginx;Nginx 有自己的獨立 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
|
||||||
+32
@@ -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 0,container 標記為 healthy
|
||||||
|
|
||||||
|
#### Scenario: PHP-FPM 未啟動時 healthcheck 失敗
|
||||||
|
- **WHEN** `app` container 啟動但 PHP-FPM 程序尚未就緒(port 9000 未開)
|
||||||
|
- **THEN** `nc -z 127.0.0.1 9000` 回傳非 0 exit code,container 維持 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 或資源
|
||||||
+19
@@ -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 使用 Redis,queue 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`,確認 phpmyadmin(8081)與 mailpit(8025)仍可正常存取
|
||||||
|
- [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 0,container 標記為 healthy
|
||||||
|
|
||||||
|
#### Scenario: PHP-FPM 未啟動時 healthcheck 失敗
|
||||||
|
- **WHEN** `app` container 啟動但 PHP-FPM 程序尚未就緒(port 9000 未開)
|
||||||
|
- **THEN** `nc -z 127.0.0.1 9000` 回傳非 0 exit code,container 維持 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 8025)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 --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 使用 Redis,queue 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** 每個雲端關鍵變數旁有說明,提示預設值在雲端環境的限制與建議替代值
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
### Requirement: Provider 課程 CRUD 測試覆蓋
|
### Requirement: Provider 課程 CRUD 測試覆蓋
|
||||||
測試套件 SHALL 驗證 `POST /api/provider/offers`、`PUT /api/provider/offers/{id}`、`DELETE /api/provider/offers/{id}` 的正常流程與所有權邊界。
|
測試套件 SHALL 驗證 `POST /api/provider/offers`、`PUT /api/provider/offers/{id}`、`DELETE /api/provider/offers/{id}` 的正常流程與所有權邊界。
|
||||||
|
|
||||||
@@ -86,9 +88,10 @@
|
|||||||
- **WHEN** Admin 送出 `PUT /api/admin/members/{id}/toggle-active`(目標會員 is_active=true)
|
- **WHEN** Admin 送出 `PUT /api/admin/members/{id}/toggle-active`(目標會員 is_active=true)
|
||||||
- **THEN** 回傳 HTTP 200,DB 記錄 is_active 由 true 變為 false
|
- **THEN** 回傳 HTTP 200,DB 記錄 is_active 由 true 變為 false
|
||||||
|
|
||||||
#### Scenario: Admin 切換教練驗證狀態
|
#### Scenario: Admin 核准教練審核
|
||||||
- **WHEN** Admin 送出 `PUT /api/admin/providers/{id}/toggle-verified`(目標教練 is_verified=false)
|
- **WHEN** Admin 對 `verification_status=pending` 的教練送出 `PUT /api/admin/verifications/{userId}/approve`
|
||||||
- **THEN** 回傳 HTTP 200,provider_profiles.is_verified 由 false 變為 true
|
- **THEN** 回傳 HTTP 200,provider_profiles.verification_status 變為 `approved`
|
||||||
|
- **NOTE** `toggle-verified` 路由已由 provider-verification-workflow 移除,驗證狀態變更一律透過審核狀態機
|
||||||
|
|
||||||
#### Scenario: 非 Admin 角色被拒絕
|
#### Scenario: 非 Admin 角色被拒絕
|
||||||
- **WHEN** role=member 的使用者送出 `GET /api/admin/members`
|
- **WHEN** role=member 的使用者送出 `GET /api/admin/members`
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -31,10 +31,10 @@ class AdminUserManagementTest extends TestCase
|
|||||||
return $member;
|
return $member;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function makeProvider(bool $isVerified = false): User
|
private function makeProvider(string $verificationStatus = 'unsubmitted'): User
|
||||||
{
|
{
|
||||||
$provider = User::factory()->create(['role' => 'provider']);
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => $isVerified]);
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => $verificationStatus]);
|
||||||
return $provider;
|
return $provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,21 +89,21 @@ class AdminUserManagementTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── toggle-verified ──────────────────────────────────────
|
// ── approve ─────────────────────────────────────────────
|
||||||
|
|
||||||
public function test_admin_toggles_provider_verified_status(): void
|
public function test_admin_approves_provider_verification(): void
|
||||||
{
|
{
|
||||||
$admin = $this->makeAdmin();
|
$admin = $this->makeAdmin();
|
||||||
$provider = $this->makeProvider(isVerified: false);
|
$provider = $this->makeProvider(verificationStatus: 'pending');
|
||||||
|
|
||||||
$this->actingAs($admin)
|
$this->actingAs($admin)
|
||||||
->putJson("/api/admin/providers/{$provider->id}/toggle-verified")
|
->putJson("/api/admin/verifications/{$provider->id}/approve")
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.is_verified', true);
|
->assertJsonPath('status', true);
|
||||||
|
|
||||||
$this->assertDatabaseHas('provider_profiles', [
|
$this->assertDatabaseHas('provider_profiles', [
|
||||||
'user_id' => $provider->id,
|
'user_id' => $provider->id,
|
||||||
'is_verified' => true,
|
'verification_status' => 'approved',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class BookingListTest extends TestCase
|
|||||||
private function makeProviderWithSchedule(): array
|
private function makeProviderWithSchedule(): array
|
||||||
{
|
{
|
||||||
$provider = User::factory()->create(['role' => 'provider']);
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => true]);
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => 'approved']);
|
||||||
|
|
||||||
$offer = DivingOffer::create([
|
$offer = DivingOffer::create([
|
||||||
'provider_id' => $provider->id,
|
'provider_id' => $provider->id,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class NotificationTriggerTest extends TestCase
|
|||||||
private function makeProvider(): User
|
private function makeProvider(): User
|
||||||
{
|
{
|
||||||
$provider = User::factory()->create(['role' => 'provider']);
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => true]);
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => 'approved']);
|
||||||
return $provider;
|
return $provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ProviderOfferCrudTest extends TestCase
|
|||||||
private function makeProvider(): User
|
private function makeProvider(): User
|
||||||
{
|
{
|
||||||
$provider = User::factory()->create(['role' => 'provider']);
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => true]);
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => 'approved']);
|
||||||
return $provider;
|
return $provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ProviderScheduleCrudTest extends TestCase
|
|||||||
private function makeProvider(): User
|
private function makeProvider(): User
|
||||||
{
|
{
|
||||||
$provider = User::factory()->create(['role' => 'provider']);
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
ProviderProfile::create(['user_id' => $provider->id, 'is_verified' => true]);
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => 'approved']);
|
||||||
return $provider;
|
return $provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user