using IntegrationGateway.Core.Abstractions; using IntegrationGateway.Core.Infrastructure; using IntegrationGateway.Core.Models; // ═══════════════════════════════════════════════════════════════ // IntegrationGateway 宿主启动程序 // // 职责: // 1. 注册 IHttpClientFactory(连接池复用) // 2. 创建并注册 OwlAdapter + MC4Adapter // 3. 并行初始化所有适配器 // 4. 注册 14 个 B 组 REST 端点 // ═══════════════════════════════════════════════════════════════ var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // ── 注册 HttpClient 工厂 ── // 命名客户端 "VolPro":用于调用 Vol.Pro A 组接口和适配器内部 HTTP 请求 // 连接池:最多 10 个并发连接,5 分钟生命周期 builder.Services.AddHttpClient("VolPro", c => { c.Timeout = TimeSpan.FromSeconds(30); c.DefaultRequestHeaders.Add("Accept", "application/json"); }).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5), MaxConnectionsPerServer = 10 }); builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseCors(); // ── 读取配置 ── var gwCfg = app.Configuration.GetSection("Gateway"); // ── 创建适配器注册中心 ── var registry = new AdapterRegistry(); // ── 创建 Vol.Pro 客户端工厂(用于 A1-A4 回调) ── var volProUrl = gwCfg["VolProBaseUrl"] ?? "http://localhost:9100"; var httpFactory = app.Services.GetRequiredService(); var clientFactory = new GatewayClientFactory(httpFactory, volProUrl); // ── 注册 OwlAdapter(多实例遍历)── var owlList = app.Configuration.GetSection("Owl").Get>() ?? new(); foreach (var o in owlList) { var code = $"Owl:{o.InstanceName ?? "default"}"; var a = new IntegrationGateway.Adapters.Owl.OwlAdapter(code, app.Services.GetRequiredService().CreateClient("VolPro"), o.BaseUrl, o.Username, o.Password); registry.Register(a); } // ── 注册 KMSAdapter(多实例遍历)── var kmsList = app.Configuration.GetSection("KMS").Get>() ?? new(); foreach (var k in kmsList) { var code = $"KMS:{k.InstanceName ?? "default"}"; var a = new IntegrationGateway.Adapters.Kms.KmsAdapter(code, app.Services.GetRequiredService().CreateClient("VolPro"), k.BaseUrl, k.ClientId, k.ClientSecret); registry.Register(a); } // ── 注册 MC4Adapter(多实例遍历)── var mc4List = app.Configuration.GetSection("MC4").Get>() ?? new(); foreach (var m in mc4List) { var code = $"MC4:{m.InstanceName ?? "default"}"; var a = new IntegrationGateway.Adapters.MC4.Mc4Adapter(code, app.Services.GetRequiredService().CreateClient("VolPro"), m.BaseUrl, m.Username, m.Password); registry.Register(a); } // ── 动态拼接 AdapterTypes 字符串 ── var adapterTypes = string.Join(",", registry.All.Select(a => a.AdapterCode)); // ── 并行初始化所有适配器 ── await registry.InitializeAllAsync(); Console.WriteLine($"[Gateway] {registry.All.Count} 个适配器已注册: {adapterTypes}"); // ── A1: 向 Vol.Pro 注册当前网关节点 ── var nodeCode = gwCfg["NodeCode"] ?? "gw-default"; var nodeToken = Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"] ?? ""; var port = app.Urls.FirstOrDefault()?.Split(':').LastOrDefault() ?? "5100"; var selfUrl = gwCfg["SelfUrl"] ?? $"http://localhost:{port}"; try { var registerReq = new GatewayRegisterRequest { NodeCode = nodeCode, Token = nodeToken, AdapterTypes = adapterTypes, BaseUrl = selfUrl }; var registerResult = await clientFactory.RegisterAsync(registerReq); Console.WriteLine($"[Gateway] A1 注册完成: nodeCode={nodeCode}, adapters={adapterTypes}"); } catch (Exception ex) { Console.Error.WriteLine($"[Gateway] A1 注册失败: {ex.Message}"); } // ── A3: 同步所有适配器设备到 Vol.Pro ── await SyncAllDevicesAsync(nodeCode, nodeToken, selfUrl); Console.WriteLine("[Gateway] A3 设备同步完成"); // ── A2: 心跳 + 自动重注册 ── var heartbeatInterval = int.TryParse(gwCfg["HeartbeatIntervalSec"], out var hs) ? hs : 15; var failCount = 0; var maxFails = 3; _ = Task.Run(async () => { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(heartbeatInterval)); while (await timer.WaitForNextTickAsync()) { try { await clientFactory.HeartbeatAsync(new GatewayHeartbeatRequest { NodeCode = nodeCode, Token = nodeToken }); failCount = 0; } catch { failCount++; Console.Error.WriteLine($"[Gateway] A2 心跳失败 ({failCount}/{maxFails})"); if (failCount >= maxFails) { Console.WriteLine("[Gateway] 心跳连续失败, 尝试重新注册..."); try { await clientFactory.RegisterAsync(new GatewayRegisterRequest { NodeCode = nodeCode, Token = nodeToken, AdapterTypes = adapterTypes, BaseUrl = selfUrl }); await SyncAllDevicesAsync(nodeCode, nodeToken, selfUrl); failCount = 0; Console.WriteLine("[Gateway] 重新注册成功"); } catch (Exception re) { Console.Error.WriteLine($"[Gateway] 重新注册失败: {re.Message}"); } } } } }); Console.WriteLine($"[Gateway] A2 心跳已启动 ({heartbeatInterval}s)"); // ═══════════════════════════════════════════════════════════════ // A 组辅助函数 // ═══════════════════════════════════════════════════════════════ async Task SyncAllDevicesAsync(string nc, string nt, string baseUrl) { var allDevices = new List(); foreach (var adapter in registry.All) { try { if (adapter is IHasFlatDevices flat) { var result = await flat.GetDevicesAsync(1, 1000); foreach (var item in result.Items) allDevices.Add(new { AdapterCode = item.AdapterCode, SourceId = item.SourceId, Name = item.Name, Category = item.Category, Group = item.Group, IsParent = item.IsParent, ParentSourceId = item.ParentSourceId, IsOnline = item.IsOnline, IpAddress = item.IpAddress, Port = item.Port, ExtraDataJson = item.Extra != null ? System.Text.Json.JsonSerializer.Serialize(item.Extra) : null }); } else if (adapter is IHasOwnDeviceTree tree) { var nodes = await tree.GetObjectTreeAsync(); FlattenTree(allDevices, nodes, adapter.AdapterCode, null); } } catch { } } if (allDevices.Any()) await clientFactory.SyncDevicesAsync(nc, nt, allDevices); } void FlattenTree(List devices, List nodes, string ac, string? parentSourceId) { foreach (var n in nodes) { devices.Add(new { AdapterCode = ac, SourceId = n.SourceId, Name = n.Name ?? n.SourceId, Category = n.Tag ?? "IoT设备", Group = "IoT设备", IsParent = n.Type == 1, ParentSourceId = parentSourceId, IsOnline = true, IpAddress = (string?)null, Port = (int?)null, ExtraDataJson = n.Option != null ? System.Text.Json.JsonSerializer.Serialize(n.Option) : null }); if (n.Children?.Count > 0) FlattenTree(devices, n.Children, ac, n.SourceId); } } // ═══════════════════════════════════════════════════════════════ // B 组路由(管理端/ Vol.Pro → 网关) // ═══════════════════════════════════════════════════════════════ // B1: 健康检查 — 返回所有适配器的健康状态和能力声明 app.MapGet("/api/gateway/health", async () => { var results = new List(); foreach (var a in registry.All) { bool healthy = false; try { healthy = await a.HealthCheckAsync(); } catch { } results.Add(new { a.AdapterCode, a.DisplayName, Healthy = healthy, a.Capabilities }); } return Results.Ok(results); }); // B2: 设备列表 — 分页获取扁平设备列表(Owl/门禁/道闸) app.MapGet("/api/gateway/devices", async (string adapter, int page, int size, string? keyword) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND", message = $"适配器 '{adapter}' 不存在或不支持扁平设备列表" }); return Results.Ok(await a.GetDevicesAsync(page, size, keyword)); }); // B3: 对象树 — 获取层级对象树(MC4.0) app.MapGet("/api/gateway/tree", async (string adapter) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED", message = $"适配器 '{adapter}' 不支持对象树" }); return Results.Ok(await a.GetObjectTreeAsync()); }); // B6a: 实时取流 — 获取视频通道的实时流地址 app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/live", async (string adapter, string deviceId) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED", message = $"适配器 '{adapter}' 不支持视频取流" }); var result = await a.GetLiveUrlAsync(deviceId); return result.WsFlv == null && result.Hls == null ? Results.Problem("未获取到流地址", statusCode: 502) : Results.Ok(result); }); // B6b: 录像回放 — 获取历史录像 HLS 地址 app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string adapter, string deviceId, DateTime start, DateTime end) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetPlaybackUrlAsync(deviceId, start, end)); }); // 截图 — 获取通道实时截图 app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string adapter, string deviceId) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetSnapshotAsync(deviceId)); }); // B7: 云台控制 — continuous 方向移动 + stop app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapter, string deviceId, PtzRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); if (req.Action == "stop") await a.PtzStopAsync(deviceId); else await a.PtzControlAsync(deviceId, req.Direction ?? "stop", req.Speed); return Results.Ok(); }); // B4: 实时点位值 — 获取 IoT 设备测点当前读数 app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, string deviceId) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetRealtimeValuesAsync(deviceId)); }); // B4-batch: 批量实时点位值 — MC4 原生批量接口,其他适配器 fallback app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); if (a is IntegrationGateway.Adapters.MC4.Mc4Adapter mc4 && req.DeviceIds?.Count > 0) { var intIds = req.DeviceIds.Select(int.Parse).ToList(); var multi = await mc4.GetMultiRealtimeValuesAsync(intIds); return Results.Ok(multi); } var results = new Dictionary>(); foreach (var deviceId in req.DeviceIds ?? new()) try { results[deviceId] = await a.GetRealtimeValuesAsync(deviceId); } catch { } return Results.Ok(results); }); // B5: 设备控制 — 向 IoT 设备下发控制指令 app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); await a.SetPointValueAsync(req.DeviceId ?? "", req.PointIndex, req.Value); return Results.Ok(); }); // B8: 告警查询 — 分页获取告警列表 app.MapGet("/api/gateway/alarms/{adapter}", async (string adapter, int page, int size, DateTime from, DateTime to, string? level, string? state) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetAlarmsAsync(page, size, from, to, level, state)); }); // B9: 告警确认 — 确认告警并写回子系统 app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string adapter, string alarmId) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); await a.ConfirmAlarmAsync(alarmId); return Results.Ok(); }); // 告警结束 — 结束告警并写回子系统 app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter, string alarmId) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); await a.EndAlarmAsync(alarmId); return Results.Ok(); }); // 录像查询 — 分页获取录像文件列表 app.MapGet("/api/gateway/recordings/{adapter}/{deviceId}", async (string adapter, string deviceId, DateTime start, DateTime end, int page, int size) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetRecordingsAsync(deviceId, start, end, page, size)); }); // B10: 设备控制 — 下发控制指令(远程开门/抬杆/授权) app.MapPost("/api/gateway/control/{adapter}", async (string adapter, GatewayControlRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); var result = await a.SendControlAsync(req.DeviceId ?? "", req.Command ?? "open", req.Parameters ?? new()); return result.Success ? Results.Ok(result) : Results.Problem(result.Message, statusCode: 502); }); // B11: 业务记录查询 — 借还/交接/授权记录 app.MapGet("/api/gateway/logs/{adapter}", async (string adapter, string logType, DateTime? from, DateTime? to, int page, int size) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.GetBusinessLogsAsync(logType, from, to, page, size)); }); // B12: 数据同步 — 向子系统写入数据(员工同步) app.MapPost("/api/gateway/sync/{adapter}", async (string adapter, SyncRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.SyncDataAsync(req.DataType ?? "staff", req.Items ?? new())); }); // B13: 数据删除 — 从子系统删除数据 app.MapDelete("/api/gateway/sync/{adapter}", async (string adapter, SyncDeleteRequest req) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" }); return Results.Ok(await a.DeleteDataAsync(req.DataType ?? "staff", req.Ids ?? new())); }); // B3: 手动同步 — 触发适配器全量设备同步 app.MapPost("/api/gateway/devices/sync", async (string adapter) => { var a = registry.FindByCode(adapter); if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND" }); // 根据适配器能力触发对应同步逻辑 if (a is IHasOwnDeviceTree tree) { var obj = await tree.GetObjectTreeAsync(); return Results.Ok(new { nodeCount = obj.Count, message = "对象树同步完成" }); } if (a is IHasFlatDevices flat) { var dev = await flat.GetDevicesAsync(1, 1000); return Results.Ok(new { deviceCount = dev.Total, message = "设备列表同步完成" }); } return Results.Ok(new { message = "无需同步" }); }); app.Run(); record PtzRequest(string? Direction, string Action, float Speed); record ControlRequest(string? DeviceId, int PointIndex, double Value); record BatchRealtimeRequest(List? DeviceIds); record GatewayControlRequest(string? DeviceId, string? Command, Dictionary? Parameters); record SyncRequest(string? DataType, List? Items); record SyncDeleteRequest(string? DataType, List? Ids); // ═══════════════════════════════════════════ // 配置 POCO // ═══════════════════════════════════════════ /// Owl 适配器配置项 public class OwlConfig { public string? InstanceName { get; set; } public string BaseUrl { get; set; } = ""; public string Username { get; set; } = "admin"; public string Password { get; set; } = "admin"; } /// MC4.0 适配器配置项 public class Mc4Config { public string? InstanceName { get; set; } public string BaseUrl { get; set; } = ""; public string Username { get; set; } = "admin"; public string Password { get; set; } = "admin"; } /// KMS 适配器配置项 public class KmsConfig { public string? InstanceName { get; set; } public string BaseUrl { get; set; } = ""; public string ClientId { get; set; } = ""; public string ClientSecret { get; set; } = ""; }