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