From 71856b483eb3bab42462b1f7dad38642a80e0f8d Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 21:19:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=B6=E6=9E=84=E5=A2=9E=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AD=98=E5=82=A8=E6=9C=8D=E5=8A=A1=E7=9A=84=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 | 18 ++++ .../Core/Services/FileStorageService.cs | 51 +++++++++ SHH.CameraSdk/Program.cs | 102 +++++++++++------- SHH.CameraSdk/Temp/UserActionFilter.cs | 55 ++++++---- 4 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 SHH.CameraSdk/Abstractions/IStorageService.cs create mode 100644 SHH.CameraSdk/Core/Services/FileStorageService.cs diff --git a/SHH.CameraSdk/Abstractions/IStorageService.cs b/SHH.CameraSdk/Abstractions/IStorageService.cs new file mode 100644 index 0000000..ae2a3af --- /dev/null +++ b/SHH.CameraSdk/Abstractions/IStorageService.cs @@ -0,0 +1,18 @@ +namespace SHH.CameraSdk; + +public interface IStorageService +{ + // 1. 基础属性:让外界知道当前是几号进程 + int ProcessId { get; } + + // 2. 设备配置相关的空架子 + Task SaveDevicesAsync(object configs); // 这里先用 object 占位,或者用您的 List + + Task LoadDevicesAsync(); + + // 3. 系统日志相关的空架子 + Task AppendSystemLogAsync(string action, string ip, string path); + + // 4. 设备审计日志相关的空架子 + Task AppendDeviceLogAsync(long deviceId, string message); +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Services/FileStorageService.cs b/SHH.CameraSdk/Core/Services/FileStorageService.cs new file mode 100644 index 0000000..3807dfc --- /dev/null +++ b/SHH.CameraSdk/Core/Services/FileStorageService.cs @@ -0,0 +1,51 @@ +namespace SHH.CameraSdk; + +public class FileStorageService : IStorageService +{ + public int ProcessId { get; } + private readonly string _basePath; // 专属数据目录 + + public FileStorageService(int processId) + { + ProcessId = processId; + + // 核心逻辑:数据隔离 + // 1号进程 -> App_Data/Process_1/ + // 2号进程 -> App_Data/Process_2/ + _basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}"); + + // 既然是框架搭建,我们要确保这个目录存在,否则后面谁写谁报错 + if (!Directory.Exists(_basePath)) + { + Directory.CreateDirectory(_basePath); + } + + Console.WriteLine($"[Storage] 存储服务已就绪。数据隔离路径: {_basePath}"); + } + + // --- 下面是未实现的空架子 --- + + public Task SaveDevicesAsync(object configs) + { + // TODO: 待实现序列化写入 + return Task.CompletedTask; + } + + public Task LoadDevicesAsync() + { + // TODO: 待实现读取 + return Task.FromResult(null); + } + + public Task AppendSystemLogAsync(string action, string ip, string path) + { + // TODO: 待实现系统日志写入 + return Task.CompletedTask; + } + + public Task AppendDeviceLogAsync(long deviceId, string message) + { + // TODO: 待实现设备日志写入 + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs index 058ceee..ccbeb14 100644 --- a/SHH.CameraSdk/Program.cs +++ b/SHH.CameraSdk/Program.cs @@ -6,7 +6,7 @@ using Microsoft.OpenApi.Models; namespace SHH.CameraSdk; /// -/// A 方案:标准控制台结构 (动态窗口版) +/// A 方案:标准控制台结构 (框架搭建版:支持动态端口与依赖注入) /// public class Program { @@ -14,41 +14,65 @@ public class Program public static async Task Main(string[] args) { // ============================================================================== - // 1. 基础设施初始化 + // 1. 身份识别与端口计算 + // ============================================================================== + + // 默认 1 号进程 + int processId = 1; + + // 如果命令行传了参数 (例如: dotnet run 2),则覆盖为 2 号进程 + if (args.Length > 0 && int.TryParse(args[0], out int pid)) + { + processId = pid; + } + + // 端口计算公式:5000 + (ID - 1) + // ID=1 -> 5000 + // ID=2 -> 5001 + int port = 5000 + (processId - 1); + + Console.Title = $"SHH Gateway - Instance #{processId} (Port: {port})"; + Console.WriteLine($"[System] 正在初始化实例 #{processId}..."); + + // ============================================================================== + // 2. 基础设施初始化 // ============================================================================== InitHardwareEnv(); + // B. 【核心】创建独立的文件存储服务 (此时只建立目录,不进行具体读写) + IStorageService storageService = new FileStorageService(processId); + // 核心设备管理器 + // 注意:暂时保持无参构造,后续我们在改造 CameraManager 时再注入 storageService using var cameraManager = new CameraManager(); - // [新增] 动态窗口管理器 (不再直接 new FrameConsumer) - // 这是一个单例服务,负责在运行期间管理所有弹出的窗口 + // 动态窗口管理器 var displayManager = new DisplayWindowManager(); // ============================================================================== - // 2. 启动 Web 监控与诊断服务 (注入两个管理器) + // 3. 启动 Web 监控与诊断服务 (注入服务与端口) // ============================================================================== - var app = await StartWebMonitoring(cameraManager, displayManager); + var app = await StartWebMonitoring(cameraManager, displayManager, storageService, port); - // 启动网络哨兵 (后台 Ping) + // 启动网络哨兵 var sentinel = new ConnectivitySentinel(cameraManager); // ============================================================================== - // 3. 业务编排:仅配置设备,不配置窗口 + // 4. 业务编排 // ============================================================================== await ConfigureBusinessLogic(cameraManager); // ============================================================================== - // 4. 启动引擎与交互 + // 5. 启动引擎与交互 // ============================================================================== Console.WriteLine("\n[系统] 正在启动全局管理引擎..."); await cameraManager.StartAsync(); - Console.WriteLine(">> 系统就绪。"); - Console.WriteLine(">>当前无播放窗口。请通过 Web 界面 '新增订阅' -> 模式选 'UI_Preview' 来动态打开。"); + Console.WriteLine($">> 系统就绪。Web 管理地址: http://localhost:{port}"); + Console.WriteLine($">> 数据存储路径: App_Data/Process_{processId}/"); Console.WriteLine(">> 按 'S' 键退出..."); - // 阻塞主线程,保持程序运行 + // 阻塞主线程 while (Console.ReadKey(true).Key != ConsoleKey.S) { Thread.Sleep(100); @@ -64,33 +88,43 @@ public class Program static void InitHardwareEnv() { - Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.3 动态窗口版) ==="); + Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.5 框架版) ==="); Console.WriteLine("[硬件] 海康驱动预热中..."); HikNativeMethods.NET_DVR_Init(); HikSdkManager.ForceWarmUp(); Console.WriteLine("[硬件] 预热完成。"); } - // [修改] 签名增加 DisplayWindowManager 参数 - static async Task StartWebMonitoring(CameraManager manager, DisplayWindowManager displayMgr) + static async Task StartWebMonitoring( + CameraManager manager, + DisplayWindowManager displayMgr, + IStorageService storage, // 接收存储服务实例 + int port) // 接收动态端口 { var builder = WebApplication.CreateBuilder(); + // 1. 配置 CORS builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { - policy.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); + policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); }); }); - // 日志屏蔽 - builder.Logging.AddFilter("Microsoft", LogLevel.Warning); - builder.Logging.AddFilter("System", LogLevel.Warning); + // 2. 日志降噪 + builder.Logging.SetMinimumLevel(LogLevel.Warning); builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); + // 3. 【核心】依赖注入注册 + // 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了 + builder.Services.AddSingleton(storage); + builder.Services.AddSingleton(manager); + builder.Services.AddSingleton(displayMgr); + + // 显式注册过滤器 (这是防止 500 错误的关键) + builder.Services.AddScoped(); + builder.Services.AddControllers(options => { // 注册全局操作日志过滤器 @@ -100,39 +134,38 @@ public class Program builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "SHH Camera Diagnostics", Version = "v1" }); + c.SwaggerDoc("v1", new OpenApiInfo { Title = $"SHH Gateway #{processIdFromPort(port)}", Version = "v1" }); }); - builder.Services.AddCors(o => o.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); - - // [关键] 注入两个单例服务,让 Controller 能调用它们 - builder.Services.AddSingleton(manager); - builder.Services.AddSingleton(displayMgr); var webApp = builder.Build(); + // 4. 配置中间件 webApp.UseSwagger(); webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1")); webApp.UseCors("AllowAll"); webApp.MapControllers(); - _ = webApp.RunAsync("http://0.0.0.0:5000"); - Console.WriteLine("[Web] 监控API已启动: http://localhost:5000"); + // 5. 启动监听 (使用动态端口) + _ = webApp.RunAsync($"http://0.0.0.0:{port}"); + Console.WriteLine($"[Web] 监控API已启动: http://localhost:{port}"); return webApp; } - // [修改] 移除 FrameConsumer 参数,不再进行硬编码订阅 + // 辅助方法:从端口反推 ID,仅用于 Swagger 标题显示 + static int processIdFromPort(int port) => port - 5000 + 1; + static async Task ConfigureBusinessLogic(CameraManager manager) { - // 1. 仅添加设备配置 + // 1. 添加测试设备 var config = new VideoSourceConfig { Id = 101, Brand = DeviceBrand.HikVision, - IpAddress = "172.16.41.206", + IpAddress = "192.168.5.9", Port = 8000, Username = "admin", - Password = "abcd1234", + Password = "RRYFOA", StreamType = 0 }; manager.AddDevice(config); @@ -148,8 +181,5 @@ public class Program StreamType = 0 }; manager.AddDevice(config2); - - // 注意:此处不再调用 Register 或 Subscribe - // 所有的播放请求都将由 WebAPI 收到前端指令后,调用 DisplayWindowManager 来动态发起 } } \ No newline at end of file diff --git a/SHH.CameraSdk/Temp/UserActionFilter.cs b/SHH.CameraSdk/Temp/UserActionFilter.cs index aa9ef05..16f3250 100644 --- a/SHH.CameraSdk/Temp/UserActionFilter.cs +++ b/SHH.CameraSdk/Temp/UserActionFilter.cs @@ -2,34 +2,49 @@ namespace SHH.CameraSdk; +/// +/// 全局用户操作过滤器 +/// 作用:拦截所有 API 请求,记录关键操作(如新增、删除、修改设备) +/// 优化:使用依赖注入 (DI) 获取存储服务,避免直接文件 IO 导致的锁冲突 +/// public class UserActionFilter : IActionFilter { - // 静态锁,防止多线程同时写文件报错 - private static readonly object _logLock = new object(); + private readonly IStorageService _storage; + // 【关键点】构造函数注入 + // ASP.NET Core 会自动把我们在 Program.cs 中注册的 IStorageService 实例传进来 + public UserActionFilter(IStorageService storage) + { + _storage = storage; + } + + /// + /// Action 执行【后】触发 + /// public void OnActionExecuted(ActionExecutedContext context) { - // 只记录非 GET 请求(即修改性质的操作) - if (context.HttpContext.Request.Method != "GET") + // 1. 获取请求的基本信息 + var method = context.HttpContext.Request.Method; + var path = context.HttpContext.Request.Path; + + // 2. 过滤逻辑:为了防止日志爆炸,我们通常只记录非 GET 请求 + // (例如:只记录 POST/PUT/DELETE 等修改性操作) + if (method != "GET") { - try - { - var user = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; - var path = context.HttpContext.Request.Path; - var method = context.HttpContext.Request.Method; - var time = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); + var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; - var logLine = $"{time} | IP: {user} | {method} {path}"; - - lock (_logLock) - { - // 追加写入到运行目录下的 user_actions.log - File.AppendAllText("user_actions.log", logLine + Environment.NewLine); - } - } - catch { /* 忽略日志写入错误,不要影响业务 */ } + // 3. 调用存储服务写入日志 + // 注意:这里我们不等待任务完成 (Fire-and-Forget),以免日志写入拖慢 API 响应速度 + // 因为 _storage.AppendSystemLogAsync 内部目前是空实现(Task.CompletedTask),所以这里绝对不会卡顿 + _ = _storage.AppendSystemLogAsync(method, ip, path); } } - public void OnActionExecuting(ActionExecutingContext context) { } + /// + /// Action 执行【前】触发 (此处不需要处理) + /// + public void OnActionExecuting(ActionExecutingContext context) + { + // Do nothing + } } \ No newline at end of file