[FEAT] 重構與深化 MQTT 通訊規範並優化 API 文件中心

1. 重構 config/api-docs.php,將 HTTP API 與 MQTT Topic 徹底解耦,建立清晰的協議分類。
2. 實作 api-docs.blade.php 雙頁籤 (HTTP/MQTT) 介面,優化「極簡奢華風」視覺呈現與互動體驗。
3. 詳細定義 MQTT 指令下發 Payload,包含遠端出貨、庫存同步、遠端找零及系統控制。
4. 新增 Heartbeat (心跳) 與 Transaction (交易) MQTT 主題規範,確保 APP 端有明確的對接基準。
5. 修正 Heartbeat Payload 參數,僅保留實際傳送的溫度與韌體版本,確保文件與現況對齊。
6. 移除 B012 (商品同步) 尚未實作的 PATCH 方法,統一只保留 GET 方法以維持系統一致性。
7. 同步更新 SKILL.md 技術規格文件,確保開發規範與實際程式碼邏輯完全對等。
This commit is contained in:
sky121113 2026-04-22 16:25:48 +08:00
parent 9114269a7a
commit ae554ef130
5 changed files with 450 additions and 252 deletions

View File

@ -116,9 +116,9 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
### 3.5 B012: 商品配置與商品主檔同步 (Unified Sync)
用於機台端獲取目前所有可販售商品的詳細配置。App 端應依據呼叫的方法決定數據處理方式。
用於機台端獲取目前所有可販售商品的詳細配置。
- **URL**: GET|PATCH /api/v1/app/machine/products/B012
- **URL**: GET /api/v1/app/machine/products/B012
- **Authentication**: Bearer Token (Header)
- **Request Body:** 無 (由 Token 自動識別機台)
@ -262,30 +262,6 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
---
### 3.11 B055: 遠端指令出貨控制 (Remote Dispense / Force Open)
用於遠端手動驅動機台出貨。通常用於補償使用者、測試機台或客服協助開門的情景。
- **URL**: GET|PUT /api/v1/app/machine/dispense/B055
- **Authentication**: Bearer Token (Header)
- **運作模式**:
- **GET (查詢)**:當收到 **MQTT 指令 (`force_dispense` 或 `compensation`)** 時呼叫。雲端會回傳待執行的貨道編號與指令 ID。
- **PUT (回報)**:實體出貨完成後回饋結果,以便雲端將該指令標記為「已執行」。
- **Response Body (GET - 查詢階段):**
| 參數 | 類型 | 說明 | 範例 |
| :--- | :--- | :--- | :--- |
| data | Array | 指令物件陣列 | [{"res1": "ID", "res2": "SlotNo"}] |
- **Request Body (PUT - 回報階段):**
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
| id | String | 是 | 雲端下發的指令 ID | "99" |
| type | String | 是 | 出貨類型代碼 (0: 成功, 其他: 失敗) | "0" |
| stock | String | 是 | 出貨後的貨道剩餘數量 | "9" |
---
## 4. 已遷移至 MQTT 的端點索引

View File

