Files
CFDivePlatform/openspec/specs/course-image-upload/spec.md
T
a620906209 0cd1047b10
Run Tests / test (pull_request) Successful in 30s
docs(openspec): 補齊所有 spec 缺少的 Purpose section 與規格標頭
repo 內 35/38 份既有 spec 都是舊版歸檔流程留下的原始 delta 內容(`##
ADDED Requirements`,缺標題與 Purpose),openspec CLI 現版本嚴格驗證
(--strict)會報錯或警告。逐一補上:

- 缺標題/Purpose 的 30 份:加上 `# <name> Specification` 標題 + 依內容
  撰寫的 Purpose 段落,`## ADDED Requirements` 改回 `## Requirements`
- compose-cloud-baseline、scheduler-container:已有標題與
  Requirements,只補 Purpose 包裝
- provider-verification:已有標題與 Purpose,只需把
  `## ADDED Requirements` 改回 `## Requirements`
- admin-user-management、notification-email:各有一條 requirement 缺
  SHALL/MUST 關鍵字(純敘述句或表格),補上規範用語,內容不變

`openspec validate --specs --strict`:38/38 全過(原本 4/38)。純文件
補齊,未變更任何 requirement 的實質內容或行為描述。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LpcY9b7y9x4fusHBTXciQ
2026-08-03 04:11:06 +08:00

