Compare commits

...

8 Commits

Author SHA1 Message Date
2702e5a655 [REFACTOR] 標準化 IoT API 方法並修正日期格式
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 57s
1. 將 B014 (參數下載) 與 B017 (貨道同步) 路由從 POST 改為 GET。
2. 移除 B017 冗餘的 machine 參數,改由 Bearer Token 自動識別。
3. 修正 B017 回傳資料中 expiry_date 的 ISO 8601 格式問題,統一為 Y-m-d。
4. 同步更新所有技術規格文件 (.agents/ 規範) 與 API 說明配置 (config/api-docs.php)。
2026-04-14 16:46:35 +08:00
6382709b90 [DOCS] 更新開發進度表
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 55s
1. 標記「遠端管理」模組為已完成。
2. 標記「儀表板」模組為已完成。
2026-04-14 16:18:05 +08:00
66f7c1ffb8 [FEAT] 實作 B017 貨道庫存全量同步 API 與多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 56s
1. 實作 B017 (reload_msg) 端點,支援貨道庫存、效期與批號的全量同步。
2. 將 B017 回傳欄位映射至 App 需求:slot_no -> tid, stock -> num。
3. 新增 expiry_date (效期) 與 batch_no (批號) 欄位支援。
4. 實作指令狀態閉環邏輯,成功同步後自動將相關 reload_stock 指令標記為成功。
5. 將指令備註欄位多語系化,新增「庫存已與機台同步」繁中、英文、日文翻譯。
6. 更新 API 技術規格文件 (.agents/skills) 與系統 API 文件配置 (config/api-docs.php)。
2026-04-14 15:06:51 +08:00
32fa28dc0f [DOCS]:初始化 MQTT 架構實作計畫與相關技術規範
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m12s
1. 新增 MQTT 即時通訊與 Topic 規範文件 (.agents/skills/mqtt-communication-specs/SKILL.md)。
2. 建立 MQTT 基礎架構實作計畫文件 (docs/mqtt-implementation-plan.md)。
3. 更新全域開發框架規範 (framework.md),納入 Go Gateway 與 EMQX 架構說明。
4. 重構 IoT 通訊處理規範 (iot-communication/SKILL.md),支援 HTTP 與 MQTT 雙軌管線。
5. 更新背景 API 規範 (api-rules.md) 與技能觸發規則 (skill-trigger.md) 以符合新架構。
2026-04-14 13:02:08 +08:00
daf8b1ebcc [FEAT] 強化機台技術員 Token 安全性
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 46s
1. 為 B000 登入發放之技術員 Token 增加 8 小時有效期限,防止 Token 永久有效之風險。
2026-04-13 17:24:57 +08:00
8f008ffb61 [FEAT] 實作 B014 機台參數下載 API 與 B000 登入認證強化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 52s
1. 強化 B000 登入接口:驗證成功後回傳 Sanctum Token 供後續初始化使用。
2. 實作 B014 (getSettings) API:整合機台、金流與發票設定,並映射至 Android App 預期欄位。
3. 強化安全性:B014 API 掛載 auth:sanctum 並執行 RBAC 權限檢查。
4. 更新 API 說明文件 (iot-spec.md, api-docs.php) 及技術規範 (SKILL.md)。
2026-04-13 17:04:52 +08:00
729890d7c7 [DOCS] 更新通訊系統與全域工具列開發藍圖
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 47s
1. 更新 docs/future_todo.md:詳列第一階段核心工具(工具列、公告系統、快捷入口、身分模擬)與第二階段行銷功能(盲盒抽獎)的開發時程。
2026-04-13 16:19:37 +08:00
ad256d3d3b [FEAT] 廣告排程功能與 UI 優化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s
1. 新增廣告排程功能,支援設定發布時間與下架時間。
2. 整合 Flatpickr 時間選擇器,提供與機台日誌一致的極簡奢華風 UI。
3. 優化廣告列表中的數字字體,套用 font-mono 與 tabular-nums,與客戶管理模組風格同步。
4. 修正 Alpine.js 資料同步邏輯,確保編輯模式下排程時間能正確回填。
2026-04-13 11:44:26 +08:00
22 changed files with 1119 additions and 76 deletions

View File

@@ -56,25 +56,32 @@ trigger: always_on
### 4.2 終端類 (IoT / Machine) — 須嚴格遵守 PDF 規格
* **API 識別碼 (workid)**: URL 中的 `{workid}` 參數固定為該 API 的功能代碼 (如 `B010`, `B017`, `B600`),不隨機台改變。
* **機台識別方式**:
1. **Header**: 透過 `Authorization: Bearer <api_token>` 識別。
2. **Request Body**: 透過 `machine``serial_no` 等欄位識別具體機台
1. **Header (推薦)**: 透過 `Authorization: Bearer <api_token>` 識別。針對 B017 等端點,雲端將自動關聯對應機台,**不需**額外帶入機台識別參數。
2. **Request Body (相容/特定模式)**: 透過 `machine``serial_no` 等欄位識別。主要用於 B000 登入或尚未取得 Token 的引導階段 (如 B014)
* **主要 Endpoint 範例**:
* **心跳上報 (B010)**: `POST /api/app/machine/status/B010`
* **交易回傳 (B600)**: `POST /api/app/B600` (Body 欄位 `req2` 為機台編號)
* **貨道庫存 (B017)**: `POST /api/app/machine/reload_msg/B017`
* **貨道庫存 (B017)**: `GET /api/app/machine/reload_msg/B017`
* **遠端出貨 (B055)**: `POST /api/app/machine/dispense/B055`
---
## ⚡ 5. 高併發處理與隊列
## ⚡ 5. IoT 高併發流向與 MQTT Gateway 整合
為了系統穩定性,以下 API **嚴禁直寫資料庫**,必須進入 **Redis Queue** 異步處理
1. **B010**: 心跳上傳(每 5-10 秒一次)。
2. **B600 / B602**: 交易與出貨紀錄。
3. **B220**: 零錢機庫存變動。
4. **B710**: 計時器狀態同步。
為了系統穩定性與高吞吐量,機台通訊的架構依循以下規範,**嚴禁直寫資料庫**
後端應立即回傳 `202 Accepted` 或業務定義的成功碼,由 Job 背景完成數據持久化。
### 5.1 MQTT 通訊端點 (高頻與事件驅動)
以下高頻或即時事件,未來將**全面改採 MQTT 協議**,透過 EMQX 與 Go Gateway 橋接:
1. **B010 (心跳)**:機台發布至 `machine/{serial_no}/heartbeat`
2. **B013 (錯誤與狀態)**:機台發布至 `machine/{serial_no}/error`
3. **B600 / B602 (交易紀錄)**:機台發布至 `machine/{serial_no}/transaction`
處理管線:
`機台 ➜ EMQX ➜ Go Gateway ➜ Redis List (mqtt_incoming_jobs) ➜ Laravel daemon (mqtt:listen) ➜ Job 異步寫入 DB`
### 5.2 HTTP 通訊端點 (資料拉取與特殊事件)
基於歷史相容、大檔傳輸(如 `B012 商品同步`)或高度安全性(如 `B014 金鑰下載`)的端點,維持使用 HTTP REST API。
若此類 API 產生寫入行為,後端應盡可能立即回傳 `202 Accepted`,並透過 Laravel Job 在背景完成數據持久化。
---

View File

