架构增加文件存储服务的支持
This commit is contained in:
18
SHH.CameraSdk/Abstractions/IStorageService.cs
Normal file
18
SHH.CameraSdk/Abstractions/IStorageService.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
|
public interface IStorageService
|
||||||
|
{
|
||||||
|
// 1. 基础属性:让外界知道当前是几号进程
|
||||||
|
int ProcessId { get; }
|
||||||
|
|
||||||
|
// 2. 设备配置相关的空架子
|
||||||
|
Task SaveDevicesAsync(object configs); // 这里先用 object 占位,或者用您的 List<VideoSourceConfig>
|
||||||
|
|
||||||
|
Task<object> LoadDevicesAsync();
|
||||||
|
|
||||||
|
// 3. 系统日志相关的空架子
|
||||||
|
Task AppendSystemLogAsync(string action, string ip, string path);
|
||||||
|
|
||||||
|
// 4. 设备审计日志相关的空架子
|
||||||
|
Task AppendDeviceLogAsync(long deviceId, string message);
|
||||||
|
}
|
||||||
51
SHH.CameraSdk/Core/Services/FileStorageService.cs
Normal file
51
SHH.CameraSdk/Core/Services/FileStorageService.cs
Normal file
@@ -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<object> LoadDevicesAsync()
|
||||||
|
{
|
||||||
|
// TODO: 待实现读取
|
||||||
|
return Task.FromResult<object>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using Microsoft.OpenApi.Models;
|
|||||||
namespace SHH.CameraSdk;
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A 方案:标准控制台结构 (动态窗口版)
|
/// A 方案:标准控制台结构 (框架搭建版:支持动态端口与依赖注入)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
@@ -14,41 +14,65 @@ public class Program
|
|||||||
public static async Task Main(string[] args)
|
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();
|
InitHardwareEnv();
|
||||||
|
|
||||||
|
// B. 【核心】创建独立的文件存储服务 (此时只建立目录,不进行具体读写)
|
||||||
|
IStorageService storageService = new FileStorageService(processId);
|
||||||
|
|
||||||
// 核心设备管理器
|
// 核心设备管理器
|
||||||
|
// 注意:暂时保持无参构造,后续我们在改造 CameraManager 时再注入 storageService
|
||||||
using var cameraManager = new CameraManager();
|
using var cameraManager = new CameraManager();
|
||||||
|
|
||||||
// [新增] 动态窗口管理器 (不再直接 new FrameConsumer)
|
// 动态窗口管理器
|
||||||
// 这是一个单例服务,负责在运行期间管理所有弹出的窗口
|
|
||||||
var displayManager = new DisplayWindowManager();
|
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);
|
var sentinel = new ConnectivitySentinel(cameraManager);
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// 3. 业务编排:仅配置设备,不配置窗口
|
// 4. 业务编排
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
await ConfigureBusinessLogic(cameraManager);
|
await ConfigureBusinessLogic(cameraManager);
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// 4. 启动引擎与交互
|
// 5. 启动引擎与交互
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
|
Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
|
||||||
await cameraManager.StartAsync();
|
await cameraManager.StartAsync();
|
||||||
|
|
||||||
Console.WriteLine(">> 系统就绪。");
|
Console.WriteLine($">> 系统就绪。Web 管理地址: http://localhost:{port}");
|
||||||
Console.WriteLine(">>当前无播放窗口。请通过 Web 界面 '新增订阅' -> 模式选 'UI_Preview' 来动态打开。");
|
Console.WriteLine($">> 数据存储路径: App_Data/Process_{processId}/");
|
||||||
Console.WriteLine(">> 按 'S' 键退出...");
|
Console.WriteLine(">> 按 'S' 键退出...");
|
||||||
|
|
||||||
// 阻塞主线程,保持程序运行
|
// 阻塞主线程
|
||||||
while (Console.ReadKey(true).Key != ConsoleKey.S)
|
while (Console.ReadKey(true).Key != ConsoleKey.S)
|
||||||
{
|
{
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(100);
|
||||||
@@ -64,33 +88,43 @@ public class Program
|
|||||||
|
|
||||||
static void InitHardwareEnv()
|
static void InitHardwareEnv()
|
||||||
{
|
{
|
||||||
Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.3 动态窗口版) ===");
|
Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.5 框架版) ===");
|
||||||
Console.WriteLine("[硬件] 海康驱动预热中...");
|
Console.WriteLine("[硬件] 海康驱动预热中...");
|
||||||
HikNativeMethods.NET_DVR_Init();
|
HikNativeMethods.NET_DVR_Init();
|
||||||
HikSdkManager.ForceWarmUp();
|
HikSdkManager.ForceWarmUp();
|
||||||
Console.WriteLine("[硬件] 预热完成。");
|
Console.WriteLine("[硬件] 预热完成。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// [修改] 签名增加 DisplayWindowManager 参数
|
static async Task<WebApplication> StartWebMonitoring(
|
||||||
static async Task<WebApplication> StartWebMonitoring(CameraManager manager, DisplayWindowManager displayMgr)
|
CameraManager manager,
|
||||||
|
DisplayWindowManager displayMgr,
|
||||||
|
IStorageService storage, // 接收存储服务实例
|
||||||
|
int port) // 接收动态端口
|
||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder();
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
|
||||||
|
// 1. 配置 CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("AllowAll", policy =>
|
options.AddPolicy("AllowAll", policy =>
|
||||||
{
|
{
|
||||||
policy.AllowAnyOrigin()
|
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowAnyMethod();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 日志屏蔽
|
// 2. 日志降噪
|
||||||
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
|
builder.Logging.SetMinimumLevel(LogLevel.Warning);
|
||||||
builder.Logging.AddFilter("System", LogLevel.Warning);
|
|
||||||
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
|
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
|
||||||
|
|
||||||
|
// 3. 【核心】依赖注入注册
|
||||||
|
// 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了
|
||||||
|
builder.Services.AddSingleton<IStorageService>(storage);
|
||||||
|
builder.Services.AddSingleton(manager);
|
||||||
|
builder.Services.AddSingleton(displayMgr);
|
||||||
|
|
||||||
|
// 显式注册过滤器 (这是防止 500 错误的关键)
|
||||||
|
builder.Services.AddScoped<UserActionFilter>();
|
||||||
|
|
||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
// 注册全局操作日志过滤器
|
// 注册全局操作日志过滤器
|
||||||
@@ -100,39 +134,38 @@ public class Program
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
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();
|
var webApp = builder.Build();
|
||||||
|
|
||||||
|
// 4. 配置中间件
|
||||||
webApp.UseSwagger();
|
webApp.UseSwagger();
|
||||||
webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1"));
|
webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1"));
|
||||||
webApp.UseCors("AllowAll");
|
webApp.UseCors("AllowAll");
|
||||||
webApp.MapControllers();
|
webApp.MapControllers();
|
||||||
|
|
||||||
_ = webApp.RunAsync("http://0.0.0.0:5000");
|
// 5. 启动监听 (使用动态端口)
|
||||||
Console.WriteLine("[Web] 监控API已启动: http://localhost:5000");
|
_ = webApp.RunAsync($"http://0.0.0.0:{port}");
|
||||||
|
Console.WriteLine($"[Web] 监控API已启动: http://localhost:{port}");
|
||||||
|
|
||||||
return webApp;
|
return webApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [修改] 移除 FrameConsumer 参数,不再进行硬编码订阅
|
// 辅助方法:从端口反推 ID,仅用于 Swagger 标题显示
|
||||||
|
static int processIdFromPort(int port) => port - 5000 + 1;
|
||||||
|
|
||||||
static async Task ConfigureBusinessLogic(CameraManager manager)
|
static async Task ConfigureBusinessLogic(CameraManager manager)
|
||||||
{
|
{
|
||||||
// 1. 仅添加设备配置
|
// 1. 添加测试设备
|
||||||
var config = new VideoSourceConfig
|
var config = new VideoSourceConfig
|
||||||
{
|
{
|
||||||
Id = 101,
|
Id = 101,
|
||||||
Brand = DeviceBrand.HikVision,
|
Brand = DeviceBrand.HikVision,
|
||||||
IpAddress = "172.16.41.206",
|
IpAddress = "192.168.5.9",
|
||||||
Port = 8000,
|
Port = 8000,
|
||||||
Username = "admin",
|
Username = "admin",
|
||||||
Password = "abcd1234",
|
Password = "RRYFOA",
|
||||||
StreamType = 0
|
StreamType = 0
|
||||||
};
|
};
|
||||||
manager.AddDevice(config);
|
manager.AddDevice(config);
|
||||||
@@ -148,8 +181,5 @@ public class Program
|
|||||||
StreamType = 0
|
StreamType = 0
|
||||||
};
|
};
|
||||||
manager.AddDevice(config2);
|
manager.AddDevice(config2);
|
||||||
|
|
||||||
// 注意:此处不再调用 Register 或 Subscribe
|
|
||||||
// 所有的播放请求都将由 WebAPI 收到前端指令后,调用 DisplayWindowManager 来动态发起
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,34 +2,49 @@
|
|||||||
|
|
||||||
namespace SHH.CameraSdk;
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局用户操作过滤器
|
||||||
|
/// 作用:拦截所有 API 请求,记录关键操作(如新增、删除、修改设备)
|
||||||
|
/// 优化:使用依赖注入 (DI) 获取存储服务,避免直接文件 IO 导致的锁冲突
|
||||||
|
/// </summary>
|
||||||
public class UserActionFilter : IActionFilter
|
public class UserActionFilter : IActionFilter
|
||||||
{
|
{
|
||||||
// 静态锁,防止多线程同时写文件报错
|
private readonly IStorageService _storage;
|
||||||
private static readonly object _logLock = new object();
|
|
||||||
|
|
||||||
|
// 【关键点】构造函数注入
|
||||||
|
// ASP.NET Core 会自动把我们在 Program.cs 中注册的 IStorageService 实例传进来
|
||||||
|
public UserActionFilter(IStorageService storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Action 执行【后】触发
|
||||||
|
/// </summary>
|
||||||
public void OnActionExecuted(ActionExecutedContext context)
|
public void OnActionExecuted(ActionExecutedContext context)
|
||||||
{
|
{
|
||||||
// 只记录非 GET 请求(即修改性质的操作)
|
// 1. 获取请求的基本信息
|
||||||
if (context.HttpContext.Request.Method != "GET")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
|
|
||||||
var path = context.HttpContext.Request.Path;
|
|
||||||
var method = context.HttpContext.Request.Method;
|
var method = context.HttpContext.Request.Method;
|
||||||
var time = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
|
var path = context.HttpContext.Request.Path;
|
||||||
|
|
||||||
var logLine = $"{time} | IP: {user} | {method} {path}";
|
// 2. 过滤逻辑:为了防止日志爆炸,我们通常只记录非 GET 请求
|
||||||
|
// (例如:只记录 POST/PUT/DELETE 等修改性操作)
|
||||||
lock (_logLock)
|
if (method != "GET")
|
||||||
{
|
{
|
||||||
// 追加写入到运行目录下的 user_actions.log
|
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
|
||||||
File.AppendAllText("user_actions.log", logLine + Environment.NewLine);
|
|
||||||
}
|
// 3. 调用存储服务写入日志
|
||||||
}
|
// 注意:这里我们不等待任务完成 (Fire-and-Forget),以免日志写入拖慢 API 响应速度
|
||||||
catch { /* 忽略日志写入错误,不要影响业务 */ }
|
// 因为 _storage.AppendSystemLogAsync 内部目前是空实现(Task.CompletedTask),所以这里绝对不会卡顿
|
||||||
|
_ = _storage.AppendSystemLogAsync(method, ip, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnActionExecuting(ActionExecutingContext context) { }
|
/// <summary>
|
||||||
|
/// Action 执行【前】触发 (此处不需要处理)
|
||||||
|
/// </summary>
|
||||||
|
public void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user