From 73d47cb4706f725159020bc35997292c75516c3e Mon Sep 17 00:00:00 2001 From: g82tt Date: Sun, 17 May 2026 04:53:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E9=83=A8=E7=BD=91=E5=85=B3=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mc4Adapter.cs | 106 +++++++++-- .../Mc4AuthHelper.cs | 21 +++ .../OwlAdapter.cs | 169 +++++++++++++++--- .../OwlAuthHelper.cs | 32 +++- .../Abstractions/IAcceptsMetadataPush.cs | 7 +- .../Abstractions/IGatewayAdapter.cs | 10 +- .../Abstractions/IHasAlarms.cs | 16 +- .../Abstractions/IHasFlatDevices.cs | 10 +- .../Abstractions/IHasOwnDeviceTree.cs | 6 +- .../Abstractions/IHasPoints.cs | 11 +- .../Abstractions/IHasRecordings.cs | 10 +- .../Abstractions/IHasStreams.cs | 17 +- .../Infrastructure/AdapterRegistry.cs | 20 ++- .../Infrastructure/GatewayClientFactory.cs | 23 ++- .../Infrastructure/RateLimiter.cs | 16 ++ .../Models/AdapterCapabilities.cs | 13 ++ .../Models/DeviceTreeNode.cs | 13 ++ .../Models/MetadataChangeSet.cs | 10 ++ .../Models/MetadataPushResult.cs | 5 + .../Models/PagedResult.cs | 7 + .../Models/PointValue.cs | 8 + .../Models/StandardAlarm.cs | 14 ++ .../Models/StandardDevice.cs | 21 +++ .../Models/StandardRecording.cs | 10 ++ .../Models/StreamUrls.cs | 10 ++ .../src/IntegrationGateway.Host/Program.cs | 91 ++++++---- 26 files changed, 597 insertions(+), 79 deletions(-) diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs index 789a761..9041b0b 100644 --- a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs +++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs @@ -6,19 +6,38 @@ using System.Text.Json; namespace IntegrationGateway.Adapters.MC4; +/// +/// MC4.0 动环监控子系统适配器。 +/// +/// 实现的能力接口: +/// - IHasOwnDeviceTree:对象树(区域→设备层级) +/// - IHasPoints:实时点位值读取 + 反向控制写值 +/// - IHasAlarms:告警查询、确认、结束 +/// +/// 限流:2 QPS(MC4.0 API 推荐值) +/// 分页转换:网关 page/size ↔ MC4.0 skip/limit +/// public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms { private readonly HttpClient _http; private readonly Mc4AuthHelper _auth; + /// 令牌桶限流器(2 QPS) private readonly RateLimiter _limiter = new(2); + /// 适配器编码,格式 "MC4:实例名" public string AdapterCode { get; } + /// 人类可读的适配器名称 public string DisplayName => $"MC4 ({AdapterCode})"; + /// 适配器能力声明 public AdapterCapabilities Capabilities => new() { HasObjectTree = true, HasPoints = true, HasAlarms = true, AcceptsControl = true }; + /// 创建 Mc4Adapter 实例 + /// 适配器编码 + /// HttpClient 实例 + /// MC4.0 服务地址 public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl) { AdapterCode = adapterCode; @@ -26,8 +45,10 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms _auth = new Mc4AuthHelper(http, baseUrl); } + /// 初始化适配器:获取 MC4.0 Token public async Task InitializeAsync() => await _auth.GetTokenAsync(); + /// 健康检查:尝试调用 MC4.0 认证接口确认可达性 public async Task HealthCheckAsync() { try @@ -39,7 +60,14 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms catch { return false; } } - // ─── IHasOwnDeviceTree ─── + // ═══════════════════════════════════════════ + // IHasOwnDeviceTree 实现 + // ═══════════════════════════════════════════ + + /// + /// 获取 MC4.0 完整对象树。 + /// Type=1 的节点为区域,Type=2 的节点为设备。 + /// public async Task> GetObjectTreeAsync() { await _limiter.WaitAsync(); @@ -51,15 +79,24 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms return tree.Select(MapNode).ToList(); } + /// MC4.0 树节点 → DeviceTreeNode 映射 private static DeviceTreeNode MapNode(Mc4TreeNode n) => new() { - Id = n.Id, SourceId = n.Id.ToString(), Name = n.Name ?? n.Id.ToString(), - Type = n.Type, ObjectType = n.ObjectType, Tag = n.Tag, + Id = n.Id, + SourceId = n.Id.ToString(), + Name = n.Name ?? n.Id.ToString(), + Type = n.Type, + ObjectType = n.ObjectType, + Tag = n.Tag, Option = n.Option ?? new Dictionary(), Children = n.Children?.Select(MapNode).ToList() ?? new() }; - // ─── IHasPoints ─── + // ═══════════════════════════════════════════ + // IHasPoints 实现 + // ═══════════════════════════════════════════ + + /// 获取指定设备的所有实时点位值 public async Task> GetRealtimeValuesAsync(string sourceDeviceId) { await _limiter.WaitAsync(); @@ -72,11 +109,15 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms var values = JsonSerializer.Deserialize>(json)!; return values.Select(v => new PointValue { - SourceDeviceId = sourceDeviceId, PointIndex = v.Index, - Value = v.Value, UpdateTime = v.Time != null ? DateTime.Parse(v.Time) : null, Interval = v.Interval + SourceDeviceId = sourceDeviceId, + PointIndex = v.Index, + Value = v.Value, + UpdateTime = v.Time != null ? DateTime.Parse(v.Time) : null, + Interval = v.Interval }).ToList(); } + /// 向指定设备的指定点位写入控制值 public async Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value) { await _limiter.WaitAsync(); @@ -86,7 +127,14 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); } - // ─── IHasAlarms ─── + // ═══════════════════════════════════════════ + // IHasAlarms 实现 + // ═══════════════════════════════════════════ + + /// + /// 分页查询告警列表。 + /// 内部完成 page/size → skip/limit 转换。 + /// public async Task> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, string? level = null, string? state = null) { @@ -98,7 +146,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms To = to.ToString("yyyy-MM-dd HH:mm:ss"), Skip = (page - 1) * size, Limit = size, - Sort = 1 + Sort = 1 // 按时间降序 }); var resp = await client.PostAsync("/api/central/alarm/query", new StringContent(body, Encoding.UTF8, "application/json")); @@ -109,17 +157,21 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms { Items = result.List?.Select(a => new StandardAlarm { - AlarmId = a.Id ?? "", DeviceId = a.Sid?.ToString(), + AlarmId = a.Id ?? "", + DeviceId = a.Sid?.ToString(), AdapterCode = AdapterCode, - Level = MapAlarmLevel(a.Level), Title = a.Desc ?? "", + Level = MapAlarmLevel(a.Level), + Title = a.Desc ?? "", OccurTime = DateTime.TryParse(a.Stime, out var st) ? st : DateTime.MinValue, Status = MapAlarmState(a.State), - ActualValue = a.Soption?.Value, ThresholdValue = a.Eoption?.Value + ActualValue = a.Soption?.Value, + ThresholdValue = a.Eoption?.Value }).ToList() ?? new(), Total = result.Total }; } + /// 确认告警(同时写回 MC4.0) public async Task ConfirmAlarmAsync(string alarmId) { await _limiter.WaitAsync(); @@ -129,6 +181,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); } + /// 结束告警(同时写回 MC4.0) public async Task EndAlarmAsync(string alarmId) { await _limiter.WaitAsync(); @@ -138,15 +191,29 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); } - private static string MapAlarmLevel(int level) => level switch { 1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示" }; - private static string MapAlarmState(int state) => state switch { 1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认" }; + /// MC4.0 告警等级数字 → 中文映射 + private static string MapAlarmLevel(int level) => level switch + { + 1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示" + }; + + /// MC4.0 告警状态数字 → 中文映射 + private static string MapAlarmState(int state) => state switch + { + 1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认" + }; } -// ─── MC4 JSON Models ─── +// ═══════════════════════════════════════════ +// MC4.0 JSON 反序列化模型(内部使用) +// ═══════════════════════════════════════════ + +/// MC4.0 对象树节点 public class Mc4TreeNode { public int Id { get; set; } public string? Name { get; set; } + /// 节点类型:1=区域,2=设备 public int Type { get; set; } public int ObjectType { get; set; } public string? Tag { get; set; } @@ -154,6 +221,7 @@ public class Mc4TreeNode public List? Children { get; set; } } +/// MC4.0 点位值 public class Mc4PointValue { public int Id { get; set; } @@ -163,25 +231,32 @@ public class Mc4PointValue public int Interval { get; set; } } +/// MC4.0 告警查询请求体 public class Mc4AlarmQuery { public string? Sid { get; set; } public string From { get; set; } = ""; public string To { get; set; } = ""; + /// 跳过的记录数(= (page-1)*size) public int Skip { get; set; } + /// 每页条数 public int Limit { get; set; } + /// 排序方式:1=时间降序 public int Sort { get; set; } } +/// MC4.0 告警查询响应 public class Mc4AlarmQueryResult { public int Total { get; set; } public List? List { get; set; } } +/// MC4.0 告警条目 public class Mc4AlarmItem { public string? Id { get; set; } + /// 设备 SID public int? Sid { get; set; } public string? Desc { get; set; } public string? EngDesc { get; set; } @@ -192,10 +267,13 @@ public class Mc4AlarmItem public string? Ctime { get; set; } public string? Cuser { get; set; } public int Type { get; set; } + /// 告警触发时阈值信息 public Mc4Option? Soption { get; set; } + /// 告警结束时阈值信息 public Mc4Option? Eoption { get; set; } } +/// MC4.0 告警阈值信息 public class Mc4Option { public double? Value { get; set; } diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs index 65b6762..51a650e 100644 --- a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs +++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs @@ -2,18 +2,34 @@ using System.Text.Json; namespace IntegrationGateway.Adapters.MC4; +/// +/// MC4.0 子系统的 Token 认证辅助类。 +/// +/// 认证流程: +/// 1. POST /api/central/auth/conf/get 获取临时 Token +/// 2. Token 有效期约 8 小时,缓存在内存中 +/// 3. 后续请求在 header["token"] 中携带 Token +/// +/// 注意:MC4.0 使用自定义 header "token" 而非标准 Authorization 头。 +/// public class Mc4AuthHelper { private readonly HttpClient _http; private readonly string _baseUrl; + /// 缓存的认证 Token private string? _token; + /// Token 过期时间(UTC),默认 8 小时 private DateTime _tokenExpiry = DateTime.MinValue; + /// 创建 MC4.0 认证辅助 + /// HttpClient 实例 + /// MC4.0 服务地址 public Mc4AuthHelper(HttpClient http, string baseUrl) { _http = http; _baseUrl = baseUrl.TrimEnd('/'); } + /// 获取有效的 Token。缓存有效则直接返回,否则重新获取。 public async Task GetTokenAsync() { if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; @@ -27,6 +43,9 @@ public class Mc4AuthHelper return _token!; } + /// + /// 创建一个已认证的 HttpClient,自动在 header["token"] 中附带 Token。 + /// public async Task GetAuthenticatedClientAsync() { var token = await GetTokenAsync(); @@ -35,7 +54,9 @@ public class Mc4AuthHelper return client; } + /// 强制清除缓存 Token public void Invalidate() => _token = null; + /// MC4.0 认证响应 public class Mc4AuthResponse { public string? Token { get; set; } } } diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs index 5b5e04a..ab0098c 100644 --- a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs @@ -6,19 +6,41 @@ using System.Net.Http.Json; namespace IntegrationGateway.Adapters.Owl; +/// +/// Owl 视频监控子系统适配器。 +/// +/// 实现的能力接口: +/// - IHasFlatDevices:设备列表(NVR)和通道列表 +/// - IHasStreams:实时取流、录像回放、云台控制、截图 +/// - IHasRecordings:录像文件查询 +/// - IAcceptsMetadataPush:设备元数据回写(如改名) +/// +/// 限流:5 QPS(Owl 推荐值) +/// PTZ 限制:仅支持 continuous 方向移动 + stop,不支持预设位 +/// public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush { private readonly HttpClient _http; private readonly OwlAuthHelper _auth; + /// 令牌桶限流器(5 QPS) private readonly RateLimiter _limiter = new(5); + /// 适配器编码,格式 "Owl:实例名" public string AdapterCode { get; } + /// 人类可读的适配器名称 public string DisplayName => $"Owl ({AdapterCode})"; + /// 适配器能力声明 public AdapterCapabilities Capabilities => new() { HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true }; + /// 创建 OwlAdapter 实例 + /// 适配器编码 + /// HttpClient 实例 + /// Owl 服务地址 + /// 登录用户名 + /// 登录密码 public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password) { AdapterCode = adapterCode; @@ -26,8 +48,10 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts _auth = new OwlAuthHelper(http, baseUrl, username, password); } + /// 初始化适配器:获取 Owl JWT Token public async Task InitializeAsync() => await _auth.GetTokenAsync(); + /// 健康检查:尝试访问 Owl /health 端点 public async Task HealthCheckAsync() { try @@ -39,7 +63,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts catch { return false; } } - // ─── IHasFlatDevices ─── + // ═══════════════════════════════════════════ + // IHasFlatDevices 实现 + // ═══════════════════════════════════════════ + + /// 分页获取 NVR 设备列表 public async Task> GetDevicesAsync(int page, int size, string? keyword = null) { await _limiter.WaitAsync(); @@ -55,7 +83,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts }; } - // ─── IHasStreams ─── + // ═══════════════════════════════════════════ + // IHasStreams 实现 + // ═══════════════════════════════════════════ + + /// 获取通道实时视频流地址 public async Task GetLiveUrlAsync(string channelId) { await _limiter.WaitAsync(); @@ -67,6 +99,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts return MapStreamUrls(play); } + /// 获取历史录像回放地址(HLS VOD 格式) public async Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end) { await _limiter.WaitAsync(); @@ -74,23 +107,31 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds(); var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds(); var token = await _auth.GetTokenAsync(); - return new StreamUrls { Hls = $"{client.BaseAddress}recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}" }; + return new StreamUrls + { + Hls = $"{client.BaseAddress}recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}" + }; } + /// 云台方向控制(continuous 模式,仅方向移动) public async Task PtzControlAsync(string channelId, string direction, float speed) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); - await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "continuous", direction, speed }); + await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", + new { action = "continuous", direction, speed }); } + /// 云台停止 public async Task PtzStopAsync(string channelId) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); - await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "stop" }); + await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", + new { action = "stop" }); } + /// 获取通道实时截图 public async Task GetSnapshotAsync(string channelId) { await _limiter.WaitAsync(); @@ -102,23 +143,38 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts return new StreamUrls { Hls = snap.Link }; } - // ─── IHasRecordings ─── - public async Task> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size) + // ═══════════════════════════════════════════ + // IHasRecordings 实现 + // ═══════════════════════════════════════════ + + /// 分页查询录像文件记录 + public async Task> GetRecordingsAsync( + string channelId, DateTime start, DateTime end, int page, int size) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds(); var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds(); - var json = await client.GetStringAsync($"/recordings?cid={channelId}&start_ms={startMs}&end_ms={endMs}&page={page}&size={size}"); + var json = await client.GetStringAsync( + $"/recordings?cid={channelId}&start_ms={startMs}&end_ms={endMs}&page={page}&size={size}"); var owl = JsonSerializer.Deserialize>(json)!; return new PagedResult { - Items = owl.Items.Select(r => new StandardRecording { Id = r.Id, ChannelId = r.Cid, StartedAt = r.StartedAt, EndedAt = r.EndedAt, Duration = r.Duration, FilePath = r.Path, Size = r.Size }).ToList(), + Items = owl.Items.Select(r => new StandardRecording + { + Id = r.Id, ChannelId = r.Cid, + StartedAt = r.StartedAt, EndedAt = r.EndedAt, + Duration = r.Duration, FilePath = r.Path, Size = r.Size + }).ToList(), Total = owl.Total }; } - // ─── IAcceptsMetadataPush ─── + // ═══════════════════════════════════════════ + // IAcceptsMetadataPush 实现 + // ═══════════════════════════════════════════ + + /// 回写设备元数据(如改名)到 Owl public async Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes) { var client = await _auth.GetAuthenticatedClientAsync(); @@ -128,26 +184,95 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts return new MetadataPushResult { Success = true }; } - // ─── Mapping ─── + // ═══════════════════════════════════════════ + // 内部映射方法 + // ═══════════════════════════════════════════ + + /// Owl 设备 → StandardDevice 映射 private static StandardDevice MapDevice(OwlDevice d) => new() { - SourceId = d.Id ?? "", Name = d.Name ?? d.Id ?? "", Category = "硬盘录像机", Group = "视频设备", - IsOnline = d.IsOnline == "1", IsParent = true, IpAddress = d.Address, + SourceId = d.Id ?? "", + Name = d.Name ?? d.Id ?? "", + Category = "硬盘录像机", + Group = "视频设备", + IsOnline = d.IsOnline == "1", + IsParent = true, + IpAddress = d.Address, Port = int.TryParse(d.Port, out var port) ? port : null, - Extra = new Dictionary { ["owlDeviceId"] = d.Id, ["protocol"] = d.Protocol ?? "GB28181", ["transport"] = d.Transport } + Extra = new Dictionary + { + ["owlDeviceId"] = d.Id, + ["protocol"] = d.Protocol ?? "GB28181", + ["transport"] = d.Transport + } }; + /// Owl 播放响应 → StreamUrls 映射(取第一个可用流) private static StreamUrls MapStreamUrls(OwlPlayResponse play) { var item = play.Items?.FirstOrDefault(); - return new StreamUrls { WsFlv = item?.WsFlv, HttpFlv = item?.HttpFlv, Hls = item?.Hls, WebRtc = item?.WebRtc, Rtmp = item?.Rtmp, Rtsp = item?.Rtsp }; + return new StreamUrls + { + WsFlv = item?.WsFlv, HttpFlv = item?.HttpFlv, Hls = item?.Hls, + WebRtc = item?.WebRtc, Rtmp = item?.Rtmp, Rtsp = item?.Rtsp + }; } } -// ─── Owl JSON Models ─── -public class OwlPagedResult { public List Items { get; set; } = new(); public int Total { get; set; } } -public class OwlDevice { public string? Id { get; set; } public string? Name { get; set; } public string? IsOnline { get; set; } public string? Protocol { get; set; } public string? Address { get; set; } public string? Port { get; set; } public string? Transport { get; set; } } -public class OwlPlayResponse { public List? Items { get; set; } } -public class OwlPlayItem { public string? WsFlv { get; set; } public string? HttpFlv { get; set; } public string? Hls { get; set; } public string? WebRtc { get; set; } public string? Rtmp { get; set; } public string? Rtsp { get; set; } } -public class OwlSnapshotResponse { public string? Link { get; set; } } -public class OwlRecording { public int Id { get; set; } public string? Cid { get; set; } public DateTime StartedAt { get; set; } public DateTime EndedAt { get; set; } public double Duration { get; set; } public string? Path { get; set; } public long Size { get; set; } } +// ═══════════════════════════════════════════ +// Owl JSON 反序列化模型(内部使用) +// ═══════════════════════════════════════════ + +/// Owl API 分页响应 +public class OwlPagedResult +{ + public List Items { get; set; } = new(); + public int Total { get; set; } +} + +/// Owl 设备(NVR) +public class OwlDevice +{ + public string? Id { get; set; } + public string? Name { get; set; } + public string? IsOnline { get; set; } + public string? Protocol { get; set; } + public string? Address { get; set; } + public string? Port { get; set; } + public string? Transport { get; set; } +} + +/// Owl 播放响应 +public class OwlPlayResponse +{ + public List? Items { get; set; } +} + +/// Owl 播放流条目 +public class OwlPlayItem +{ + public string? WsFlv { get; set; } + public string? HttpFlv { get; set; } + public string? Hls { get; set; } + public string? WebRtc { get; set; } + public string? Rtmp { get; set; } + public string? Rtsp { get; set; } +} + +/// Owl 截图响应 +public class OwlSnapshotResponse +{ + public string? Link { get; set; } +} + +/// Owl 录像记录 +public class OwlRecording +{ + public int Id { get; set; } + public string? Cid { get; set; } + public DateTime StartedAt { get; set; } + public DateTime EndedAt { get; set; } + public double Duration { get; set; } + public string? Path { get; set; } + public long Size { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs index 7b7fc91..37aa8be 100644 --- a/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs +++ b/gateway/src/IntegrationGateway.Adapters.Owl/OwlAuthHelper.cs @@ -5,46 +5,74 @@ using System.Net.Http.Json; namespace IntegrationGateway.Adapters.Owl; +/// +/// Owl 子系统的 RSA 加密认证辅助类。 +/// +/// 认证流程: +/// 1. GET /login/key 获取 RSA 公钥(Base64 编码) +/// 2. 用公钥加密 {username, password} JSON +/// 3. POST /login 发送加密后的凭据换取 JWT Token +/// +/// Token 在内存中缓存约 2.5 天(Owl 默认 3 天有效期),过期前自动刷新。 +/// public class OwlAuthHelper { private readonly HttpClient _http; private readonly string _baseUrl; private readonly string _username; private readonly string _password; + /// 缓存的 JWT Token private string? _token; + /// Token 过期时间(UTC) private DateTime _tokenExpiry = DateTime.MinValue; + /// 创建 Owl 认证辅助 + /// HttpClient 实例 + /// Owl 服务地址,如 http://localhost:15123 + /// Owl 登录用户名 + /// Owl 登录密码 public OwlAuthHelper(HttpClient http, string baseUrl, string username, string password) { _http = http; _baseUrl = baseUrl.TrimEnd('/'); _username = username; _password = password; } + /// + /// 获取有效的 JWT Token。如果缓存有效则直接返回,否则执行完整登录流程。 + /// public async Task GetTokenAsync() { if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; + // 第一步:获取 RSA 公钥 var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key"); var keyData = JsonSerializer.Deserialize(keyResp); var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!)); + // 第二步:RSA 加密凭据 using var rsa = RSA.Create(); rsa.ImportFromPem(publicKey); var plain = JsonSerializer.Serialize(new { username = _username, password = _password }); var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1); var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) }); + // 第三步:登录换取 Token var resp = await _http.PostAsync($"{_baseUrl}/login", new StringContent(payload, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var loginResult = await resp.Content.ReadFromJsonAsync(); _token = loginResult!.Token; - _tokenExpiry = DateTime.UtcNow.AddDays(2.5); + _tokenExpiry = DateTime.UtcNow.AddDays(2.5); // 保守设置,Owl 默认 3 天 return _token; } + /// 强制清除缓存的 Token,下次调用 GetTokenAsync 将重新登录 public void Invalidate() => _token = null; + /// + /// 创建一个已认证的 HttpClient,自动附带 Authorization: Bearer 头。 + /// 每次调用都创建一个新实例,避免状态污染。 + /// public async Task GetAuthenticatedClientAsync() { var token = await GetTokenAsync(); @@ -53,6 +81,8 @@ public class OwlAuthHelper return client; } + /// 登录密钥响应 public class LoginKeyResponse { public string? Key { get; set; } } + /// 登录响应 public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } } } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs index ed2d0b5..6483470 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs @@ -2,8 +2,13 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 元数据回写(Owl 设备改名等) +/// +/// 元数据回写接口。适用于支持管理端修改设备属性的子系统(如 Owl 设备改名)。 +/// public interface IAcceptsMetadataPush : IGatewayAdapter { + /// 向子系统回写设备元数据变更 + /// 子系统设备原始 ID + /// 变更集,仅非 null 字段会被更新 Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IGatewayAdapter.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IGatewayAdapter.cs index 25c139e..acbb199 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IGatewayAdapter.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IGatewayAdapter.cs @@ -2,12 +2,20 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 所有适配器必须实现的基础接口 +/// +/// 网关适配器基础接口。所有子系统适配器必须实现此接口。 +/// 定义了适配器的元信息、生命周期和健康检查能力。 +/// public interface IGatewayAdapter { + /// 适配器编码,格式 "类型:实例",如 "Owl:main"、"MC4:31ku" string AdapterCode { get; } + /// 人类可读的适配器显示名称 string DisplayName { get; } + /// 适配器能力声明(声明实现哪些能力接口) AdapterCapabilities Capabilities { get; } + /// 懒加载初始化(建立连接、获取认证 Token 等)。失败不阻塞网关启动。 Task InitializeAsync(); + /// 健康检查。返回 true 表示适配器及子系统可达。 Task HealthCheckAsync(); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs index 229ddea..a8a57af 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs @@ -2,11 +2,25 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 告警查询 + 确认 + 结束(MC4.0 / Owl AI 可选) +/// +/// 告警接口。适用于具有告警功能的子系统(如 MC4.0 / Owl AI 事件)。 +/// 支持告警查询、确认和结束操作。 +/// public interface IHasAlarms : IGatewayAdapter { + /// 分页查询告警列表 + /// 页码 + /// 每页条数 + /// 告警开始时间下限 + /// 告警开始时间上限 + /// 告警等级过滤(可选) + /// 告警状态过滤(可选) Task> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, string? level = null, string? state = null); + /// 确认告警(同时写回子系统) + /// 子系统告警 ID Task ConfirmAlarmAsync(string alarmId); + /// 结束告警(同时写回子系统) + /// 子系统告警 ID Task EndAlarmAsync(string alarmId); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs index 22828f1..89dd721 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs @@ -2,8 +2,16 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 扁平设备列表(Owl/门禁/道闸) +/// +/// 扁平设备列表接口。适用于设备无层级关系或层级由网关自行构建的子系统(如 Owl/门禁/道闸)。 +/// public interface IHasFlatDevices : IGatewayAdapter { + /// + /// 分页获取设备列表。 + /// + /// 页码(从 1 开始) + /// 每页条数 + /// 设备名称模糊搜索关键词 Task> GetDevicesAsync(int page, int size, string? keyword = null); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs index b70528b..d39dc5a 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs @@ -2,8 +2,12 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 自有对象树(MC4.0) +/// +/// 自有对象树接口。适用于具有层级对象树的子系统(如 MC4.0)。 +/// 返回的 DeviceTreeNode 中 Type=1 为区域节点,Type=2 为设备节点。 +/// public interface IHasOwnDeviceTree : IGatewayAdapter { + /// 获取子系统的完整对象树 Task> GetObjectTreeAsync(); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs index 09178c6..b104c16 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs @@ -2,9 +2,18 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 实时点位值 + 控制(MC4.0 动环) +/// +/// 实时点位值接口。适用于 IoT 动环类子系统(如 MC4.0)。 +/// 支持读取设备测点实时值和反向控制写值。 +/// public interface IHasPoints : IGatewayAdapter { + /// 获取指定设备的全部实时点位值 + /// 子系统设备原始 ID Task> GetRealtimeValuesAsync(string sourceDeviceId); + /// 向指定设备的指定点位写入控制值 + /// 子系统设备原始 ID + /// 点位索引 + /// 目标值 Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasRecordings.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasRecordings.cs index 07898b3..86cd3e3 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasRecordings.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasRecordings.cs @@ -2,9 +2,17 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 录像回放(Owl) +/// +/// 录像回放查询接口。适用于具有录像存储功能的子系统(如 Owl)。 +/// public interface IHasRecordings : IGatewayAdapter { + /// 分页查询录像记录 + /// 通道 ID + /// 录像开始时间下限 + /// 录像开始时间上限 + /// 页码 + /// 每页条数 Task> GetRecordingsAsync( string channelId, DateTime start, DateTime end, int page, int size); } diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs index 7b65aac..e4b171e 100644 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs @@ -2,12 +2,27 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Abstractions; -/// 视频流 + PTZ + 截图(Owl) +/// +/// 视频流接口。适用于视频监控类子系统(如 Owl)。 +/// 支持实时取流、录像回放、云台控制和截图。 +/// public interface IHasStreams : IGatewayAdapter { + /// 获取实时视频流地址 + /// 通道 ID Task GetLiveUrlAsync(string channelId); + /// 获取历史录像回放地址(HLS VOD) + /// 通道 ID + /// 回放开始时间 + /// 回放结束时间 Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); + /// 云台方向控制(continuous 模式) + /// 通道 ID + /// 方向:up/down/left/right/zoom_in/zoom_out + /// 速度 0.0-1.0 Task PtzControlAsync(string channelId, string direction, float speed); + /// 云台停止 Task PtzStopAsync(string channelId); + /// 获取通道实时截图 Task GetSnapshotAsync(string channelId); } diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs index b585dbd..f70ec92 100644 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs @@ -3,12 +3,23 @@ using IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Infrastructure; +/// +/// 适配器注册中心。管理所有子系统适配器的生命周期。 +/// 支持注册、查找、健康检查和并行初始化。 +/// 单个适配器初始化失败不影响其他适配器。 +/// public class AdapterRegistry { + /// 已注册的适配器列表 private readonly List _adapters = new(); + /// 注册一个适配器实例 public void Register(IGatewayAdapter adapter) => _adapters.Add(adapter); + /// + /// 并行初始化所有适配器。 + /// 每个适配器在独立 Task 中初始化,单个失败仅输出错误日志,不抛出异常。 + /// public async Task InitializeAllAsync() { await Task.WhenAll(_adapters.Select(a => Task.Run(async () => @@ -16,19 +27,26 @@ public class AdapterRegistry try { await a.InitializeAsync(); } catch (Exception ex) { - Console.Error.WriteLine($"[AdapterRegistry] {a.AdapterCode} init failed: {ex.Message}"); + Console.Error.WriteLine($"[AdapterRegistry] {a.AdapterCode} 初始化失败: {ex.Message}"); } }))); } + /// 所有已注册适配器(只读) public IReadOnlyList All => _adapters.AsReadOnly(); + /// 按适配器编码查找指定类型的适配器 + /// 目标能力接口类型 + /// 适配器编码,如 "Owl:main" + /// 找到的适配器实例,未找到或类型不匹配返回 null public T? FindByCode(string adapterCode) where T : class, IGatewayAdapter => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode && a is T) as T; + /// 按适配器编码查找(不限定能力类型) public IGatewayAdapter? FindByCode(string adapterCode) => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode); + /// 获取所有在线适配器 public IReadOnlyList GetOnlineAdapters() => _adapters.AsReadOnly(); } diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/GatewayClientFactory.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/GatewayClientFactory.cs index ef6c65f..4ee9790 100644 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/GatewayClientFactory.cs +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/GatewayClientFactory.cs @@ -1,22 +1,32 @@ using System.Net.Http.Json; -using System.Text; using System.Text.Json; namespace IntegrationGateway.Core.Infrastructure; +/// +/// Vol.Pro HTTP 客户端工厂。封装网关调用 Vol.Pro A 组接口的逻辑。 +/// 管理 HttpClient 生命周期和连接池复用。 +/// public class GatewayClientFactory { private readonly IHttpClientFactory _httpFactory; private readonly string _volProBaseUrl; + /// + /// 创建客户端工厂 + /// + /// ASP.NET Core IHttpClientFactory + /// Vol.Pro 后端地址,如 http://localhost:9100 public GatewayClientFactory(IHttpClientFactory httpFactory, string volProBaseUrl) { _httpFactory = httpFactory; _volProBaseUrl = volProBaseUrl.TrimEnd('/'); } + /// 创建带连接池复用的 HttpClient public HttpClient CreateClient() => _httpFactory.CreateClient("VolPro"); + /// A1: 网关注册。向 Vol.Pro 注册网关节点信息。 public async Task RegisterAsync(GatewayRegisterRequest req) { var http = CreateClient(); @@ -25,6 +35,7 @@ public class GatewayClientFactory return await resp.Content.ReadFromJsonAsync(); } + /// A2: 心跳上报。每 15 秒调用一次。 public async Task HeartbeatAsync(GatewayHeartbeatRequest req) { var http = CreateClient(); @@ -32,6 +43,7 @@ public class GatewayClientFactory return resp.IsSuccessStatusCode; } + /// A3: 设备数据同步。向 Vol.Pro 上送设备列表。 public async Task SyncDevicesAsync(string nodeCode, string token, List devices) { var http = CreateClient(); @@ -41,6 +53,7 @@ public class GatewayClientFactory return await resp.Content.ReadFromJsonAsync(); } + /// A4: 告警同步。向 Vol.Pro 上送告警列表。 public async Task SyncAlarmsAsync(string nodeCode, string token, List alarms) { var http = CreateClient(); @@ -51,16 +64,24 @@ public class GatewayClientFactory } } +/// 网关注册请求体 public class GatewayRegisterRequest { + /// 网关节点编码 public string NodeCode { get; set; } = ""; + /// 认证令牌 public string Token { get; set; } = ""; + /// 适配器类型列表(逗号分隔) public string AdapterTypes { get; set; } = ""; + /// 网关自身地址 public string BaseUrl { get; set; } = ""; } +/// 心跳请求体 public class GatewayHeartbeatRequest { + /// 网关节点编码 public string NodeCode { get; set; } = ""; + /// 认证令牌 public string Token { get; set; } = ""; } diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs index 51e1c48..bdb3d54 100644 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs @@ -1,19 +1,35 @@ namespace IntegrationGateway.Core.Infrastructure; +/// +/// 令牌桶限流器。控制对第三方子系统的请求频率,防止超出 API 配额。 +/// 每个适配器实例持有独立的限流器。 +/// +/// 算法:启动时桶内有 tokensPerSecond 个令牌,每次请求消耗一个令牌, +/// 令牌按 (1000/tokensPerSecond) 毫秒的速率补充。 +/// public class RateLimiter { private readonly SemaphoreSlim _semaphore; private readonly int _intervalMs; + /// + /// 创建限流器 + /// + /// 每秒允许的请求数(QPS) public RateLimiter(int tokensPerSecond) { _semaphore = new SemaphoreSlim(tokensPerSecond, tokensPerSecond); _intervalMs = 1000 / tokensPerSecond; } + /// + /// 等待获取一个令牌。如果当前没有可用令牌,阻塞直到有令牌被释放。 + /// + /// 取消令牌 public async Task WaitAsync(CancellationToken ct = default) { await _semaphore.WaitAsync(ct); + // 在后台任务中延迟补充令牌 _ = Task.Run(async () => { await Task.Delay(_intervalMs, ct); diff --git a/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs b/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs index ca1c36d..a3acecb 100644 --- a/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs +++ b/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs @@ -1,14 +1,27 @@ namespace IntegrationGateway.Core.Models; +/// +/// 适配器能力声明。每个适配器在注册时声明自己实现了哪些能力接口。 +/// 网关通过此声明判断适配器支持的操作,将请求路由到正确的适配器。 +/// public class AdapterCapabilities { + /// 是否支持自有对象树(如 MC4.0 的区域→设备层级树) public bool HasObjectTree { get; set; } + /// 是否支持扁平设备列表(如 Owl 的 NVR 列表) public bool HasFlatDevices { get; set; } + /// 是否支持实时点位值读取 public bool HasPoints { get; set; } + /// 是否支持视频取流 public bool HasStreams { get; set; } + /// 是否支持云台控制(PTZ) public bool HasPtz { get; set; } + /// 是否支持录像回放 public bool HasRecordings { get; set; } + /// 是否支持告警查询与处理 public bool HasAlarms { get; set; } + /// 是否接受反向控制(点位写值) public bool AcceptsControl { get; set; } + /// 是否接受元数据回写(如设备改名) public bool AcceptsMetadataPush { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs b/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs index 282b778..e5800a3 100644 --- a/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs +++ b/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs @@ -1,13 +1,26 @@ namespace IntegrationGateway.Core.Models; +/// +/// 设备树节点。用于 MC4.0 等具有层级对象树的子系统。 +/// Type=1 表示区域节点,Type=2 表示设备节点。 +/// Option 字典承载节点扩展属性。 +/// public class DeviceTreeNode { + /// 子系统原始 ID public int Id { get; set; } + /// 字符串形式的源 ID public string SourceId { get; set; } = ""; + /// 节点名称 public string Name { get; set; } = ""; + /// 节点类型:1=区域,2=设备 public int Type { get; set; } + /// MC4.0 对象类型编码 public int ObjectType { get; set; } + /// 节点标签(如 温湿度/烟雾/门磁) public string? Tag { get; set; } + /// 节点扩展属性 public Dictionary? Option { get; set; } + /// 子节点列表 public List Children { get; set; } = new(); } diff --git a/gateway/src/IntegrationGateway.Core/Models/MetadataChangeSet.cs b/gateway/src/IntegrationGateway.Core/Models/MetadataChangeSet.cs index 38a6390..8bcecf7 100644 --- a/gateway/src/IntegrationGateway.Core/Models/MetadataChangeSet.cs +++ b/gateway/src/IntegrationGateway.Core/Models/MetadataChangeSet.cs @@ -1,11 +1,21 @@ namespace IntegrationGateway.Core.Models; +/// +/// 元数据变更集。用于管理端向子系统回写设备元数据。 +/// 仅非 null 的字段会被更新到子系统。 +/// public class MetadataChangeSet { + /// 新设备名称 public string? Name { get; set; } + /// 新设备种类 public string? Category { get; set; } + /// 新设备分组 public string? Group { get; set; } + /// 新 IP 地址 public string? IpAddress { get; set; } + /// 新端口 public int? Port { get; set; } + /// 扩展属性变更 public Dictionary? Extra { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/MetadataPushResult.cs b/gateway/src/IntegrationGateway.Core/Models/MetadataPushResult.cs index 6d1c4de..9cfbd63 100644 --- a/gateway/src/IntegrationGateway.Core/Models/MetadataPushResult.cs +++ b/gateway/src/IntegrationGateway.Core/Models/MetadataPushResult.cs @@ -1,7 +1,12 @@ namespace IntegrationGateway.Core.Models; +/// +/// 元数据回写结果。 +/// public class MetadataPushResult { + /// 操作是否成功 public bool Success { get; set; } + /// 失败时的错误信息 public string? Message { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs b/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs index 90e077a..87604d5 100644 --- a/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs +++ b/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs @@ -1,7 +1,14 @@ namespace IntegrationGateway.Core.Models; +/// +/// 统一分页容器,所有适配器返回分页数据时使用。 +/// 适配器内部完成 skip/limit 到 page/size 的语义转换。 +/// +/// 分页条目的类型 public class PagedResult { + /// 当前页数据列表 public List Items { get; set; } = new(); + /// 总记录数(用于前端分页组件计算总页数) public int Total { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/PointValue.cs b/gateway/src/IntegrationGateway.Core/Models/PointValue.cs index 87b4443..110a5ea 100644 --- a/gateway/src/IntegrationGateway.Core/Models/PointValue.cs +++ b/gateway/src/IntegrationGateway.Core/Models/PointValue.cs @@ -1,10 +1,18 @@ namespace IntegrationGateway.Core.Models; +/// +/// 设备实时点位值。描述 IoT 设备的一个测点当前读数。 +/// public class PointValue { + /// 设备在子系统中的原始 ID public string SourceDeviceId { get; set; } = ""; + /// 点位索引(同一设备可能有多个测点) public int PointIndex { get; set; } + /// 当前数值 public double Value { get; set; } + /// 数据更新时间 public DateTime? UpdateTime { get; set; } + /// 数据上报间隔(秒) public int Interval { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs b/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs index 3831804..351bbeb 100644 --- a/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs +++ b/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs @@ -1,15 +1,29 @@ namespace IntegrationGateway.Core.Models; +/// +/// 统一告警模型。所有适配器的告警数据统一映射为此格式。 +/// 告警等级和状态使用中文字符串,方便前端直接展示。 +/// public class StandardAlarm { + /// 告警在子系统中的唯一 ID public string AlarmId { get; set; } = ""; + /// 关联的设备 Vol.Pro DeviceId(同步后解析) public string? DeviceId { get; set; } + /// 来源适配器标识 public string AdapterCode { get; set; } = ""; + /// 告警等级:提示 / 普通 / 重要 / 紧急 public string Level { get; set; } = "提示"; + /// 告警标题(简短描述) public string Title { get; set; } = ""; + /// 告警详细内容 public string? Content { get; set; } + /// 告警发生时间 public DateTime OccurTime { get; set; } + /// 告警状态:未确认 / 已确认 / 已结束 public string Status { get; set; } = "未确认"; + /// 告警触发时的实际值(如温度超标时的实际温度) public double? ActualValue { get; set; } + /// 告警阈值 public double? ThresholdValue { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs b/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs index f934de6..ca2f43a 100644 --- a/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs +++ b/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs @@ -1,17 +1,38 @@ namespace IntegrationGateway.Core.Models; +/// +/// 统一设备模型,网关与 Vol.Pro 之间传输设备数据的标准格式。 +/// AdapterCode + SourceId 联合唯一标识一个设备。 +/// Extra 字典承载适配器特有属性,避免污染核心字段。 +/// public class StandardDevice { + /// Vol.Pro 侧主键(同步后由 Vol.Pro 回填) public int DeviceId { get; set; } + /// 来源适配器标识,格式 "类型:实例",如 "Owl:main" public string AdapterCode { get; set; } = ""; + /// 子系统原始设备 ID(GB28181 编码 / MC4 sid) public string SourceId { get; set; } = ""; + /// 设备名称(管理员可修改字段) public string Name { get; set; } = ""; + /// 设备种类,如 摄像机/温湿度变送器(管理员可修改字段) public string Category { get; set; } = ""; + /// 设备分组,如 视频设备/IoT设备(管理员可修改字段) public string Group { get; set; } = ""; + /// 是否为父设备(有子设备) public bool IsParent { get; set; } + /// 父设备在子系统中的原始 ID,用于构建层级关系 public string? ParentSourceId { get; set; } + /// 在线状态 public bool IsOnline { get; set; } + /// 设备 IP 地址 public string? IpAddress { get; set; } + /// 设备端口号 public int? Port { get; set; } + /// + /// 适配器扩展属性 JSON。 + /// 示例:摄像机 {"owlDeviceId":"gb_xxx","protocol":"GB28181"} + /// IoT设备 {"mc4DeviceId":1001,"pointIndex":0,"unit":"℃"} + /// public Dictionary? Extra { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs b/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs index 7fe1d94..9884db0 100644 --- a/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs +++ b/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs @@ -1,12 +1,22 @@ namespace IntegrationGateway.Core.Models; +/// +/// 统一录像记录模型。描述一段视频录像的元信息。 +/// public class StandardRecording { + /// 录像记录 ID public int Id { get; set; } + /// 所属通道 ID(子系统中的通道标识) public string? ChannelId { get; set; } + /// 录像开始时间 public DateTime StartedAt { get; set; } + /// 录像结束时间 public DateTime EndedAt { get; set; } + /// 录像时长(秒) public double Duration { get; set; } + /// 录像文件路径或 URL public string? FilePath { get; set; } + /// 录像文件大小(字节) public long Size { get; set; } } diff --git a/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs b/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs index 339583a..9ce18cd 100644 --- a/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs +++ b/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs @@ -1,11 +1,21 @@ namespace IntegrationGateway.Core.Models; +/// +/// 视频流地址集合。包含多种协议格式的流地址, +/// 前端根据浏览器能力选择合适的协议播放。 +/// public class StreamUrls { + /// WebSocket-FLV 地址(低延迟,推荐) public string? WsFlv { get; set; } + /// HTTP-FLV 地址 public string? HttpFlv { get; set; } + /// HLS 地址(兼容性好,延迟较高) public string? Hls { get; set; } + /// WebRTC 地址(超低延迟) public string? WebRtc { get; set; } + /// RTMP 地址 public string? Rtmp { get; set; } + /// RTSP 地址 public string? Rtsp { get; set; } } diff --git a/gateway/src/IntegrationGateway.Host/Program.cs b/gateway/src/IntegrationGateway.Host/Program.cs index c75e03e..b49a5b7 100644 --- a/gateway/src/IntegrationGateway.Host/Program.cs +++ b/gateway/src/IntegrationGateway.Host/Program.cs @@ -2,9 +2,21 @@ 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); -// 注册 IHttpClientFactory +// ── 注册 HttpClient 工厂 ── +// 命名客户端 "VolPro":用于调用 Vol.Pro A 组接口和适配器内部 HTTP 请求 +// 连接池:最多 10 个并发连接,5 分钟生命周期 builder.Services.AddHttpClient("VolPro", c => { c.Timeout = TimeSpan.FromSeconds(30); @@ -17,20 +29,20 @@ builder.Services.AddHttpClient("VolPro", c => var app = builder.Build(); -// 读取配置 +// ── 读取配置 ── var gwCfg = app.Configuration.GetSection("Gateway"); var owlCfg = app.Configuration.GetSection("Owl"); var mc4Cfg = app.Configuration.GetSection("MC4"); -// 创建适配器注册中心 +// ── 创建适配器注册中心 ── var registry = new AdapterRegistry(); -// 创建 VolPro 客户端工厂 +// ── 创建 Vol.Pro 客户端工厂(用于 A1-A4 回调) ── var volProUrl = gwCfg["VolProBaseUrl"] ?? "http://localhost:9100"; var httpFactory = app.Services.GetRequiredService(); var clientFactory = new GatewayClientFactory(httpFactory, volProUrl); -// 注册 OwlAdapter +// ── 注册 OwlAdapter ── var owlHttp = app.Services.GetRequiredService().CreateClient("VolPro"); var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter( "Owl:main", owlHttp, @@ -40,7 +52,7 @@ var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter( ); registry.Register(owlAdapter); -// 注册 MC4Adapter +// ── 注册 MC4Adapter ── var mc4Http = app.Services.GetRequiredService().CreateClient("VolPro"); var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter( "MC4:31ku", mc4Http, @@ -48,13 +60,16 @@ var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter( ); registry.Register(mc4Adapter); -// 并行初始化适配器 +// ── 并行初始化所有适配器 ── await registry.InitializeAllAsync(); -Console.WriteLine($"[Gateway] {registry.All.Count} adapter(s) registered"); +Console.WriteLine($"[Gateway] {registry.All.Count} 个适配器已注册"); -// ═══ B 组路由 ═══ +// ═══════════════════════════════════════════════════════════════ +// B 组路由(管理端 / Vol.Pro → 网关) +// 所有路由通过适配器编码查找对应适配器,按能力接口分发请求 +// ═══════════════════════════════════════════════════════════════ -// B1: 健康检查 +// B1: 健康检查 — 返回所有适配器的健康状态和能力声明 app.MapGet("/api/gateway/health", async () => { var results = new List(); @@ -67,34 +82,34 @@ app.MapGet("/api/gateway/health", async () => return Results.Ok(results); }); -// B2: 设备列表 +// 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 '{adapter}' not found or does not support flat devices" }); + if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND", message = $"适配器 '{adapter}' 不存在或不支持扁平设备列表" }); return Results.Ok(await a.GetDevicesAsync(page, size, keyword)); }); -// B3: 对象树 +// 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 = $"Tree not supported by '{adapter}'" }); + if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED", message = $"适配器 '{adapter}' 不支持对象树" }); return Results.Ok(await a.GetObjectTreeAsync()); }); -// B6a: 实时流 +// 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 = $"Streams not supported by '{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("No stream URL returned", statusCode: 502) + ? Results.Problem("未获取到流地址", statusCode: 502) : Results.Ok(result); }); -// B6b: 回放 +// B6b: 录像回放 — 获取历史录像 HLS 地址 app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string adapter, string deviceId, DateTime start, DateTime end) => { var a = registry.FindByCode(adapter); @@ -102,7 +117,7 @@ app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string a 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); @@ -110,7 +125,7 @@ app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string return Results.Ok(await a.GetSnapshotAsync(deviceId)); }); -// B7: PTZ +// B7: 云台控制 — continuous 方向移动 + stop app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapter, string deviceId, PtzRequest req) => { var a = registry.FindByCode(adapter); @@ -120,7 +135,7 @@ app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapt return Results.Ok(); }); -// B4: 实时点值 +// B4: 实时点位值 — 获取 IoT 设备测点当前读数 app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, string deviceId) => { var a = registry.FindByCode(adapter); @@ -128,7 +143,7 @@ app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, return Results.Ok(await a.GetRealtimeValuesAsync(deviceId)); }); -// B5: 控制 +// B5: 设备控制 — 向 IoT 设备下发控制指令 app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) => { var a = registry.FindByCode(adapter); @@ -137,7 +152,7 @@ app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, Co return Results.Ok(); }); -// B8: 告警查询 +// 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); @@ -145,7 +160,7 @@ app.MapGet("/api/gateway/alarms/{adapter}", async (string adapter, int page, int return Results.Ok(await a.GetAlarmsAsync(page, size, from, to, level, state)); }); -// B9: 告警确认 +// B9: 告警确认 — 确认告警并写回子系统 app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string adapter, string alarmId) => { var a = registry.FindByCode(adapter); @@ -154,7 +169,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string ada return Results.Ok(); }); -// 告警结束 +// 告警结束 — 结束告警并写回子系统 app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter, string alarmId) => { var a = registry.FindByCode(adapter); @@ -163,7 +178,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter 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); @@ -171,27 +186,39 @@ app.MapGet("/api/gateway/recordings/{adapter}/{deviceId}", async (string adapter return Results.Ok(await a.GetRecordingsAsync(deviceId, start, end, page, size)); }); -// B3: 手动同步 +// 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 = "tree synced" }); + 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 = "devices synced" }); + return Results.Ok(new { deviceCount = dev.Total, message = "设备列表同步完成" }); } - return Results.Ok(new { message = "no sync needed" }); + return Results.Ok(new { message = "无需同步" }); }); app.Run(); -// 请求 DTO +// ═══════════════════════════════════════════════ +// B 组请求 DTO +// ═══════════════════════════════════════════════ + +/// 云台控制请求 +/// 方向:up/down/left/right/zoom_in/zoom_out/stop +/// 动作类型:continuous 或 stop +/// 速度 0.0-1.0 record PtzRequest(string? Direction, string Action, float Speed); + +/// 设备控制请求 +/// 目标设备 SourceId +/// 点位索引 +/// 目标值 record ControlRequest(string? DeviceId, int PointIndex, double Value);