@@ -6,19 +6,28 @@ trigger: always_on
## 1. 專案概述
* **目標**打造一個強大且穩定的智能販賣機後台管理系統Cloud 平台),負責管理機台、商品、銷售數據以及提供給端點機台串接的 API。
* **核心架構**:採用 **傳統單體式架構 (Monolithic Architecture)** Laravel Blade 模板引擎進行伺服器端渲染 (SSR)。
* **核心架構**:採用 **Monorepo 單體式架構**,以 Laravel 為核心進行伺服器端渲染 (SSR) 與 API 服務,並搭配 **Go MQTT Gateway** 作為高併發 IoT 通訊的前置接收層。兩者透過 **Redis** 進行異步橋接,確保職責分離與系統穩定性
* **工作流程**:後端處理業務邏輯與資料庫存取,並透過 Blade 引擎渲染包含 Tailwind CSS 類別的 HTML。前端互動行為由輕量級 Alpine.js 負責UI 元件以 Preline UI 為主體。
## 2. 技術棧 (Tech Stack)
### 2.1 後端核心 (Laravel)
* **後端框架**PHP 8.5 / Laravel 12
* **核心組件**Redis (用於高併發 IoT 隊列與快取,為系統穩定之必要條件)
* **核心組件**Redis (用於高併發 IoT 隊列、MQTT 橋接與快取,為系統穩定之必要條件)
* **資料庫**MySQL 8.0
* **開發環境**Laravel Sail (Docker / WSL2)
### 2.2 IoT 通訊層 (MQTT Gateway)
* **MQTT Broker**EMQX 5 (負責維持機台長連線與訊息路由)
* **Gateway 語言**Go (負責訂閱 MQTT Topic、預處理訊息、轉發至 Redis)
* **橋接機制**Redis List (`mqtt_incoming_jobs`),由 Laravel 常駐指令 (`mqtt:listen`) 消費
### 2.3 前端
* **前端視圖 (View)**Laravel Blade
* **前端互動 (JS)**Alpine.js (專注於行為,不負責渲染)
* **介面與樣式 (CSS)**Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)。
* **重要規範**Preline UI 僅作為「原子組件」與「JS 互動邏輯」的參考庫。整體的「佈局」與「美學」必須嚴格遵守「極簡奢華風 UI 實作規範 (SKILL.md)」。
* **前端建置工具**Vite
* **資料庫**MySQL 8.0
* **開發環境**Laravel Sail (Docker / WSL2)
## 3. 目錄結構與慣例
@@ -29,31 +38,111 @@ trigger: always_on
* **Routes**`routes/web.php` 用於後台管理介面;`routes/api.php` 提供外部或機台調用介面 (需 V1 版本化)。
* **Services** (建議)`app/Services/{Domain}/`,將商業邏輯與資料異動封裝於 Service 中。
* **Traits**`app/Traits/ApiResponse.php` 用於統一 API JSON 回傳格式。
* **Jobs**`app/Jobs/{Domain}/`**高併發 IoT 場景之必要實作**。所有日誌、心跳上報必須進入 Redis Queue 進行背景異步處理,嚴禁在 API 直連 DB 寫入日誌
* **Jobs**`app/Jobs/{Domain}/`用於異步處理 IoT 資料寫入、通知發送等背景任務
* **Console Commands**`app/Console/Commands/`,包含 MQTT 橋接守護進程 (`mqtt:listen`) 等常駐指令。
### 3.2 前端 (Blade / Tailwind / Alpine)
* **Views (頁面)**:位於 `resources/views/`。通常依功能建立資料夾(如 `resources/views/admin/machines/index.blade.php`)。
* **Layouts (版面)**:位於 `resources/views/layouts/`。定義全站的共用版面結構(如 header, sidebar, footer
* **Components (組件)**:位於 `resources/views/components/`。封裝可重用的 Blade 元件(如 Button, Modal, Table支援透過 `<x-button>` 語法呼叫。
## 4. 開發標準 (Coding Standards)
### 3.3 MQTT Gateway (Go)
Go 專案以 Monorepo 形式置於專案根目錄下的 `mqtt-gateway/` 資料夾,獨立於 Laravel 程式碼:
* **進入點**`mqtt-gateway/main.go`
* **模組管理**`mqtt-gateway/go.mod` / `go.sum`
* **內部分層**
* `mqtt-gateway/internal/handler/` — 各 Topic 的訊息處理邏輯(如 heartbeat、transaction、error
* `mqtt-gateway/internal/bridge/` — Redis 橋接層,負責將處理後的 JSON 推入 `mqtt_incoming_jobs` List。
* `mqtt-gateway/config/` — 環境變數與 EMQX / Redis 連線設定。
> [!CAUTION]
> Go Gateway 的職責僅限於「接收、驗證、轉發」。**嚴禁**在 Go 中實作任何商業邏輯(如庫存扣減、通知發送),所有業務處理必須統一在 Laravel Service 層完成。
## 4. IoT 通訊架構 (MQTT + HTTP 雙軌制)
本系統的機台通訊採用 **MQTT 與 HTTP 雙軌並行** 的策略,依據通訊特性選擇最適合的協議。
### 4.1 整體資料流向
```
機台 (Android APP)
├─ [高頻/即時] MQTT 長連線 ──→ EMQX Broker ──→ Go Gateway ──→ Redis List ──→ Laravel mqtt:listen ──→ Job ──→ MySQL
└─ [低頻/大檔] HTTP REST ──→ Laravel API Controller ──→ (必要時) Job ──→ MySQL
```
### 4.2 MQTT 通訊端點 (高頻與事件驅動)
以下端點因高頻率或即時性需求,採用 MQTT 協議通訊:
| API 代碼 | Topic 格式 | 用途 | QoS |
| :--- | :--- | :--- | :--- |
| B010 | `machine/{serial_no}/heartbeat` | 心跳上報 (每 10 秒) | 0 |
| B013 | `machine/{serial_no}/error` | 故障與異常狀態上報 | 1 |
| B600 | `machine/{serial_no}/transaction` | 交易紀錄回傳 | 1 |
**雲端→機台指令下發**:透過 `machine/{serial_no}/command` Topic 推送,取代原本 B010 Response 中的 `status` 欄位輪詢機制,實現毫秒級即時指令。
### 4.3 HTTP 通訊端點 (資料拉取與敏感操作)
以下端點因資料量大、安全性要求高或為 Request/Response 模式,維持使用 HTTP REST API
| API 代碼 | 用途 | 維持 HTTP 的原因 |
| :--- | :--- | :--- |
| B000 | 維運人員登入 | 無狀態認證HTTP 更自然 |
| B012 | 商品配置同步 | 大量資料的 GET 拉取 |
| B014 | 金鑰與參數下載 | 高安全性敏感操作,需嚴格 RBAC |
| B009 | 貨道庫存回報 | 低頻操作,由維運人員觸發 |
### 4.4 Redis 橋接機制 (Go ↔ Laravel)
Go Gateway 與 Laravel 之間透過 Redis List 進行單向異步橋接:
* **Redis Key**`mqtt_incoming_jobs`
* **Go 端 (生產者)**:將 MQTT 收到的 Payload 包裝成標準 JSON 後,執行 `RPUSH mqtt_incoming_jobs {json}`
* **Laravel 端 (消費者)**:常駐指令 `php artisan mqtt:listen` 持續執行 `BLPOP mqtt_incoming_jobs`,取得 JSON 後解碼並分派至對應的 Laravel Job (如 `ProcessHeartbeat`, `ProcessTransaction`)。
**JSON 橋接格式規範**
```json
{
"type": "heartbeat",
"serial_no": "M-001",
"received_at": "2026-04-14T09:00:00+08:00",
"payload": {
"current_page": 1,
"firmware_version": "1.0.5",
"temperature": 25.5
}
}
```
> [!IMPORTANT]
> **為何不讓 Go 直接寫入 Laravel Queue** 因為 Laravel Queue 的 Payload 包含 PHP 序列化物件字串 (`serialize()`)Go 無法安全產生此格式。透過獨立的 Redis List + 純 JSON可徹底解耦兩端的技術依賴。
### 4.5 MQTT 連線認證
機台連線 EMQX 時,使用 `serial_no` 作為 Username、`api_token` 作為 Password。驗證流程
1. **Laravel 端 (Token 派發時)**B014 下發 `api_token` 時,同步執行 `Redis::set("machine_auth:{serial_no}", hash(api_token))`
2. **EMQX 端 (連線驗證時)**:配置 Redis Auth Plugin直接查詢 Redis 進行極速驗證 (毫秒級),不經過 MySQL。
3. **Token 更新/撤銷時**Laravel 更新或刪除機台 Token 時,必須同步更新或刪除 Redis 中的對應快取。
## 5. 開發標準 (Coding Standards)
* **命名規範**
* Controllers: `PascalCaseController.php` (例如 `MachineController.php`)
* Models: `PascalCase.php` (例如 `Machine.php`)
* Blade Views: `kebab-case.blade.php` 或按資源名稱 (例如 `index.blade.php`, `create.blade.php`)
* Routes uri: `kebab-case` (例如 `/machine-logs`)
* Go 檔案: `snake_case.go` (例如 `heartbeat_handler.go`)
* **回傳格式**
* Web 路由:回傳 `view()`,表單驗證失敗時直接使用 Laravel 內建的 redirect with errors。
* API 路由:回傳標準 JSON 格式的 `JsonResponse`
## 5. UI 與前端開發指南
## 6. UI 與前端開發指南
* **樣式撰寫**:全面使用 Tailwind CSS utility classes**避免撰寫自訂 CSS**(除非少數特定動畫或覆寫)。
* **UI 元件庫**:遵循 **Preline UI** 的類別與 HTML 結構進行開發。
* **前端腳本**
* 優先使用 **Alpine.js** (`x-data`, `x-show`, `@click` 等) 在 HTML 標籤內完成簡單的 DOM 狀態切換與互動邏輯。
* 避免在 Blade 內撰寫冗長的 `<script>` Vanilla JS若邏輯過於複雜可將 Alpine state 獨立成 js 檔案再於 Vite 引入,但原則上保持輕量。
## 6. 多語系 I18n 規範 (Multi-language Standards)
## 7. 多語系 I18n 規範 (Multi-language Standards)
* **視圖開發**:所有使用者可見的文字、按鈕、提示訊息,必須使用 Laravel 的 `@lang('key')``__('key')` 函式包裹。
* **語系 Key 命名**:語系 Key 必須採用 **英文原始詞彙 (English phrases)** 作為 Key 名稱為原則,以提高代碼可讀性並作為預設回退(除非該字串過長,才建議使用點號分隔的 key
* 範例:使用 `__('Account Settings')`
@@ -61,7 +150,7 @@ trigger: always_on
* 主語系檔案位於 `lang/` 目錄。
* 開發新功能時,必須同步更新以下三個 JSON 翻譯檔:`zh_TW.json` (主要)、`en.json` (預設)、`ja.json` (日文)。
## 7. AI 協作規則 (給 Antigravity AI)
## 8. AI 協作規則 (給 Antigravity AI)
* **角色設定**:你是一位專業的全端開發工程師助手。
* **代碼生成指令**
* 所有的解釋說明請使用 **繁體中文**。
@@ -70,23 +159,24 @@ trigger: always_on
* **【多語系強制要求】** 任何新增的 Blade UI 區塊,禁止硬編碼 (Hard-coded) 中文或英文。必須使用 `__('...')` 並同步在 `lang/*.json` 補上翻譯。
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
* 開發新功能時,請建立標準的 Controller 搭配對應的 `resources/views/.../` 目錄。
* **【Go Gateway 開發】** 修改 `mqtt-gateway/` 內的 Go 程式碼時嚴禁加入商業邏輯。Go 僅負責訊息接收、格式轉換與 Redis 轉發。
## 8. 運行機制 (Docker / Sail)
## 9. 運行機制 (Docker / Sail)
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
* **啟動環境**`./vendor/bin/sail up -d`
* **啟動環境**`./vendor/bin/sail up -d`(將同時啟動 Laravel、MySQL、Redis、EMQX、Go Gateway
* **執行 PHP 指令**`./vendor/bin/sail php -v`
* **執行 Artisan 指令**`./vendor/bin/sail artisan route:list`
* **執行 Composer**`./vendor/bin/sail composer install`
* **執行 Node/NPM**`./vendor/bin/sail npm run dev`
## 8. 部署與查修環境 (CI/CD)
## 10. 部署與查修環境 (CI/CD)
* **自動化部署**:專案具備基於 Gitea Actions 的 CI/CD 自動化部署流程 (`.gitea/workflows/`)。
* **Demo 環境 (對應 `demo` 分支)**
* 透過 `deploy-demo.yaml`,合併或推送到 `demo` 分支會自動部署至 `demo-cloud.taiwan-star.com.tw`
* 登入伺服器查修:`ssh gitea_work`,路徑為 `/var/www/star-cloud-demo`
## 9. 瀏覽器測試規範 (Browser Testing)
## 11. 瀏覽器測試規範 (Browser Testing)
當需要進行瀏覽器自動化測試或手動驗證時,必須遵守以下連線資訊:
* **本地測試網址**`http://localhost:8090/` (注意:非 8000 或 8080)

View File

@@ -14,7 +14,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
| 觸發詞 / 情境 | 對應 Skill | 路徑 |
|---|---|---|
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報, MQTT, Topic, Broker, EMQX | **IoT 通訊與高併發處理規範 / MQTT 通訊規範** | `.agents/skills/iot-communication/SKILL.md` <br> `.agents/skills/mqtt-communication-specs/SKILL.md` |
| B010, B017, B600, B055, API 規格, 通訊協議, 狀態碼, 頁面碼, 範例, JSON | **API 技術規格與通訊協議規範** | `.agents/skills/api-technical-specs/SKILL.md` |
| 介面, UI, 設計, 佈局, CSS, Tailwind, 奢華, 深色模式, Light Mode, Dark Mode, Blade, 樣式, 間距, 陰影, 動畫, 畫面, 頁面 | **極簡奢華風 UI 實作規範** | `.agents/skills/ui-minimal-luxury/SKILL.md` |
| 查詢、撈資料、Query、Controller、下拉選單、Eloquent、N+1、`->get()`、select、交易、Transaction、Bulk、分頁、索引 | **資料庫與 ORM 最佳實踐規範** | `/home/mama/.gemini/antigravity/global_skills/database-best-practices/SKILL.md` |

View File

@@ -12,8 +12,18 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
- **類型嚴格**:文件定義的類型 (Integer, Float, String) 必須在後端 Model 與前端文件中心嚴格遵守。
## 2. 身份認證 (Authentication)
- **Bearer Token**:所有 API 必須在 Header 帶入 Authorization: Bearer <api_token>。
- **身分綁定**:後端透過 Token 自動識別 machine_id禁止在 Body 帶入 machine 或 key 欄位。
本系統採用兩階段認證模式:
### 2.1 維運人員認證 (User Authentication)
- **核發端點**B000 (登入)。
- **使用端點**B014 (參數下載)。
- **方式**:使用 Laravel Sanctum 核發之 **User Token**
- **Header**`Authorization: Bearer <user_token>`
### 2.2 機台通訊認證 (Machine Authentication)
- **適用 API**B010, B012, B013, B600 等後續通訊。
- **方式**:使用機台專屬之 **api_token**
- **Header**`Authorization: Bearer <api_token>`
---
@@ -40,6 +50,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| 參數 | 類型 | 說明 | 範例 |
| :--- | :--- | :--- | :--- |
| message | String | 驗證結果 (Success 或 Failed) | Success |
| token | String | **臨時身份認證 Token** (用於 B014) | 1|abcdefg... |
---
@@ -216,3 +227,69 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| **0415** | Pickup door error | error | 取貨門異常 |
| **5402** | Pickup door not closed | warning | **取貨門未關** (警告) |
| **5403** | Elevator failure | error | 昇降系統故障 |
---
### 3.7 B014: 機台參數與金鑰下載 (Config Download)
用於機台引導階段 (Provisioning),向雲端請求支付金鑰、發票設定及機台正式 API Token。
- **URL**: GET /api/v1/app/machine/setting/B014
- **Authentication**: **User Token** (Sanctum Header)
- **Request Body:** 無 (由 Query String 帶入 `machine` 參數)
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
| machine | String | 是 | 機台編號 (serial_no) | M-001 |
- **Response Body (Success 200):**
| 欄位 (Key) | 說明 | 備註 |
| :--- | :--- | :--- |
| **t050v01** | 機台序號 | 即 machine_id |
| **api_token** | **機台正式 Token** | 初始化後應存於本地,後續 API 認證用 |
| **t050v41** | 玉山特店編號 | ESUN Merchant ID |
| **t050v42** | 玉山終端編號 | ESUN Terminal ID |
| **t050v43** | 玉山 Hash Key | ESUN Hash |
| **t050v34** | 發票特店 ID | Invoice Merchant ID |
| **t050v35** | 發票 Hash Key | Invoice Key |
| **t050v36** | 發票 Hash IV | Invoice IV |
| **TP_APP_ID** | 趨勢支付 AppID | TrendPay ID |
| **TP_APP_KEY** | 趨勢支付 Key | TrendPay Key |
> [!CAUTION]
> **安全性規範**B014 會回傳敏感金鑰與正式 Token背景必須強制進行 RBAC 校驗。只有當前登入的人員具備該機台管理權限時,後端才允許發放資料。
---
### 3.8 B017: 貨道庫存同步 (Slot Synchronization)
用於機台端獲取目前所有貨道的最新庫存、效期與狀態。通常由 B010 回應 `status: 49` 觸發。
- **URL**: GET /api/v1/app/machine/reload_msg/B017
- **Authentication**: Bearer Token (Header) 或 API Key (Body/Query)
- **Request Body:** 無 (由 Token 自動識別機台)
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
- **Response Body:**
| 參數 | 類型 | 說明 | 範例 |
| :--- | :--- | :--- | :--- |
| success | Boolean | 請求是否成功 | true |
| code | Integer | 200 | 200 |
| data | Array | 貨道數據陣列 (依 slot_no 排序) | 見下表 |
**data 陣列內部欄位:**
| 欄位 | 類型 | 說明 | 範例 |
| :--- | :--- | :--- | :--- |
| tid | String | 貨道編號 | "1" |
| num | Integer | 當前庫存數量 | 10 |
| expiry_date | String | 商品效期 | "2026-12-31" |
| batch_no | String | 批號 | "B202604" |
| product_id | Integer | 商品 ID | 1 |
| capacity | Integer | 貨道容量上限 | 15 |
| status | String | 貨道狀態 ("1": 啟用, "0": 停用) | "1" |
> [!IMPORTANT]
> **同步機制**B017 為全量同步。App 收到回應後應更新本地資料庫對應貨道的數值。成功請求後,雲端會自動將相關的 `reload_stock` 指令標記為 `success`

View File

@@ -9,13 +9,25 @@ description: 規範智能販賣機與 Cloud 平台間的高頻通訊處理流程
## 1. 處理管線 (Processing Pipeline)
所有來自機台的非即時性資料(日誌、心跳、狀態上報)必須遵循以下 pipeline
所有來自機台的非即時性資料(日誌、心跳、狀態上報)必須遵循以下 pipeline。依據通訊協議不同,進入點有兩條路徑
### 1.1 HTTP 管線 (低頻/大檔操作)
適用於 B000, B009, B012, B014, B017 等低頻、同步需求或大資料量的端點:
1. **API Controller (接收層)**:驗證 Request 合法性,隨即分派 (Dispatch) 任務至 Queue並回傳 `202 Accepted`
2. **Job (異步層)**:由背景 Worker 讀取隊列任務,呼叫對應 Service 處理。
3. **Service (邏輯層)**:封裝商業邏輯,更新資料庫。
4. **Model (儲存層)**:執行資料存取。
### 1.2 MQTT 管線 (高頻/即時操作)
適用於 B010 (心跳), B013 (異常), B600 (交易) 等高頻或即時性端點:
1. **Go Gateway (接收層)**:訂閱 EMQX Topic提取 `serial_no`,包裝成標準 JSON。
2. **Redis List (橋接層)**Go 執行 `RPUSH mqtt_incoming_jobs {json}`
3. **Laravel `mqtt:listen` (消費層)**:常駐指令 `BLPOP` 取出 JSON根據 `type` 分派至對應 Job。
4. **Job ➜ Service ➜ Model**:與 HTTP 管線後半段相同。
> [!TIP]
> 兩條管線的 **Job / Service / Model 層完全共用**,差異僅在「進入點」。這確保了業務邏輯不會因為通訊協議不同而分裂。
> [!IMPORTANT]
> **嚴禁**在 API Controller 直接進行資料庫寫入操作(針對機台發訊端點)。
@@ -53,9 +65,14 @@ public function handle(MachineService $service): void
## 4. 速率限制 (Rate Limiting)
- 所有的 IoT API 必須在 `routes/api.php` 中使用 `throttle:api` 或自定義 Middleware。
### 4.1 HTTP 端點
- 所有的 IoT HTTP API 必須在 `routes/api.php` 中使用 `throttle:api` 或自定義 Middleware。
- 針對單一機台 ID 應限制其每一分鐘的最高連線數,防止遭受攻擊或機台 Bug 導致的連線暴衝。
### 4.2 MQTT 端點
- 限速由 **EMQX Broker** 的 Rate Limiting 功能負責(非 Laravel Middleware
- Go Gateway 層可額外實作簡易的 Token Bucket當某台機台每秒超過閾值時丟棄訊息並記錄 Warning Log。
## 5. 檢核項目 (Checklist)
- [ ] 是否使用了 `ApiResponse` Trait
- [ ] 業務邏輯是否已封裝至 `App\Services`
@@ -64,13 +81,16 @@ public function handle(MachineService $service): void
## 6. API 規格定義 (API Specifications)
> [!IMPORTANT]
> **規格分離原則**:本技能僅規範「通訊處理邏輯」。關於具體的 API 欄位、參數命名、狀態代碼對照與範例,請務必參閱專屬技能規範:
> - **[API 技術規格與通訊協議規範](file:///home/mama/projects/star-cloud/.agents/skills/api-technical-specs/SKILL.md)**
> **規格分離原則**:本技能僅規範「通訊處理邏輯」。關於具體的欄位定義與資料格式,請參閱對應的專屬技能規範:
> - **HTTP 端點**[API 技術規格與通訊協議規範](file:///home/mama/projects/star-cloud/.agents/skills/api-technical-specs/SKILL.md)
> - **MQTT 端點**[MQTT 即時通訊與 Topic 規範](file:///home/mama/projects/star-cloud/.agents/skills/mqtt-communication-specs/SKILL.md)
### 常見端點處理模式
1. **B010 (心跳)**:高頻點,必須進入 Redis Queue。更新 `last_heartbeat_at` 與感測器快照。
2. **B600 (交易)**:高價值點,必須進入任務隊列並支援重試。建立 `Transaction` 紀錄
3. **B017 (貨道)**:回覆較大資料量,應確保 Service 層具備緩存 (Cache) 機制
1. **B010 (心跳)**:高頻點,**MQTT 管線** (`machine/+/heartbeat`)。更新 `last_heard_at` 與感測器快照。
2. **B013 (異常)**:事件驅動點,走 **MQTT 管線** (`machine/+/error`)。寫入 `machine_logs` 並觸發告警
3. **B600 (交易)**:高價值點,走 **MQTT 管線** (`machine/+/transaction`)。建立 `Transaction` 紀錄並支援重試
4. **B012 (商品同步)**:大資料量,走 **HTTP 管線**。應確保 Service 層具備緩存 (Cache) 機制。
5. **B055 (遠端出貨)**:雲端下發指令,走 **MQTT 下行管線** (`machine/{id}/command`)。
---

View File

@@ -0,0 +1,100 @@
---
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 (相容性最高)
- **預設埠號**1883 (TCP / 測試用), 8883 (SSL/TLS / 正式用)
- **Keep-Alive**:建議設定為 30 ~ 60 秒。
### 1.2 身份認證 (Authentication)
機台連線時必須提供以下憑據:
- **Username**:機台編號 (`serial_no`),例如 `M-001`
- **Password**:機台正式 Token (`api_token`),由 B014 API 取得。
- **Client ID**:建議格式為 `SC_{serial_no}_{random_suffix}`,確保唯一性。
---
## 2. 主題架構 (Topic Topology)
我們採用目錄式的層級結構,方便未來進行萬台設備的管理與 ACL 權限切分。
| 主題名稱 (Topic) | 方向 | QoS | 用途說明 |
| :--- | :--- | :--- | :--- |
| `machine/{serial_no}/heartbeat` | 設備 ➜ 雲端 | 0 | 每 10 秒上報心跳、溫度、目前的頁面碼。 |
| `machine/{serial_no}/error` | 設備 ➜ 雲端 | 1 | 發生硬體故障、卡貨或門未關時立即上報。 |
| `machine/{serial_no}/transaction` | 設備 ➜ 雲端 | 1 | 交易完成、出貨結果的回報。 |
| `machine/{serial_no}/command` | 雲端 ➜ 設備 | 1 | 雲端下發的即時指令(出貨、更新、重啟)。 |
---
## 3. 資料載體規範 (Payload Definitions)
所有 Payload 統一採用 **JSON** 格式,字母一律為 **snake_case**
### 3.1 心跳上報 (Heartbeat) - `machine/{id}/heartbeat`
比照原 B010 邏輯,但去除不必要的 HTTP Header 開銷。
```json
{
"current_page": 1,
"firmware_version": "1.0.5",
"temperature": 25.5,
"door_status": 0,
"timestamp": "2026-04-14T09:00:00+08:00"
}
```
### 3.2 異常上報 (Error/Event) - `machine/{id}/error`
比照原 B013 邏輯。
```json
{
"tid": 12,
"error_code": "0403",
"log": "Slot jammed at slot 12",
"timestamp": "2026-04-14T09:05:00+08:00"
}
```
### 3.3 雲端指令 (Downstream Commands) - `machine/{id}/command`
這是雲端主動下發給機台的訊息,取代原本 B010 Response 的輪詢等待。
```json
{
"command": "dispense",
"payload": {
"slot_no": 5,
"transaction_id": "T202604140001"
},
"message_id": "MSG_123456789"
}
```
**常用指令集:**
- `reboot`: 機台重啟。
- `reload_config`: 重新下載參數 (B014)。
- `reload_products`: 重新同步商品 (B012)。
- `dispense`: 遠端出貨指令 (B055)。
---
## 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}/heartbeat`Payload 為 `{"status": "offline"}`。當連線異常中斷時,雲端能立刻得知。
---
## 5. 與 REST API 的同步關係
當 MQTT 通訊正常時,機台應停止定時呼叫 B010 HTTP API。若 MQTT 斷線超過 3 分鐘,則退回 (Fallback) 使用 HTTP 輪詢模式以維持基礎通訊。

View File

@@ -21,7 +21,9 @@ class AdvertisementController extends AdminController
// Tab 1: 廣告列表
$advertisements = Advertisement::with('company')->latest()->paginate(10);
$allAds = Advertisement::active()->get();
// Tab 2: 機台廣告設置 (所需資料) - 隱藏已過期的廣告
$allAds = Advertisement::playing()->get();
// Tab 2: 機台廣告設置 (所需資料)
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
@@ -54,6 +56,8 @@ class AdvertisementController extends AdminController
$request->type === 'image' ? 'max:10240' : 'max:51200', // Image 10MB, Video 50MB
],
'company_id' => 'nullable|exists:companies,id',
'start_at' => 'nullable|date',
'end_at' => 'nullable|date|after_or_equal:start_at',
]);
$user = auth()->user();
@@ -71,13 +75,15 @@ class AdvertisementController extends AdminController
$companyId = $user->company_id;
}
Advertisement::create([
$advertisement = Advertisement::create([
'company_id' => $companyId,
'name' => $request->name,
'type' => $request->type,
'duration' => (int) $request->duration,
'url' => Storage::disk('public')->url($path),
'is_active' => true,
'start_at' => $request->start_at,
'end_at' => $request->end_at,
]);
if ($request->wantsJson()) {
@@ -99,6 +105,8 @@ class AdvertisementController extends AdminController
'duration' => 'required|in:15,30,60',
'is_active' => 'boolean',
'company_id' => 'nullable|exists:companies,id',
'start_at' => 'nullable|date',
'end_at' => 'nullable|date|after_or_equal:start_at',
];
if ($request->hasFile('file')) {
@@ -111,7 +119,7 @@ class AdvertisementController extends AdminController
$request->validate($rules);
$data = $request->only(['name', 'type', 'duration']);
$data = $request->only(['name', 'type', 'duration', 'start_at', 'end_at']);
$data['is_active'] = $request->has('is_active');
$user = auth()->user();
@@ -150,7 +158,7 @@ class AdvertisementController extends AdminController
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
}
public function destroy(Advertisement $advertisement)
public function destroy(Request $request, Advertisement $advertisement)
{
// 檢查是否有機台正投放中
if ($advertisement->machineAdvertisements()->exists()) {

View File

@@ -112,7 +112,8 @@ class MachineAuthController extends Controller
);
return response()->json([
'message' => 'Success'
'message' => 'Success',
'token' => $user->createToken('technician-setup', ['*'], now()->addHours(8))->plainTextToken
]);
}
}

View File

@@ -168,35 +168,46 @@ class MachineController extends Controller
/**
* B017: Get Slot Info & Stock (Synchronous)
*/
/**
* B017: Get Slot Info & Stock (Synchronous - Full Sync)
*/
public function getSlots(Request $request)
{
$machine = $request->get('machine');
$slots = $machine->slots()->with('product')->get();
// 依貨道編號排序 (Sorted by slot_no as requested)
$slots = $machine->slots()->with('product')->orderBy('slot_no')->get();
// 自動轉 Success: 若機台來撈 B017代表之前的 reload_stock 指令已成功被機台響應
// 同時處理 sent 與 pending 狀態,確保狀態機正確關閉
\App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
->where('command_type', 'reload_stock')
->where('status', 'sent')
->update(['status' => 'success', 'executed_at' => now()]);
->whereIn('status', ['pending', 'sent'])
->update([
'status' => 'success',
'executed_at' => now(),
'note' => __('Inventory synced with machine')
]);
return response()->json([
'success' => true,
'code' => 200,
'data' => $slots->map(function ($slot) {
return [
'slot_no' => $slot->slot_no,
'product_id' => $slot->product_id,
'stock' => $slot->stock,
'capacity' => $slot->capacity,
'price' => $slot->price,
'status' => $slot->status,
'expiry_date' => $slot->expiry_date,
'tid' => $slot->slot_no,
'num' => (int)$slot->stock,
'expiry_date' => $slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : null,
'batch_no' => $slot->batch_no,
// 保留原始欄位以供除錯或未來擴充
'product_id' => $slot->product_id,
'capacity' => $slot->max_stock,
'status' => $slot->is_active ? '1' : '0',
];
})
]);
}
/**
* B710: Sync Timer status (Asynchronous)
*/
@@ -273,7 +284,7 @@ class MachineController extends Controller
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
->with([
'advertisement' => function ($query) {
$query->active();
$query->playing();
}
])
->get()
@@ -456,4 +467,86 @@ class MachineController extends Controller
'message' => 'Error report accepted',
], 202); // 202 Accepted
}
/**
* B014: Download Machine Settings & Config (Synchronous, Requires User Auth)
* 用於機台引導階段,同步金流、發票與機台專屬 API Token。
*/
public function getSettings(Request $request)
{
$serialNo = $request->input('machine');
$user = $request->user();
// 1. 查找機台 (忽略全局範圍以進行認領)
$machine = Machine::withoutGlobalScopes()
->with(['paymentConfig', 'company'])
->where('serial_no', $serialNo)
->first();
if (!$machine) {
return response()->json([
'success' => false,
'code' => 404,
'message' => 'Machine not found'
], 404);
}
// 2. 權限加強驗證 (RBAC)
$isAuthorized = false;
if ($user->isSystemAdmin()) {
$isAuthorized = true;
} elseif ($machine->company_id === $user->company_id) {
// 公司管理員或已授權員工才能存取
if ($user->is_admin || $user->machines()->where('machine_id', $machine->id)->exists()) {
$isAuthorized = true;
}
}
if (!$isAuthorized) {
return response()->json([
'success' => false,
'code' => 403,
'message' => 'Forbidden: You do not have permission to configure this machine'
], 403);
}
// 3. 獲取關聯設定
$paymentSettings = $machine->paymentConfig->settings ?? [];
$companySettings = $machine->company->settings ?? [];
// 4. 映射 App 預期欄位 (嚴格遵守 HttpAPI.java 結構)
$data = [
't050v01' => $machine->serial_no,
'api_token' => $machine->api_token, // 向 App 核發正式通訊 Token
// 玉山支付
't050v41' => $paymentSettings['esun_store_id'] ?? '',
't050v42' => $paymentSettings['esun_term_id'] ?? '',
't050v43' => $paymentSettings['esun_hash'] ?? '',
// 電子發票 (綠界)
't050v34' => $companySettings['invoice_merchant_id'] ?? '',
't050v35' => $companySettings['invoice_hash_key'] ?? '',
't050v36' => $companySettings['invoice_hash_iv'] ?? '',
't050v38' => $companySettings['invoice_email'] ?? '',
// 趨勢支付 (TrendPay/Greenpay)
'TP_APP_ID' => $paymentSettings['tp_app_id'] ?? '',
'TP_APP_KEY' => $paymentSettings['tp_app_key'] ?? '',
'TP_PARTNER_KEY' => $paymentSettings['tp_partner_key'] ?? '',
// 各類行動支付特店 ID
'TP_LINE_MERCHANT_ID' => $paymentSettings['tp_line_merchant_id'] ?? '',
'TP_PS_MERCHANT_ID' => $paymentSettings['tp_ps_merchant_id'] ?? '',
'TP_EASY_MERCHANT_ID' => $paymentSettings['tp_easy_merchant_id'] ?? '',
'TP_PI_MERCHANT_ID' => $paymentSettings['tp_pi_merchant_id'] ?? '',
'TP_JKO_MERCHANT_ID' => $paymentSettings['tp_jko_merchant_id'] ?? '',
];
return response()->json([
'success' => true,
'code' => 200,
'data' => [$data] // App 預期的是包含單一物件的陣列
]);
}
}

View File

@@ -18,11 +18,15 @@ class Advertisement extends Model
'duration',
'url',
'is_active',
'start_at',
'end_at',
];
protected $casts = [
'duration' => 'integer',
'is_active' => 'boolean',
'start_at' => 'datetime',
'end_at' => 'datetime',
];
/**
@@ -48,4 +52,21 @@ class Advertisement extends Model
{
return $query->where('is_active', true);
}
/**
* Scope a query to only include advertisements that should be playing now.
*/
public function scopePlaying($query)
{
$now = now();
return $query->where('is_active', true)
->where(function ($q) use ($now) {
$q->whereNull('start_at')
->orWhere('start_at', '<=', $now);
})
->where(function ($q) use ($now) {
$q->whereNull('end_at')
->orWhere('end_at', '>=', $now);
});
}
}

