diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs index 3e6aa59..2458f72 100644 --- a/SHH.CameraSdk/Controllers/MonitorController.cs +++ b/SHH.CameraSdk/Controllers/MonitorController.cs @@ -5,25 +5,24 @@ namespace SHH.CameraSdk; /// /// 视频源实时状态监控 API 控制器 -/// 核心功能:提供相机设备遥测数据查询、单设备详情查询、设备截图获取接口 -/// 适用场景:Web 监控大屏、移动端状态查询、第三方系统集成 +/// 核心功能:提供相机设备遥测数据查询、单设备详情查询、系统日志查询 /// [ApiController] -[Route("api/[controller]")] +[Route("api/monitor")] // [建议] 显式指定路由为小写,确保与前端 ${API}/monitor/... 匹配 public class MonitorController : ControllerBase { #region --- 依赖注入 (Dependency Injection) --- - /// 相机管理器实例:提供设备状态与遥测数据访问能力 private readonly CameraManager _cameraManager; + private readonly IStorageService _storage; // [新增] 存储服务引用 /// - /// 构造函数:通过依赖注入获取 CameraManager 实例 + /// 构造函数:注入 CameraManager 和 IStorageService /// - /// 相机管理器 - public MonitorController(CameraManager cameraManager) + public MonitorController(CameraManager cameraManager, IStorageService storage) { _cameraManager = cameraManager; + _storage = storage; } #endregion @@ -31,14 +30,9 @@ public class MonitorController : ControllerBase #region --- API 接口定义 (API Endpoints) --- /// - /// 获取全量相机实时遥测数据快照(支持跨域) - /// - /// - /// 返回数据包含:设备ID、名称、IP地址、运行状态、在线状态、实时FPS、累计帧数、健康度评分、最后错误信息 + /// 获取全量相机实时遥测数据快照 /// 适用场景:监控大屏首页数据看板 - /// [cite: 191, 194] - /// - /// 200 OK + 遥测数据列表 + /// [HttpGet("dashboard")] public IActionResult GetDashboard() { @@ -49,18 +43,13 @@ public class MonitorController : ControllerBase /// /// 获取指定相机的详细运行指标 /// - /// 相机设备唯一标识 - /// 200 OK + 设备详情 | 404 Not Found [HttpGet("{id}")] public IActionResult GetDeviceDetail(long id) { - // 查询指定设备 var device = _cameraManager.GetDevice(id); - // 设备不存在返回 404 if (device == null) return NotFound($"设备 ID: {id} 不存在"); - // 构造设备详情返回对象 - var deviceDetail = new + return Ok(new { device.Id, device.Status, @@ -69,53 +58,60 @@ public class MonitorController : ControllerBase device.TotalFrames, device.Config.Name, device.Config.IpAddress - }; - - return Ok(deviceDetail); + }); } /// /// 获取指定相机的实时截图 /// - /// 相机设备唯一标识 - /// 200 OK + JPEG 图片流 | 504 Gateway Timeout [HttpGet("snapshot/{id}")] public async Task GetSnapshot(long id) { - // 调用截图协调器获取实时截图,设置 2 秒超时 - // 超时保护:避免 HTTP 线程因设备异常长时间挂起 + // 假设您有 SnapshotCoordinator 单例,此处保留原逻辑 var imageBytes = await SnapshotCoordinator.Instance.RequestSnapshotAsync(id, 2000); - // 截图超时或设备无响应,返回 504 超时状态码 if (imageBytes == null) { return StatusCode(StatusCodes.Status504GatewayTimeout, "截图请求超时或设备未响应"); } - // 返回 JPEG 格式图片流,支持浏览器直接预览 return File(imageBytes, "image/jpeg"); } /// - /// 获取指定相机的诊断信息(含审计日志) + /// 获取指定相机的深度诊断信息(包含持久化的审计日志) /// - /// 相机设备唯一标识 - /// 200 OK + 诊断信息 | 404 Not Found [HttpGet("diagnose/{id}")] - public IActionResult GetDeviceDiagnostic(long id) + public async Task GetDeviceDiagnostic(long id) { var device = _cameraManager.GetDevice(id); - if (device == null) return NotFound(); + if (device == null) return NotFound("设备不存在"); + + // [修正] 改为从 StorageService 读取文件日志 + // 这样即使重启程序,历史日志也能查到 + var logs = await _storage.GetDeviceLogsAsync((int)id, 50); return Ok(new { + // 基础信息 Id = device.Id, Status = device.Status.ToString(), RealFps = device.RealFps, TotalFrames = device.TotalFrames, - // 关键:将 BaseVideoSource 中的日志列表返回给前端 - // 注意:属性名 AuditLogs 会被序列化为 auditLogs (首字母小写),符合前端预期 - AuditLogs = device.GetAuditLogs() + + // 实时状态 + BasicInfo = device.Config, + RealTimeStats = new + { + device.RealFps, + device.TotalFrames, + device.IsPhysicalOnline, + device.Width, + device.Height + }, + + // [关键] 持久化日志 + AuditLogs = logs }); } @@ -125,26 +121,12 @@ public class MonitorController : ControllerBase /// 获取系统操作日志(读取最新的 50 条) /// [HttpGet("system-logs")] - public IActionResult GetSystemLogs() + public async Task GetSystemLogs() { - try - { - var logPath = "user_actions.log"; - if (!System.IO.File.Exists(logPath)) - { - return Ok(new List { "暂无操作记录" }); - } + // [修正] 彻底废弃手动读文件,改用 Service + // Service 内部会自动处理锁、路径 (App_Data/Process_X/system.log) 和异常 + var logs = await _storage.GetSystemLogsAsync(50); - // 读取文件 -> 取最后50行 -> 倒序排列(最新在前) - var logs = System.IO.File.ReadLines(logPath) - .TakeLast(50) - .Reverse() - .ToList(); - return Ok(logs); - } - catch (Exception ex) - { - return StatusCode(500, $"读取日志失败: {ex.Message}"); - } + return Ok(logs); } } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index ac6d87a..b5356a5 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -140,7 +140,7 @@ public class CameraManager : IDisposable, IAsyncDisposable { var device = CreateDeviceInstance(config); // 默认设为运行状态,让协调器稍后去连接 - device.IsRunning = true; + //device.IsRunning = true; _cameraPool.TryAdd(config.Id, device); loadedCount++; } diff --git a/SHH.CameraSdk/Core/Services/FileStorageService.cs b/SHH.CameraSdk/Core/Services/FileStorageService.cs index 3797faf..41dd9af 100644 --- a/SHH.CameraSdk/Core/Services/FileStorageService.cs +++ b/SHH.CameraSdk/Core/Services/FileStorageService.cs @@ -1,105 +1,165 @@ using System.Text.Json; -namespace SHH.CameraSdk +namespace SHH.CameraSdk; + +public class FileStorageService : IStorageService { - public class FileStorageService : IStorageService + public int ProcessId { get; } + private readonly string _baseDir; + private readonly string _devicesPath; + private readonly string _systemLogPath; // 系统日志路径 + private readonly string _logsDir; // 设备日志文件夹 + + // 【关键优化】双锁分离:配置读写和日志读写互不干扰 + private readonly SemaphoreSlim _configLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _logLock = new SemaphoreSlim(1, 1); + + // JSON 配置 (保持不变) + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { - public int ProcessId { get; } - private readonly string _baseDir; - private readonly string _devicesPath; - private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); + WriteIndented = true, + IncludeFields = true, + PropertyNameCaseInsensitive = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; - // [关键修复] 配置序列化选项,解决“只存属性不存字段”的问题 - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + public FileStorageService(int processId) + { + ProcessId = processId; + + // 目录结构: + // App_Data/Process_1/ + // ├── devices.json + // ├── system.log + // └── logs/ + // ├── device_101.log + // └── device_102.log + + _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}"); + _devicesPath = Path.Combine(_baseDir, "devices.json"); + _systemLogPath = Path.Combine(_baseDir, "system.log"); + _logsDir = Path.Combine(_baseDir, "logs"); + + if (!Directory.Exists(_baseDir)) Directory.CreateDirectory(_baseDir); + if (!Directory.Exists(_logsDir)) Directory.CreateDirectory(_logsDir); + + Console.WriteLine($"[Storage] 服务就绪 | 日志路径: {_systemLogPath}"); + } + + // ================================================================== + // 1. 设备配置管理 (使用 _configLock) + // ================================================================== + + public async Task SaveDevicesAsync(IEnumerable configs) + { + await _configLock.WaitAsync(); + try { - WriteIndented = true, // 格式化 JSON,让人眼可读 - IncludeFields = true, // [核心] 允许序列化 public int Id; 这种字段 - PropertyNameCaseInsensitive = true, // 忽略大小写差异 - NumberHandling = JsonNumberHandling.AllowReadingFromString // 允许 "8000" 读为 int 8000 - }; - - public FileStorageService(int processId) - { - ProcessId = processId; - - _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}"); - _devicesPath = Path.Combine(_baseDir, "devices.json"); - - if (!Directory.Exists(_baseDir)) Directory.CreateDirectory(_baseDir); - - Console.WriteLine($"[Storage] 路径: {_devicesPath}"); + var json = JsonSerializer.Serialize(configs, _jsonOptions); + await File.WriteAllTextAsync(_devicesPath, json); } - - public async Task SaveDevicesAsync(IEnumerable configs) + catch (Exception ex) { - await _fileLock.WaitAsync(); - try - { - // [调试] 打印正在保存的数量,确保 Manager 传过来的数据是对的 - // Console.WriteLine($"[Debug] 正在保存 {configs.Count()} 台设备..."); - - var json = JsonSerializer.Serialize(configs, _jsonOptions); - await File.WriteAllTextAsync(_devicesPath, json); - - // [调试] 打印部分 JSON 内容,验证是否为空对象 "{}" - // if (json.Length < 200) Console.WriteLine($"[Debug] JSON 内容: {json}"); - } - catch (Exception ex) - { - Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}"); - } - finally - { - _fileLock.Release(); - } + Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}"); } + finally { _configLock.Release(); } + } - public async Task> LoadDevicesAsync() + public async Task> LoadDevicesAsync() + { + if (!File.Exists(_devicesPath)) return new List(); + + await _configLock.WaitAsync(); + try { - if (!File.Exists(_devicesPath)) - { - Console.WriteLine("[Storage] ⚠️ 配置文件不存在,将使用空列表"); - return new List(); - } + var json = await File.ReadAllTextAsync(_devicesPath); + if (string.IsNullOrWhiteSpace(json)) return new List(); - await _fileLock.WaitAsync(); - try - { - var json = await File.ReadAllTextAsync(_devicesPath); - - if (string.IsNullOrWhiteSpace(json)) return new List(); - - // [调试] 打印读取到的原始 JSON - // Console.WriteLine($"[Debug] 读取文件内容: {json.Substring(0, Math.Min(json.Length, 100))}..."); - - var list = JsonSerializer.Deserialize>(json, _jsonOptions); - - // 二次校验:如果读出来列表不为空,但 ID 全是 0,说明序列化还是没对上 - if (list != null && list.Count > 0 && list[0].Id == 0 && list[0].Port == 0) - { - Console.WriteLine("[Storage] ⚠️ 警告:读取到设备,但字段似乎为空。请检查 VideoSourceConfig 是否使用了 private 属性?"); - } - - return list ?? new List(); - } - catch (Exception ex) - { - Console.WriteLine($"[Storage] ❌ 读取配置失败: {ex.Message}"); - // 出错时返回空列表,不要抛出异常,否则 StartAsync 会崩溃 - return new List(); - } - finally - { - _fileLock.Release(); - } + var list = JsonSerializer.Deserialize>(json, _jsonOptions); + return list ?? new List(); } + catch (Exception ex) + { + Console.WriteLine($"[Storage] ❌ 读取配置失败: {ex.Message}"); + return new List(); + } + finally { _configLock.Release(); } + } - // ================================================================== - // 日志部分 (保持空实现以免干扰) - // ================================================================== - public Task AppendSystemLogAsync(string action, string ip, string path) => Task.CompletedTask; - public Task> GetSystemLogsAsync(int count) => Task.FromResult(new List()); - public Task AppendDeviceLogAsync(int deviceId, string message) => Task.CompletedTask; - public Task> GetDeviceLogsAsync(int deviceId, int count) => Task.FromResult(new List()); + // ================================================================== + // 2. 系统操作日志 (使用 _logLock) + // ================================================================== + + public async Task AppendSystemLogAsync(string action, string ip, string path) + { + // 格式: [时间] | IP | 动作 路径 + var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + var line = $"[{time}] | {ip} | {action} {path}"; + + await _logLock.WaitAsync(); // 等待日志锁 + try + { + // 追加写入 (Async) + await File.AppendAllTextAsync(_systemLogPath, line + Environment.NewLine); + } + catch { /* 忽略日志写入错误,别崩了主程序 */ } + finally { _logLock.Release(); } + } + + public async Task> GetSystemLogsAsync(int count) + { + if (!File.Exists(_systemLogPath)) return new List { "暂无日志" }; + + await _logLock.WaitAsync(); + try + { + // 读取所有行 (如果日志文件非常大,这里建议优化为倒序读取,但几MB以内没问题) + var lines = await File.ReadAllLinesAsync(_systemLogPath); + + // 取最后 N 行,并反转(让最新的显示在最上面) + return lines.TakeLast(count).Reverse().ToList(); + } + catch (Exception ex) + { + return new List { $"读取失败: {ex.Message}" }; + } + finally { _logLock.Release(); } + } + + // ================================================================== + // 3. 设备审计日志 (使用 _logLock) + // ================================================================== + + public async Task AppendDeviceLogAsync(int deviceId, string message) + { + var path = Path.Combine(_logsDir, $"device_{deviceId}.log"); + var time = DateTime.Now.ToString("MM-dd HH:mm:ss"); + var line = $"{time} > {message}"; + + await _logLock.WaitAsync(); // 复用日志锁,防止多文件同时IO导致磁盘抖动 + try + { + await File.AppendAllTextAsync(path, line + Environment.NewLine); + } + catch { } + finally { _logLock.Release(); } + } + + public async Task> GetDeviceLogsAsync(int deviceId, int count) + { + var path = Path.Combine(_logsDir, $"device_{deviceId}.log"); + if (!File.Exists(path)) return new List(); + + await _logLock.WaitAsync(); + try + { + var lines = await File.ReadAllLinesAsync(path); + return lines.TakeLast(count).Reverse().ToList(); + } + catch + { + return new List(); + } + finally { _logLock.Release(); } } } \ No newline at end of file