diff --git a/.agents/skills/mqtt-communication-specs/SKILL.md b/.agents/skills/mqtt-communication-specs/SKILL.md index 329b7fa..60ade80 100644 --- a/.agents/skills/mqtt-communication-specs/SKILL.md +++ b/.agents/skills/mqtt-communication-specs/SKILL.md @@ -279,6 +279,7 @@ Mqtt3Client client = MqttClient.builder() - `change`: 遠端找零(夾帶 amount 參數)。 - `update_inventory`: 遠端即時修改庫存。 - `dispense`: 遠端出貨指令。 +- `update_ads`: 遠端廣告同步指令。 **`update_inventory` Payload 範例:** ```json diff --git a/app/Http/Controllers/Admin/AdvertisementController.php b/app/Http/Controllers/Admin/AdvertisementController.php index 76e589f..3b7ad67 100644 --- a/app/Http/Controllers/Admin/AdvertisementController.php +++ b/app/Http/Controllers/Admin/AdvertisementController.php @@ -282,4 +282,64 @@ class AdvertisementController extends AdminController 'message' => __('Assignment removed successfully.') ]); } + + /** + * 同步廣告至機台 (透過 MQTT 發送 update_ads 指令) + */ + public function syncToMachine(Request $request, Machine $machine, \App\Services\Machine\MqttService $mqttService) + { + // 併行檢查:若有 pending 指令,超過 1 分鐘視為逾時可覆蓋 + $pendingCommand = \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id) + ->where('command_type', 'update_ads') + ->where('status', 'pending') + ->latest() + ->first(); + + if ($pendingCommand) { + $isTimedOut = $pendingCommand->created_at->lt(now()->subMinute()); + + if (!$isTimedOut) { + return response()->json([ + 'success' => false, + 'message' => __('This machine has a pending command. Please wait.') + ], 422); + } + + // 超過 1 分鐘:視為逾時,自動標記舊指令 + $pendingCommand->update([ + 'status' => 'timeout', + 'note' => __('Superseded by new command (Timeout)'), + ]); + } + + $command = \App\Models\Machine\RemoteCommand::create([ + 'machine_id' => $machine->id, + 'user_id' => auth()->id(), + 'command_type' => 'update_ads', + 'payload' => [], + 'status' => 'pending', + 'remark' => $request->filled('note') ? $request->input('note') : __('Manual Sync Ads'), + ]); + + $success = $mqttService->pushCommand( + $machine->serial_no, + 'update_ads', + [], + (string) $command->id + ); + + if ($success) { + return response()->json([ + 'success' => true, + 'message' => __('Sync command sent successfully.') + ]); + } + + $command->update(['status' => 'failed', 'note' => 'MQTT push failed']); + + return response()->json([ + 'success' => false, + 'message' => __('Failed to send sync command.') + ], 500); + } } diff --git a/config/api-docs.php b/config/api-docs.php index 5e8774e..66f01a4 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -674,6 +674,24 @@ return [ ] ], ], + [ + 'name' => '指令下發:廣告同步 (Update Ads)', + 'slug' => 'mqtt-command-update-ads', + 'action' => 'SUB', + 'topic' => 'machine/{serial_no}/command', + 'qos' => 1, + 'description' => '雲端主動下發「廣告同步」指令。指示機台重新拉取 B005 廣告清單。', + 'payload_parameters' => [ + 'command' => ['type' => 'string', 'description' => '固定為 "update_ads"'], + 'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'], + 'payload' => ['type' => 'object', 'description' => '空物件 (無額外參數)'], + ], + 'payload_example' => [ + 'command' => 'update_ads', + 'command_id' => '1122334456', + 'payload' => new \stdClass() + ], + ], [ 'name' => '指令下發:遠端找零 (Change)', 'slug' => 'mqtt-command-change', diff --git a/lang/en.json b/lang/en.json index 51a1f4c..6774098 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1943,5 +1943,6 @@ "Total Orders": "Total Orders", "Real-time fleet status and revenue monitoring": "Real-time fleet status and revenue monitoring", "[StaffCard] Verification failed: :uid": "[StaffCard] Verification failed: :uid", - "[PickupCode] Verification failed: :code": "[PickupCode] Verification failed: :code" + "[PickupCode] Verification failed: :code": "[PickupCode] Verification failed: :code", + "Sync Ads": "Sync Ads" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index c98001b..cc55e6c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1942,5 +1942,6 @@ "Total Orders": "本日の合計注文", "Real-time fleet status and revenue monitoring": "フリート全体のリアルタイムステータスと収益監視", "[StaffCard] Verification failed: :uid": "[スタッフカード] 認証失敗: :uid", - "[PickupCode] Verification failed: :code": "[受取コード] 認証失敗: :code" + "[PickupCode] Verification failed: :code": "[受取コード] 認証失敗: :code", + "Sync Ads": "広告を同期" } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 163e489..4da5ba6 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1591,6 +1591,7 @@ "This is a system administrator role. Its name is locked to ensure system stability.": "這是系統管理員角色,名稱已鎖定以確保系統穩定性。", "This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。", "This slot has a pending command. Please wait.": "此貨道已有指令正在執行,請稍後。", + "This machine has a pending command. Please wait.": "此機台已有指令正在執行,請稍後。", "This slot has a pending update. Please wait for the previous command to complete.": "此貨道已有更新指令在執行中,請等候前一個指令完成。", "This slot is syncing with the machine. Please wait.": "此貨道正在與機台同步中,請稍候。", "Ticket": "憑證", @@ -1943,5 +1944,10 @@ "Total Orders": "今日訂單", "Real-time fleet status and revenue monitoring": "全機台即時狀態與營收監控", "[StaffCard] Verification failed: :uid": "[員工卡] 驗證失敗: :uid", - "[PickupCode] Verification failed: :code": "[取貨碼] 驗證失敗: :code" + "[PickupCode] Verification failed: :code": "[取貨碼] 驗證失敗: :code", + "Sync to Machine": "同步廣告至機台", + "Manual Sync Ads": "手動同步機台廣告", + "Sync command sent successfully.": "同步指令已成功送出", + "Failed to send sync command.": "無法送出同步指令", + "Sync Ads": "同步廣告" } \ No newline at end of file diff --git a/resources/views/admin/ads/index.blade.php b/resources/views/admin/ads/index.blade.php index 42f3bf6..ede43b7 100644 --- a/resources/views/admin/ads/index.blade.php +++ b/resources/views/admin/ads/index.blade.php @@ -22,7 +22,8 @@ $baseRoute = 'admin.data-config.advertisements'; "getMachineAds": "{{ route($baseRoute . ".machine.get", ":id") }}", "assign": "{{ route($baseRoute . ".assign") }}", "reorder": "{{ route($baseRoute . ".assignments.reorder") }}", - "removeAssignment": "{{ route($baseRoute . ".assignment.remove", ":id") }}" + "removeAssignment": "{{ route($baseRoute . ".assignment.remove", ":id") }}", + "syncToMachine": "{{ route($baseRoute . ".machine.sync", ":id") }}" }'> @@ -369,13 +370,26 @@ $baseRoute = 'admin.data-config.advertisements';
-
+
- +
+
+ +
+
+ +
+
@@ -509,6 +523,76 @@ $baseRoute = 'admin.data-config.advertisements'; @include('admin.ads.partials.ad-modal') @include('admin.ads.partials.assign-modal') + + +
__('Remote Change'), 'Remote Dispense' => __('Remote Dispense'), 'Adjust Stock & Expiry' => __('Adjust Stock & Expiry'), + 'Sync Ads' => __('Sync Ads'), 'Pending' => __('Pending'), 'Sent' => __('Sent'), 'Success' => __('Success'), @@ -441,7 +442,8 @@ 'unlock': this.translations['Lock Page Unlock'], 'change': this.translations['Remote Change'], 'dispense': this.translations['Remote Dispense'], - 'reload_stock': this.translations['Adjust Stock & Expiry'] + 'reload_stock': this.translations['Adjust Stock & Expiry'], + 'update_ads': this.translations['Sync Ads'] }; return names[type] || type; }, @@ -951,7 +953,12 @@
-
+
+ {{ __('Target Machine') }} + +
+
diff --git a/resources/views/admin/remote/partials/tab-history-index.blade.php b/resources/views/admin/remote/partials/tab-history-index.blade.php index 2f0dc22..b81fef1 100644 --- a/resources/views/admin/remote/partials/tab-history-index.blade.php +++ b/resources/views/admin/remote/partials/tab-history-index.blade.php @@ -83,6 +83,7 @@ 'unlock' => __('Lock Page Unlock'), 'change' => __('Remote Change'), 'dispense' => __('Remote Dispense'), + 'update_ads' => __('Sync Ads'), ]" :selected="request('command_type')" :placeholder="__('All Command Types')" :hasSearch="false" @change="searchInTab('history')" /> diff --git a/routes/web.php b/routes/web.php index fab816d..b0bde7a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -173,6 +173,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name( Route::post('/advertisements/assign', [App\Http\Controllers\Admin\AdvertisementController::class, 'assign'])->name('advertisements.assign'); Route::post('/advertisements/assignments/reorder', [App\Http\Controllers\Admin\AdvertisementController::class, 'reorderAssignments'])->name('advertisements.assignments.reorder'); Route::delete('/advertisements/assignment/{id}', [App\Http\Controllers\Admin\AdvertisementController::class, 'removeAssignment'])->name('advertisements.assignment.remove'); + Route::post('/advertisements/machine/{machine}/sync', [App\Http\Controllers\Admin\AdvertisementController::class, 'syncToMachine'])->name('advertisements.machine.sync'); }); Route::get('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class, 'accounts'])->name('sub-accounts')->middleware('can:menu.data-config.sub-accounts');