View File

@@ -8,6 +8,113 @@ return [
[
'name' => '機台核心通訊 (IoT Core)',
'apis' => [
[
'name' => 'B000: 維運人員登入認證 (Technician Login)',
'slug' => 'b000-tech-login',
'method' => 'POST',
'path' => '/api/v1/app/admin/login/B000',
'description' => '機台啟動引導的第一步。維運人員輸入個人帳密與機台編號進行認證,成功後核發臨時 Sanctum Token 供後續 B014 下載敏感設定使用。',
'headers' => [
'Content-Type' => 'application/json',
],
'parameters' => [
'username' => [
'type' => 'string',
'required' => true,
'description' => '維運人員帳號',
'example' => 'admin_test'
],
'password' => [
'type' => 'string',
'required' => true,
'description' => '維運人員密碼',
'example' => 'password123'
],
'machine' => [
'type' => 'string',
'required' => true,
'description' => '機台序號 (Serial No)',
'example' => 'SN202604130001'
],
],
'response_parameters' => [
'message' => [
'type' => 'string',
'description' => '回應訊息',
'example' => 'Success'
],
'token' => [
'type' => 'string',
'description' => '臨時身份認證 Token (Sanctum)',
'example' => '1|abcdefg...'
],
],
'request' => [
'username' => 'admin_test',
'password' => 'password123',
'machine' => 'SN202604130001'
],
'response' => [
'message' => 'Success',
'token' => '1|abcdefg...'
],
],
[
'name' => 'B014: 機台參數與金鑰下載 (Config Download)',
'slug' => 'b014-config-download',
'method' => 'GET',
'path' => '/api/v1/app/machine/setting/B014',
'description' => '機台引導階段的第二步。在人員登入後,透過此介面下載金流金鑰、電子發票設定與機台專屬通訊 Token。',
'headers' => [
'Authorization' => 'Bearer <user_token>',
'Content-Type' => 'application/json',
],
'parameters' => [
'machine' => [
'type' => 'string',
'required' => true,
'description' => '機台序號',
'example' => 'SN202604130001'
],
],
'response_parameters' => [
'success' => [
'type' => 'boolean',
'description' => '是否成功',
'example' => true
],
'data' => [
'type' => 'array',
'description' => '配置物件陣列。包含t050v01 (序號), api_token (通訊 Token), t050v41~43 (玉山設定), t050v34~38 (發票設定), TP_... (趨勢/手機支付設定)',
'example' => [
[
't050v01' => 'SN202604130001',
'api_token' => 'mac_token_...',
't050v41' => '80812345',
't050v34' => '2000132',
'TP_APP_ID' => 'GP_001'
]
]
],
],
'request' => [],
'response' => [
'success' => true,
'code' => 200,
'data' => [
[
't050v01' => 'SN202604130001',
'api_token' => 'mac_token_...',
't050v41' => '80812345',
't050v42' => '9001',
't050v43' => 'hash_key',
't050v34' => '2000132',
'TP_APP_ID' => 'GP_001'
]
]
],
'notes' => '此 API 受 auth:sanctum 保護,必須在 Header 帶上從 B000 取得的 Token。'
],
[
'name' => 'B005: 廣告清單同步 (Ad Sync)',
'slug' => 'b005-ad-sync',
@@ -350,6 +457,55 @@ return [
],
'notes' => '硬體代碼對照表見後端 MachineService::ERROR_CODE_MAP 定義。
0402: 出貨成功, 0403: 貨道卡貨, 0202: 貨道缺貨, 0415: 取貨門異常...等。'
],
[
'name' => 'B017: 貨道庫存同步 (Slot Synchronization)',
'slug' => 'b017-slot-sync',
'method' => 'GET',
'path' => '/api/v1/app/machine/reload_msg/B017',
'description' => '用於機台端獲獲取所有貨道的最新庫存、效期與狀態。通常由 B010 回傳 status: 49 時觸發。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
],
'parameters' => [],
'response_parameters' => [
'success' => [
'type' => 'boolean',
'description' => '是否成功',
'example' => true
],
'data' => [
'type' => 'array',
'description' => '貨道數據陣列。',
'example' => [
[
'tid' => '1',
'num' => 10,
'expiry_date' => '2026-12-31',
'batch_no' => 'B2026',
'status' => '1'
]
]
],
],
'request' => [],
'response' => [
'success' => true,
'code' => 200,
'data' => [
[
'tid' => '1',
'num' => 10,
'expiry_date' => '2026-12-31',
'batch_no' => 'B2026',
'product_id' => 1,
'capacity' => 15,
'status' => '1'
]
]
],
'notes' => 'B017 為全量同步。實作上後端會依據 slot_no 進行排序,並將相關指令狀態更新為已完成。'
]
]
]

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('advertisements', function (Blueprint $table) {
$table->dateTime('start_at')->nullable()->after('url')->comment('發布時間');
$table->dateTime('end_at')->nullable()->after('start_at')->comment('下架時間');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('advertisements', function (Blueprint $table) {
$table->dropColumn(['start_at', 'end_at']);
});
}
};

