warn('未配置 PEOPLE_COUNTING_URL'); return self::SUCCESS; } try { $remote = Http::timeout(10)->acceptJson()->get($base.'/api/device-health'); } catch (\Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; } if (! $remote->successful()) { $this->error('海康健康接口返回 '.$remote->status()); return self::FAILURE; } $byKey = collect($remote->json('devices', []))->keyBy(fn ($r) => trim((string) ($r['ip'] ?? '')).'#'.(int) ($r['channel'] ?? 1)); $settings = CameraMonitorSetting::current(); CameraDevice::query()->where('monitor_enabled', true)->with('health')->each(function (CameraDevice $camera) use ($byKey, $settings) { if ($camera->isInMaintenance()) return; $r = $byKey->get(trim($camera->ip).'#'.(int) $camera->channel); if (! $r) return; $health = $camera->health ?: $camera->health()->firstOrCreate([]); $status = (string) ($r['healthStatus'] ?? ($r['pushStatus'] ?? ($r['sdkStatus'] ?? 'UNKNOWN'))); $health->fill(['status' => $status, 'sdk_status' => $r['sdkStatus'] ?? null, 'tcp_reachable' => $r['tcpReachable'] ?? null, 'last_pdc_at' => $r['lastPdcAt'] ?? null, 'pdc_count' => $r['pdcCount'] ?? 0, 'push_age_seconds' => (($r['pushAgeSeconds'] ?? -1) >= 0 ? $r['pushAgeSeconds'] : null), 'reconnect_attempts' => $r['autoReconnectFailures'] ?? 0, 'observed_at' => now()])->save(); if (! ($r['manualActionRequired'] ?? false) || ! $camera->notification_enabled || ! $settings->send_manual_action_email) return; $alert = CameraAlert::query()->where('camera_device_id', $camera->id)->where('alert_type', 'MANUAL_ACTION_REQUIRED')->whereIn('status', ['OPEN','ACKNOWLEDGED'])->latest('id')->first(); if (! $alert) { $alert = CameraAlert::create(['camera_device_id' => $camera->id, 'venue_id' => $camera->venue_id, 'alert_type' => 'MANUAL_ACTION_REQUIRED', 'status' => 'OPEN', 'severity' => 'critical', 'title' => '摄像头需要人工处理', 'reason' => '自动重连多次失败,请检查设备电源、网络或重启摄像头后手动重试。', 'first_occurred_at' => now(), 'last_occurred_at' => now()]); $this->notify($alert, $camera, $settings); } else { $alert->update(['last_occurred_at' => now()]); } }); return self::SUCCESS; } private function notify(CameraAlert $alert, CameraDevice $camera, CameraMonitorSetting $settings): void { $tz = $settings->timezone ?: 'Asia/Shanghai'; $now = Carbon::now($tz); $start = Carbon::parse($now->format('Y-m-d').' '.$settings->notification_start_time, $tz); $end = Carbon::parse($now->format('Y-m-d').' '.$settings->notification_end_time, $tz); if (! $now->between($start, $end, true)) return; foreach (array_values(array_filter($settings->recipient_emails ?? [])) as $to) { try { Mail::raw("场馆:".($camera->venue_name ?: '未绑定')."\n摄像头:{$camera->name}\nIP:{$camera->ip}\n通道:{$camera->channel}\n状态:需要人工处理\n最后推送:".($camera->health?->last_pdc_at?->toDateTimeString() ?: '无')."\n原因:自动重连多次失败,请检查设备电源、网络或重启摄像头后到后台手动重试。", function ($m) use ($to) { $m->to($to)->subject('海康摄像头需要人工处理'); }); } catch (\Throwable $e) { $this->error($to.': '.$e->getMessage()); } $alert->increment('notification_count'); $alert->update(['last_notified_at' => now()]); $alert->notifications()->create(['channel' => 'email', 'receiver' => $to, 'send_status' => 'sent', 'sent_at' => now(), 'attempts' => 1]); } } }