diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md
index 175f16e..d83428c 100644
--- a/.agents/skills/api-technical-specs/SKILL.md
+++ b/.agents/skills/api-technical-specs/SKILL.md
@@ -19,7 +19,29 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
## 3. 機台核心 API (IoT Endpoints)
-### 3.1 B010: 心跳上報與狀態同步
+### 3.1 B000: 機台本地管理員同步登入
+用於機台 Android 端維護人員登入與進入設定頁。此 API 無狀態,且為例外不強制檢查 Bearer Token 的端點。
+
+- **URL**: `POST /api/v1/app/admin/login/B000`
+- **Request Body:**
+| 參數 | 類型 | 必填 | 說明 | 範例 |
+| :--- | :--- | :--- | :--- | :--- |
+| `machine` | String | 是 | 機台編號 (serial_no) | `M-001` |
+| `Su_Account` | String | 是 | 系統管理員或公司管理員帳號 | `admin` |
+| `Su_Password` | String | 是 | 密碼 | `password123` |
+| `ip` | String | 否 | 用戶端 IP (相容舊版) | `192.168.1.100` |
+| `type` | String | 否 | 裝置類型代碼 (相容舊版) | `2` |
+
+- **Response Body:**
+> [!IMPORTANT]
+> 為了相容 Java APP 現有邏輯,這裡嚴格規定成功必須回傳字串 `Success`。
+| 參數 | 類型 | 說明 | 範例 |
+| :--- | :--- | :--- | :--- |
+| `message` | String | 驗證結果 (`Success` 或 `Failed`) | `Success` |
+
+---
+
+### 3.2 B010: 心跳上報與狀態同步
用於確認機台在線狀態、更新感測數據、提交事件日誌並獲取雲端指令。
- **URL**: `POST /api/v1/app/machine/status/B010`
@@ -56,23 +78,20 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
**雲端指令代碼 (status):**
- `49`: reload B017 (貨道同步)
-- `50`: reload B005 (基礎參數)
- `51`: reboot (重啟系統)
- `60`: reboot card machine (刷卡機重啟)
-- `61`: checkout (結帳)
-- `70`: unlock (解鎖) / `71`: lock (鎖定)
-- `72`: sellCode reload B023 (即期品)
-- `75`: exp reload B026 (效期)
-- `79`: read B050 (參數讀取)
-- `81`: sync timer status (B710)
-- `85`: reload B0552 (出貨腳本)
+- `61`: checkout (觸發結帳)
+- `70`: unlock (解鎖)
+- `71`: lock (鎖定)
+- `85`: reload B0552 (遠端出貨)
+- `待定義`: change (遠端找零 - 註:指令中心有此功能,但目前 Java App 尚無對接對應的連動事件)
---
-### 3.2 B017: 貨道與庫存同步 (規劃中)
+### 3.3 B017: 貨道與庫存同步 (規劃中)
- **URL**: `POST /api/v1/app/machine/reload_msg/B017`
- 說明:當機台收到 B010 回應 `status: 49` 時,應呼叫此 API 同步最新貨道佈局。
-### 3.3 B600: 交易數據回傳 (規劃中)
+### 3.4 B600: 交易數據回傳 (規劃中)
- **URL**: `POST /api/v1/app/B600`
- 說明:交易完成後提交支付方式、金額、商品與出貨結果。
diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php
index 21b3c21..e2ed289 100644
--- a/app/Http/Controllers/Admin/RemoteController.php
+++ b/app/Http/Controllers/Admin/RemoteController.php
@@ -23,7 +23,7 @@ class RemoteController extends Controller
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
$query->where('command_type', '!=', 'reload_stock')
->latest()
- ->limit(10);
+ ->limit(5);
}])->find($request->machine_id);
}
diff --git a/app/Http/Controllers/Api/V1/App/MachineAuthController.php b/app/Http/Controllers/Api/V1/App/MachineAuthController.php
new file mode 100644
index 0000000..601e3dc
--- /dev/null
+++ b/app/Http/Controllers/Api/V1/App/MachineAuthController.php
@@ -0,0 +1,92 @@
+validate([
+ 'machine' => 'required|string',
+ 'Su_Account' => 'required|string',
+ 'Su_Password' => 'required|string',
+ 'ip' => 'nullable|string',
+ 'type' => 'nullable|string',
+ ]);
+
+ // 2. 透過帳號尋找使用者 (允許使用 username 或 email)
+ $user = User::where('username', $validated['Su_Account'])
+ ->orWhere('email', $validated['Su_Account'])
+ ->first();
+
+ // 若無此帳號或密碼錯誤
+ if (!$user || !Hash::check($validated['Su_Password'], $user->password)) {
+ Log::warning("B000 機台登入失敗: 帳密錯誤", [
+ 'account' => $validated['Su_Account'],
+ 'machine' => $validated['machine']
+ ]);
+ // 按現行設定,Java 端只認 Success 字串,其餘視為帳密錯誤
+ return response()->json(['message' => 'Failed']);
+ }
+
+ // 3. 取得機台物件
+ // 因為此 API 無狀態 (沒有登入 session),為了避免被 global scope 擋住,直接取消所有 scope 來撈取
+ $machine = Machine::withoutGlobalScopes()->where('serial_no', $validated['machine'])->first();
+
+ if (!$machine) {
+ Log::warning("B000 機台登入失敗: 伺服器找不到該機台", [
+ 'machine_serial' => $validated['machine']
+ ]);
+ return response()->json(['message' => 'Failed']);
+ }
+
+ // 4. RBAC 權限驗證 (遵循多租戶與機台授權規範)
+ if ($user->isSystemAdmin()) {
+ // [系統管理員] : 擁有最高權限,可登入平台下轄所有機台,直接放行
+
+ } elseif ($user->is_admin) {
+ // [公司管理員] : 不需要檢查 machine_user 表,但【必須驗證】該機台是否隸屬於他的公司
+ if ($machine->company_id !== $user->company_id) {
+ Log::warning("B000 機台登入失敗: 企圖越權登入其他公司的機台", [
+ 'user_id' => $user->id,
+ 'user_company' => $user->company_id,
+ 'machine_company' => $machine->company_id
+ ]);
+ return response()->json(['message' => 'Forbidden']);
+ }
+
+ } else {
+ // [一般租戶帳號] : (包括補貨員等)必須嚴格檢查該帳號有沒有被分配到這台機器的 machine_user 關聯授權
+ if (!$user->machines()->where('machine_id', $machine->id)->exists()) {
+ Log::warning("B000 機台登入失敗: 該帳號沒有此機台的授權", [
+ 'user_id' => $user->id,
+ 'machine_id' => $machine->id
+ ]);
+ return response()->json(['message' => 'Forbidden']);
+ }
+ }
+
+ // 5. 驗證完美通過!回傳固定字串 Success 讓 Java 端放行
+ Log::info("B000 機台登入成功", [
+ 'account' => $user->username,
+ 'machine' => $machine->serial_no
+ ]);
+
+ return response()->json([
+ 'message' => 'Success'
+ ]);
+ }
+}
diff --git a/config/api-docs.php b/config/api-docs.php
index 0072f78..0564612 100644
--- a/config/api-docs.php
+++ b/config/api-docs.php
@@ -91,11 +91,10 @@ return [
'status' => [
'type' => 'string',
'description' => '雲端指令代碼。對照表:
-49: reload B017 (貨道同步), 50: reload B005 (基礎參數), 51: reboot (重啟)
-60: reboot card machine (刷卡機重啟), 61: checkout (結帳)
-70: unlock (解鎖), 71: lock (鎖定), 72: sellCode reload B023 (即期品)
-75: exp reload B026 (效期), 79: read B050 (參數讀取)
-81: sync timer status (B710), 85: reload B0552 (出貨腳本)',
+49: reload B017 (貨道同步), 51: reboot (重啟系統)
+60: reboot card machine (刷卡機重啟), 61: checkout (觸發結帳)
+70: unlock (解鎖), 71: lock (鎖定), 85: reload B0552 (遠端出貨)
+待定義: change (遠端找零 - 目前 Java App 尚無對接)',
'example' => '49'
],
],
diff --git a/lang/en.json b/lang/en.json
index 6140afe..7c912da 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -484,6 +484,7 @@
"PARTNER_KEY": "PARTNER_KEY",
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
"PNG, JPG up to 2MB": "PNG, JPG up to 2MB",
+ "PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP up to 10MB",
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
"Page Lock Status": "Page Lock Status",
"Parameters": "Parameters",
@@ -645,6 +646,7 @@
"Selected": "Selected",
"Selected Date": "Search Date",
"Selection": "Selection",
+ "set": "set",
"Serial & Version": "Serial & Version",
"Serial NO": "SERIAL NO",
"Serial No": "Serial No",
diff --git a/lang/ja.json b/lang/ja.json
index 9821926..811f2ae 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -493,6 +493,7 @@
"PARTNER_KEY": "PARTNER_KEY",
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
"PNG, JPG up to 2MB": "2MB以下のPNG、JPG",
+ "PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP 最大10MB",
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
"Page Lock Status": "ページロック状態",
"Parameters": "パラメータ",
@@ -645,6 +646,7 @@
"Selected": "選択中",
"Selected Date": "検索日",
"Selection": "選択",
+ "set": "設定済み",
"Serial & Version": "シリアルとバージョン",
"Serial NO": "シリアル番号",
"Serial No": "シリアル番号",
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index cb68500..472856c 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -539,6 +539,7 @@
"PARTNER_KEY": "PARTNER_KEY",
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
"PNG, JPG up to 2MB": "支援 PNG, JPG (最大 2MB)",
+ "PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
"PS_LEVEL": "PS_LEVEL",
"PS_MERCHANT_ID": "全盈+Pay 商店代號",
"Page Lock Status": "頁面鎖定狀態",
@@ -735,6 +736,7 @@
"Selected": "已選擇",
"Selected Date": "查詢日期",
"Selection": "已選擇",
+ "set": "已設定",
"Serial & Version": "序號與版本",
"Serial NO": "機台序號",
"Serial No": "機台序號",
diff --git a/resources/views/admin/basic-settings/machines/edit.blade.php b/resources/views/admin/basic-settings/machines/edit.blade.php
index 46bd89c..5130957 100644
--- a/resources/views/admin/basic-settings/machines/edit.blade.php
+++ b/resources/views/admin/basic-settings/machines/edit.blade.php
@@ -231,7 +231,7 @@
- {{ __('Optimized for display. Supported formats: JPG, PNG, WebP (Max 10MB).') }} + {{ __('PNG, JPG, WEBP up to 10MB') }}
diff --git a/routes/api.php b/routes/api.php index 8719c43..b96400d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -47,6 +47,12 @@ 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'); + }); + 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']);