star-cloud/.agents/skills/mqtt-communication-specs/SKILL.md
sky121113 e01e8077f9 [FEAT] 新增訂單收件人姓名遮罩與商品詞彙通用化調整
1. 於 Order Model 新增收件人姓名遮罩邏輯 (masked_pickup_recipient),將 member_barcode 進行去識別化處理以保護隱私,並保留括號/換行等後綴資訊。
2. 新增 OrderPickupRecipientMaskTest 單元測試,驗證中文字元及包含換行/括號的後綴遮罩正確性。
3. 更新後台銷售管理介面 (order-detail-panel.blade.php、tab-orders.blade.php),將原先顯示明文的 member_barcode 改用遮罩後的屬性展示。
4. 將機器通訊協定與狀態代碼中的「便當 (Bento)」字詞統一調整為「商品 (Product)」,以符合各機型通用語意。
5. 更新相關語系翻譯檔案 (zh_TW.json, en.json, ja.json) 及 MQTT 通訊規範文件中的詞彙對應。
6. 修改 MachineLog 中的翻譯取得邏輯,確保能透過錯誤代碼表對應到最新的商品語意。
2026-05-29 14:37:28 +08:00

492 lines
18 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.

---
name: MQTT 即時通訊與 Topic 規範
description: 定義 Star Cloud 與終端機台 (IoT) 之間的 MQTT 全域通訊拓撲、主題 (Topic) 結構、資料載體 (Payload) 格式與安全性認證機制。
---
# MQTT 即時通訊與 Topic 規範 (MQTT Protocol Specs)
本文件定義機台與 Star Cloud 之間的高併發即時通訊標準。MQTT 主要用於處理高頻率且對即時性要求高的訊息(如心跳、遠端指令),與 HTTP REST API 形成雙軌互補架構。
## 1. 連線基礎設定 (Connection Basics)
### 1.1 Broker 資訊
- **協議版本**MQTT v3.1.1 (相容性最高)
- **Keep-Alive**:建議設定為 30 ~ 60 秒。
**各連線通道埠號:**
| 通道 | 埠號 | 用途 | 使用者 |
| :--- | :--- | :--- | :--- |
| TCP | 1883 | Docker 內部 / 區網測試 | Go Gateway、開發除錯 |
| SSL/TLS | 8883 | 加密 TCP (正式環境) | Go Gateway (選配) |
| WebSocket | 8083 | EMQX 原生 WS 監聽 | NPM 反向代理目標 |
| **WSS (公網)** | **443** | App 端透過 NPM 反代連線 | **Android / iOS App** |
### 1.2 身份認證與識別 (Authentication & Identity)
機台連線時必須提供以下憑據:
- **Username**:機台編號 (`serial_no`),例如 `SN00001`
- **Password**:機台 `api_token` 的 SHA256 雜湊值,由 B014 API 取得。
- **Client ID**:格式建議為 **`SC_{serial_no}`**(確保同一機台僅能有一個連線,新連線將踢除舊連線)。
**EMQX 認證機制**
- **資料結構**Redis Hash (`HSET`)。Key 格式為 `{prefix}machine_auth:{username}`,包含 `password` 欄位。
- **查詢指令**EMQX 使用 `HMGET {prefix}machine_auth:${username} password` 進行驗證。
- **Laravel 同步**:透過 `MachineService::syncMqttAuth()` 或批量指令 `php artisan mqtt:sync-auth` 維護。
> [!CAUTION]
> **嚴禁使用 Redis String (`SET`) 儲存認證資料**。EMQX 5.x 的 Redis Auth Plugin 僅支援 `HMGET` 讀取 Hash 結構。若誤用 String認證將永遠失敗。
### 1.3 Go Gateway 認證
- **Username**`star-cloud-gateway` (固定值)。
- **Password**:與 Redis Hash `machine_auth:star-cloud-gateway` 中的 `password` 欄位對應。
- Go Gateway 的認證資料須透過 Laravel 側寫入 Redis。
---
## 1-A. App 端 WSS 連線規範 (WebSocket Secure)
App (Android) 透過 **WSS** 穿越公網連線 Broker。路徑`App → Cloudflare → NPM (port 8083 反代) → EMQX`。
### 連線參數
| 參數 | 值 |
| :--- | :--- |
| **URL** | `wss://mqtt.setdomains.work:443/mqtt` |
| **Username** | `{machineID}` (即 `serial_no`) |
| **Password** | `{api_token}` 原始值B014 取得,**不做 SHA256** |
| **Client ID** | `SC_{machineID}_{random}` |
| **Clean Session** | `true` |
> [!CAUTION]
> App 傳入原始 TokenEMQX 端的 `password_hash_algorithm` 須設為 `sha256`,由 Broker 自動雜湊比對 Redis 中的值。
### Android 範例 (HiveMQ)
```java
Mqtt3Client client = MqttClient.builder()
.useMqttVersion3()
.identifier("SC_" + machineID)
.serverHost("mqtt.setdomains.work")
.serverPort(443)
.sslWithDefaultConfig()
.webSocketConfig().serverPath("/mqtt").applyWebSocketConfig()
.simpleAuth().username(machineID).password(apiToken.getBytes()).applySimpleAuth()
.willPublish()
.topic("machine/" + machineID + "/status")
.payload("{\"status\":\"offline\"}".getBytes())
.applyWillPublish()
.buildBlocking();
```
### 重連策略 (Reconnection Strategy)
- **指數退避重連**斷線後採指數退避1s → 2s → 4s → ... → 最大 60s嘗試重連 WSS。
- **無 HTTP Fallback**:不再退回 HTTP 輪詢 (B010),斷線期間的狀態與交易將依賴 App 本地暫存(詳見第 6 節「斷線保障機制」)。
---
## 2. 主題架構 (Topic Topology)
我們採用目錄式的層級結構,方便未來進行萬台設備的管理與 ACL 權限切分。
| 主題名稱 (Topic) | 方向 | QoS | 用途說明 |
| :--- | :--- | :--- | :--- |
| `machine/{serial_no}/heartbeat` | 設備 ➜ 雲端 | 0 | 每 1 分鐘上報狀態大包裝(溫度、韌體版本等)。 |
| `machine/{serial_no}/error` | 設備 ➜ 雲端 | 1 | 發生硬體故障、卡貨或門未關時立即上報。 |
| `machine/{serial_no}/transaction` | 設備 ➜ 雲端 | 1 | 交易生命週期上報 (B600 建立 / B601 發票 / B602 出貨結果),透過 `action` 欄位區分。 |
| `machine/{serial_no}/command` | 雲端 ➜ 設備 | 1 | 雲端下發的即時指令(出貨、更新、重啟)。 |
| `machine/{serial_no}/command/ack` | 設備 ➜ 雲端 | 1 | 機台回覆指令執行結果B055 出貨回報)。 |
---
## 3. 資料載體規範 (Payload Definitions)
所有 Payload 統一採用 **JSON** 格式,字母一律為 **snake_case**
### 3.1 心跳上報 (Heartbeat) - `machine/{serial_no}/heartbeat`
這是機台最主要的狀態上報包,整合了關鍵的機台資訊。
```json
{
"firmware_version": "2.1.6",
"temperature": 20
}
```
> [!NOTE]
> 為了降低傳輸開銷,目前僅要求上報 **韌體版本** 與 **溫度** (整數型態)。其餘狀態如連線/斷線已透過 LWT 機制處理。
> 雲端對於溫度的日誌記錄規則:**只要有變動 (current !== last) 即寫入日誌**,並維持每 4 小時一次的保底記錄。
### 3.2 異常與錯誤上報 (B013)
當機台發生硬體故障(如卡貨、通訊中斷)時,發布至此 Topic。
- **Topic**: `machine/{serial_no}/error`
- **Payload (JSON)**:
```json
{
"tid": 12,
"error_code": "0403"
}
```
#### B013 硬體代碼對照表 (由 MachineService 自動翻譯)
> [!IMPORTANT]
> **出貨結果判定**:僅 `0402` (出貨成功) 與 `0424` (購買成功) 視為成功,其餘出貨結果碼均應觸發退款。
> 完整代碼定義於 `MachineService::ERROR_CODE_MAP`。
**基本出貨流程:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0401** | Dispensing in progress | info | 正在出貨 |
| **0402** | Dispense successful | info | ✅ 出貨成功 |
| **0403** | Slot jammed | error | **貨道卡貨** (重大異常) |
| **0404** | Motor not stopped | warning | 電機未正常停止 |
| **0405** | Motor not found | error | 電機不存在 |
| **0406** | Motor not found (0406) | error | 電機不存在 (同 0405) |
| **0407** | Elevator failure | error | 昇降機故障 |
**昇降機系統:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040A** | Elevator belt self-check error | error | 昇降機皮帶自檢錯誤 |
| **0410** | Elevator rising | info | 昇降機正在上升 |
| **0411** | Elevator descending | info | 昇降機正在下降 |
| **0412** | Elevator rise error | error | 昇降機上升錯誤 |
| **0413** | Elevator descent error | error | 昇降機下降錯誤 |
| **0425** | Elevator support rod return error | error | 昇降台撐桿回位錯誤 |
| **0428** | Elevator support rod push error | error | 昇降台撐桿推出錯誤 |
| **0429** | Elevator enter microwave error | error | 昇降台進微波爐錯誤 |
| **0430** | Elevator exit microwave error | error | 昇降台出微波爐錯誤 |
**微波爐取貨口門:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0414** | Microwave pickup door closing | info | 正在關閉微波爐取貨口門 |
| **0415** | Microwave pickup door close error | error | 微波爐取貨口門關閉錯誤 |
| **0426** | Microwave pickup door opening | info | 正在打開微波爐取貨口門 |
| **0427** | Microwave pickup door open error | error | 微波爐取貨口門打開錯誤 |
**微波爐進貨口門:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0416** | Microwave delivery door opening | info | 正在打開微波爐進貨口門 |
| **0417** | Microwave delivery door open error | error | 微波爐進貨口門打開錯誤 |
| **0418** | Pushing product into microwave | info | 正在將商品推入微波爐 |
| **0419** | Microwave delivery door closing | info | 正在關閉微波爐進貨口門 |
| **0420** | Microwave delivery door close error | error | 微波爐進貨口門關閉錯誤 |
**微波爐內部:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0421** | Product not detected in microwave | error | 微波爐內未檢測到商品 |
| **0422** | Product heating | info | 商品正在加熱 |
| **0423** | Product heating remaining time | info | 商品加熱剩餘時間秒 |
| **0424** | Purchase successful | info | ✅ 購買成功 |
| **0431** | Microwave push rod push error | error | 微波爐內推桿推出錯誤 |
| **0432** | Microwave push rod retract error | error | 微波爐內推桿收回錯誤 |
**鮮奶機專用:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040B** | Milk dispense timeout | error | 按鍵出奶超時 |
| **040C** | Cup dropper failure | error | 漏杯器漏杯失敗 |
| **040D** | Cup drop not detected | error | 未檢測到杯子掉落 |
**手環機專用:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040E** | Bracelet scan failed | error | 沒掃碼到手環 |
| **040F** | Bracelet drop not detected | error | 沒檢測到手環掉入取貨斗 |
**取貨口門 / 購物車模式 / 液體機:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0433** | Pickup door opened (cart mode) | info | 取貨口門打開成功 (購物車) |
| **0434** | Pickup door closed (cart mode) | info | 取貨口門關閉成功 (購物車) |
| **0435** | Liquid dispense paused | warning | 暫停出液 (液體機) |
| **0441** | Pickup door unlocked | info | 取貨口門開鎖上報 |
| **04FF** | Purchase terminated | error | 購買終止 |
### 3.3 交易生命週期上報 (Transaction) - `machine/{serial_no}/transaction`
此 Topic 整合了交易生命週期的三個階段,透過 `action` 欄位區分:
#### B600 交易建立 (`action: create`)
```json
{
"action": "create",
"flow_id": "T202604140001",
"payment_type": 1,
"total_amount": 25.0,
"discount_amount": 0.0,
"pay_amount": 25.0,
"items": [
{
"product_id": "5",
"price": 25.0,
"quantity": 1
}
]
}
```
#### B601 發票紀錄 (`action: invoice`)
```json
{
"action": "invoice",
"flow_id": "T202604140001",
"invoice_no": "AB12345678",
"invoice_date": "2026-04-22",
"machine_time": "2026-04-22 14:30:15",
"amount": 25.0,
"random_no": "1234",
"love_code": ""
}
```
#### B602 出貨結果 (`action: dispense`)
```json
{
"action": "dispense",
"flow_id": "T202604140001",
"slot_no": "3",
"product_id": "5",
"amount": 25.0,
"dispense_status": 1,
"remaining_stock": 9,
"machine_time": "2026-04-22 14:30:00"
}
```
#### B600+ 交易完成統整 (`action: finalize`)
這是目前推薦的標準流程,將交易、發票與出貨結果一次性上報,確保資料完整性。
```json
{
"action": "finalize",
"order": {
"order_no": "T202508080001",
"flow_id": "CLIENT_UID_123",
"total_amount": 100,
"pay_amount": 100,
"original_amount": 100,
"discount_amount": 0,
"change_amount": 0,
"payment_type": 1,
"payment_status": 1,
"code_id": "對應指令的ID或取貨碼(如適用)",
"payment_request": "{}",
"payment_response": "{}",
"member_barcode": "MBR999",
"invoice_info": "12345678-0912345678",
"machine_time": "2025-08-08 10:00:00",
"points_used": 0,
"items": [
{
"product_id": 1,
"price": 100,
"quantity": 1
}
]
},
"invoice": {
"invoice_no": "AB12345678",
"invoice_date": "2025-08-08",
"machine_time": "2025-08-08 10:01:30",
"amount": 100,
"random_number": "1234",
"love_code": "",
"carrier_id": "/ABC1234",
"carrier_type": "3J0002",
"business_tax_id": "12345678",
"rtn_code": "1",
"rtn_msg": "成功"
},
"dispense": [
{
"slot_no": "1",
"product_id": 1,
"amount": 100,
"dispense_status": 1,
"remaining_stock": 9,
"member_barcode": "MBR999",
"machine_time": "2025-08-08 10:01:00",
"points_used": 0
}
]
}
```
> [!NOTE]
> - `serial_no` 不需放在 Payload 中Go Gateway 自動從 Topic 路徑提取並注入。
> - 若 Payload 不含 `action` 欄位,系統預設視為 `create`(向後相容既有 B600 格式)。
> - **閉環勾稽參數**:當使用特定支付方式 (如 `payment_type: 6`=取貨碼, `5`=通行碼, `41`=員工卡) 時,請務必在 `order` 中帶入 `code_id` 欄位。後端會根據此 ID 完成對應的狀態更新與核銷。
### 3.4 雲端指令 (Downstream Commands) - `machine/{serial_no}/command`
這是雲端主動下發給機台的訊息,取代原本 B010 Response 的輪詢等待。
```json
{
"command": "dispense",
"command_id": "20",
"payload": {
"new_stock": 6,
"old_stock": 7,
"slot_no": "1"
}
}
```
**常用指令集:**
- `reboot`: 遠端重啟機台系統。
- `reboot_card`: 遠端重啟讀卡機。
- `checkout`: 遠端結帳。
- `lock`: 鎖定頁鎖定。
- `unlock`: 鎖定頁解鎖。
- `change`: 遠端找零(夾帶 amount 參數)。
- `update_inventory`: 遠端即時修改庫存。
- `dispense`: 遠端出貨指令。
- `update_ads`: 遠端廣告同步指令。
- `update_products`: 遠端商品資料同步指令(觸發機台調用 B012
**`update_inventory` Payload 範例:**
```json
{
"command": "update_inventory",
"command_id": "3",
"payload": {
"slot_no": "3",
"stock": 6,
"expiry_date": null,
"batch_no": "B2026042201"
}
}
```
**`update_products` Payload 範例:**
```json
{
"command": "update_products",
"command_id": "123",
"payload": {
"remark": "手動同步商品資料"
}
}
```
### 3.5 指令回覆 (Command ACK) - `machine/{serial_no}/command/ack`
機台執行完雲端下發的指令後,透過此 Topic 回報執行結果。雲端收到後會根據 `command_id` 更新操作紀錄狀態,若結果為 `failed` 且為庫存指令,將觸發自動回滾 (Rollback)。
**Payload 格式:**
```json
{
"command_id": "3",
"result": "success",
"stock": 6,
"message": "Update successful"
}
```
**欄位說明:**
- `command_id`: (Required) 雲端下發指令時夾帶的 ID。
- `result`: (Required) 執行結果,固定為 `success``failed`
- `stock`: (Optional) 執行該指令後,該貨道的最終剩餘庫存。
- `message`: (Optional) 額外的執行訊息或失敗原因。
| 欄位 | 類型 | 必填 | 說明 |
| :--- | :--- | :--- | :--- |
| `command_id` | String | 是 | 雲端下發的指令 ID對應 `remote_commands.id`|
| `result` | String | 是 | 執行結果:`"success"` 或 `"0"` 代表成功,其他代表失敗 |
| `stock` | String | 否 | 出貨後的貨道剩餘庫存數量 |
> [!NOTE]
> 此 Topic 取代原本 B055 的 HTTP PUT 回報路由。Go Gateway 將此 4 層 Topic 識別為 `type: command_ack` 推入 Redis。
### 3.6 屬性上報 (Attribute Updates)
過去曾支援獨立的 `temperature``firmware_version` Topic現已**廢棄**。
機台應將所有屬性更新集中於 `machine/{serial_no}/heartbeat` 的 JSON Payload 中發送,以降低連線開銷並簡化後端處理。
> [!IMPORTANT]
> **集中化原則**:除非極特殊的即時性需求,否則所有機台狀態(溫度、版本、門狀態、頁面碼)應統一包在 Heartbeat 訊息中。
---
## 4. 安全與 QOS 規範
1. **存取控制 (ACL)**EMQX Broker 必須設定 ACL禁止機台 A 訂閱機台 B 的 Topic。機台僅能訂閱與發布包含自身 `serial_no` 的路徑。
2. **QoS 策略**
- **QoS 0**:適用於高頻率心跳,即使掉一兩次包也不影響系統判斷。
- **QoS 1**適用於交易與指令確保「至少送達一次」。App 端收到指令後應回覆回執。
3. **遺囑訊息 (Last Will and Testament)**
機台 Connect 時應設定 Last Will 於 `machine/{serial_no}/status`Payload 為 `{"status": "offline"}`。當連線異常中斷時,雲端能立刻得知。
4. **傳輸加密**
- **App 端 (公網)**:強制使用 WSS (`wss://`)TLS 由 NPM / Cloudflare 終止。
- **Go Gateway (內網)**:預設走 TCP (`tcp://emqx:1883`),區網環境可免 TLS。正式環境建議升級至 `ssl://emqx:8883`
---
## 5. Redis 橋接注意事項
### 5.1 Key 前綴一致性
Laravel 的 Redis 驅動會自動在 Key 前加上前綴 (預設 `star_cloud_database_`)。Go Gateway 不使用 Laravel Redis 客戶端,因此**必須在程式碼中手動加上此前綴**
```go
// main.go
prefixedIncomingKey := "star_cloud_database_" + cfg.IncomingQueueKey
pusher := bridge.NewRedisPusher(rdb, prefixedIncomingKey)
```
> [!CAUTION]
> 若 Go 與 Laravel 操作不同前綴的 Key訊息將永遠無法傳達。修改 Laravel 的 `config/database.php` 中 Redis prefix 後,必須同步修改 Go 端的前綴字串。
### 5.2 長連線穩定性
Laravel `mqtt:listen` 使用 `BLPOP` 長時間阻塞等待。為防止 PHP 端主動關閉連線,必須在 `config/database.php` 設定:
```php
'redis' => [
'default' => [
'read_timeout' => -1, // 永不超時
'timeout' => 10.0, // 初始連線超時
],
],
```
---
## 6. 斷線保障機制
App 端的通訊策略為 **MQTT 唯一通道 + 本地暫存補發**,不維護 HTTP Fallback
| 狀態 | 心跳 | 指令接收 | 交易/發票/出貨 |
| :--- | :--- | :--- | :--- |
| WSS 連線正常 | MQTT 發布 (1min) | MQTT 訂閱 | MQTT 發布 |
| WSS 斷線 | 暫停,等待重連 | 不可用 | 暫存至本地 SQLite 佇列 |
| WSS 恢復 | 恢復 MQTT 發布 | 恢復 MQTT 訂閱 | 自動逐筆補發暫存訊息 |
**本地暫存補發規則:**
- 重連後延遲 2 秒開始補發,每筆間隔 200ms
- 重試上限 10 次,超過標記為 failed
- 暫存超過 72 小時的訊息直接丟棄
> [!CAUTION]
> **已遷移至 MQTT 的端點 (B600/B601/B602/B010/B013) 不保留 HTTP 路由。** 斷線時依賴 App 本地暫存機制保證資料完整性。
---
## 7. 常用維運指令
| 指令 | 用途 |
|------|------|
| `sail artisan mqtt:sync-auth` | 將所有機台認證資料從 MySQL 同步至 Redis Hash |
| `sail artisan mqtt:listen` | 啟動 MQTT 訊息消費者 (常駐執行) |
| `docker compose build mqtt-gateway` | 重新編譯 Go Gateway |
| `docker restart star-cloud-mqtt-gateway` | 重啟 Gateway 容器 |
| `docker restart star-cloud-queue` | 重啟 Queue Worker (程式碼變更後必須執行) |