View File

@@ -4,6 +4,67 @@
---
## 🔐 B000: 維運人員登入認證 (Technician Login)
機台引導階段 (Provisioning) 的第一步,用於核發臨時身份 Token 以便後續下載敏感設定。
### 1. API 資訊
- **Endpoint**: `POST /api/v1/app/admin/login/B000`
- **認證方式**: 無 (需傳入 `username`, `password`, `machine`)
- **回應內容**: `token` (Sanctum Token)
### 2. 回應範例
```json
{
"message": "Success",
"token": "3|abcdef1234567890..."
}
```
---
## 🔑 B014: 機台參數與金鑰下載 (Config Download)
下載機台運作所需的支付金鑰、電子發票設定與正式通訊 Token。
### 1. API 資訊
- **Endpoint**: `POST /api/v1/app/machine/setting/B014`
- **認證方式**: **Bearer Token** (需帶上 B000 取得的 Token)
- **Header**: `Authorization: Bearer {token}`
### 2. 請求參數
- `machine`: 機台序號 (Serial No)
### 3. 回應規格 (欄位映射)
| 欄位 | 說明 | 來源範例 |
| :--- | :--- | :--- |
| `t050v01` | 機台序號 | `SN2026041301` |
| `api_token` | **機台正式 Token** | 後續 B010/B600 認證用 |
| `t050v41` | 玉山特店編號 | `ESUN_STORE_ID` |
| `t050v43` | 玉山 Hash Key | `ESUN_HASH` |
| `t050v34` | 發票特店 ID | `INV_MID` |
| `TP_APP_ID` | 趨勢支付 AppID | `TP_APP_ID` |
### 4. 回應範例 (JSON)
```json
{
"success": true,
"code": 200,
"data": [
{
"t050v01": "SN2026041301",
"api_token": "mac_token_...",
"t050v41": "8081234567",
"t050v42": "9001",
"t050v43": "password123",
"t050v34": "2000132",
"TP_APP_ID": "GREEN_001",
"...": "..."
}
]
}
```
---
## 🟢 B010: 心跳上報與狀態同步 (Heartbeat & Status)
機台定時(建議每 5-10 秒)上送,用於確認連線狀態、溫度及門禁狀態。

