# AI 商品生成手冊

> 本手冊供 AI Agent 自動生成商品並上架到電商平台使用。  
> 涵蓋：API 認證、圖片生成（Stable Diffusion）、圖片上傳、商品建立、BOM 設定、上架流程。

---

## 目錄

1. [平台概述](#1-平台概述)
2. [API 認證 — Bearer Token](#2-api-認證--bearer-token)
3. [完整上架流程（Step-by-Step）](#3-完整上架流程step-by-step)
4. [圖片生成 — Stable Diffusion API](#4-圖片生成--stable-diffusion-api)
5. [圖片上傳 API](#5-圖片上傳-api)
6. [商品建立 API](#6-商品建立-api)
7. [BOM（物料清單）說明](#7-bom物料清單說明)
8. [其他商品管理 API](#8-其他商品管理-api)
9. [API 錯誤碼速查](#9-api-錯誤碼速查)
10. [完整範例腳本（Python）](#10-完整範例腳本python)

---

## 1. 平台概述

本平台為多租戶電商系統，每間商店有獨立的 `slug`（路徑識別碼）。  
所有外部 API 路徑格式：

```
https://{HOST}/api/v1/{SHOP_SLUG}/...
```

| 欄位 | 說明 | 範例 |
|------|------|------|
| `HOST` | 伺服器網域 | `example.com` |
| `SHOP_SLUG` | 商店唯一識別碼 | `my-shop` |

---

## 2. API 認證 — Bearer Token

### 2.1 取得 API 金鑰

API 金鑰由商家在後台手動產生（**目前無 API 端點，需人工操作**）：

1. 登入商家後台 → 設定 → API 金鑰
2. 建立金鑰，填寫用途說明（如：`AI 新增商品`）
3. **複製金鑰明文**（僅顯示一次，後台只儲存 bcrypt hash）

### 2.2 加入請求 Header

所有 `/api/v1/` 路徑都需要此 Header：

```http
Authorization: Bearer <YOUR_API_KEY>
```

### 2.3 認證錯誤

| HTTP 狀態 | 原因 |
|-----------|------|
| `401` | 未帶 Authorization header |
| `403` | 金鑰無效或已停用 |
| `404` | 商店 slug 不存在或商店未啟用 |

---

## 3. 完整上架流程（Step-by-Step）

```
┌─────────────────────────────────────────────┐
│  AI Agent 商品上架流程                        │
│                                             │
│  1. 生成商品圖片                              │
│     └─ Stable Diffusion API → PNG/JPG       │
│                                             │
│  2. 上傳圖片到圖庫                            │
│     └─ POST /api/v1/{slug}/media/upload     │
│        回傳 media_id                         │
│                                             │
│  3. 建立商品                                 │
│     └─ POST /api/v1/{slug}/products         │
│        帶入 media_id + 商品資訊               │
│        回傳 product_id                       │
│                                             │
│  4. （選用）設定排序                          │
│     └─ PATCH /api/v1/{slug}/products/{id}  │
│            /display_order                   │
└─────────────────────────────────────────────┘
```

---

## 4. 圖片生成 — Stable Diffusion API

### 4.1 推薦使用 Stable Diffusion WebUI (AUTOMATIC1111)

本地端啟動 WebUI API mode：

```bash
python launch.py --api --nowebui
```

預設 API 地址：`http://127.0.0.1:7860`

### 4.2 文字生圖（txt2img）

```http
POST http://127.0.0.1:7860/sdapi/v1/txt2img
Content-Type: application/json
```

**請求 Body：**

```json
{
  "prompt": "product photography, white background, studio lighting, [商品名稱], high quality, 4k",
  "negative_prompt": "blurry, low quality, text, watermark, people, hands",
  "width": 1024,
  "height": 1024,
  "steps": 20,
  "cfg_scale": 7,
  "sampler_name": "DPM++ 2M Karras",
  "n_iter": 1,
  "batch_size": 1
}
```

**回應：**

```json
{
  "images": ["<base64 encoded PNG>"],
  "info": "..."
}
```

### 4.3 商品攝影 Prompt 建議

| 商品類型 | 建議 Prompt 補充 |
|---------|----------------|
| 食品 | `food photography, close-up, appetizing, natural light` |
| 衣物 | `flat lay, clothing, clean white background, fashion photography` |
| 電子產品 | `product shot, minimalist, studio light, sharp focus` |
| 飾品 | `jewelry photography, macro, reflective surface, luxury` |

**通用 Negative Prompt：**

```
blurry, low quality, watermark, text, logo, person, hands, extra fingers, deformed, ugly background
```

### 4.4 將 Base64 圖片轉為檔案（Python）

```python
import base64

def save_sd_image(b64_string: str, output_path: str):
    image_data = base64.b64decode(b64_string)
    with open(output_path, "wb") as f:
        f.write(image_data)
```

---

## 5. 圖片上傳 API

### `POST /api/v1/{slug}/media/upload`

將圖片上傳到商家圖庫，取回 `media_id` 供建立商品使用。

**Content-Type：** `multipart/form-data`  
**欄位名稱：** `file`（可傳多個）

#### 限制

| 項目 | 限制 |
|------|------|
| 格式 | JPG / PNG / GIF / WEBP |
| 單檔大小 | ≤ 15 MB |
| 最大尺寸 | 4096 × 4096 px |

#### 範例請求（curl）

```bash
curl -X POST "https://{HOST}/api/v1/{SHOP_SLUG}/media/upload" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -F "file=@/path/to/product_image.jpg"
```

#### 成功回應（201）

```json
{
  "ok": true,
  "media": [
    {
      "id": "a1b2c3d4-...",
      "file_url": "https://example.com/uploads/shop-id/abc123.jpg",
      "thumb_url": "https://example.com/uploads/shop-id/abc123_thumb.jpg",
      "original_name": "product_image.jpg",
      "width": 1024,
      "height": 1024,
      "file_size_bytes": 245760
    }
  ],
  "errors": []
}
```

> `media[0].id` 即為後續建立商品時使用的 `main_image_id`。

#### 多圖上傳（同時上傳主圖 + 相簿圖）

```bash
curl -X POST "https://{HOST}/api/v1/{SHOP_SLUG}/media/upload" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -F "file=@main.jpg" \
  -F "file=@gallery1.jpg" \
  -F "file=@gallery2.jpg"
```

回應的 `media` 陣列順序對應上傳順序。

---

## 6. 商品建立 API

### `POST /api/v1/{slug}/products`

**Content-Type：** `application/json`

#### 請求 Body 欄位

| 欄位 | 型別 | 必填 | 說明 |
|------|------|:----:|------|
| `name` | string | ✅ | 商品名稱（最長 200 字） |
| `price` | number | ✅ | 售價（非負數，最多 2 位小數） |
| `description` | string | ❌ | 商品描述（最長 5000 字，支援 HTML） |
| `cost_price` | number | ❌ | 成本價（不影響顯示，用於帳務統計） |
| `product_status` | string | ❌ | `"active"`（立即上架）/ `"inactive"`（草稿）/ `"preorder"`（預購）。預設 `"active"` |
| `is_active` | boolean | ❌ | 舊版相容欄位；若已填 `product_status` 則以 `product_status` 為準 |
| `parent_category_name` | string | ❌ | 父分類名稱。不存在則自動建立 |
| `category_names` | array[string] | ❌ | 子分類名稱陣列。不存在則自動建立 |
| `main_image_id` | string (UUID) | ❌ | 主圖的 `media_id`（由圖片上傳 API 取得） |
| `gallery_image_ids` | array[string] | ❌ | 相簿圖 `media_id` 陣列 |

#### 範例請求

```json
POST /api/v1/my-shop/products
Authorization: Bearer sk_live_xxxxxxxxxxxx
Content-Type: application/json

{
  "name": "有機蜂蜜禮盒（500g）",
  "price": 480,
  "cost_price": 180,
  "description": "<p>純天然有機蜂蜜，無添加防腐劑，每盒附精美提袋。</p>",
  "product_status": "active",
  "parent_category_name": "食品",
  "category_names": ["蜂蜜", "禮盒"],
  "main_image_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "gallery_image_ids": [
    "b2c3d4e5-...",
    "c3d4e5f6-..."
  ]
}
```

#### 成功回應（201）

```json
{
  "ok": true,
  "product": {
    "id": "d4e5f6g7-...",
    "name": "有機蜂蜜禮盒（500g）",
    "price": 480.0,
    "cost_price": 180.0,
    "is_active": true,
    "product_status": "active",
    "category_names": ["蜂蜜", "禮盒"],
    "main_image_url": "https://example.com/uploads/.../main.jpg",
    "gallery_count": 2
  }
}
```

#### 失敗回應（400）

```json
{
  "ok": false,
  "error": "price 格式錯誤，需為非負數值"
}
```

#### 分類結構說明

平台支援兩層分類結構：

```
父分類（parent_category_name）
  └── 子分類1（category_names[0]）
  └── 子分類2（category_names[1]）
```

- 若 `parent_category_name` 不存在，API 會自動建立
- 若 `category_names` 中的名稱不存在，API 會自動建立並掛在指定父分類下
- 同一商品可屬於多個子分類

---

## 7. BOM（物料清單）說明

### 7.1 什麼是 BOM

BOM（Bill of Materials）定義商品由哪些原料組成，以及每件商品需要消耗多少原料。  
系統會在訂單成立時自動扣減原料庫存，並在庫存不足時自動暫停商品（`auto_pause = TRUE`）。

### 7.2 BOM 相關資料表

```sql
-- 原料庫存
materials (
    id            UUID,
    tenant_id     UUID,
    name          VARCHAR(200),   -- 原料名稱（如：蜂蜜原液、玻璃罐）
    unit          VARCHAR(20),    -- 單位（如：ml、個、g）
    quantity      NUMERIC,        -- 當前庫存數量
    low_stock_threshold  NUMERIC, -- 低庫存警戒線
    cost_per_unit NUMERIC         -- 每單位成本
)

-- 商品-原料關聯
product_materials (
    product_id       UUID,
    material_id      UUID,
    quantity_needed  NUMERIC  -- 每件商品需要多少原料
)
```

### 7.3 BOM 操作（目前需透過商家後台）

**外部 API 目前不支援直接設定 BOM**，需透過商家後台頁面操作：

1. 後台 → 庫存管理 → 建立原料（填入初始庫存）
2. 後台 → 商品管理 → 編輯商品 → BOM 欄位 → 設定原料與用量

### 7.4 BOM 對 API 建立商品的影響

使用 API 建立的商品，`auto_pause` 預設為 `TRUE`：

- 若該商品**有 BOM**：訂單成立 → 扣庫存 → 庫存歸零 → 自動下架
- 若該商品**無 BOM**（純靠 `cost_price`）：庫存不受影響，不會自動暫停

> **建議**：AI 建立的商品若為無限庫存商品（如：數位商品、按需生產），  
> 可在建立後，透過後台關閉該商品的 `auto_pause` 選項。

---

## 8. 其他商品管理 API

### 8.1 查詢商品列表

```http
GET /api/v1/{slug}/products?status=all
Authorization: Bearer <YOUR_API_KEY>
```

| 參數 | 值 | 說明 |
|------|----|------|
| `status` | `all` / `active` / `inactive` | 篩選狀態（預設 `all`） |

**回應：**

```json
{
  "ok": true,
  "products": [
    {
      "id": "...",
      "name": "有機蜂蜜禮盒",
      "price": 480.0,
      "is_active": true,
      "categories": ["蜂蜜", "禮盒"],
      "created_at": "2026-04-17T10:30:00+00:00"
    }
  ],
  "total": 1
}
```

### 8.2 上架商品

```http
POST /api/v1/{slug}/products/{product_id}/list
Authorization: Bearer <YOUR_API_KEY>
```

**回應：**

```json
{ "ok": true, "product_id": "...", "name": "...", "is_active": true }
```

### 8.3 下架商品

```http
POST /api/v1/{slug}/products/{product_id}/unlist
Authorization: Bearer <YOUR_API_KEY>
```

### 8.4 設定商品排序（曝光度）

數值越小越靠前顯示，預設 9999。

```http
PATCH /api/v1/{slug}/products/{product_id}/display_order
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{ "display_order": 1 }
```

傳入 `null` 或不傳則重設為 9999。

**回應：**

```json
{ "ok": true, "product_id": "...", "name": "...", "display_order": 1 }
```

### 8.5 查詢圖庫

```http
GET /api/v1/{slug}/media?page=1&per_page=20&q=蜂蜜
Authorization: Bearer <YOUR_API_KEY>
```

**回應：**

```json
{
  "ok": true,
  "media": [
    {
      "id": "a1b2c3...",
      "file_url": "https://...",
      "original_name": "honey.jpg",
      "width": 1024,
      "height": 1024,
      "created_at": "2026-04-17T10:30:00+00:00"
    }
  ],
  "total": 5,
  "page": 1,
  "pages": 1
}
```

---

## 9. API 錯誤碼速查

| HTTP 狀態 | 常見原因 | 對策 |
|-----------|---------|------|
| `400` | 欄位格式錯誤、缺少必填欄位 | 檢查 `error` 欄位說明 |
| `401` | 未帶 Authorization header | 加入 `Authorization: Bearer ...` |
| `403` | 金鑰錯誤或已停用 | 確認金鑰是否有效 |
| `404` | 商店 slug 不存在、商品不存在 | 確認 slug / product_id |
| `500` | 伺服器/資料庫錯誤 | 回報給管理員，查看 server log |

---

## 10. 完整範例腳本（Python）

以下示範完整流程：生成圖片 → 上傳 → 建立商品。

```python
import requests
import base64
import io
import json

# ─────────────────────────────────────────────
# 設定
# ─────────────────────────────────────────────
SHOP_HOST   = "https://example.com"   # 替換為實際網域
SHOP_SLUG   = "my-shop"               # 替換為商店 slug
API_KEY     = "sk_live_xxxxxxxxxxxx"  # 替換為實際 API 金鑰
SD_API_URL  = "http://127.0.0.1:7860" # Stable Diffusion WebUI 地址

HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# ─────────────────────────────────────────────
# Step 1: 用 Stable Diffusion 生成商品圖
# ─────────────────────────────────────────────
def generate_product_image(product_name: str) -> bytes:
    payload = {
        "prompt": (
            f"product photography, {product_name}, white background, "
            "studio lighting, high quality, 4k, sharp focus"
        ),
        "negative_prompt": (
            "blurry, low quality, watermark, text, logo, person, hands, "
            "extra fingers, deformed, dark background"
        ),
        "width": 1024,
        "height": 1024,
        "steps": 25,
        "cfg_scale": 7,
        "sampler_name": "DPM++ 2M Karras",
        "batch_size": 1,
    }
    resp = requests.post(f"{SD_API_URL}/sdapi/v1/txt2img", json=payload, timeout=120)
    resp.raise_for_status()
    b64 = resp.json()["images"][0]
    return base64.b64decode(b64)

# ─────────────────────────────────────────────
# Step 2: 上傳圖片到商家圖庫
# ─────────────────────────────────────────────
def upload_image(image_bytes: bytes, filename: str) -> str:
    """Returns media_id (UUID string)"""
    files = {"file": (filename, io.BytesIO(image_bytes), "image/png")}
    resp = requests.post(
        f"{SHOP_HOST}/api/v1/{SHOP_SLUG}/media/upload",
        headers=HEADERS,
        files=files,
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    if not data.get("ok"):
        raise RuntimeError(f"圖片上傳失敗: {data}")
    return data["media"][0]["id"]

# ─────────────────────────────────────────────
# Step 3: 建立商品
# ─────────────────────────────────────────────
def create_product(
    name: str,
    price: float,
    description: str,
    main_image_id: str,
    parent_category: str = None,
    categories: list = None,
    cost_price: float = None,
) -> dict:
    payload = {
        "name": name,
        "price": price,
        "description": description,
        "product_status": "active",
        "main_image_id": main_image_id,
    }
    if cost_price is not None:
        payload["cost_price"] = cost_price
    if parent_category:
        payload["parent_category_name"] = parent_category
    if categories:
        payload["category_names"] = categories

    resp = requests.post(
        f"{SHOP_HOST}/api/v1/{SHOP_SLUG}/products",
        headers={**HEADERS, "Content-Type": "application/json"},
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    if not data.get("ok"):
        raise RuntimeError(f"商品建立失敗: {data}")
    return data["product"]

# ─────────────────────────────────────────────
# 主流程
# ─────────────────────────────────────────────
def auto_list_product(
    name: str,
    price: float,
    description: str,
    parent_category: str = None,
    categories: list = None,
    cost_price: float = None,
):
    print(f"[1/3] 生成圖片：{name}")
    img_bytes = generate_product_image(name)

    print(f"[2/3] 上傳圖片...")
    media_id = upload_image(img_bytes, f"{name}_main.png")
    print(f"      media_id = {media_id}")

    print(f"[3/3] 建立商品...")
    product = create_product(
        name=name,
        price=price,
        description=description,
        main_image_id=media_id,
        parent_category=parent_category,
        categories=categories,
        cost_price=cost_price,
    )
    print(f"      完成！product_id = {product['id']}")
    return product


# ─────────────────────────────────────────────
# 範例呼叫
# ─────────────────────────────────────────────
if __name__ == "__main__":
    # 單一商品
    auto_list_product(
        name="有機蜂蜜禮盒（500g）",
        price=480,
        description="<p>純天然有機蜂蜜，無添加防腐劑，每盒附精美提袋。</p>",
        parent_category="食品",
        categories=["蜂蜜", "禮盒"],
        cost_price=180,
    )

    # 批次上架多件商品
    product_list = [
        {
            "name": "玫瑰精油香皂",
            "price": 150,
            "description": "天然植物精油，溫和不刺激。",
            "parent_category": "保養品",
            "categories": ["香皂", "精油"],
        },
        {
            "name": "竹炭牙刷（3入）",
            "price": 120,
            "description": "環保竹炭纖維刷毛，細緻清潔。",
            "parent_category": "日用品",
            "categories": ["口腔護理"],
        },
    ]
    for p in product_list:
        auto_list_product(**p)
```

---

## 附錄 A：product_status 狀態說明

| 值 | is_active | 說明 |
|----|-----------|------|
| `active` | `true` | 立即上架，消費者可見、可購買 |
| `inactive` | `false` | 草稿/下架，消費者不可見 |
| `preorder` | `true` | 預購中，消費者可見並可下預購訂單，不消耗庫存 |

---

## 附錄 B：API 路徑總覽

| 方法 | 路徑 | 功能 |
|------|------|------|
| `POST` | `/api/v1/{slug}/media/upload` | 上傳圖片 |
| `GET` | `/api/v1/{slug}/media` | 查詢圖庫 |
| `GET` | `/api/v1/{slug}/products` | 查詢商品列表 |
| `POST` | `/api/v1/{slug}/products` | 建立商品 |
| `POST` | `/api/v1/{slug}/products/{id}/list` | 上架商品 |
| `POST` | `/api/v1/{slug}/products/{id}/unlist` | 下架商品 |
| `PATCH` | `/api/v1/{slug}/products/{id}/display_order` | 設定排序 |
