From 3d4eb34ca9b15923d1f5862fee03c8d96eddf4e3 Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 22:24:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=91=84=E5=83=8F=E5=A4=B4?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=A2=9E=E3=80=81=E6=94=B9=E3=80=81=E5=88=A0?= =?UTF-8?q?=E7=9A=84=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=AD=98=E5=82=A8=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SHH.CameraSdk/Abstractions/IStorageService.cs | 26 +- .../Abstractions/Models/VideoSourceConfig.cs | 1 + SHH.CameraSdk/Core/Manager/CameraManager.cs | 323 +++++++++--------- .../Core/Services/FileStorageService.cs | 128 +++++-- SHH.CameraSdk/Program.cs | 62 ++-- 5 files changed, 306 insertions(+), 234 deletions(-) diff --git a/SHH.CameraSdk/Abstractions/IStorageService.cs b/SHH.CameraSdk/Abstractions/IStorageService.cs index ae2a3af..d0dcf3f 100644 --- a/SHH.CameraSdk/Abstractions/IStorageService.cs +++ b/SHH.CameraSdk/Abstractions/IStorageService.cs @@ -2,17 +2,27 @@ public interface IStorageService { - // 1. 基础属性:让外界知道当前是几号进程 + // 1. 基础属性 int ProcessId { get; } - // 2. 设备配置相关的空架子 - Task SaveDevicesAsync(object configs); // 这里先用 object 占位,或者用您的 List - - Task LoadDevicesAsync(); + // 2. 设备配置管理 + // 保存:接收 VideoSourceConfig 集合 + Task SaveDevicesAsync(IEnumerable configs); - // 3. 系统日志相关的空架子 + // 加载:返回 VideoSourceConfig 列表 + Task> LoadDevicesAsync(); + + // 3. 系统日志 + // 记录系统操作 (如 POST /api/cameras) Task AppendSystemLogAsync(string action, string ip, string path); - // 4. 设备审计日志相关的空架子 - Task AppendDeviceLogAsync(long deviceId, string message); + // 获取系统日志 + Task> GetSystemLogsAsync(int count); + + // 4. 设备审计日志 + // 记录单设备日志 (统一使用 int deviceId) + Task AppendDeviceLogAsync(int deviceId, string message); + + // 获取单设备日志 + Task> GetDeviceLogsAsync(int deviceId, int count); } \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs index 04bee6c..68bd92f 100644 --- a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs +++ b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs @@ -35,6 +35,7 @@ public class VideoSourceConfig public string Password { get; set; } = string.Empty; /// 渲染句柄(可选):用于硬解码时直接绑定显示窗口,提升渲染性能 + [JsonIgnore] public IntPtr RenderHandle { get; set; } = IntPtr.Zero; /// 物理通道号(IPC 通常为 1;NVR 对应接入的摄像头通道索引) diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index ad572fe..ac6d87a 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -1,11 +1,8 @@ namespace SHH.CameraSdk; /// -/// [管理层] 视频源总控管理器 (V3.3.1 修复版) +/// [管理层] 视频源总控管理器 (V3.5 持久化集成版) /// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈 -/// 核心修复: -/// 1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问 -/// 2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性 /// public class CameraManager : IDisposable, IAsyncDisposable { @@ -24,11 +21,24 @@ public class CameraManager : IDisposable, IAsyncDisposable private volatile bool _isDisposed; /// - /// [Fix Bug A: 动态失效] 协调器引擎运行状态标记 - /// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致 + /// 协调器引擎运行状态标记 + /// 使用 volatile 关键字确保多线程环境下的内存可见性 /// private volatile bool _isEngineStarted = false; + // [新增] 存储服务引用 + private readonly IStorageService _storage; + + #endregion + + #region --- 构造函数 (Constructor) --- + + // [修改] 注入 IStorageService + public CameraManager(IStorageService storage) + { + _storage = storage; + } + #endregion #region --- 2. 设备管理 (Device Management) --- @@ -39,29 +49,8 @@ public class CameraManager : IDisposable, IAsyncDisposable /// 相机设备配置信息 public void AddDevice(VideoSourceConfig config) { - //// [安全防护] 销毁过程中禁止添加新设备 - //if (_isDisposed) return; - //// 防止重复添加同一设备 - //if (_cameraPool.ContainsKey(config.Id)) return; - - //// 1. 根据设备品牌实例化对应的驱动实现类 - //BaseVideoSource source = config.Brand switch - //{ - // DeviceBrand.HikVision => new HikVideoSource(config), - // _ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}") - //}; - - //// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 - //if (_isEngineStarted) - //{ - // source.IsRunning = true; - //} - - //// 3. 将设备注册到内存池与协调器,纳入统一管理 - //if (_cameraPool.TryAdd(config.Id, source)) - //{ - // _coordinator.Register(source); - //} + // [安全防护] 销毁过程中禁止添加新设备 + if (_isDisposed) return; // 使用工厂方法创建 var device = CreateDeviceInstance(config); @@ -69,93 +58,130 @@ public class CameraManager : IDisposable, IAsyncDisposable if (!_cameraPool.TryAdd(config.Id, device)) { // 如果添加失败(ID冲突),由于 device 还没被使用,直接释放掉 - // 这里不需要 await,因为刚 new 出来的对象还没 connect device.DisposeAsync().AsTask().Wait(); throw new InvalidOperationException($"设备 ID {config.Id} 已存在"); } + + // 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 + if (_isEngineStarted) + { + device.IsRunning = true; + } + + // [新增] 自动保存到文件 + SaveChanges(); } /// /// 根据设备ID获取指定的视频源实例 /// - /// 设备唯一标识 - /// 视频源实例 / 不存在则返回 null public BaseVideoSource? GetDevice(long id) => _cameraPool.TryGetValue(id, out var source) ? source : null; /// /// 获取当前管理的所有相机设备 /// - /// 设备实例集合 public IEnumerable GetAllDevices() { return _cameraPool.Values.ToList(); } /// - /// 从管理池中移除指定设备并释放资源 + /// [修改] 异步移除设备 (从 RemoveDevice 改为 RemoveDeviceAsync) /// - /// 设备唯一标识 - public void RemoveDevice(long id) + public async Task RemoveDeviceAsync(long id) { if (_cameraPool.TryRemove(id, out var device)) { // 记录日志 - System.Console.WriteLine($"[Manager] 正在移除设备 {id}..."); + Console.WriteLine($"[Manager] 正在移除设备 {id}..."); - // 1. 停止物理连接 (异步转同步等待,防止资源未释放) - // 在实际高并发场景建议改为 RemoveDeviceAsync - device.StopAsync().GetAwaiter().GetResult(); + // 1. 停止物理连接 + await device.StopAsync(); - // 2. 释放资源 (销毁非托管句柄) - device.Dispose(); + // 2. 释放资源 + await device.DisposeAsync(); - System.Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); + Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); + + // [新增] 自动保存到文件 + SaveChanges(); } } + // 为了兼容旧代码保留同步方法,但不推荐使用 + public void RemoveDevice(long id) => RemoveDeviceAsync(id).Wait(); + #endregion #region --- 3. 生命周期控制 (Engine Lifecycle) --- /// - /// 启动视频管理引擎,初始化SDK并启动协调器自愈循环 + /// 启动视频管理引擎,加载配置,初始化SDK并启动协调器自愈循环 /// public async Task StartAsync() { - // 防护:已销毁则抛出异常 - if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager)); - // 防护:避免重复启动 + if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager)); if (_isEngineStarted) return; - // 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境 + // ========================================================= + // 1. [新增] 从文件加载设备配置 + // ========================================================= + try + { + Console.WriteLine("[Manager] 正在检查本地配置文件..."); + var savedConfigs = await _storage.LoadDevicesAsync(); + + int loadedCount = 0; + foreach (var config in savedConfigs) + { + // 防止ID冲突(虽然文件里理论上不重复) + if (!_cameraPool.ContainsKey(config.Id)) + { + var device = CreateDeviceInstance(config); + // 默认设为运行状态,让协调器稍后去连接 + device.IsRunning = true; + _cameraPool.TryAdd(config.Id, device); + loadedCount++; + } + } + if (loadedCount > 0) + Console.WriteLine($"[Manager] 已从文件恢复 {loadedCount} 台设备配置"); + } + catch (Exception ex) + { + Console.WriteLine($"[Manager] 加载配置文件警告: {ex.Message}"); + } + + // ========================================================= + // 2. 全局驱动环境预初始化 + // ========================================================= HikSdkManager.Initialize(); - // 不要运行,手动运行 - //// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程 - //foreach (var source in _cameraPool.Values) - //{ - // source.IsRunning = true; - //} - - // 标记引擎启动状态,后续新增设备自动激活 + // 标记引擎启动状态 _isEngineStarted = true; - // 3. 启动协调器后台自愈循环(标记为 LongRunning 提升调度优先级) + // ========================================================= + // 3. 启动协调器后台自愈循环 + // ========================================================= _ = Task.Factory.StartNew( () => _coordinator.RunCoordinationLoopAsync(_globalCts.Token), _globalCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - System.Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); + // 这里传递 _cameraPool 给协调器(如果协调器是独立引用的,可能需要 Register 逻辑, + // 但根据您的代码,协调器似乎是依赖外部注册或共享引用的。 + // *注意*:如果 Coordinator 需要显式注册,请在这里补上: + foreach (var dev in _cameraPool.Values) _coordinator.Register(dev); + + Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); await Task.CompletedTask; } /// /// 获取当前所有相机的全局状态简报 /// - /// 包含设备ID、IP、运行状态的元组集合 public IEnumerable<(long Id, string Ip, VideoSourceStatus Status)> GetGlobalStatus() { return _cameraPool.Values.Select(v => (v.Id, v.Config.IpAddress, v.Status)); @@ -168,7 +194,6 @@ public class CameraManager : IDisposable, IAsyncDisposable /// /// 获取所有相机的健康度报告 /// - /// 相机健康度报告集合 public IEnumerable GetDetailedTelemetry() { return _cameraPool.Values.Select(cam => new CameraHealthReport @@ -177,28 +202,24 @@ public class CameraManager : IDisposable, IAsyncDisposable Ip = cam.Config.IpAddress, Status = cam.Status.ToString(), LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常" - // 扩展:可补充 RealFps/DropFrames/ReconnectCount 等指标 }); } /// - /// [新增] 获取全量相机实时遥测数据快照 - /// 用于 WebAPI 实时监控大屏展示 + /// 获取全量相机实时遥测数据快照 (MonitorController 使用) /// - /// 相机遥测数据快照集合 public IEnumerable GetTelemetrySnapshot() { - // 立即物化列表,防止枚举过程中集合被修改导致异常 return _cameraPool.Values.Select(cam => { - // 健康度评分算法(示例):基于设备状态与实时帧率综合判定 + // 健康度评分算法 int healthScore = 100; if (cam.Status == VideoSourceStatus.Faulted) healthScore = 0; else if (cam.Status == VideoSourceStatus.Reconnecting) healthScore = 60; else if (cam.RealFps < 1.0 && cam.Status == VideoSourceStatus.Playing) - healthScore = 40; // 有连接状态但无有效流 + healthScore = 40; return new CameraTelemetryInfo { @@ -208,12 +229,11 @@ public class CameraManager : IDisposable, IAsyncDisposable Status = cam.Status.ToString(), IsOnline = cam.IsPhysicalOnline, Fps = cam.RealFps, - Bitrate = cam.RealBitrate, // [新增] 映射基类属性 + Bitrate = cam.RealBitrate, TotalFrames = cam.TotalFrames, HealthScore = healthScore, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, - Timestamp = System.DateTime.Now, - // [新增] 映射分辨率 + Timestamp = DateTime.Now, Width = cam.Width, Height = cam.Height, }; @@ -227,9 +247,6 @@ public class CameraManager : IDisposable, IAsyncDisposable /// /// 智能更新设备配置 (含冷热分离逻辑) /// - /// 设备唯一标识 - /// 配置更新传输对象 - /// 设备不存在时抛出 public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto) { if (!_cameraPool.TryGetValue(deviceId, out var device)) @@ -242,7 +259,7 @@ public class CameraManager : IDisposable, IAsyncDisposable var oldConfig = device.Config; var newConfig = oldConfig.DeepCopy(); - // 3. 映射 DTO 值 (仅当不为空时修改) + // 3. 映射 DTO 值 if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress; if (dto.Port != null) newConfig.Port = dto.Port.Value; if (dto.Username != null) newConfig.Username = dto.Username; @@ -250,10 +267,10 @@ public class CameraManager : IDisposable, IAsyncDisposable if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value; if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value; if (dto.Name != null) newConfig.Name = dto.Name; - if (dto.RenderHandle != null) newConfig.RenderHandle = (System.IntPtr)dto.RenderHandle.Value; + if (dto.RenderHandle != null) newConfig.RenderHandle = (IntPtr)dto.RenderHandle.Value; + if (dto.Brand != null) newConfig.Brand = (DeviceBrand)dto.Brand; // 4. 判定冷热更新 - // 核心参数变更 -> 冷重启 bool needColdRestart = newConfig.IpAddress != oldConfig.IpAddress || newConfig.Port != oldConfig.Port || @@ -265,8 +282,6 @@ public class CameraManager : IDisposable, IAsyncDisposable if (needColdRestart) { device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)"); - - // 记录之前的运行状态 bool wasRunning = device.IsRunning; // A. 彻底停止 @@ -275,7 +290,7 @@ public class CameraManager : IDisposable, IAsyncDisposable // B. 写入新配置 device.UpdateConfig(newConfig); - // C. 如果之前是运行意图,则自动重启连接 + // C. 自动重启 if (wasRunning) await device.StartAsync(); } else @@ -285,146 +300,128 @@ public class CameraManager : IDisposable, IAsyncDisposable // A. 更新配置数据 device.UpdateConfig(newConfig); - // B. 在线应用策略 (无需断线) + // B. 在线应用策略 if (device.IsOnline) { var options = new DynamicStreamOptions { StreamType = dto.StreamType, - RenderHandle = dto.RenderHandle.HasValue ? (System.IntPtr)dto.RenderHandle : null + RenderHandle = dto.RenderHandle.HasValue ? (IntPtr)dto.RenderHandle : null }; - // 触发驱动层的 OnApplyOptions device.ApplyOptions(options); } } + + // [新增] 保存文件 + SaveChanges(); + } + + /// + /// 全量替换更新 (兼容接口) + /// + public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig) + { + if (!_cameraPool.TryGetValue(id, out var oldDevice)) + throw new KeyNotFoundException($"设备 #{id} 不存在"); + + bool wasRunning = oldDevice.IsRunning || + oldDevice.Status == VideoSourceStatus.Playing || + oldDevice.Status == VideoSourceStatus.Connecting; + + Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中..."); + + try + { + await oldDevice.StopAsync(); + await oldDevice.DisposeAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}"); + } + + var newDevice = CreateDeviceInstance(newConfig); + _cameraPool[id] = newDevice; + + Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。"); + + if (wasRunning) + { + await newDevice.StartAsync(); + } + + // [新增] 保存文件 + SaveChanges(); } #endregion #region --- 6. 资源清理 (Disposal) --- - /// - /// 同步销毁:内部调用异步销毁逻辑,等待销毁完成 - /// public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); - /// - /// [修复 Bug L & Bug γ] 异步执行全局资源清理 - /// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK,防止非托管内存泄漏 - /// public async ValueTask DisposeAsync() { - // 防护:避免重复销毁 if (_isDisposed) return; - // 标记为已销毁,禁止后续操作 _isDisposed = true; _isEngineStarted = false; try { - // 1. 发送全局取消信号,立即停止协调器所有后台扫描任务 _globalCts.Cancel(); - // 2. [Fix Bug L] 锁定设备池快照并清空,防止并发修改导致异常 var devices = _cameraPool.Values.ToArray(); _cameraPool.Clear(); - // 3. 并行销毁所有相机设备,释放设备持有的非托管资源 var disposeTasks = devices.Select(async device => { try { await device.DisposeAsync(); } - catch { /* 隔离单个设备销毁异常,不影响其他设备 */ } + catch { } }); await Task.WhenAll(disposeTasks); - // 4. [Fix Bug γ: 二次伤害] 彻底卸载全局 SDK 环境 - // 加 try-catch 防护极端场景(如进程强制终止时 SDK 已被系统回收) try { HikSdkManager.Uninitialize(); } - catch - { - // 忽略卸载异常,保证销毁流程正常结束 - } + catch { } } finally { - // 释放取消令牌源资源 _globalCts.Dispose(); } } #endregion - /// - /// 更新设备配置(热重载) - /// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启 - /// - public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig) - { - // 1. 检查设备是否存在 - if (!_cameraPool.TryGetValue(id, out var oldDevice)) - { - throw new KeyNotFoundException($"设备 #{id} 不存在"); - } + #region --- 7. 内部辅助 (Helpers) --- - // 2. 捕获旧状态(用于决定是否需要自动重启新设备) - // 如果旧设备正在运行或尝试连接中,我们在更新后应该尝试恢复它 - bool wasRunning = oldDevice.IsRunning || - oldDevice.Status == VideoSourceStatus.Playing || - oldDevice.Status == VideoSourceStatus.Connecting; - - // 获取旧的流控需求(如果希望更新配置后,之前的订阅依然生效,需要把需求搬过去) - // 这里简化处理:更新配置通常意味着环境变了,我们选择清空旧订阅,让前端重新下发,或者你可以手动拷贝 - // var oldRequirements = oldDevice.Controller.GetCurrentRequirements(); - - Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中..."); - - // 3. 【关键步骤】优雅停止并销毁旧实例 - // 必须先 Stop 再 Dispose,确保 SDK 句柄(如 lUserId, lRealPlayHandle)被释放 - try - { - await oldDevice.StopAsync(); // 停止取流 - await oldDevice.DisposeAsync(); // 注销登录、释放非托管资源 - } - catch (Exception ex) - { - Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}"); - // 继续执行,不要因为旧设备销毁失败阻断新配置的应用 - } - - // 4. 使用新配置创建新实例 - // 建议将创建逻辑提取为私有方法 CreateDeviceInstance,避免与 AddDevice 代码重复 - var newDevice = CreateDeviceInstance(newConfig); - - // 5. 原子替换 (ConcurrentDictionary 的索引器赋值是线程安全的) - _cameraPool[id] = newDevice; - - Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。"); - - // 6. 状态恢复 - if (wasRunning) - { - Console.WriteLine($"[Manager] 检测到设备 #{id} 之前为运行状态,正在自动重启..."); - // 不等待 StartAsync 完成,避免阻塞 HTTP 请求太久 - // 如果希望前端看到转圈直到启动完成,则加上 await - await newDevice.StartAsync(); - } - } - - /// - /// [辅助工厂方法] 根据配置创建具体的驱动实例 - /// 请确保你的 AddDevice 方法也改为调用此方法,减少重复代码 - /// private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config) { return config.Brand switch { DeviceBrand.HikVision => new HikVideoSource(config), - // 如果你有大华或其他品牌,在这里扩展 - // DeviceBrand.Dahua => new DahuaVideoSource(config), _ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}") }; } + + /// + /// [新增] 触发异步保存 (Fire-and-Forget) + /// 不阻塞当前 API 线程,让后台存储服务去排队写入 + /// + private void SaveChanges() + { + try + { + var allConfigs = _cameraPool.Values.Select(d => d.Config).ToList(); + // 异步调用存储服务,不使用 await 以免阻塞 API 响应 + _ = _storage.SaveDevicesAsync(allConfigs); + } + catch (Exception ex) + { + Console.WriteLine($"[Manager] 触发保存失败: {ex.Message}"); + } + } + + #endregion } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Services/FileStorageService.cs b/SHH.CameraSdk/Core/Services/FileStorageService.cs index 3807dfc..3797faf 100644 --- a/SHH.CameraSdk/Core/Services/FileStorageService.cs +++ b/SHH.CameraSdk/Core/Services/FileStorageService.cs @@ -1,51 +1,105 @@ -namespace SHH.CameraSdk; +using System.Text.Json; -public class FileStorageService : IStorageService +namespace SHH.CameraSdk { - public int ProcessId { get; } - private readonly string _basePath; // 专属数据目录 - - public FileStorageService(int processId) + public class FileStorageService : IStorageService { - ProcessId = processId; + public int ProcessId { get; } + private readonly string _baseDir; + private readonly string _devicesPath; + private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); - // 核心逻辑:数据隔离 - // 1号进程 -> App_Data/Process_1/ - // 2号进程 -> App_Data/Process_2/ - _basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}"); - - // 既然是框架搭建,我们要确保这个目录存在,否则后面谁写谁报错 - if (!Directory.Exists(_basePath)) + // [关键修复] 配置序列化选项,解决“只存属性不存字段”的问题 + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { - Directory.CreateDirectory(_basePath); + 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}"); } - Console.WriteLine($"[Storage] 存储服务已就绪。数据隔离路径: {_basePath}"); - } + public async Task SaveDevicesAsync(IEnumerable configs) + { + await _fileLock.WaitAsync(); + try + { + // [调试] 打印正在保存的数量,确保 Manager 传过来的数据是对的 + // Console.WriteLine($"[Debug] 正在保存 {configs.Count()} 台设备..."); - // --- 下面是未实现的空架子 --- + var json = JsonSerializer.Serialize(configs, _jsonOptions); + await File.WriteAllTextAsync(_devicesPath, json); - public Task SaveDevicesAsync(object configs) - { - // TODO: 待实现序列化写入 - return Task.CompletedTask; - } + // [调试] 打印部分 JSON 内容,验证是否为空对象 "{}" + // if (json.Length < 200) Console.WriteLine($"[Debug] JSON 内容: {json}"); + } + catch (Exception ex) + { + Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}"); + } + finally + { + _fileLock.Release(); + } + } - public Task LoadDevicesAsync() - { - // TODO: 待实现读取 - return Task.FromResult(null); - } + public async Task> LoadDevicesAsync() + { + if (!File.Exists(_devicesPath)) + { + Console.WriteLine("[Storage] ⚠️ 配置文件不存在,将使用空列表"); + return new List(); + } - public Task AppendSystemLogAsync(string action, string ip, string path) - { - // TODO: 待实现系统日志写入 - return Task.CompletedTask; - } + await _fileLock.WaitAsync(); + try + { + var json = await File.ReadAllTextAsync(_devicesPath); - public Task AppendDeviceLogAsync(long deviceId, string message) - { - // TODO: 待实现设备日志写入 - return Task.CompletedTask; + 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(); + } + } + + // ================================================================== + // 日志部分 (保持空实现以免干扰) + // ================================================================== + 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()); } } \ No newline at end of file diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs index ccbeb14..2a51231 100644 --- a/SHH.CameraSdk/Program.cs +++ b/SHH.CameraSdk/Program.cs @@ -44,7 +44,7 @@ public class Program // 核心设备管理器 // 注意:暂时保持无参构造,后续我们在改造 CameraManager 时再注入 storageService - using var cameraManager = new CameraManager(); + using var cameraManager = new CameraManager(storageService); // 动态窗口管理器 var displayManager = new DisplayWindowManager(); @@ -60,13 +60,17 @@ public class Program // ============================================================================== // 4. 业务编排 // ============================================================================== + + // 【关键修复 1】先 StartAsync,让它先从文件把 999 号设备读进内存 + await cameraManager.StartAsync(); + + // 【关键修复 2】文件加载完后,再决定要不要加默认设备 await ConfigureBusinessLogic(cameraManager); // ============================================================================== // 5. 启动引擎与交互 // ============================================================================== Console.WriteLine("\n[系统] 正在启动全局管理引擎..."); - await cameraManager.StartAsync(); Console.WriteLine($">> 系统就绪。Web 管理地址: http://localhost:{port}"); Console.WriteLine($">> 数据存储路径: App_Data/Process_{processId}/"); @@ -112,9 +116,9 @@ public class Program }); }); - // 2. 日志降噪 - builder.Logging.SetMinimumLevel(LogLevel.Warning); - builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); + //// 2. 日志降噪 + //builder.Logging.SetMinimumLevel(LogLevel.Warning); + //builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); // 3. 【核心】依赖注入注册 // 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了 @@ -157,29 +161,35 @@ public class Program static async Task ConfigureBusinessLogic(CameraManager manager) { - // 1. 添加测试设备 - var config = new VideoSourceConfig + try { - Id = 101, - Brand = DeviceBrand.HikVision, - IpAddress = "192.168.5.9", - Port = 8000, - Username = "admin", - Password = "RRYFOA", - StreamType = 0 - }; - manager.AddDevice(config); + // 1. 添加测试设备 + var config = new VideoSourceConfig + { + Id = 101, + Brand = DeviceBrand.HikVision, + IpAddress = "192.168.5.9", + Port = 8000, + Username = "admin", + Password = "RRYFOA", + StreamType = 0 + }; + manager.AddDevice(config); - var config2 = new VideoSourceConfig + var config2 = new VideoSourceConfig + { + Id = 102, + Brand = DeviceBrand.HikVision, + IpAddress = "172.16.41.20", + Port = 8000, + Username = "admin", + Password = "abcd1234", + StreamType = 0 + }; + manager.AddDevice(config2); + } + catch { - Id = 102, - Brand = DeviceBrand.HikVision, - IpAddress = "172.16.41.20", - Port = 8000, - Username = "admin", - Password = "abcd1234", - StreamType = 0 - }; - manager.AddDevice(config2); + } } } \ No newline at end of file