|
|
<?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::connectTimeout(5)->timeout(20)->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), 'last_tcp_error' => $r['networkReason'] ?? null, 'reconnect_attempts' => $r['autoReconnectFailures'] ?? 0, 'observed_at' => now()])->save();
|
|
|
if (($r['sdkStatus'] ?? null) === 'ONLINE' && $camera->venue_id) {
|
|
|
CameraAlert::query()->where('venue_id', $camera->venue_id)->whereIn('status', ['OPEN', 'ACKNOWLEDGED'])->update(['status' => 'RESOLVED', 'recovered_at' => now()]);
|
|
|
}
|
|
|
$abnormal = in_array($status, ['NETWORK_DOWN', 'LOGIN_FAILED', 'ARM_FAILED', 'PUSH_STALE', 'NO_PUSH_YET', 'PUSH_SUSPECT'], true);
|
|
|
if (! $abnormal) return;
|
|
|
$needsManual = (bool) ($r['manualActionRequired'] ?? false);
|
|
|
$alertType = $needsManual ? 'MANUAL_ACTION_REQUIRED' : 'CAMERA_ABNORMAL';
|
|
|
$reason = $r['networkReason'] ?? match ($status) {
|
|
|
'NETWORK_DOWN' => '服务器无法连接摄像头网络或 SDK 端口,请检查设备电源、网络和路由。',
|
|
|
'LOGIN_FAILED' => '网络可连接,但摄像头 SDK 登录失败,请检查账号、密码和设备权限。',
|
|
|
'ARM_FAILED' => '摄像头登录成功,但布防失败,请检查告警通道和布防参数。',
|
|
|
'PUSH_STALE' => '超过设定时间没有收到客流推送。',
|
|
|
'NO_PUSH_YET' => 'Java 服务启动后尚未收到该摄像头的客流推送。',
|
|
|
default => '摄像头客流推送状态异常。',
|
|
|
};
|
|
|
$alert = CameraAlert::query()->where('camera_device_id', $camera->id)->where('alert_type', $alertType)->whereIn('status', ['OPEN','ACKNOWLEDGED'])->latest('id')->first();
|
|
|
if (! $alert) {
|
|
|
$alert = CameraAlert::create(['camera_device_id' => $camera->id, 'venue_id' => $camera->venue_id, 'alert_type' => $alertType, 'status' => 'OPEN', 'severity' => $needsManual ? 'critical' : 'warning', 'title' => $needsManual ? '摄像头需要人工处理' : '摄像头状态异常', 'reason' => $reason, 'first_occurred_at' => now(), 'last_occurred_at' => now()]);
|
|
|
} else { $alert->update(['last_occurred_at' => now(), 'reason' => $reason]); }
|
|
|
$alreadySent = $alert->notifications()->where('channel', 'email')->where('send_status', 'sent')->exists();
|
|
|
if ($needsManual && $camera->notification_enabled && $settings->send_manual_action_email && ! $alreadySent) $this->notify($alert, $camera, $settings);
|
|
|
});
|
|
|
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('海康摄像头需要人工处理'); });
|
|
|
$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]);
|
|
|
} catch (\Throwable $e) {
|
|
|
$this->error($to.': '.$e->getMessage());
|
|
|
$alert->notifications()->create(['channel' => 'email', 'receiver' => $to, 'send_status' => 'failed', 'error_message' => $e->getMessage(), 'attempts' => 1]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|