View File

@@ -15,22 +15,34 @@ gantt
excludes weekends
section Phase 1 基礎建設
資料表 + IoT API + 異步管線 :active, p1, 2026-03-16, 5d
資料表 + IoT API + 異步管線 :done, p1, 2026-03-16, 5d
section Phase 2 核心營運
後台核心營運頁面整合 :p2, after p1, 50d
帳號權限 + 資料主檔 + 機台 + 遠端 + 儀表板 :done, p2a, 2026-03-23, 30d
MQTT 基礎架構 :active, mqtt, 2026-04-16, 3d
全域工具列與溝通系統 :todo, toolbar, 2026-04-21, 4d
銷售管理 :todo, p2d, after toolbar, 3d
倉庫管理 :todo, p2f, after p2d, 6d
section Phase 3 進階模組
進階分析與垂直模組 :p3, after p2, 30d
進階分析與垂直模組 :todo, p3, after p2f, 15d
```
## 二、詳細時程對照表
| 階段 (Phase) | 關鍵任務摘要 | 預估天數 | 預計工作日期 | 狀態 |
| :--- | :--- | :---: | :---: | :---: |
| **Phase 1** | 28 張資料表 Migration + B010~B710 核心 API + Redis 異步 Job | **5 工作** | 03/16 ~ 03/20 | 進行中 |
| **Phase 2** | **後台核心營運頁面** (帳號權限、資料設定、機台、銷售、遠端、倉庫、儀表板) | **50 工作** | 03/23 ~ 05/29 | 規劃中 |
| **Phase 3** | **進階垂直模組** (分析、稽核、會員、APP、Line、預約、特殊權限) | **30 工作** | 06/01 ~ 07/10 | 規劃中 |
| **Phase 1** | 資料表 Migration + IoT 核心 API + Redis 異步 Job | **5 天** | 03/16 ~ 03/20 | ✅ 完成 |
| **Phase 2A** | 帳號與權限基礎 (帳號管理、子帳號、角色、RBAC) | **8 ** | 03/23 ~ 04/03 | ✅ 完成 |
| **Phase 2B** | 基礎資料主檔 (商品、廣告、點數、識別證) | **5 ** | 04/06 ~ 04/10 | ✅ 完成 |
| **Phase 2C** | 機台管理 (列表、日誌、權限、稼動率、效期、維修) | **7 天** | 04/13 ~ 04/23 | ✅ 完成 |
| **Phase 2E** | 遠端管理 (庫存、重啟、出貨、鎖定等 7 項) | **8 天** | 04/30 ~ 05/11 | ✅ 完成 |
| **Phase 2G** | 儀表板 | **2 天** | 05/28 ~ 05/29 | ✅ 完成 |
| **MQTT** | EMQX + Go Gateway + Laravel 橋接 | **3 天** | 04/16 ~ 04/18 | 🔴 待開發 |
| **全域工具列** | 下載中心、通知、帳號模擬、公告系統、快捷入口 | **4 天** | 04/21 ~ 04/24 | 🟡 待開發 |
| **Phase 2D** | 銷售管理 (銷售紀錄、取貨碼、促銷、通行碼) | **3 天** | 04/25 ~ 04/29 | 🟢 待開發 |
| **Phase 2F** | 倉庫管理 (倉庫、庫存、調撥、採購、補貨) | **6 天** | 04/30 ~ 05/07 | 🟢 待開發 |
| **Phase 3** | 進階垂直模組 (分析、稽核、會員、APP、Line、預約) | **15 天** | 05/08 ~ 05/28 | 🔵 待開發 |
---
@@ -39,7 +51,7 @@ gantt
> [!IMPORTANT]
> 開發順序依**功能相依性**排列:先建帳號與權限基礎 → 再建商品等主檔資料 → 然後是依賴主檔的機台與銷售 → 接著是營運急需的遠端管理與倉庫管理 → 最後是匯總數據的儀表板。Phase 3 則從分析報表開始,逐步擴展至行銷與第三方聯動。
### ⚡ Phase 1基礎建設 (03/16 ~ 03/20)
### ⚡ Phase 1基礎建設 (03/16 ~ 03/20) ✅ 已完成
| 任務類別 | 內容 | 日期 |
| :--- | :--- | :---: |
@@ -47,10 +59,27 @@ gantt
| IoT API 端點 | B010 心跳、B017 庫存、B055 出貨、B600/B601/B602 金流 | 03/18 - 03/19 |
| 異步管線 | Redis Queue Job + Service 層、B650 會員驗證 | 03/20 |
### 🔌 MQTT 基礎架構 (04/16 ~ 04/18) 🔴 待開發
| 日期 | 任務 |
| :---: | :--- |
| **04/16 (三)** | EMQX 佈署至 compose.yaml + Go Gateway 上行開發 |
| **04/17 (四)** | Go Gateway 上行完成 + 下行開發 |
| **04/18 (五)** | Laravel mqtt:listen + MqttCommandService + 端對端測試 |
### 🛠️ 全域工具列與溝通系統 (04/21 ~ 04/24) 🟡 待開發
| 日期 | 任務 |
| :---: | :--- |
| **04/21 (一)** | ☁️ 下載任務中心 |
| **04/22 (二)** | 🔔 通知中心 + ❓ 幫助/客服中心 |
| **04/23 (三)** | 🎭 帳號切換與身分模擬 + 📢 系統公告管理 |
| **04/24 (四)** | 🛡️ 登錄強制公告 + 🚀 儀表板快捷入口 |
### 🏛️ Phase 2核心營運子選單 (03/23 ~ 05/29)
共 51 項子選單,依功能相依性分為七個開發階段。
#### 📌 A. 帳號與權限基礎 (03/23 ~ 04/03)
#### 📌 A. 帳號與權限基礎 (03/23 ~ 04/03) ✅ 已完成
> 為何優先:帳號、角色、權限是所有後台模組的存取控管基礎,必須最先到位。
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
@@ -72,7 +101,7 @@ gantt
| 15 | | 其他功能 | 04/03 | 其餘未分類功能之權限控管 |
| 16 | | AI智能預測 | 04/03 | AI 預測功能的存取權限設定 |
#### 📌 B. 基礎資料主檔 (04/06 ~ 04/10)
#### 📌 B. 基礎資料主檔 (04/06 ~ 04/10) ✅ 已完成
> 為何第二:商品主檔是機台貨道、倉庫、銷售等後續模組的共同基礎資料。
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
@@ -83,7 +112,7 @@ gantt
| 20 | | 點數設定 | 04/10 | 點數發放規則與兌換比例設定 |
| 21 | | 識別證 | 04/10 | 員工/維修人員識別證管理 |
#### 📌 C. 機台管理 (04/13 ~ 04/23)
#### 📌 C. 機台管理 (04/13 ~ 04/23) ✅ 已完成
> 為何第三:機台是核心營運實體,須在商品主檔建好後才能綁定貨道。
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
@@ -107,7 +136,7 @@ gantt
| 32 | | 通行碼 | 04/29 | 通行碼發放與使用紀錄 |
| 33 | | 來店禮 | 04/29 | 到店即贈的禮品活動設定 |
#### 📌 E. 遠端管理 (04/30 ~ 05/11)
#### 📌 E. 遠端管理 (04/30 ~ 05/11) ✅ 已完成
> 為何第五:營運最迫切需要的即時控制能力,直接串接 Phase 1 的 B010/B055 API。
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
@@ -136,7 +165,7 @@ gantt
| 49 | | 人員庫存 | 05/27 | 補貨人員攜帶庫存管理 |
| 50 | | 回庫單 | 05/27 | 退回倉庫的商品登記與核銷 |
#### 📌 G. 儀表板 (05/28 ~ 05/29)
#### 📌 G. 儀表板 (05/28 ~ 05/29) ✅ 已完成
> 為何最後:儀表板匯總機台、銷售、遠端指令、倉庫等全部數據,必須等上游模組完成才有意義。
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |

43
docs/future_todo.md Normal file
View File

@@ -0,0 +1,43 @@
# Star Cloud 近期開發待辦清單 (Target Roadmap)
本文件列出了 Star Cloud 系統近期優先開發的功能模組,旨在強化系統的營運溝通能力與非同步處理效率。
---
## 🟢 核心開發階段:全域工具列與通訊系統
*本階段為目前唯一開發重心*
### 1. 全域工具列升級 (Header Toolbar)
| 功能項目 | 具體描述 | 預計開發時間 |
| :--- | :--- | :--- |
| **☁️ 下載任務中心** | 整合 Redis Queue 處理耗時報表匯出。商戶點擊匯出後背景執行,完成後透過 Header 圖示點擊下載。 | 2 天 |
| **🔔 通知中心** | 串接 Laravel Database Notifications顯示系統消息、機台警告與業務通知帶有紅點提示。 | 1 天 |
| **❓ 幫助/客服中心** | 於 Header 置入問號圖示,點擊觸發側邊抽屜 (Offcanvas),展示 FAQ 與客服聯繫窗口。 | 0.5 天 |
| **🎭 帳號切換與身分模擬** | **整合於頭像下拉選單**:支援「系統管理員切換租戶」與「租戶管理員切換子帳號」,提供顯眼的頂部模擬狀態橫幅。 | 1.5 天 |
### 2. 公告與溝通系統 (Communication System)
| 功能項目 | 具體描述 | 預計開發時間 |
| :--- | :--- | :--- |
| **📢 系統公告管理** | 建立後台發布介面,支援針對全體或特定租戶發布「一般」或「重要」公告。 | 1.5 天 |
| **🛡️ 登錄強制公告** | 實作具備「滑動解鎖」功能的彈窗。使用者必須將公告滑到底部,解鎖按鈕後才能進入 Dashboard。 | 1 天 |
### 3. 儀表板優化 (Dashboard Enhancement)
| 功能項目 | 具體描述 | 預計開發時間 |
| :--- | :--- | :--- |
| **🚀 儀表板快捷入口** | 在儀表板頂部加入一排快捷圖示(如:機台管理、訂單查詢、會員中心),方便商戶快速跳轉核心功能。 | 0.5 天 |
---
## 🟡 第二階段:進階行銷與營運工具
*優先順序:中 | 預計總工時:約 5 個開發日*
| 功能項目 | 具體描述 | 預計開發時間 |
| :--- | :--- | :--- |
| **🎁 互動盲盒抽獎** | **後台端**:實作中獎機率配置、獎項庫存管理、活動排程。**終端 API**:提供給機台大螢幕 H5/React 遊戲呼叫的開獎與配置介面。 | 4 天 |
---
## 📝 實作標準
1. **UI/UX**: 必須符合 `ui-minimal-luxury` 規範Outfit 字體、青色點綴、柔和投影)。
2. **安全性**: 權限控制必須嚴格過濾 `company_id`,公告需支援「已讀紀錄」追蹤。
3. **效能**: 下載中心必須使用非同步隊列,嚴禁在 Request 週期內執行耗時匯出。

View File

@@ -0,0 +1,191 @@
# Star Cloud MQTT 基礎架構實作計畫 (Phase 1)
本計畫旨在建立 Star Cloud 的高併發通訊基石,包含佈署 EMQX Broker、開發 Go MQTT Gateway並建立與 Laravel 之間的 Redis **雙向**異步橋接機制。
---
## User Review Required
> [!IMPORTANT]
> **雙向通訊架構**:本計畫不只處理「機台 ➜ 雲端」的上報,也同時建立「雲端 ➜ 機台」的指令下發通道。後台管理員在按下「遠端出貨」按鈕時Laravel 會將指令推入 Redis由 Go Gateway 轉發至 EMQX再即時送達機台 APP。
> [!WARNING]
> **資源配額**Go Gateway 雖然輕量,但在 Docker 環境中仍建議設定 `mem_limit` 避免極端情況下的資源爭搶。
---
## 系統架構總覽
```
機台 Android APP
├─ [上行] Publish ──→ EMQX ──→ Go Gateway ──→ Redis List (mqtt_incoming_jobs) ──→ Laravel mqtt:listen ──→ Job ──→ MySQL
└─ [下行] Subscribe ←── EMQX ←── Go Gateway ←── Redis List (mqtt_outgoing_commands) ←── Laravel MqttCommandService
```
---
## Proposed Changes
### 1. 基礎設施佈署 (Infrastructure)
#### [MODIFY] [compose.yaml](file:///home/mama/projects/star-cloud/compose.yaml)
- **新增 `emqx` 服務**
- Image: `emqx/emqx:5.10.3`開源版最終穩定版Apache 2.0 授權)
- Ports: `1883:1883` (MQTT), `8083:8083` (WebSocket), `18083:18083` (Dashboard)
- 加入 `sail` 網路
- 配置 Redis Auth 插件環境變數,指向 `star-cloud-redis`
- **新增 `mqtt-gateway` 服務**
- 使用 `mqtt-gateway/Dockerfile` 進行 Multi-stage build
- 連接至 `sail` 網路
- 依賴 `emqx``redis`
- 環境變數從 `.env` 讀取
#### [MODIFY] [.env](file:///home/mama/projects/star-cloud/.env)
- 新增以下環境變數:
```env
# MQTT / EMQX
MQTT_BROKER_HOST=emqx
MQTT_BROKER_PORT=1883
EMQX_DASHBOARD_PORT=18083
# Go Gateway
MQTT_GATEWAY_CLIENT_ID=star-cloud-gateway
MQTT_REDIS_HOST=star-cloud-redis
MQTT_REDIS_PORT=6379
```
---
### 2. Go MQTT Gateway 開發 (The Bridge)
#### [NEW] 完整目錄結構
```
mqtt-gateway/
├── Dockerfile ← Multi-stage build (builder + alpine)
├── go.mod
├── go.sum
├── main.go ← 進入點初始化、訊號監聽、graceful shutdown
├── config/
│ └── config.go ← 讀取環境變數 (EMQX_ADDR, REDIS_ADDR 等)
├── internal/
│ ├── handler/
│ │ ├── heartbeat_handler.go ← 處理 machine/+/heartbeat
│ │ ├── error_handler.go ← 處理 machine/+/error
│ │ └── transaction_handler.go ← 處理 machine/+/transaction
│ └── bridge/
│ ├── redis_consumer.go ← [下行] BLPOP mqtt_outgoing_commands轉發至 EMQX
│ └── redis_pusher.go ← [上行] RPUSH mqtt_incoming_jobs
```
#### [上行邏輯] 機台 ➜ 雲端
1. 訂閱 `machine/+/heartbeat`, `machine/+/error`, `machine/+/transaction`
2. 從 Topic 路徑提取 `serial_no`
3. 包裝成 `BridgePayload`
```json
{
"type": "heartbeat",
"serial_no": "M-001",
"payload": { "current_page": 1, "temperature": 25.5 },
"received_at": "2026-04-14T09:00:00+08:00"
}
```
4. 執行 `RPUSH mqtt_incoming_jobs {json}`
#### [下行邏輯] 雲端 ➜ 機台 (新增)
1. Go Gateway 啟動一條 Goroutine持續 `BLPOP mqtt_outgoing_commands`
2. 取得 JSON 後解析目標 `serial_no``command` 內容。
3. Publish 至 `machine/{serial_no}/command` (QoS 1)。
```json
{
"target": "M-001",
"command": "dispense",
"payload": { "slot_no": 5, "transaction_id": "T202604140001" },
"message_id": "MSG_123456789"
}
```
#### [NEW] mqtt-gateway/Dockerfile
```dockerfile
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o gateway .
# Stage 2: Run
FROM alpine:3.20
COPY --from=builder /app/gateway /usr/local/bin/gateway
CMD ["gateway"]
```
---
### 3. Laravel 端實作
#### [NEW] app/Console/Commands/ListenMqttQueue.php
- Artisan Command: `mqtt:listen`
- 使用 `BLPOP mqtt_incoming_jobs` 阻塞式監聽
- 根據 `type` 分派至對應的 Laravel Job
- `heartbeat``ProcessHeartbeatJob`
- `error``ProcessMachineErrorJob`
- `transaction``ProcessTransactionJob`
#### [NEW] app/Services/Machine/MqttCommandService.php
- 提供 `sendCommand(string $serialNo, string $command, array $payload)` 方法
- 將指令 JSON 推入 Redis List `mqtt_outgoing_commands`
- 供 Controller 呼叫(例如後台管理員按下「遠端出貨」按鈕)
#### [NEW] app/Jobs/Machine/ (三個 Job)
- `ProcessHeartbeatJob.php`: 更新 `last_heard_at`、溫度、頁面碼
- `ProcessMachineErrorJob.php`: 寫入 `machine_logs`,觸發告警通知
- `ProcessTransactionJob.php`: 更新庫存、建立交易紀錄
#### [MODIFY] [MachineAuthController.php](file:///home/mama/projects/star-cloud/app/Http/Controllers/Api/V1/App/MachineAuthController.php)
- 在 B014 核發 `api_token` 後,同步寫入 Redis
`Redis::set("machine_auth:{$serial_no}", hash('sha256', $token));`
- Token 更新/撤銷時,同步刪除 Redis 中的對應 Key
---
## Architecture Decisions (Confirmed)
### ✅ EMQX 版本策略
- **統一鎖定**:開發與正式環境皆使用 `emqx/emqx:5.10.3`(開源版最後一個穩定版本)。
- **選擇理由**EMQX 從 5.9.0 起合併為統一版本6.x 改用 BSL 商業授權。`5.10.3` 是純開源 (Apache 2.0) 的最終穩定版,功能完整且免授權費用。
### ✅ 下行指令安全性(無需額外 OTP
App 連線 MQTT 時已使用 `api_token` 作為密碼完成身份認證,因此 **MQTT 連線本身即為認證通道**。安全性由以下三層保障:
1. **連線層**App 使用 `serial_no` + `api_token` 連線 EMQX未通過驗證的裝置無法訂閱任何 Topic。
2. **ACL 層**EMQX 存取控制確保只有 Go Gateway 能 Publish 到 `machine/{id}/command`,機台 App 無法偽造指令。
3. **冪等性層**:每條下行指令的 Payload 包含唯一的 `message_id`App 端應記錄已執行的 ID防止重複執行同一條指令。
> [!NOTE]
> **結論**:上行用 Token 當密碼、下行用 ACL 當門禁、`message_id` 當防重複鎖。三層防護已足夠,不需要額外的 OTP 機制。
---
## Verification Plan
### 1. 基礎設施驗證
- 執行 `./vendor/bin/sail up -d`
- 造訪 `http://localhost:18083` 確認 EMQX Dashboard 正常啟動
- 確認 Go Gateway Container 的 logs 顯示 "Connected to EMQX" 與 "Connected to Redis"
### 2. 上行通訊測試 (機台 ➜ 雲端)
- 使用 MQTTX 工具連接 `localhost:1883`
- 發送心跳 JSON 至 `machine/TEST-001/heartbeat`
- 檢查 Laravel 日誌確認 `mqtt:listen` 成功接收並分派 Job
- 檢查 MySQL `machines` 表的 `last_heard_at` 是否更新
### 3. 下行通訊測試 (雲端 ➜ 機台)
- 在 MQTTX 訂閱 `machine/TEST-001/command`
- 透過 Laravel Tinker 呼叫 `MqttCommandService::sendCommand('TEST-001', 'reboot', [])`
- 確認 MQTTX 收到 reboot 指令的 JSON
### 4. 壓力測試
- 使用 Go Script 模擬 500 台機台同時發送心跳
- 監控 Redis List 長度與 Laravel Worker 的處理速率

