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