商品 API
概述
商品 API 提供商品資訊的完整 CRUD 功能,支援商品管理、搜尋過濾、圖片上傳等操作。
相關 User Stories:
端點列表
| 方法 | 路徑 | 說明 | 權限 |
|---|---|---|---|
| GET | /api/v1/products | 列出商品(支援搜尋、過濾、分頁) | ROLE_SALES 或更高 |
| GET | /api/v1/products/:id | 取得商品詳情 | ROLE_SALES 或更高 |
| POST | /api/v1/products | 創建商品 | ROLE_OWNER 或 ROLE_MANAGER |
| PUT | /api/v1/products/:id | 更新商品(完整替換) | ROLE_OWNER 或 ROLE_MANAGER |
| PATCH | /api/v1/products/:id | 更新商品(部分更新) | ROLE_OWNER 或 ROLE_MANAGER |
| PATCH | /api/v1/products/:id/status | 更新商品狀態(上架/下架) | ROLE_OWNER 或 ROLE_MANAGER |
| DELETE | /api/v1/products/:id | 刪除商品 | ROLE_OWNER 或 ROLE_ADMIN |
| POST | /api/v1/products/upload-image | 上傳商品圖片 | ROLE_OWNER 或 ROLE_MANAGER |
資料模型
Product 實體
interface Product {
id: string; // 商品 ID(UUID)
tenantId: string; // 租戶 ID
sku: string; // SKU 編號(自動生成)
name: string; // 商品名稱
category: ProductCategory; // 商品分類
description?: string; // 商品描述
basePrice: number; // 基礎價格(NT$)
costPrice?: number; // 成本價格(僅管理者可見)
stock: number; // 庫存數量
lowStockThreshold: number; // 低庫存警戒值(預設 5)
stockUnit: string; // 庫存單位(如「件」、「盆」)
status: ProductStatus; // 上架狀態
isFeatured: boolean; // 是否熱門商品
isNew: boolean; // 是否新品
mainImageUrl: string; // 主圖 URL
galleryImageUrls: string[]; // 副圖 URL 列表(最多 5 張)
createdAt: string; // 創建時間(ISO 8601)
updatedAt: string; // 更新時間(ISO 8601)
createdBy?: string; // 創建者 ID
updatedBy?: string; // 更新者 ID
}
商品分類 (ProductCategory)
| 值 | 代碼 | 說明 |
|---|---|---|
bouquet | BQT | 花束 |
pot | POT | 盆花 |
stand | STD | 高架花籃 |
basket | BSK | 花籃 |
box | BOX | 盒花 |
gift | GFT | 禮品 |
custom | CST | 客製化專案 |
商品狀態 (ProductStatus)
| 值 | 說明 |
|---|---|
active | 已上架(可販售) |
inactive | 已下架(不可販售) |
庫存狀態 (StockStatus) - 計算屬性
| 值 | 說明 | 計算規則 |
|---|---|---|
in_stock | 有庫存 | stock > lowStockThreshold |
low_stock | 低庫存 | 0 < stock <= lowStockThreshold |
out_of_stock | 缺貨 | stock = 0 |
SKU 生成規則
格式:{租戶代碼}-{分類代碼}-{流水號}
範例:FS01-BQT-0001(花店01 的第一個花束商品)
端點詳細規格
GET /api/v1/products
描述: 列出商品,支援搜尋、多條件過濾和分頁
權限: ROLE_SALES 或更高
Query Parameters:
| 參數 | 類型 | 必填 | 說明 | 預設值 |
|---|---|---|---|---|
| search | string | No | 搜尋關鍵字(模糊匹配名稱、SKU、描述) | - |
| category | string[] | No | 商品分類(可多選) | - |
| status | string[] | No | 商品狀態(可多選) | - |
| stockStatus | string[] | No | 庫存狀態(可多選) | - |
| isFeatured | boolean | No | 是否熱門商品 | - |
| isNew | boolean | No | 是否新品 | - |
| priceMin | number | No | 最低價格 | - |
| priceMax | number | No | 最高價格 | - |
| page | integer | No | 頁碼(從 1 開始) | 1 |
| limit | integer | No | 每頁數量 | 20 |
| sortBy | string | No | 排序欄位(name, sku, basePrice, stock, createdAt) | createdAt |
| sortOrder | string | No | 排序方向(asc, desc) | desc |
請求範例:
GET /api/v1/products?category=bouquet&category=pot&status=active&priceMin=500&priceMax=2000&page=1&limit=20
Authorization: Bearer {access_token}
響應範例 (200 OK):
Headers:
X-Total-Count: 45
X-Page: 1
X-Per-Page: 20
Link: </api/v1/products?page=1&limit=20>; rel="first", </api/v1/products?page=2&limit=20>; rel="next", </api/v1/products?page=3&limit=20>; rel="last"
Body:
[
{
"id": "prod-001",
"tenantId": "tenant-001",
"sku": "FS01-BQT-0001",
"name": "經典紅玫瑰花束(12朵)",
"category": "bouquet",
"description": "嚴選進口紅玫瑰,象徵熱情與愛情",
"basePrice": 1200,
"costPrice": 600,
"stock": 50,
"lowStockThreshold": 5,
"stockUnit": "束",
"status": "active",
"isFeatured": true,
"isNew": false,
"mainImageUrl": "https://example.com/images/rose-bouquet-main.jpg",
"galleryImageUrls": [
"https://example.com/images/rose-bouquet-1.jpg",
"https://example.com/images/rose-bouquet-2.jpg"
],
"createdAt": "2025-10-01T08:00:00Z",
"updatedAt": "2025-11-15T10:30:00Z"
}
]
GET /api/v1/products/:id
描述: 取得單一商品詳情
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 商品 ID |
請求範例:
GET /api/v1/products/prod-001
Authorization: Bearer {access_token}
響應範例 (200 OK):
{
"id": "prod-001",
"tenantId": "tenant-001",
"sku": "FS01-BQT-0001",
"name": "經典紅玫瑰花束(12朵)",
"category": "bouquet",
"description": "嚴選進口紅玫瑰,象徵熱情與愛情",
"basePrice": 1200,
"costPrice": 600,
"stock": 50,
"lowStockThreshold": 5,
"stockUnit": "束",
"status": "active",
"isFeatured": true,
"isNew": false,
"mainImageUrl": "https://example.com/images/rose-bouquet-main.jpg",
"galleryImageUrls": [
"https://example.com/images/rose-bouquet-1.jpg",
"https://example.com/images/rose-bouquet-2.jpg"
],
"createdAt": "2025-10-01T08:00:00Z",
"updatedAt": "2025-11-15T10:30:00Z",
"createdBy": "user-001",
"updatedBy": "user-002"
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 商品不存在 | 找不到指定的商品 |
POST /api/v1/products
描述: 創建新商品
權限: ROLE_OWNER 或 ROLE_MANAGER
Content-Type:
application/json- 純 JSON 格式multipart/form-data- 包含圖片上傳(binary-mixed 模式)
請求體 (CreateProductRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| category | string | Yes | 商品分類 |
| name | string | Yes | 商品名稱 |
| description | string | No | 商品描述 |
| basePrice | number | Yes | 基礎價格(>= 0) |
| costPrice | number | No | 成本價格 |
| stock | integer | No | 庫存數量(預設 0) |
| lowStockThreshold | integer | No | 低庫存警戒值(預設 5) |
| stockUnit | string | No | 庫存單位(預設「件」) |
| status | string | No | 商品狀態(預設 active) |
| isFeatured | boolean | No | 是否熱門商品(預設 false) |
| isNew | boolean | No | 是否新品(預設 true) |
| images | array | No | 商品圖片(第一張為主圖,最多 6 張) |
請求範例 (JSON):
POST /api/v1/products
Authorization: Bearer {access_token}
Content-Type: application/json
{
"category": "bouquet",
"name": "粉色康乃馨花束",
"description": "母親節限定款,粉嫩溫馨",
"basePrice": 980,
"costPrice": 450,
"stock": 30,
"lowStockThreshold": 5,
"stockUnit": "束",
"isFeatured": true,
"isNew": true,
"images": [
"https://example.com/images/carnation-main.jpg",
"https://example.com/images/carnation-1.jpg"
]
}
請求範例 (FormData - binary-mixed):
POST /api/v1/products
Authorization: Bearer {access_token}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="data"
Content-Type: application/json
{"category":"bouquet","name":"粉色康乃馨花束","basePrice":980,"images":["$$file:0","$$file:1"]}
------WebKitFormBoundary
Content-Disposition: form-data; name="file_0"; filename="main.jpg"
Content-Type: image/jpeg
[binary data]
------WebKitFormBoundary
Content-Disposition: form-data; name="file_1"; filename="detail.jpg"
Content-Type: image/jpeg
[binary data]
------WebKitFormBoundary--
響應範例 (201 Created):
{
"id": "prod-002",
"tenantId": "tenant-001",
"sku": "FS01-BQT-0002",
"name": "粉色康乃馨花束",
"category": "bouquet",
"description": "母親節限定款,粉嫩溫馨",
"basePrice": 980,
"costPrice": 450,
"stock": 30,
"lowStockThreshold": 5,
"stockUnit": "束",
"status": "active",
"isFeatured": true,
"isNew": true,
"mainImageUrl": "https://example.com/images/carnation-main.jpg",
"galleryImageUrls": ["https://example.com/images/carnation-1.jpg"],
"createdAt": "2025-12-22T10:00:00Z",
"updatedAt": "2025-12-22T10:00:00Z",
"createdBy": "user-001"
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 請填寫商品名稱 | 名稱為空 |
| 400 | 請選擇商品分類 | 分類為空 |
| 400 | 請填寫有效的售價 | 價格無效或為負數 |
| 409 | 商品名稱已存在 | 同租戶內名稱重複 |
PUT /api/v1/products/:id
描述: 更新商品(完整替換)
權限: ROLE_OWNER 或 ROLE_MANAGER
注意: 建議使用 PATCH 進行部分更新,PUT 會替換整個資源
請求體: 同 POST,所有欄位都需提供
響應: 同 POST 的響應格式
PATCH /api/v1/products/:id
描述: 部分更新商品(僅更新提供的欄位)
權限: ROLE_OWNER 或 ROLE_MANAGER
Content-Type:
application/jsonmultipart/form-data(包含圖片時)
請求體 (UpdateProductRequest):
所有欄位皆為可選,僅更新提供的欄位:
| 欄位 | 類型 | 說明 |
|---|---|---|
| name | string | 商品名稱 |
| description | string | 商品描述 |
| basePrice | number | 基礎價格 |
| costPrice | number | 成本價格 |
| stock | integer | 庫存數量 |
| lowStockThreshold | integer | 低庫存警戒值 |
| stockUnit | string | 庫存單位 |
| isFeatured | boolean | 是否熱門商品 |
| isNew | boolean | 是否新品 |
| images | array | 商品圖片 |
請求範例:
PATCH /api/v1/products/prod-001
Authorization: Bearer {access_token}
Content-Type: application/json
{
"basePrice": 1350,
"stock": 45,
"isFeatured": false
}
響應範例 (200 OK):
返回更新後的完整商品資料
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 商品不存在 | 找不到指定的商品 |
| 409 | 商品名稱已存在 | 名稱與其他商品重複 |
PATCH /api/v1/products/:id/status
描述: 更新商品上架狀態
權限: ROLE_OWNER 或 ROLE_MANAGER
請求體 (UpdateProductStatusRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| status | string | Yes | 目標狀態(active 或 inactive) |
請求範例:
PATCH /api/v1/products/prod-001/status
Authorization: Bearer {access_token}
Content-Type: application/json
{
"status": "inactive"
}
響應範例 (200 OK):
返回更新後的完整商品資料
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 無效的狀態值 | 狀態值不是 active 或 inactive |
| 404 | 商品不存在 | 找不到指定的商品 |
DELETE /api/v1/products/:id
描述: 刪除商品
權限: ROLE_OWNER 或 ROLE_ADMIN
業務規則:
- 僅 OWNER 或 ADMIN 可刪除商品
- 有相關訂單的商品無法刪除(需先處理訂單)
請求範例:
DELETE /api/v1/products/prod-001
Authorization: Bearer {access_token}
響應範例 (204 No Content):
無響應體
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 403 | 權限不足,僅店主可刪除商品 | 非 OWNER/ADMIN 角色 |
| 404 | 商品不存在 | 找不到指定的商品 |
| 409 | 此商品有相關訂單,無法刪除 | 存在關聯訂單 |
POST /api/v1/products/upload-image
描述: 上傳商品圖片(獨立上傳端點)
權限: ROLE_OWNER 或 ROLE_MANAGER
Content-Type: multipart/form-data
請求體:
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| file | File | Yes | 圖片檔案(支援 jpg, png, webp) |
檔案限制:
- 最大檔案大小:5MB
- 支援格式:image/jpeg, image/png, image/webp
請求範例:
POST /api/v1/products/upload-image
Authorization: Bearer {access_token}
Content-Type: multipart/form-data
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="product.jpg"
Content-Type: image/jpeg
[binary data]
------WebKitFormBoundary--
響應範例 (201 Created):
{
"id": "img-001",
"url": "https://storage.example.com/products/img-001.jpg"
}
業務規則
商品名稱唯一性
- 同一租戶內,商品名稱必須唯一
- 創建或更新時會檢查重複
- 重複時返回 409 Conflict
SKU 自動生成
- 創建商品時自動生成 SKU
- 格式:
{租戶代碼}-{分類代碼}-{流水號} - SKU 不可修改
刪除限制
- 有相關訂單的商品無法刪除
- 返回 409 Conflict 並包含相關訂單數量
多租戶隔離
- 所有商品資料自動附加
tenantId - 查詢自動過濾為當前租戶的商品
- 跨租戶訪問返回 404 或空列表
圖片處理
- 主圖 (mainImageUrl):images 陣列的第一張
- 副圖 (galleryImageUrls):images 陣列的其餘圖片
- 最多 6 張圖片(1 張主圖 + 5 張副圖)
錯誤碼總覽
| HTTP 狀態碼 | 錯誤碼 | 說明 |
|---|---|---|
| 400 | BAD_REQUEST | 請求格式錯誤或缺少必填欄位 |
| 401 | AUTH_TOKEN_INVALID | Token 無效或過期 |
| 403 | FORBIDDEN | 權限不足 |
| 404 | NOT_FOUND | 資源不存在 |
| 409 | CONFLICT | 資源衝突(名稱重複、有關聯訂單等) |
| 500 | INTERNAL_ERROR | 伺服器內部錯誤 |
最後更新: 2025-12-22