View File

@@ -1145,5 +1145,13 @@
"Service Terms": "Service Periods",
"Contract": "Contract",
"Warranty": "Warranty",
"Software": "Software"
"Software": "Software",
"Schedule": "Schedule",
"Immediate": "Immediate",
"Indefinite": "Indefinite",
"Ongoing": "Ongoing",
"Waiting": "Waiting",
"Publish Time": "Publish Time",
"Expired Time": "Expired Time",
"Inventory synced with machine": "Inventory synced with machine"
}

View File

@@ -1144,5 +1144,13 @@
"Service Terms": "サービス期間",
"Contract": "契約",
"Warranty": "保証",
"Software": "ソフトウェア"
"Software": "ソフトウェア",
"Schedule": "スケジュール設定",
"Immediate": "即時",
"Indefinite": "無期限",
"Ongoing": "進行中",
"Waiting": "待機中",
"Publish Time": "公開時間",
"Expired Time": "終了時間",
"Inventory synced with machine": "在庫が機体と同期されました"
}

View File

@@ -1145,5 +1145,13 @@
"Service Terms": "服務期程",
"Contract": "合約",
"Warranty": "保固",
"Software": "軟體"
"Software": "軟體",
"Schedule": "排程區間",
"Immediate": "立即",
"Indefinite": "無限期",
"Ongoing": "進行中",
"Waiting": "等待中",
"Publish Time": "發布時間",
"Expired Time": "下架時間",
"Inventory synced with machine": "庫存已與機台同步"
}