@ -198,24 +198,23 @@ Mqtt3Client client = MqttClient.builder()
```json
{
"command": "dispense",
"command_id": "20",
"payload": {
"slot_no": 5,
"transaction_id": "T202604140001"
},
"command_id": "MSG_123456789"
"new_stock": 6,
"old_stock": 7,
"slot_no": "1"
}
}
```
**常用指令集:**
- `reboot`: 遠端重啟機台系統。
- `reboot_card`: 遠端重啟讀卡機。
- `checkout`: 遠端結帳/登出指令。
- `lock`: 遠端鎖定機台(暫停服務)。
- `unlock`: 遠端解鎖機台(恢復服務)。
- `change`: 遠端修改數值(夾帶 amount 參數)。
- `reload_config`: 重新下載參數 (B014)。
- `reload_products`: 重新同步商品 (B012適用於商品主檔、圖文變更)。
- `update_inventory`: 遠端即時修改庫存 (直接夾帶數量更新 SQLite取代 HTTP)。
- `dispense`: 遠端出貨指令 (B055)。
- `checkout`: 遠端結帳。
- `lock`: 鎖定頁鎖定。
- `unlock`: 鎖定頁解鎖。
- `change`: 遠端找零(夾帶 amount 參數)。
- `update_inventory`: 遠端即時修改庫存。
- `dispense`: 遠端出貨指令。
**`update_inventory` Payload 範例:**
```json
@ -291,11 +290,11 @@ Mqtt3Client client = MqttClient.builder()
## 5. Redis 橋接注意事項
### 5.1 Key 前綴一致性
Laravel 的 Redis 驅動會自動在 Key 前加上前綴 (預設 `starcloud_database_`)。Go Gateway 不使用 Laravel Redis 客戶端,因此**必須在程式碼中手動加上此前綴**
Laravel 的 Redis 驅動會自動在 Key 前加上前綴 (預設 `star_cloud_database_`)。Go Gateway 不使用 Laravel Redis 客戶端,因此**必須在程式碼中手動加上此前綴**
```go
// main.go
prefixedIncomingKey := "starcloud_database_" + cfg.IncomingQueueKey
prefixedIncomingKey := "star_cloud_database_" + cfg.IncomingQueueKey
pusher := bridge.NewRedisPusher(rdb, prefixedIncomingKey)
```

View File

@ -1,10 +1,11 @@
<?php
return [
'title' => 'Star Cloud IoT API 說明文件',
'title' => 'Star Cloud IoT 通訊協議',
'version' => 'v1.0.0',
'description' => '此文件提供 Star Cloud 智能販賣機 IoT 端點通訊協議說明,供硬體端與前端開發者調研與串接使用。',
'categories' => [
'description' => '此文件提供 Star Cloud 智能販賣機 IoT 端點通訊協議說明,包含主動拉取 (HTTP REST) 與被動接收 (MQTT) 雙軌通訊機制,供硬體端與前端開發者調研與串接使用。',
'http_apis' => [
[
'name' => '機台核心通訊 (IoT Core)',
'apis' => [
@ -231,9 +232,9 @@ return [
[
'name' => 'B012: 商品配置與商品主檔同步 (Unified Sync)',
'slug' => 'b012-unified-sync',
'method' => 'GET/PATCH',
'method' => 'GET',
'path' => '/api/v1/app/machine/products/B012',
'description' => '用於機台端獲取目前所有可販售商品的詳細配置。GET 為全量同步PATCH 為增量更新。',
'description' => '用於機台端獲取目前所有可販售商品的詳細配置 (全量同步)。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
@ -351,7 +352,7 @@ return [
[
'name' => 'B024: 取貨碼/通行碼驗證與消耗回報',
'slug' => 'b024-access-code',
'method' => 'POST/PUT',
'method' => 'POST',
'path' => '/api/v1/app/sell/access-code/B024',
'description' => '處理代碼取貨流程。POST 用於驗證碼有效性PUT 用於回報出貨成功並消耗代碼。',
'headers' => [
@ -388,7 +389,7 @@ return [
[
'name' => 'B027: 贈品碼/優惠券驗證與消耗回報',
'slug' => 'b027-freebie-code',
'method' => 'POST/PUT',
'method' => 'POST',
'path' => '/api/v1/app/sell/free-gift/B027',
'description' => '處理贈品券與 0 元購活動。邏輯與 B024 相似但對象為行銷贈品。',
'headers' => [
@ -412,26 +413,168 @@ return [
'message' => 'Free gift verified'
],
],
[
'name' => 'B055: 遠端指令出貨控制 (Remote Dispense)',
'slug' => 'b055-remote-dispense',
'method' => 'POST/PUT',
'path' => '/api/v1/app/machine/dispense/B055',
'description' => '遠端手動驅動機台出貨。POST 用於獲取待處理指令PUT 用於回報出貨完成。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
],
'parameters' => [
'id' => ['type' => 'string', 'description' => '指令 ID (PUT)'],
'stock' => ['type' => 'string', 'description' => '剩餘庫存 (PUT)'],
],
'request' => [],
'response' => [
'success' => true,
'data' => [
['slot_no' => '1', 'order_id' => 'RE-123']
]
]
],
'mqtt_topics' => [
[
'name' => '指令與通訊 (Commands & Flow)',
'topics' => [
[
'name' => '機台心跳上報 (Heartbeat)',
'slug' => 'mqtt-heartbeat',
'action' => 'PUB',
'topic' => 'machine/{serial_no}/heartbeat',
'qos' => 0,
'description' => '機台每分鐘固定上報的狀態大包裝。整合了溫度、韌體版本與機台系統級日誌。',
'payload_parameters' => [
'temperature' => ['type' => 'number', 'description' => '目前機台溫度'],
'firmware_version' => ['type' => 'string', 'description' => 'APP 韌體版本'],
],
'payload_example' => [
'temperature' => 20.5,
'firmware_version' => '2.1.6'
],
],
[
'name' => '交易生命週期上報 (Transaction)',
'slug' => 'mqtt-transaction',
'action' => 'PUB',
'topic' => 'machine/{serial_no}/transaction',
'qos' => 1,
'description' => '整合交易生命週期的三個階段:交易建立 (create)、發票開立 (invoice) 以及出貨結果 (dispense)。',
'payload_parameters' => [
'action' => ['type' => 'string', 'description' => '生命週期動作 (create, invoice, dispense)'],
'payment_type' => ['type' => 'string', 'description' => '支付方式 (action 為 create 時必填)'],
'amount' => ['type' => 'number', 'description' => '交易金額 (create/invoice 時使用)'],
'slot_no' => ['type' => 'string', 'description' => '出貨貨道號碼'],
'invoice_no' => ['type' => 'string', 'description' => '發票號碼 (action 為 invoice 時必填)'],
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,如 success, failed)'],
],
'payload_example' => [
'action' => 'create',
'payment_type' => 'credit_card',
'amount' => 25.0,
'slot_no' => '3',
'product_id' => '5'
],
'notes' => '備註:此端點雲端後台暫未實作對應邏輯,但 APP 端可先行按照此結構開發。'
],
[
'name' => '指令下發:遠端出貨 (Dispense)',
'slug' => 'mqtt-command-dispense',
'action' => 'SUB',
'topic' => 'machine/{serial_no}/command',
'qos' => 1,
'description' => '雲端主動下發「遠端出貨」指令。機台收到後應立即驅動馬達進行出貨,出貨完成或失敗後回報 ACK。',
'payload_parameters' => [
'command' => ['type' => 'string', 'description' => '固定為 "dispense"'],
'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'],
'payload' => ['type' => 'object', 'description' => '出貨參數'],
'payload.slot_no' => ['type' => 'string', 'description' => '欲出貨的貨道號碼'],
'payload.old_stock' => ['type' => 'integer', 'description' => '出貨前的庫存量'],
'payload.new_stock' => ['type' => 'integer', 'description' => '出貨後應剩餘的庫存量 (樂觀扣減結果)'],
],
'payload_example' => [
'command' => 'dispense',
'command_id' => '9988776655',
'payload' => [
'slot_no' => '1',
'old_stock' => 10,
'new_stock' => 9
]
],
],
[
'name' => '指令下發:庫存同步 (Update Inventory)',
'slug' => 'mqtt-command-update-inventory',
'action' => 'SUB',
'topic' => 'machine/{serial_no}/command',
'qos' => 1,
'description' => '雲端主動下發「庫存更新」指令 (如從後台修改庫存、效期時觸發)。',
'payload_parameters' => [
'command' => ['type' => 'string', 'description' => '固定為 "update_inventory"'],
'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'],
'payload' => ['type' => 'object', 'description' => '更新參數'],
'payload.data' => ['type' => 'array', 'description' => '欲更新的貨道資料陣列'],
'payload.data[].slot_no' => ['type' => 'string', 'description' => '貨道號碼'],
'payload.data[].stock' => ['type' => 'integer', 'description' => '最新庫存量'],
'payload.data[].expiry_date' => ['type' => 'string', 'description' => '最新效期 (YYYY-MM-DD)'],
'payload.data[].batch_no' => ['type' => 'string', 'description' => '批號 (可為 null)'],
],
'payload_example' => [
'command' => 'update_inventory',
'command_id' => '5566778899',
'payload' => [
'data' => [
[
'slot_no' => '1',
'stock' => 10,
'expiry_date' => '2026-12-31',
'batch_no' => null
]
]
]
],
],
[
'name' => '指令下發:遠端找零 (Change)',
'slug' => 'mqtt-command-change',
'action' => 'SUB',
'topic' => 'machine/{serial_no}/command',
'qos' => 1,
'description' => '雲端主動下發「找零」指令。指示機台退還指定金額的現金。',
'payload_parameters' => [
'command' => ['type' => 'string', 'description' => '固定為 "change"'],
'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'],
'payload' => ['type' => 'object', 'description' => '找零參數'],
'payload.amount' => ['type' => 'integer', 'description' => '欲找零/退款的金額'],
],
'payload_example' => [
'command' => 'change',
'command_id' => '1122334455',
'payload' => [
'amount' => 50
]
],
],
[
'name' => '指令下發:系統控制 (System Control)',
'slug' => 'mqtt-command-system',
'action' => 'SUB',
'topic' => 'machine/{serial_no}/command',
'qos' => 1,
'description' => '雲端主動下發的系統級別控制指令。包含重啟、設備鎖定解鎖、購物車結帳等無額外參數的指令。',
'payload_parameters' => [
'command' => ['type' => 'string', 'description' => '系統指令類型。支援reboot (重啟機台), reboot_card (重啟刷卡機), checkout (購物車結帳), lock (設備鎖定), unlock (設備解鎖)'],
'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'],
'payload' => ['type' => 'object', 'description' => '空物件 (無額外參數)'],
],
'payload_example' => [
'command' => 'reboot',
'command_id' => '9999999999',
'payload' => new \stdClass()
],
],
[
'name' => '指令執行回報 (Machine to Cloud)',
'slug' => 'mqtt-command-ack',
'action' => 'PUB',
'topic' => 'machine/{serial_no}/command/ack',
'qos' => 1,
'description' => '機台執行完雲端指令後的主動回報。此回報極為重要,雲端將依此決定交易是否完成或需進行庫存回滾。',
'payload_parameters' => [
'command_id' => ['type' => 'string', 'description' => '對應下發時的指令 ID'],
'result' => ['type' => 'string', 'description' => '執行結果,僅接受: success 或 failed'],
'stock' => ['type' => 'integer', 'description' => '執行指令後的最終剩餘庫存 (選填,通常出貨指令需帶上)'],
'message' => ['type' => 'string', 'description' => '額外的錯誤訊息 (選填)'],
],
'payload_example' => [
'command_id' => '12345',
'result' => 'success',
'stock' => 9
],
]
]

View File

@ -9,9 +9,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<!-- CSS (Vite or CDN for layout) -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@ -21,7 +19,6 @@
--color-luxury-deep: #f8fafc;
--color-luxury-card: #ffffff;
--color-accent: #0891b2;
/* Cyan-600 for light mode */
}
body {
@ -51,25 +48,6 @@
max-width: 1400px;
}
.method-badge {
padding: 4px 10px;
border-radius: 6px;
font-weight: 800;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.method-post {
background: #0891b2;
color: #ffffff;
}
.method-get {
background: #10b981;
color: #ffffff;
}
.param-table th {
text-align: left;
padding: 0.75rem 1rem;
@ -94,6 +72,7 @@
font-family: 'ui-monospace', monospace;
font-size: 14px;
line-height: 1.5;
overflow-x: auto;
}
.luxury-nav-section {
@ -138,7 +117,6 @@
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@ -147,27 +125,45 @@
</style>
</head>
<body x-data="{ activeSection: '{{ $docs['categories'][0]['apis'][0]['slug'] ?? '' }}' }">
<body x-data="{
currentTab: 'http',
activeSection: '{{ $docs['http_apis'][0]['apis'][0]['slug'] ?? '' }}'
}">
<!-- Sidebar -->
<div class="sidebar shadow-sm">
<div class="p-6 border-b border-slate-100">
<h1 class="font-display text-2xl font-black tracking-tighter text-slate-900">
STAR CLOUD <span class="text-cyan-600">文件</span>
<div class="sidebar shadow-sm flex flex-col">
<div class="p-6 border-b border-slate-100 pb-4">
<h1 class="font-display text-2xl font-black tracking-tighter text-slate-900 leading-none">
STAR CLOUD <span class="text-cyan-600">API</span>
</h1>
<p class="text-[11px] font-black text-slate-400 uppercase tracking-widest mt-1">API 參考手冊 {{ $docs['version']
}}</p>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1 mb-4">通訊參考手冊 {{ $docs['version'] }}</p>
<!-- Tab Controls -->
<div class="flex gap-2 bg-slate-100 p-1 rounded-xl">
<button @click="currentTab = 'http'; activeSection = '{{ $docs['http_apis'][0]['apis'][0]['slug'] ?? '' }}'"
:class="currentTab === 'http' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="flex-1 py-1.5 text-[11px] font-black uppercase tracking-widest rounded-lg transition-all">
HTTP
</button>
<button @click="currentTab = 'mqtt'; activeSection = '{{ $docs['mqtt_topics'][0]['topics'][0]['slug'] ?? '' }}'"
:class="currentTab === 'mqtt' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="flex-1 py-1.5 text-[11px] font-black uppercase tracking-widest rounded-lg transition-all">
MQTT
</button>
</div>
</div>
<nav class="mt-2 overflow-y-auto" style="height: calc(100vh - 100px);">
@foreach($docs['categories'] as $category)
<nav class="flex-1 overflow-y-auto py-2">
<!-- HTTP Nav -->
<div x-show="currentTab === 'http'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
@foreach($docs['http_apis'] as $category)
<div class="luxury-nav-section">{{ $category['name'] }}</div>
@foreach($category['apis'] as $api)
<a href="#{{ $api['slug'] }}" class="luxury-nav-link"
:class="{ 'active': activeSection === '{{ $api['slug'] }}' }"
@click="activeSection = '{{ $api['slug'] }}'">
<div class="flex items-center gap-2 overflow-hidden w-full">
<span class="flex-shrink-0 text-[10px] w-10 text-center font-black px-1.5 py-0.5 rounded-md uppercase tracking-tighter {{ $api['method'] === 'GET' ? 'bg-emerald-50 text-emerald-600 border border-emerald-200' : ($api['method'] === 'POST' ? 'bg-cyan-50 text-cyan-600 border border-cyan-200' : 'bg-amber-50 text-amber-600 border border-amber-200') }}">
<span class="flex-shrink-0 text-[10px] w-10 text-center font-black px-1 py-0.5 rounded uppercase tracking-tighter {{ $api['method'] === 'GET' ? 'bg-emerald-50 text-emerald-600 border border-emerald-200' : ($api['method'] === 'POST' ? 'bg-cyan-50 text-cyan-600 border border-cyan-200' : 'bg-amber-50 text-amber-600 border border-amber-200') }}">
{{ $api['method'] }}
</span>
<span class="truncate">{{ $api['name'] }}</span>
@ -175,11 +171,32 @@
</a>
@endforeach
@endforeach
</div>
<!-- MQTT Nav -->
<div x-show="currentTab === 'mqtt'" style="display: none;" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
@foreach($docs['mqtt_topics'] as $category)
<div class="luxury-nav-section">{{ $category['name'] }}</div>
@foreach($category['topics'] as $topic)
<a href="#{{ $topic['slug'] }}" class="luxury-nav-link"
:class="{ 'active': activeSection === '{{ $topic['slug'] }}' }"
@click="activeSection = '{{ $topic['slug'] }}'">
<div class="flex items-center gap-2 overflow-hidden w-full">
<span class="flex-shrink-0 text-[10px] w-10 text-center font-black px-1 py-0.5 rounded uppercase tracking-tighter {{ $topic['action'] === 'PUB' ? 'bg-violet-50 text-violet-600 border border-violet-200' : 'bg-indigo-50 text-indigo-600 border border-indigo-200' }}">
{{ $topic['action'] }}
</span>
<span class="truncate">{{ $topic['name'] }}</span>
</div>
</a>
@endforeach
@endforeach
</div>
</nav>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="main-content pb-24">
<!-- Title Card -->
<div class="mb-10 p-8 bg-white rounded-3xl border border-slate-100 shadow-sm">
<h2 class="font-display text-4xl font-black tracking-tight text-slate-900 mb-4">{{ $docs['title'] }}</h2>
<p class="text-slate-500 leading-relaxed text-lg max-w-3xl font-medium">
@ -187,10 +204,11 @@
</p>
</div>
@foreach($docs['categories'] as $category)
<!-- HTTP Content -->
<div x-show="currentTab === 'http'" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform translate-y-4" x-transition:enter-end="opacity-100 transform translate-y-0">
@foreach($docs['http_apis'] as $category)
@foreach($category['apis'] as $api)
<div id="{{ $api['slug'] }}" class="mb-16 animate-luxury-in" x-intersect="activeSection = '{{ $api['slug'] }}'">
<div class="mb-4"></div>
<div id="{{ $api['slug'] }}" class="mb-16 animate-luxury-in scroll-mt-10" x-intersect.margin.-200px="activeSection = '{{ $api['slug'] }}'">
<div class="flex items-center gap-4 mb-6">
<span class="px-3 py-1 text-sm font-black rounded-lg uppercase tracking-widest {{ $api['method'] === 'GET' ? 'bg-emerald-100 text-emerald-700' : ($api['method'] === 'POST' ? 'bg-cyan-100 text-cyan-700' : 'bg-amber-100 text-amber-700') }}">
@ -204,50 +222,37 @@
<div class="mb-6 overflow-hidden rounded-2xl border border-slate-200">
<div class="bg-slate-900 px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<span
class="px-3 py-1 bg-cyan-500 text-white text-xs font-black rounded-lg uppercase letter-spacing-widest">{{
$api['method'] }}</span>
<code
class="text-slate-300 font-mono text-base">{{ config('app.url') }}{{ $api['path'] }}</code>
<span class="px-3 py-1 bg-cyan-500 text-white text-xs font-black rounded-lg uppercase letter-spacing-widest">{{ $api['method'] }}</span>
<code class="text-slate-300 font-mono text-base break-all">{{ config('app.url') }}{{ $api['path'] }}</code>
</div>
<span
class="text-slate-500 text-[10px] font-black uppercase tracking-widest hidden sm:block">Endpoint
URL</span>
<span class="text-slate-500 text-[10px] font-black uppercase tracking-widest hidden sm:block">Endpoint URL</span>
</div>
@if(isset($api['headers']) && count($api['headers']) > 0)
<div class="bg-slate-50 p-6 border-t border-slate-200">
<h4
class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
請求標頭 (Headers)
</h4>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">請求標頭 (Headers)</h4>
<div class="space-y-4">
@foreach($api['headers'] as $key => $val)
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span class="text-slate-500 font-bold min-w-[140px] text-sm font-mono tracking-tight">{{
$key }}</span>
<span class="text-slate-500 font-bold min-w-[140px] text-sm font-mono tracking-tight">{{ $key }}</span>
<div class="flex-1">
<code
class="text-cyan-700 font-bold bg-white px-3 py-1.5 rounded-lg border border-cyan-100 text-sm italic shadow-sm break-all block sm:inline-block w-full sm:w-auto">
{{ $val }}
</code>
<code class="text-cyan-700 font-bold bg-white px-3 py-1.5 rounded-lg border border-cyan-100 text-sm italic shadow-sm block sm:inline-block">{{ $val }}</code>
</div>
</div>
@endforeach
</div>
</div>
@endif
</div>
<!-- Parameters -->
@if(isset($api['parameters']) && count($api['parameters']) > 0)
<div class="mb-8">
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">請求主體 (Parameters)</h4>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">請求參數 (Parameters)</h4>
<div class="overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white">
<table class="w-full param-table">
<thead>
<tr>
<th class="w-[20%]">欄位名稱</th>
<th class="w-[25%]">欄位名稱</th>
<th class="w-[15%]">型態</th>
<th>說明</th>
</tr>
@ -262,19 +267,14 @@
@endif
</td>
<td>
<span
class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">
{{ $param['type'] }}
</span>
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
</td>
<td class="text-slate-600 leading-relaxed font-semibold">
{{ $param['description'] }}
@if(isset($param['example']))
<div class="mt-2 flex items-center gap-2">
<span
class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
<code
class="text-sm text-cyan-600 font-bold">{{ is_array($param['example']) ? json_encode($param['example'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $param['example'] }}</code>
<span class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
<code class="text-sm text-cyan-600 font-bold">{{ is_array($param['example']) ? json_encode($param['example'], JSON_UNESCAPED_UNICODE) : $param['example'] }}</code>
</div>
@endif
</td>
@ -284,24 +284,25 @@
</table>
</div>
</div>
@endif
<!-- Request & Response Examples -->
<div class="space-y-12 mt-12">
<!-- Request Example -->
<div class="space-y-8 mt-10">
@if(isset($api['request']) && !empty($api['request']))
<div>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-6">請求範例 (Request Body)</h4>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">請求範例 (Request Body)</h4>
<pre><code>{{ json_encode($api['request'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
</div>
@endif
<!-- Response Examples -->
@if(isset($api['response_parameters']) && count($api['response_parameters']) > 0)
<div>
@if(isset($api['response_parameters']))
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">回應參數說明 (Response Parameters)</h4>
<div class="overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white mb-8">
<div class="overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white mb-6">
<table class="w-full param-table">
<thead>
<tr>
<th class="w-[20%]">欄位名稱</th>
<th class="w-[25%]">欄位名稱</th>
<th class="w-[15%]">型態</th>
<th>說明</th>
</tr>
@ -310,53 +311,132 @@
@foreach($api['response_parameters'] as $name => $param)
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="font-black text-slate-900 text-sm italic">{{ $name }}</td>
<td>
<span class="px-2 py-1 rounded bg-slate-100 text-[10px] font-black text-slate-600 uppercase tracking-wider">
{{ $param['type'] }}
</span>
</td>
<td class="text-slate-600 font-semibold text-sm whitespace-pre-line">
{{ $param['description'] }}
@if(isset($param['example']))
<div class="mt-2 flex items-center gap-2">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
<code class="text-xs text-cyan-600 font-bold bg-cyan-50/50 px-2 py-0.5 rounded">{{ is_array($param['example']) ? json_encode($param['example'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $param['example'] }}</code>
<td><span class="px-2 py-1 rounded bg-slate-100 text-[10px] font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span></td>
<td class="text-slate-600 font-semibold text-sm whitespace-pre-line">{{ $param['description'] }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@if(isset($api['response']) && !empty($api['response']))
<div>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">回應範例 (Response Body)</h4>
<pre><code>{{ json_encode($api['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
</div>
@endif
@if(isset($api['notes']))
<div class="p-6 bg-cyan-50/50 border border-cyan-100 rounded-3xl mt-6">
<h5 class="text-cyan-700 font-black mb-2 flex items-center gap-2 text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
實作備註
</h5>
<p class="text-cyan-800 font-semibold leading-relaxed text-base">{{ $api['notes'] }}</p>
</div>
@endif
</div>
<div class="mt-16 mb-8 border-b border-slate-100"></div>
</div>
@endforeach
@endforeach
</div>
<!-- MQTT Content -->
<div x-show="currentTab === 'mqtt'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform translate-y-4" x-transition:enter-end="opacity-100 transform translate-y-0">
@foreach($docs['mqtt_topics'] as $category)
@foreach($category['topics'] as $topic)
<div id="{{ $topic['slug'] }}" class="mb-16 animate-luxury-in scroll-mt-10" x-intersect.margin.-200px="activeSection = '{{ $topic['slug'] }}'">
<div class="flex items-center gap-4 mb-6">
<span class="px-3 py-1 text-sm font-black rounded-lg uppercase tracking-widest {{ $topic['action'] === 'PUB' ? 'bg-violet-100 text-violet-700' : 'bg-indigo-100 text-indigo-700' }}">
{{ $topic['action'] }}
</span>
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $topic['name'] }}</h3>
</div>
<p class="text-slate-600 mb-8 text-lg font-medium">{{ $topic['description'] }}</p>
<!-- Topic Block -->
<div class="mb-8 overflow-hidden rounded-2xl border border-slate-200">
<div class="bg-slate-900 px-6 py-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="px-3 py-1 {{ $topic['action'] === 'PUB' ? 'bg-violet-500' : 'bg-indigo-500' }} text-white text-xs font-black rounded-lg uppercase letter-spacing-widest shadow-sm">{{ $topic['action'] }}</span>
<code class="text-slate-300 font-mono text-base sm:text-lg break-all">{{ $topic['topic'] }}</code>
</div>
<div class="flex items-center gap-4">
<span class="text-slate-400 font-mono text-xs font-bold px-2 py-1 bg-slate-800 rounded">QoS {{ $topic['qos'] ?? 0 }}</span>
<span class="text-slate-500 text-[10px] font-black uppercase tracking-widest hidden sm:block">MQTT Topic</span>
</div>
</div>
</div>
<!-- Payload Parameters -->
@if(isset($topic['payload_parameters']) && count($topic['payload_parameters']) > 0)
<div class="mb-8">
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">資料載體 (Payload Parameters)</h4>
<div class="overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white">
<table class="w-full param-table">
<thead>
<tr>
<th class="w-[25%]">欄位名稱</th>
<th class="w-[15%]">型態</th>
<th>說明</th>
</tr>
</thead>
<tbody>
@foreach($topic['payload_parameters'] as $name => $param)
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="font-black text-slate-900 text-base">
{{ $name }}
@if($param['required'] ?? true)
<span class="text-rose-500 ml-1">*</span>
@endif
</td>
<td>
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
</td>
<td class="text-slate-600 leading-relaxed font-semibold">
{{ $param['description'] }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">回應範例 (Response Body)</h4>
<pre><code>{{ json_encode($api['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
<!-- Payload Example -->
@if(isset($topic['payload_example']) && !empty($topic['payload_example']))
<div class="mt-8">
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2">
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
Payload JSON 範例
</h4>
<pre><code>{{ json_encode($topic['payload_example'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
</div>
@endif
@if(isset($api['notes']))
<div class="p-8 bg-cyan-50/50 border border-cyan-100 rounded-3xl">
<h5 class="text-cyan-700 font-black mb-4 flex items-center gap-2 text-xl">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
@if(isset($topic['notes']))
<div class="p-6 bg-indigo-50/50 border border-indigo-100 rounded-3xl mt-8">
<h5 class="text-indigo-700 font-black mb-2 flex items-center gap-2 text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
實作備註
</h5>
<p class="text-cyan-800 font-semibold leading-relaxed text-lg whitespace-pre-line">
{{ $api['notes'] }}
</p>
<p class="text-indigo-800 font-semibold leading-relaxed text-base whitespace-pre-line">{{ $topic['notes'] }}</p>
</div>
@endif
</div>
<div class="mt-16 mb-8 border-b border-slate-100"></div>
</div>
@endforeach
@endforeach
</div>
</div>
<!-- Font Awesome or Lucide (Simplified) -->
<script src="https://unpkg.com/lucide@latest"></script>
<script>
lucide.createIcons();
</script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</body>
</html>

View File

@ -69,7 +69,7 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
Route::put('products/supplementary/B009', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportSlotList']);
// 統一商品主檔 API (B012 整合版)
Route::match(['get', 'patch'], 'machine/products/B012', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getProducts']);
Route::get('machine/products/B012', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getProducts']);
// 遠端指令出貨控制 (B055) — PUT 回報已遷移至 MQTT command/ack Topic