112 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# course-image-upload Specification
## Purpose
定義課程封面與相簿圖片的伺服器端壓縮規則、上傳/刪除端點行為、課程刪除時的圖片清理,以及圖片透過 host bind mount 持久化的機制。
## Requirements
### Requirement: 伺服器端圖片壓縮
系統 SHALL 在儲存課程封面與相簿圖片前進行壓縮:長邊超過 2048px 時等比縮小至 2048px 內(僅縮不放大),一律轉存 JPEG(quality 85,副檔名 `.jpg`uuid 檔名)。與聊天圖片管線(`scaleDown(2048) + toJpeg(85)`)參數一致,共用 `App\Traits\CompressesImages`
#### Scenario: 大圖縮小
- **WHEN** Provider 上傳長邊 > 2048px 的圖片
- **THEN** 儲存的檔案長邊 ≤ 2048px,格式為 JPEG
#### Scenario: 小圖不放大
- **WHEN** Provider 上傳長邊 ≤ 2048px 的圖片
- **THEN** 儲存的檔案維持原尺寸,格式轉為 JPEG
#### Scenario: PNG/WebP 轉存 JPEG
- **WHEN** Provider 上傳 png 或 webp 格式圖片
- **THEN** 儲存的檔案為 `.jpg`,回傳的 URL 指向轉存後檔案
### Requirement: Provider 上傳課程封面
Provider SHALL 能為自己的課程上傳一張封面圖片,新上傳會覆蓋舊封面並刪除舊實體檔案。上傳大小上限為 10MB(原 2MB;伺服器端會壓縮,放寬以容納手機原圖)。
#### Scenario: 成功上傳封面
- **WHEN** Provider 送出 `POST /api/provider/offers/{id}/cover`,包含 `image` 檔案(jpeg/png/webp,≤10MB
- **THEN** 系統壓縮後儲存至 `public` disk 的 `offers/{offer_id}/cover/` 目錄(`.jpg`),更新 `diving_offers.cover_image`,回傳 `cover_image_url`
#### Scenario: 10MB 以內的手機原圖可直接上傳
- **WHEN** 上傳 2MB~10MB 之間的圖片(原規格會拒絕)
- **THEN** 系統接受並壓縮儲存
#### Scenario: 覆蓋封面時刪除舊檔
- **WHEN** Provider 上傳新封面,且原本已有封面
- **THEN** 系統先刪除舊實體檔案,再儲存新檔案
#### Scenario: 檔案格式驗證
- **WHEN** 上傳的檔案不是 jpeg/jpg/png/webp
- **THEN** 系統回傳 422
#### Scenario: 檔案大小驗證
- **WHEN** 上傳檔案超過 10MB10240KB
- **THEN** 系統回傳 422
#### Scenario: 不可上傳他人課程的封面
- **WHEN** Provider 嘗試上傳不屬於自己課程的封面
- **THEN** 系統回傳 403
### Requirement: Provider 刪除課程封面
Provider SHALL 能刪除自己課程的封面,同步移除實體檔案。
#### Scenario: 成功刪除封面
- **WHEN** Provider 送出 `DELETE /api/provider/offers/{id}/cover`
- **THEN** 系統刪除實體檔案,將 `diving_offers.cover_image` 設為 null,回傳 200
#### Scenario: 無封面時刪除不報錯
- **WHEN** Provider 刪除封面,但 `cover_image` 本來就是 null
- **THEN** 系統直接回傳 200,不報錯
### Requirement: Provider 上傳課程相簿圖片
Provider SHALL 能為課程上傳最多 3 張相簿圖片,sort_order 不連續為預期行為(刪除後重新上傳序號接續最大值)。
#### Scenario: 成功上傳相簿圖片
- **WHEN** Provider 送出 `POST /api/provider/offers/{id}/images`,包含 `image` 檔案(格式與大小同封面限制:jpeg/png/webp、≤10MB),且目前相簿圖片數 < 3
- **THEN** 系統壓縮後儲存至 `offers/{offer_id}/gallery/` 目錄(`.jpg`),建立 `course_images` 紀錄,`sort_order = MAX(sort_order) + 1`,回傳新圖片資訊
#### Scenario: 超過 3 張上限
- **WHEN** 課程已有 3 張相簿圖片,Provider 再次上傳
- **THEN** 系統回傳 422message:「相簿最多 3 張圖片」
#### Scenario: 不可上傳他人課程的相簿
- **WHEN** Provider 嘗試上傳不屬於自己課程的相簿
- **THEN** 系統回傳 403
### Requirement: Provider 刪除相簿圖片
Provider SHALL 能刪除自己課程的特定相簿圖片,同步移除實體檔案。
#### Scenario: 成功刪除相簿圖片
- **WHEN** Provider 送出 `DELETE /api/provider/images/{image_id}`
- **THEN** 系統刪除實體檔案,刪除 `course_images` 紀錄,回傳 200
#### Scenario: 不可刪除他人相簿圖片
- **WHEN** Provider 嘗試刪除不屬於自己課程的相簿圖片
- **THEN** 系統回傳 403
### Requirement: 課程刪除時清理圖片目錄
DivingOffer 刪除時,系統 SHALL 自動清除 `offers/{offer_id}/` 整個目錄,防止孤兒檔案累積。
#### Scenario: 刪除課程時清除圖片
- **WHEN** DivingOffer 被刪除(`static::deleting()` observer 觸發)
- **THEN** `Storage::disk('public')->deleteDirectory("offers/{$offer->id}")` 刪除封面與相簿所有實體檔案
### Requirement: 圖片 URL 隨課程資料一同回傳
公開課程 API SHALL 在回傳課程資料時包含 `cover_image_url`(含 APP_URL 與 port 的完整 URL)與 `images`(相簿陣列)。
#### Scenario: 有封面時回傳完整 URL
- **WHEN** 任何人取得課程資料(`GET /api/diving-offers/{id}` 或列表)
- **THEN** `cover_image_url` 回傳可直接用於 `<img src>` 的完整 URL(如 `http://host:port/storage/offers/...`);無封面時回傳 null
#### Scenario: 相簿陣列回傳
- **WHEN** 取得課程詳情 `GET /api/diving-offers/{id}`
- **THEN** `images` 回傳相簿圖片陣列,每筆含 `id``url``sort_order`,依 `sort_order ASC` 排序;無相簿時回傳空陣列
### Requirement: 圖片儲存持久化(Bind Mount
圖片 SHALL 儲存於 `./storage/app/public/`host bind mount),`app``nginx` 容器透過 `./:/var/www` 共享同一目錄,跨容器重建後仍可存取。`APP_URL` 必須包含正確 port(如 `http://localhost:8080`)才能產生正確的圖片 URL。
#### Scenario: 容器重建後圖片保留
- **WHEN** 執行 `docker compose up --build` 並重新啟動容器
- **THEN** 先前上傳的圖片仍可正常存取(URL 不變),因圖片在 host 目錄不受容器重建影響