View File

@@ -71,6 +71,7 @@ $baseRoute = 'admin.data-config.advertisements';
@endif
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Duration') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Schedule') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
@@ -104,15 +105,34 @@ $baseRoute = 'admin.data-config.advertisements';
{{ __($ad->type) }}
</span>
</td>
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-black text-slate-700 dark:text-slate-200">
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-mono font-bold text-slate-700 dark:text-slate-200">
{{ $ad->duration }}s
</td>
<td class="px-6 py-4 text-center whitespace-nowrap">
<div class="flex flex-col items-center gap-0.5">
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('From') }}: {{ $ad->start_at?->format('Y-m-d H:i') ?? __('Immediate') }}</span>
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('To') }}: {{ $ad->end_at?->format('Y-m-d H:i') ?? __('Indefinite') }}</span>
</div>
</td>
<td class="px-6 py-4 text-center">
@if($ad->is_active)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Active') }}</span>
@else
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
@endif
@php
$now = now();
$isStarted = !$ad->start_at || $ad->start_at <= $now;
$isExpired = $ad->end_at && $ad->end_at < $now;
$isPlaying = $ad->is_active && $isStarted && !$isExpired;
@endphp
<div class="flex flex-col items-center gap-1">
@if(!$ad->is_active)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
@elseif($isExpired)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Expired') }}</span>
@elseif(!$isStarted)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">{{ __('Waiting') }}</span>
@else
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Ongoing') }}</span>
@endif
</div>
</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end items-center gap-2">
@@ -211,7 +231,7 @@ $baseRoute = 'admin.data-config.advertisements';
</button>
<div class="flex-1 min-w-0 flex flex-col justify-center cursor-pointer group-hover:text-cyan-500 transition-colors" @click="openPreview(assign.advertisement)">
<p class="text-xs font-black text-slate-700 dark:text-white truncate transition-colors" x-text="assign.advertisement.name"></p>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
<p class="text-[11px] font-mono font-bold text-slate-400 uppercase tracking-tight mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
</div>
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
@@ -324,7 +344,7 @@ $baseRoute = 'admin.data-config.advertisements';
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
<span class="text-cyan-400 font-black tracking-widest text-xs uppercase" x-text="(currentSequenceIndex + 1) + ' / ' + sequenceAds.length"></span>
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
<span class="text-white/80 font-bold tracking-widest text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
<span class="text-white/80 font-mono font-bold tracking-tight text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
</div>
<div class="flex items-center gap-2">
@@ -385,7 +405,9 @@ $baseRoute = 'admin.data-config.advertisements';
type: 'image',
duration: 15,
is_active: true,
url: ''
url: '',
start_at: '',
end_at: ''
},
// Assign Modal
@@ -604,6 +626,17 @@ $baseRoute = 'admin.data-config.advertisements';
}
},
formatDateForInput(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}/${month}/${day} ${hours}:${minutes}`;
},
async submitAssignment() {
try {
const response = await fetch(this.urls.assign, {
@@ -650,6 +683,14 @@ $baseRoute = 'admin.data-config.advertisements';
if (document.querySelector('#ad_company_select')) {
window.HSSelect.getInstance('#ad_company_select')?.setValue(this.adForm.company_id || '');
}
// 確保 Flatpickr 實例同步顯示目前的時間值
if (this.$refs.startAtPicker?._flatpickr) {
this.$refs.startAtPicker._flatpickr.setDate(this.adForm.start_at);
}
if (this.$refs.endAtPicker?._flatpickr) {
this.$refs.endAtPicker._flatpickr.setDate(this.adForm.end_at);
}
});
}
});
@@ -685,7 +726,7 @@ $baseRoute = 'admin.data-config.advertisements';
openAddModal() {
this.adFormMode = 'add';
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '' };
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '', start_at: '', end_at: '' };
this.fileName = '';
this.mediaPreview = null;
this.isAdModalOpen = true;
@@ -693,7 +734,11 @@ $baseRoute = 'admin.data-config.advertisements';
openEditModal(ad) {
this.adFormMode = 'edit';
this.adForm = { ...ad };
this.adForm = {
...ad,
start_at: this.formatDateForInput(ad.start_at),
end_at: this.formatDateForInput(ad.end_at)
};
this.fileName = '';
this.mediaPreview = ad.url; // Use existing URL as preview
this.isAdModalOpen = true;

View File

@@ -94,6 +94,51 @@
</div>
</div>
<!-- Scheduling -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Publish Time') }}
</label>
<div class="relative group/input">
<input type="text" name="start_at" x-ref="startAtPicker" x-model="adForm.start_at"
x-init="flatpickr($refs.startAtPicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: window.flatpickrLocale,
onClose: (selectedDates, dateStr) => { adForm.start_at = dateStr; }
})"
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
placeholder="YYYY/MM/DD HH:MM">
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Expired Time') }}
</label>
<div class="relative group/input">
<input type="text" name="end_at" x-ref="endAtPicker" x-model="adForm.end_at"
x-init="flatpickr($refs.endAtPicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: window.flatpickrLocale,
onClose: (selectedDates, dateStr) => { adForm.end_at = dateStr; }
})"
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
placeholder="YYYY/MM/DD HH:MM">
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
</div>
</div>
</div>
<!-- File Upload (Luxury UI Pattern) -->
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">

View File

@@ -51,12 +51,15 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
// 機台管理員 B000 登入驗證 (由於此階段機台未帶 Token 無法通過 iot.auth)
Route::prefix('app')->group(function () {
Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1');
// 機台啟動引導與參數下載 (需人員登入 Token)
Route::middleware('auth:sanctum')->get('machine/setting/B014', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSettings']);
});
Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () {
// 心跳與狀態 (B010, B017, B710, B220)
Route::post('machine/status/B010', [App\Http\Controllers\Api\V1\App\MachineController::class, 'heartbeat']);
Route::post('machine/reload_msg/B017', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSlots']);
Route::get('machine/reload_msg/B017', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSlots']);
Route::post('machine/timer/B710', [App\Http\Controllers\Api\V1\App\MachineController::class, 'syncTimer']);
Route::post('machine/coins/B220', [App\Http\Controllers\Api\V1\App\MachineController::class, 'syncCoinInventory']);
Route::post('machine/member/verify/B650', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyMember']);