|
|
<?php
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
use App\Models\CameraAlert;
|
|
|
use App\Models\CameraDevice;
|
|
|
use App\Models\CameraMonitorSetting;
|
|
|
use Illuminate\Console\Command;
|
|
|
use Illuminate\Support\Carbon;
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
|
|
class CameraMonitorSyncCommand extends Command
|
|
|
{
|
|
|
protected $signature = 'camera-monitor:sync';
|
|
|
protected $description = '同步海康摄像头健康状态,仅在需要人工处理时发送邮件';
|
|
|
|
|
|
public function handle(): int
|
|
|
{
|
|
|
$base = rtrim((string) config('services.people_counting.url', env('PEOPLE_COUNTING_URL', '')), '/');
|
|
|
$base = preg_replace('#/api/people-counting$#i', '', $base);
|
|
|
if (! $base) { $this->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]);
|
|
|
}
|
|
|
}
|
|
|
}
|