serialNo = $serialNo; $this->payload = $payload; } /** * 處理 B055 出貨回報 ACK * 原始邏輯移植自 MachineController::reportDispenseResult() */ public function handle(): void { $commandId = $this->payload['command_id'] ?? null; $resultCode = $this->payload['result'] ?? null; $stockReported = $this->payload['stock'] ?? null; if (empty($commandId)) { Log::warning('MQTT CommandAck: Missing command_id', [ 'serial_no' => $this->serialNo, 'payload' => $this->payload ]); return; } $command = RemoteCommand::find($commandId); if (!$command) { Log::warning('MQTT CommandAck: Command not found', [ 'serial_no' => $this->serialNo, 'command_id' => $commandId ]); return; } $machine = $command->machine; // 【優先執行】更新機台為在線狀態 (只要收到訊息就代表在線,不受指令狀態影響) if ($machine) { $machine->update([ 'last_heartbeat_at' => now(), 'status' => 'online', ]); } // 核心邏輯:只有處於 pending 狀態的指令才處理後續的業務邏輯 (如庫存異動) if ($command->status !== 'pending') { Log::info('MQTT CommandAck: Command already processed, ignoring business logic', [ 'command_id' => $commandId, 'current_status' => $command->status, ]); return; } // 判定執行結果 (result: "success" 或 "0" 代表成功) $status = in_array($resultCode, ['success', '0'], true) ? 'success' : 'failed'; $payloadUpdates = [ 'reported_stock' => $stockReported, 'report_result' => $resultCode, ]; $machine = $command->machine; if ($command->command_type === 'dispense') { $slotNo = $command->payload['slot_no'] ?? null; $slot = $machine->slots()->where('slot_no', $slotNo)->first(); if ($slot) { if ($status === 'success') { // 若出貨成功,且 APP 回報了最新的庫存值,則以 APP 為準進行校準 // 否則維持預扣後的狀態 (不需要額外動作,因為下發時已扣除) if ($stockReported !== null) { $slot->update(['stock' => (int)$stockReported]); $payloadUpdates['new_stock'] = (int)$stockReported; } ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Remote dispense successful for slot :slot", 'info', ['slot' => $slotNo] ); } else { // 若出貨失敗,執行回滾 (Rollback):將預扣的庫存加回去 $oldStock = $command->payload['old_stock'] ?? $slot->stock; $slot->update(['stock' => $oldStock]); $payloadUpdates['rolled_back'] = true; $payloadUpdates['new_stock'] = $oldStock; Log::warning('MQTT Dispense failed, rolled back inventory', [ 'serial_no' => $this->serialNo, 'slot_no' => $slotNo, 'rolled_back_to' => $oldStock ]); } } } elseif ($command->command_type === 'reload_stock') { // 若同步失敗,自動 Rollback 回滾庫存 if ($status === 'failed' && isset($command->payload['slot_no'], $command->payload['old'])) { $slotNo = $command->payload['slot_no']; $slot = $machine->slots()->where('slot_no', $slotNo)->first(); if ($slot) { $oldData = $command->payload['old']; $slot->update([ 'stock' => $oldData['stock'], 'expiry_date' => $oldData['expiry_date'] ?? null, 'batch_no' => $oldData['batch_no'] ?? null, ]); Log::warning('MQTT CommandAck failed, rolled back inventory', [ 'serial_no' => $this->serialNo, 'slot_no' => $slotNo, 'rolled_back_to' => $oldData ]); } } } $command->update([ 'status' => $status, 'executed_at' => now(), 'payload' => array_merge($command->payload, $payloadUpdates), ]); Log::info("MQTT CommandAck processed", [ 'serial_no' => $this->serialNo, 'command_id' => $commandId, 'status' => $status, ]); } }