全部网关代码添加详细中文注释

This commit is contained in:
2026-05-17 04:53:03 +08:00
parent df0c4cc4b2
commit 73d47cb470
26 changed files with 597 additions and 79 deletions

View File

@@ -6,19 +6,38 @@ using System.Text.Json;
namespace IntegrationGateway.Adapters.MC4; namespace IntegrationGateway.Adapters.MC4;
/// <summary>
/// MC4.0 动环监控子系统适配器。
///
/// 实现的能力接口:
/// - IHasOwnDeviceTree对象树区域→设备层级
/// - IHasPoints实时点位值读取 + 反向控制写值
/// - IHasAlarms告警查询、确认、结束
///
/// 限流2 QPSMC4.0 API 推荐值)
/// 分页转换:网关 page/size ↔ MC4.0 skip/limit
/// </summary>
public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
{ {
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly Mc4AuthHelper _auth; private readonly Mc4AuthHelper _auth;
/// <summary>令牌桶限流器2 QPS</summary>
private readonly RateLimiter _limiter = new(2); private readonly RateLimiter _limiter = new(2);
/// <summary>适配器编码,格式 "MC4:实例名"</summary>
public string AdapterCode { get; } public string AdapterCode { get; }
/// <summary>人类可读的适配器名称</summary>
public string DisplayName => $"MC4 ({AdapterCode})"; public string DisplayName => $"MC4 ({AdapterCode})";
/// <summary>适配器能力声明</summary>
public AdapterCapabilities Capabilities => new() public AdapterCapabilities Capabilities => new()
{ {
HasObjectTree = true, HasPoints = true, HasAlarms = true, AcceptsControl = true HasObjectTree = true, HasPoints = true, HasAlarms = true, AcceptsControl = true
}; };
/// <summary>创建 Mc4Adapter 实例</summary>
/// <param name="adapterCode">适配器编码</param>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">MC4.0 服务地址</param>
public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl) public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl)
{ {
AdapterCode = adapterCode; AdapterCode = adapterCode;
@@ -26,8 +45,10 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
_auth = new Mc4AuthHelper(http, baseUrl); _auth = new Mc4AuthHelper(http, baseUrl);
} }
/// <summary>初始化适配器:获取 MC4.0 Token</summary>
public async Task InitializeAsync() => await _auth.GetTokenAsync(); public async Task InitializeAsync() => await _auth.GetTokenAsync();
/// <summary>健康检查:尝试调用 MC4.0 认证接口确认可达性</summary>
public async Task<bool> HealthCheckAsync() public async Task<bool> HealthCheckAsync()
{ {
try try
@@ -39,7 +60,14 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
catch { return false; } catch { return false; }
} }
// ─── IHasOwnDeviceTree ─── // ═══════════════════════════════════════════
// IHasOwnDeviceTree 实现
// ═══════════════════════════════════════════
/// <summary>
/// 获取 MC4.0 完整对象树。
/// Type=1 的节点为区域Type=2 的节点为设备。
/// </summary>
public async Task<List<DeviceTreeNode>> GetObjectTreeAsync() public async Task<List<DeviceTreeNode>> GetObjectTreeAsync()
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -51,15 +79,24 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
return tree.Select(MapNode).ToList(); return tree.Select(MapNode).ToList();
} }
/// <summary>MC4.0 树节点 → DeviceTreeNode 映射</summary>
private static DeviceTreeNode MapNode(Mc4TreeNode n) => new() private static DeviceTreeNode MapNode(Mc4TreeNode n) => new()
{ {
Id = n.Id, SourceId = n.Id.ToString(), Name = n.Name ?? n.Id.ToString(), Id = n.Id,
Type = n.Type, ObjectType = n.ObjectType, Tag = n.Tag, SourceId = n.Id.ToString(),
Name = n.Name ?? n.Id.ToString(),
Type = n.Type,
ObjectType = n.ObjectType,
Tag = n.Tag,
Option = n.Option ?? new Dictionary<string, object?>(), Option = n.Option ?? new Dictionary<string, object?>(),
Children = n.Children?.Select(MapNode).ToList() ?? new() Children = n.Children?.Select(MapNode).ToList() ?? new()
}; };
// ─── IHasPoints ─── // ═══════════════════════════════════════════
// IHasPoints 实现
// ═══════════════════════════════════════════
/// <summary>获取指定设备的所有实时点位值</summary>
public async Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId) public async Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -72,11 +109,15 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(json)!; var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(json)!;
return values.Select(v => new PointValue return values.Select(v => new PointValue
{ {
SourceDeviceId = sourceDeviceId, PointIndex = v.Index, SourceDeviceId = sourceDeviceId,
Value = v.Value, UpdateTime = v.Time != null ? DateTime.Parse(v.Time) : null, Interval = v.Interval PointIndex = v.Index,
Value = v.Value,
UpdateTime = v.Time != null ? DateTime.Parse(v.Time) : null,
Interval = v.Interval
}).ToList(); }).ToList();
} }
/// <summary>向指定设备的指定点位写入控制值</summary>
public async Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value) public async Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -86,7 +127,14 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
new StringContent(body, Encoding.UTF8, "application/json")); new StringContent(body, Encoding.UTF8, "application/json"));
} }
// ─── IHasAlarms ─── // ═══════════════════════════════════════════
// IHasAlarms 实现
// ═══════════════════════════════════════════
/// <summary>
/// 分页查询告警列表。
/// 内部完成 page/size → skip/limit 转换。
/// </summary>
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
string? level = null, string? state = null) 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"), To = to.ToString("yyyy-MM-dd HH:mm:ss"),
Skip = (page - 1) * size, Skip = (page - 1) * size,
Limit = size, Limit = size,
Sort = 1 Sort = 1 // 按时间降序
}); });
var resp = await client.PostAsync("/api/central/alarm/query", var resp = await client.PostAsync("/api/central/alarm/query",
new StringContent(body, Encoding.UTF8, "application/json")); new StringContent(body, Encoding.UTF8, "application/json"));
@@ -109,17 +157,21 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
{ {
Items = result.List?.Select(a => new StandardAlarm Items = result.List?.Select(a => new StandardAlarm
{ {
AlarmId = a.Id ?? "", DeviceId = a.Sid?.ToString(), AlarmId = a.Id ?? "",
DeviceId = a.Sid?.ToString(),
AdapterCode = AdapterCode, 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, OccurTime = DateTime.TryParse(a.Stime, out var st) ? st : DateTime.MinValue,
Status = MapAlarmState(a.State), Status = MapAlarmState(a.State),
ActualValue = a.Soption?.Value, ThresholdValue = a.Eoption?.Value ActualValue = a.Soption?.Value,
ThresholdValue = a.Eoption?.Value
}).ToList() ?? new(), }).ToList() ?? new(),
Total = result.Total Total = result.Total
}; };
} }
/// <summary>确认告警(同时写回 MC4.0</summary>
public async Task ConfirmAlarmAsync(string alarmId) public async Task ConfirmAlarmAsync(string alarmId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -129,6 +181,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
new StringContent(body, Encoding.UTF8, "application/json")); new StringContent(body, Encoding.UTF8, "application/json"));
} }
/// <summary>结束告警(同时写回 MC4.0</summary>
public async Task EndAlarmAsync(string alarmId) public async Task EndAlarmAsync(string alarmId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -138,15 +191,29 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
new StringContent(body, Encoding.UTF8, "application/json")); new StringContent(body, Encoding.UTF8, "application/json"));
} }
private static string MapAlarmLevel(int level) => level switch { 1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示" }; /// <summary>MC4.0 告警等级数字 → 中文映射</summary>
private static string MapAlarmState(int state) => state switch { 1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认" }; private static string MapAlarmLevel(int level) => level switch
{
1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示"
};
/// <summary>MC4.0 告警状态数字 → 中文映射</summary>
private static string MapAlarmState(int state) => state switch
{
1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认"
};
} }
// ─── MC4 JSON Models ─── // ═══════════════════════════════════════════
// MC4.0 JSON 反序列化模型(内部使用)
// ═══════════════════════════════════════════
/// <summary>MC4.0 对象树节点</summary>
public class Mc4TreeNode public class Mc4TreeNode
{ {
public int Id { get; set; } public int Id { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
/// <summary>节点类型1=区域2=设备</summary>
public int Type { get; set; } public int Type { get; set; }
public int ObjectType { get; set; } public int ObjectType { get; set; }
public string? Tag { get; set; } public string? Tag { get; set; }
@@ -154,6 +221,7 @@ public class Mc4TreeNode
public List<Mc4TreeNode>? Children { get; set; } public List<Mc4TreeNode>? Children { get; set; }
} }
/// <summary>MC4.0 点位值</summary>
public class Mc4PointValue public class Mc4PointValue
{ {
public int Id { get; set; } public int Id { get; set; }
@@ -163,25 +231,32 @@ public class Mc4PointValue
public int Interval { get; set; } public int Interval { get; set; }
} }
/// <summary>MC4.0 告警查询请求体</summary>
public class Mc4AlarmQuery public class Mc4AlarmQuery
{ {
public string? Sid { get; set; } public string? Sid { get; set; }
public string From { get; set; } = ""; public string From { get; set; } = "";
public string To { get; set; } = ""; public string To { get; set; } = "";
/// <summary>跳过的记录数(= (page-1)*size</summary>
public int Skip { get; set; } public int Skip { get; set; }
/// <summary>每页条数</summary>
public int Limit { get; set; } public int Limit { get; set; }
/// <summary>排序方式1=时间降序</summary>
public int Sort { get; set; } public int Sort { get; set; }
} }
/// <summary>MC4.0 告警查询响应</summary>
public class Mc4AlarmQueryResult public class Mc4AlarmQueryResult
{ {
public int Total { get; set; } public int Total { get; set; }
public List<Mc4AlarmItem>? List { get; set; } public List<Mc4AlarmItem>? List { get; set; }
} }
/// <summary>MC4.0 告警条目</summary>
public class Mc4AlarmItem public class Mc4AlarmItem
{ {
public string? Id { get; set; } public string? Id { get; set; }
/// <summary>设备 SID</summary>
public int? Sid { get; set; } public int? Sid { get; set; }
public string? Desc { get; set; } public string? Desc { get; set; }
public string? EngDesc { get; set; } public string? EngDesc { get; set; }
@@ -192,10 +267,13 @@ public class Mc4AlarmItem
public string? Ctime { get; set; } public string? Ctime { get; set; }
public string? Cuser { get; set; } public string? Cuser { get; set; }
public int Type { get; set; } public int Type { get; set; }
/// <summary>告警触发时阈值信息</summary>
public Mc4Option? Soption { get; set; } public Mc4Option? Soption { get; set; }
/// <summary>告警结束时阈值信息</summary>
public Mc4Option? Eoption { get; set; } public Mc4Option? Eoption { get; set; }
} }
/// <summary>MC4.0 告警阈值信息</summary>
public class Mc4Option public class Mc4Option
{ {
public double? Value { get; set; } public double? Value { get; set; }

View File

@@ -2,18 +2,34 @@ using System.Text.Json;
namespace IntegrationGateway.Adapters.MC4; namespace IntegrationGateway.Adapters.MC4;
/// <summary>
/// MC4.0 子系统的 Token 认证辅助类。
///
/// 认证流程:
/// 1. POST /api/central/auth/conf/get 获取临时 Token
/// 2. Token 有效期约 8 小时,缓存在内存中
/// 3. 后续请求在 header["token"] 中携带 Token
///
/// 注意MC4.0 使用自定义 header "token" 而非标准 Authorization 头。
/// </summary>
public class Mc4AuthHelper public class Mc4AuthHelper
{ {
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly string _baseUrl; private readonly string _baseUrl;
/// <summary>缓存的认证 Token</summary>
private string? _token; private string? _token;
/// <summary>Token 过期时间UTC默认 8 小时</summary>
private DateTime _tokenExpiry = DateTime.MinValue; private DateTime _tokenExpiry = DateTime.MinValue;
/// <summary>创建 MC4.0 认证辅助</summary>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">MC4.0 服务地址</param>
public Mc4AuthHelper(HttpClient http, string baseUrl) public Mc4AuthHelper(HttpClient http, string baseUrl)
{ {
_http = http; _baseUrl = baseUrl.TrimEnd('/'); _http = http; _baseUrl = baseUrl.TrimEnd('/');
} }
/// <summary>获取有效的 Token。缓存有效则直接返回否则重新获取。</summary>
public async Task<string> GetTokenAsync() public async Task<string> GetTokenAsync()
{ {
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
@@ -27,6 +43,9 @@ public class Mc4AuthHelper
return _token!; return _token!;
} }
/// <summary>
/// 创建一个已认证的 HttpClient自动在 header["token"] 中附带 Token。
/// </summary>
public async Task<HttpClient> GetAuthenticatedClientAsync() public async Task<HttpClient> GetAuthenticatedClientAsync()
{ {
var token = await GetTokenAsync(); var token = await GetTokenAsync();
@@ -35,7 +54,9 @@ public class Mc4AuthHelper
return client; return client;
} }
/// <summary>强制清除缓存 Token</summary>
public void Invalidate() => _token = null; public void Invalidate() => _token = null;
/// <summary>MC4.0 认证响应</summary>
public class Mc4AuthResponse { public string? Token { get; set; } } public class Mc4AuthResponse { public string? Token { get; set; } }
} }

View File

@@ -6,19 +6,41 @@ using System.Net.Http.Json;
namespace IntegrationGateway.Adapters.Owl; namespace IntegrationGateway.Adapters.Owl;
/// <summary>
/// Owl 视频监控子系统适配器。
///
/// 实现的能力接口:
/// - IHasFlatDevices设备列表NVR和通道列表
/// - IHasStreams实时取流、录像回放、云台控制、截图
/// - IHasRecordings录像文件查询
/// - IAcceptsMetadataPush设备元数据回写如改名
///
/// 限流5 QPSOwl 推荐值)
/// PTZ 限制:仅支持 continuous 方向移动 + stop不支持预设位
/// </summary>
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
{ {
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly OwlAuthHelper _auth; private readonly OwlAuthHelper _auth;
/// <summary>令牌桶限流器5 QPS</summary>
private readonly RateLimiter _limiter = new(5); private readonly RateLimiter _limiter = new(5);
/// <summary>适配器编码,格式 "Owl:实例名"</summary>
public string AdapterCode { get; } public string AdapterCode { get; }
/// <summary>人类可读的适配器名称</summary>
public string DisplayName => $"Owl ({AdapterCode})"; public string DisplayName => $"Owl ({AdapterCode})";
/// <summary>适配器能力声明</summary>
public AdapterCapabilities Capabilities => new() public AdapterCapabilities Capabilities => new()
{ {
HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true
}; };
/// <summary>创建 OwlAdapter 实例</summary>
/// <param name="adapterCode">适配器编码</param>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">Owl 服务地址</param>
/// <param name="username">登录用户名</param>
/// <param name="password">登录密码</param>
public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password) public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password)
{ {
AdapterCode = adapterCode; AdapterCode = adapterCode;
@@ -26,8 +48,10 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
_auth = new OwlAuthHelper(http, baseUrl, username, password); _auth = new OwlAuthHelper(http, baseUrl, username, password);
} }
/// <summary>初始化适配器:获取 Owl JWT Token</summary>
public async Task InitializeAsync() => await _auth.GetTokenAsync(); public async Task InitializeAsync() => await _auth.GetTokenAsync();
/// <summary>健康检查:尝试访问 Owl /health 端点</summary>
public async Task<bool> HealthCheckAsync() public async Task<bool> HealthCheckAsync()
{ {
try try
@@ -39,7 +63,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
catch { return false; } catch { return false; }
} }
// ─── IHasFlatDevices ─── // ═══════════════════════════════════════════
// IHasFlatDevices 实现
// ═══════════════════════════════════════════
/// <summary>分页获取 NVR 设备列表</summary>
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null) public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -55,7 +83,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
}; };
} }
// ─── IHasStreams ─── // ═══════════════════════════════════════════
// IHasStreams 实现
// ═══════════════════════════════════════════
/// <summary>获取通道实时视频流地址</summary>
public async Task<StreamUrls> GetLiveUrlAsync(string channelId) public async Task<StreamUrls> GetLiveUrlAsync(string channelId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -67,6 +99,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
return MapStreamUrls(play); return MapStreamUrls(play);
} }
/// <summary>获取历史录像回放地址HLS VOD 格式)</summary>
public async Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end) public async Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -74,23 +107,31 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds(); var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds(); var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
var token = await _auth.GetTokenAsync(); 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}"
};
} }
/// <summary>云台方向控制continuous 模式,仅方向移动)</summary>
public async Task PtzControlAsync(string channelId, string direction, float speed) public async Task PtzControlAsync(string channelId, string direction, float speed)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync(); 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 });
} }
/// <summary>云台停止</summary>
public async Task PtzStopAsync(string channelId) public async Task PtzStopAsync(string channelId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync(); 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" });
} }
/// <summary>获取通道实时截图</summary>
public async Task<StreamUrls> GetSnapshotAsync(string channelId) public async Task<StreamUrls> GetSnapshotAsync(string channelId)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
@@ -102,23 +143,38 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
return new StreamUrls { Hls = snap.Link }; return new StreamUrls { Hls = snap.Link };
} }
// ─── IHasRecordings ─── // ═══════════════════════════════════════════
public async Task<PagedResult<StandardRecording>> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size) // IHasRecordings 实现
// ═══════════════════════════════════════════
/// <summary>分页查询录像文件记录</summary>
public async Task<PagedResult<StandardRecording>> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size)
{ {
await _limiter.WaitAsync(); await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync(); var client = await _auth.GetAuthenticatedClientAsync();
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds(); var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
var endMs = new DateTimeOffset(end).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<OwlPagedResult<OwlRecording>>(json)!; var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlRecording>>(json)!;
return new PagedResult<StandardRecording> return new PagedResult<StandardRecording>
{ {
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 Total = owl.Total
}; };
} }
// ─── IAcceptsMetadataPush ─── // ═══════════════════════════════════════════
// IAcceptsMetadataPush 实现
// ═══════════════════════════════════════════
/// <summary>回写设备元数据(如改名)到 Owl</summary>
public async Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes) public async Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes)
{ {
var client = await _auth.GetAuthenticatedClientAsync(); var client = await _auth.GetAuthenticatedClientAsync();
@@ -128,26 +184,95 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
return new MetadataPushResult { Success = true }; return new MetadataPushResult { Success = true };
} }
// ─── Mapping ─── // ═══════════════════════════════════════════
// 内部映射方法
// ═══════════════════════════════════════════
/// <summary>Owl 设备 → StandardDevice 映射</summary>
private static StandardDevice MapDevice(OwlDevice d) => new() private static StandardDevice MapDevice(OwlDevice d) => new()
{ {
SourceId = d.Id ?? "", Name = d.Name ?? d.Id ?? "", Category = "硬盘录像机", Group = "视频设备", SourceId = d.Id ?? "",
IsOnline = d.IsOnline == "1", IsParent = true, IpAddress = d.Address, 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, Port = int.TryParse(d.Port, out var port) ? port : null,
Extra = new Dictionary<string, object?> { ["owlDeviceId"] = d.Id, ["protocol"] = d.Protocol ?? "GB28181", ["transport"] = d.Transport } Extra = new Dictionary<string, object?>
{
["owlDeviceId"] = d.Id,
["protocol"] = d.Protocol ?? "GB28181",
["transport"] = d.Transport
}
}; };
/// <summary>Owl 播放响应 → StreamUrls 映射(取第一个可用流)</summary>
private static StreamUrls MapStreamUrls(OwlPlayResponse play) private static StreamUrls MapStreamUrls(OwlPlayResponse play)
{ {
var item = play.Items?.FirstOrDefault(); 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<T> { public List<T> Items { get; set; } = new(); public int Total { get; set; } } // Owl JSON 反序列化模型(内部使用)
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<OwlPlayItem>? 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; } } /// <summary>Owl API 分页响应</summary>
public class OwlSnapshotResponse { public string? Link { get; set; } } public class OwlPagedResult<T>
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; } } {
public List<T> Items { get; set; } = new();
public int Total { get; set; }
}
/// <summary>Owl 设备NVR</summary>
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; }
}
/// <summary>Owl 播放响应</summary>
public class OwlPlayResponse
{
public List<OwlPlayItem>? Items { get; set; }
}
/// <summary>Owl 播放流条目</summary>
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; }
}
/// <summary>Owl 截图响应</summary>
public class OwlSnapshotResponse
{
public string? Link { get; set; }
}
/// <summary>Owl 录像记录</summary>
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; }
}

View File

@@ -5,46 +5,74 @@ using System.Net.Http.Json;
namespace IntegrationGateway.Adapters.Owl; namespace IntegrationGateway.Adapters.Owl;
/// <summary>
/// Owl 子系统的 RSA 加密认证辅助类。
///
/// 认证流程:
/// 1. GET /login/key 获取 RSA 公钥Base64 编码)
/// 2. 用公钥加密 {username, password} JSON
/// 3. POST /login 发送加密后的凭据换取 JWT Token
///
/// Token 在内存中缓存约 2.5 天Owl 默认 3 天有效期),过期前自动刷新。
/// </summary>
public class OwlAuthHelper public class OwlAuthHelper
{ {
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly string _baseUrl; private readonly string _baseUrl;
private readonly string _username; private readonly string _username;
private readonly string _password; private readonly string _password;
/// <summary>缓存的 JWT Token</summary>
private string? _token; private string? _token;
/// <summary>Token 过期时间UTC</summary>
private DateTime _tokenExpiry = DateTime.MinValue; private DateTime _tokenExpiry = DateTime.MinValue;
/// <summary>创建 Owl 认证辅助</summary>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">Owl 服务地址,如 http://localhost:15123</param>
/// <param name="username">Owl 登录用户名</param>
/// <param name="password">Owl 登录密码</param>
public OwlAuthHelper(HttpClient http, string baseUrl, string username, string password) public OwlAuthHelper(HttpClient http, string baseUrl, string username, string password)
{ {
_http = http; _baseUrl = baseUrl.TrimEnd('/'); _http = http; _baseUrl = baseUrl.TrimEnd('/');
_username = username; _password = password; _username = username; _password = password;
} }
/// <summary>
/// 获取有效的 JWT Token。如果缓存有效则直接返回否则执行完整登录流程。
/// </summary>
public async Task<string> GetTokenAsync() public async Task<string> GetTokenAsync()
{ {
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
// 第一步:获取 RSA 公钥
var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key"); var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key");
var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(keyResp); var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(keyResp);
var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!)); var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!));
// 第二步RSA 加密凭据
using var rsa = RSA.Create(); using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey); rsa.ImportFromPem(publicKey);
var plain = JsonSerializer.Serialize(new { username = _username, password = _password }); var plain = JsonSerializer.Serialize(new { username = _username, password = _password });
var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1); var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1);
var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) }); var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) });
// 第三步:登录换取 Token
var resp = await _http.PostAsync($"{_baseUrl}/login", var resp = await _http.PostAsync($"{_baseUrl}/login",
new StringContent(payload, Encoding.UTF8, "application/json")); new StringContent(payload, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
var loginResult = await resp.Content.ReadFromJsonAsync<LoginResponse>(); var loginResult = await resp.Content.ReadFromJsonAsync<LoginResponse>();
_token = loginResult!.Token; _token = loginResult!.Token;
_tokenExpiry = DateTime.UtcNow.AddDays(2.5); _tokenExpiry = DateTime.UtcNow.AddDays(2.5); // 保守设置Owl 默认 3 天
return _token; return _token;
} }
/// <summary>强制清除缓存的 Token下次调用 GetTokenAsync 将重新登录</summary>
public void Invalidate() => _token = null; public void Invalidate() => _token = null;
/// <summary>
/// 创建一个已认证的 HttpClient自动附带 Authorization: Bearer 头。
/// 每次调用都创建一个新实例,避免状态污染。
/// </summary>
public async Task<HttpClient> GetAuthenticatedClientAsync() public async Task<HttpClient> GetAuthenticatedClientAsync()
{ {
var token = await GetTokenAsync(); var token = await GetTokenAsync();
@@ -53,6 +81,8 @@ public class OwlAuthHelper
return client; return client;
} }
/// <summary>登录密钥响应</summary>
public class LoginKeyResponse { public string? Key { get; set; } } public class LoginKeyResponse { public string? Key { get; set; } }
/// <summary>登录响应</summary>
public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } } public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } }
} }

View File

@@ -2,8 +2,13 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>元数据回写Owl 设备改名等)</summary> /// <summary>
/// 元数据回写接口。适用于支持管理端修改设备属性的子系统(如 Owl 设备改名)。
/// </summary>
public interface IAcceptsMetadataPush : IGatewayAdapter public interface IAcceptsMetadataPush : IGatewayAdapter
{ {
/// <summary>向子系统回写设备元数据变更</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="changes">变更集,仅非 null 字段会被更新</param>
Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes);
} }

View File

@@ -2,12 +2,20 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>所有适配器必须实现的基础接口</summary> /// <summary>
/// 网关适配器基础接口。所有子系统适配器必须实现此接口。
/// 定义了适配器的元信息、生命周期和健康检查能力。
/// </summary>
public interface IGatewayAdapter public interface IGatewayAdapter
{ {
/// <summary>适配器编码,格式 "类型:实例",如 "Owl:main"、"MC4:31ku"</summary>
string AdapterCode { get; } string AdapterCode { get; }
/// <summary>人类可读的适配器显示名称</summary>
string DisplayName { get; } string DisplayName { get; }
/// <summary>适配器能力声明(声明实现哪些能力接口)</summary>
AdapterCapabilities Capabilities { get; } AdapterCapabilities Capabilities { get; }
/// <summary>懒加载初始化(建立连接、获取认证 Token 等)。失败不阻塞网关启动。</summary>
Task InitializeAsync(); Task InitializeAsync();
/// <summary>健康检查。返回 true 表示适配器及子系统可达。</summary>
Task<bool> HealthCheckAsync(); Task<bool> HealthCheckAsync();
} }

View File

@@ -2,11 +2,25 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>告警查询 + 确认 + 结束MC4.0 / Owl AI 可选)</summary> /// <summary>
/// 告警接口。适用于具有告警功能的子系统(如 MC4.0 / Owl AI 事件)。
/// 支持告警查询、确认和结束操作。
/// </summary>
public interface IHasAlarms : IGatewayAdapter public interface IHasAlarms : IGatewayAdapter
{ {
/// <summary>分页查询告警列表</summary>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
/// <param name="from">告警开始时间下限</param>
/// <param name="to">告警开始时间上限</param>
/// <param name="level">告警等级过滤(可选)</param>
/// <param name="state">告警状态过滤(可选)</param>
Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
string? level = null, string? state = null); string? level = null, string? state = null);
/// <summary>确认告警(同时写回子系统)</summary>
/// <param name="alarmId">子系统告警 ID</param>
Task ConfirmAlarmAsync(string alarmId); Task ConfirmAlarmAsync(string alarmId);
/// <summary>结束告警(同时写回子系统)</summary>
/// <param name="alarmId">子系统告警 ID</param>
Task EndAlarmAsync(string alarmId); Task EndAlarmAsync(string alarmId);
} }

View File

@@ -2,8 +2,16 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>扁平设备列表Owl/门禁/道闸)</summary> /// <summary>
/// 扁平设备列表接口。适用于设备无层级关系或层级由网关自行构建的子系统(如 Owl/门禁/道闸)。
/// </summary>
public interface IHasFlatDevices : IGatewayAdapter public interface IHasFlatDevices : IGatewayAdapter
{ {
/// <summary>
/// 分页获取设备列表。
/// </summary>
/// <param name="page">页码(从 1 开始)</param>
/// <param name="size">每页条数</param>
/// <param name="keyword">设备名称模糊搜索关键词</param>
Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null); Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null);
} }

View File

@@ -2,8 +2,12 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>自有对象树MC4.0</summary> /// <summary>
/// 自有对象树接口。适用于具有层级对象树的子系统(如 MC4.0)。
/// 返回的 DeviceTreeNode 中 Type=1 为区域节点Type=2 为设备节点。
/// </summary>
public interface IHasOwnDeviceTree : IGatewayAdapter public interface IHasOwnDeviceTree : IGatewayAdapter
{ {
/// <summary>获取子系统的完整对象树</summary>
Task<List<DeviceTreeNode>> GetObjectTreeAsync(); Task<List<DeviceTreeNode>> GetObjectTreeAsync();
} }

View File

@@ -2,9 +2,18 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>实时点位值 + 控制MC4.0 动环)</summary> /// <summary>
/// 实时点位值接口。适用于 IoT 动环类子系统(如 MC4.0)。
/// 支持读取设备测点实时值和反向控制写值。
/// </summary>
public interface IHasPoints : IGatewayAdapter public interface IHasPoints : IGatewayAdapter
{ {
/// <summary>获取指定设备的全部实时点位值</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId); Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId);
/// <summary>向指定设备的指定点位写入控制值</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="pointIndex">点位索引</param>
/// <param name="value">目标值</param>
Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value);
} }

View File

@@ -2,9 +2,17 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>录像回放Owl</summary> /// <summary>
/// 录像回放查询接口。适用于具有录像存储功能的子系统(如 Owl
/// </summary>
public interface IHasRecordings : IGatewayAdapter public interface IHasRecordings : IGatewayAdapter
{ {
/// <summary>分页查询录像记录</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="start">录像开始时间下限</param>
/// <param name="end">录像开始时间上限</param>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
Task<PagedResult<StandardRecording>> GetRecordingsAsync( Task<PagedResult<StandardRecording>> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size); string channelId, DateTime start, DateTime end, int page, int size);
} }

View File

@@ -2,12 +2,27 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions; namespace IntegrationGateway.Core.Abstractions;
/// <summary>视频流 + PTZ + 截图Owl</summary> /// <summary>
/// 视频流接口。适用于视频监控类子系统(如 Owl
/// 支持实时取流、录像回放、云台控制和截图。
/// </summary>
public interface IHasStreams : IGatewayAdapter public interface IHasStreams : IGatewayAdapter
{ {
/// <summary>获取实时视频流地址</summary>
/// <param name="channelId">通道 ID</param>
Task<StreamUrls> GetLiveUrlAsync(string channelId); Task<StreamUrls> GetLiveUrlAsync(string channelId);
/// <summary>获取历史录像回放地址HLS VOD</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="start">回放开始时间</param>
/// <param name="end">回放结束时间</param>
Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end);
/// <summary>云台方向控制continuous 模式)</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="direction">方向up/down/left/right/zoom_in/zoom_out</param>
/// <param name="speed">速度 0.0-1.0</param>
Task PtzControlAsync(string channelId, string direction, float speed); Task PtzControlAsync(string channelId, string direction, float speed);
/// <summary>云台停止</summary>
Task PtzStopAsync(string channelId); Task PtzStopAsync(string channelId);
/// <summary>获取通道实时截图</summary>
Task<StreamUrls> GetSnapshotAsync(string channelId); Task<StreamUrls> GetSnapshotAsync(string channelId);
} }

View File

@@ -3,12 +3,23 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Infrastructure; namespace IntegrationGateway.Core.Infrastructure;
/// <summary>
/// 适配器注册中心。管理所有子系统适配器的生命周期。
/// 支持注册、查找、健康检查和并行初始化。
/// 单个适配器初始化失败不影响其他适配器。
/// </summary>
public class AdapterRegistry public class AdapterRegistry
{ {
/// <summary>已注册的适配器列表</summary>
private readonly List<IGatewayAdapter> _adapters = new(); private readonly List<IGatewayAdapter> _adapters = new();
/// <summary>注册一个适配器实例</summary>
public void Register(IGatewayAdapter adapter) => _adapters.Add(adapter); public void Register(IGatewayAdapter adapter) => _adapters.Add(adapter);
/// <summary>
/// 并行初始化所有适配器。
/// 每个适配器在独立 Task 中初始化,单个失败仅输出错误日志,不抛出异常。
/// </summary>
public async Task InitializeAllAsync() public async Task InitializeAllAsync()
{ {
await Task.WhenAll(_adapters.Select(a => Task.Run(async () => await Task.WhenAll(_adapters.Select(a => Task.Run(async () =>
@@ -16,19 +27,26 @@ public class AdapterRegistry
try { await a.InitializeAsync(); } try { await a.InitializeAsync(); }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"[AdapterRegistry] {a.AdapterCode} init failed: {ex.Message}"); Console.Error.WriteLine($"[AdapterRegistry] {a.AdapterCode} 初始化失败: {ex.Message}");
} }
}))); })));
} }
/// <summary>所有已注册适配器(只读)</summary>
public IReadOnlyList<IGatewayAdapter> All => _adapters.AsReadOnly(); public IReadOnlyList<IGatewayAdapter> All => _adapters.AsReadOnly();
/// <summary>按适配器编码查找指定类型的适配器</summary>
/// <typeparam name="T">目标能力接口类型</typeparam>
/// <param name="adapterCode">适配器编码,如 "Owl:main"</param>
/// <returns>找到的适配器实例,未找到或类型不匹配返回 null</returns>
public T? FindByCode<T>(string adapterCode) where T : class, IGatewayAdapter public T? FindByCode<T>(string adapterCode) where T : class, IGatewayAdapter
=> _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode && a is T) as T; => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode && a is T) as T;
/// <summary>按适配器编码查找(不限定能力类型)</summary>
public IGatewayAdapter? FindByCode(string adapterCode) public IGatewayAdapter? FindByCode(string adapterCode)
=> _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode); => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode);
/// <summary>获取所有在线适配器</summary>
public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters() public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters()
=> _adapters.AsReadOnly(); => _adapters.AsReadOnly();
} }

View File

@@ -1,22 +1,32 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace IntegrationGateway.Core.Infrastructure; namespace IntegrationGateway.Core.Infrastructure;
/// <summary>
/// Vol.Pro HTTP 客户端工厂。封装网关调用 Vol.Pro A 组接口的逻辑。
/// 管理 HttpClient 生命周期和连接池复用。
/// </summary>
public class GatewayClientFactory public class GatewayClientFactory
{ {
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly string _volProBaseUrl; private readonly string _volProBaseUrl;
/// <summary>
/// 创建客户端工厂
/// </summary>
/// <param name="httpFactory">ASP.NET Core IHttpClientFactory</param>
/// <param name="volProBaseUrl">Vol.Pro 后端地址,如 http://localhost:9100</param>
public GatewayClientFactory(IHttpClientFactory httpFactory, string volProBaseUrl) public GatewayClientFactory(IHttpClientFactory httpFactory, string volProBaseUrl)
{ {
_httpFactory = httpFactory; _httpFactory = httpFactory;
_volProBaseUrl = volProBaseUrl.TrimEnd('/'); _volProBaseUrl = volProBaseUrl.TrimEnd('/');
} }
/// <summary>创建带连接池复用的 HttpClient</summary>
public HttpClient CreateClient() => _httpFactory.CreateClient("VolPro"); public HttpClient CreateClient() => _httpFactory.CreateClient("VolPro");
/// <summary>A1: 网关注册。向 Vol.Pro 注册网关节点信息。</summary>
public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req) public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req)
{ {
var http = CreateClient(); var http = CreateClient();
@@ -25,6 +35,7 @@ public class GatewayClientFactory
return await resp.Content.ReadFromJsonAsync<JsonDocument>(); return await resp.Content.ReadFromJsonAsync<JsonDocument>();
} }
/// <summary>A2: 心跳上报。每 15 秒调用一次。</summary>
public async Task<bool> HeartbeatAsync(GatewayHeartbeatRequest req) public async Task<bool> HeartbeatAsync(GatewayHeartbeatRequest req)
{ {
var http = CreateClient(); var http = CreateClient();
@@ -32,6 +43,7 @@ public class GatewayClientFactory
return resp.IsSuccessStatusCode; return resp.IsSuccessStatusCode;
} }
/// <summary>A3: 设备数据同步。向 Vol.Pro 上送设备列表。</summary>
public async Task<JsonDocument?> SyncDevicesAsync(string nodeCode, string token, List<object> devices) public async Task<JsonDocument?> SyncDevicesAsync(string nodeCode, string token, List<object> devices)
{ {
var http = CreateClient(); var http = CreateClient();
@@ -41,6 +53,7 @@ public class GatewayClientFactory
return await resp.Content.ReadFromJsonAsync<JsonDocument>(); return await resp.Content.ReadFromJsonAsync<JsonDocument>();
} }
/// <summary>A4: 告警同步。向 Vol.Pro 上送告警列表。</summary>
public async Task<JsonDocument?> SyncAlarmsAsync(string nodeCode, string token, List<object> alarms) public async Task<JsonDocument?> SyncAlarmsAsync(string nodeCode, string token, List<object> alarms)
{ {
var http = CreateClient(); var http = CreateClient();
@@ -51,16 +64,24 @@ public class GatewayClientFactory
} }
} }
/// <summary>网关注册请求体</summary>
public class GatewayRegisterRequest public class GatewayRegisterRequest
{ {
/// <summary>网关节点编码</summary>
public string NodeCode { get; set; } = ""; public string NodeCode { get; set; } = "";
/// <summary>认证令牌</summary>
public string Token { get; set; } = ""; public string Token { get; set; } = "";
/// <summary>适配器类型列表(逗号分隔)</summary>
public string AdapterTypes { get; set; } = ""; public string AdapterTypes { get; set; } = "";
/// <summary>网关自身地址</summary>
public string BaseUrl { get; set; } = ""; public string BaseUrl { get; set; } = "";
} }
/// <summary>心跳请求体</summary>
public class GatewayHeartbeatRequest public class GatewayHeartbeatRequest
{ {
/// <summary>网关节点编码</summary>
public string NodeCode { get; set; } = ""; public string NodeCode { get; set; } = "";
/// <summary>认证令牌</summary>
public string Token { get; set; } = ""; public string Token { get; set; } = "";
} }

View File

@@ -1,19 +1,35 @@
namespace IntegrationGateway.Core.Infrastructure; namespace IntegrationGateway.Core.Infrastructure;
/// <summary>
/// 令牌桶限流器。控制对第三方子系统的请求频率,防止超出 API 配额。
/// 每个适配器实例持有独立的限流器。
///
/// 算法:启动时桶内有 tokensPerSecond 个令牌,每次请求消耗一个令牌,
/// 令牌按 (1000/tokensPerSecond) 毫秒的速率补充。
/// </summary>
public class RateLimiter public class RateLimiter
{ {
private readonly SemaphoreSlim _semaphore; private readonly SemaphoreSlim _semaphore;
private readonly int _intervalMs; private readonly int _intervalMs;
/// <summary>
/// 创建限流器
/// </summary>
/// <param name="tokensPerSecond">每秒允许的请求数QPS</param>
public RateLimiter(int tokensPerSecond) public RateLimiter(int tokensPerSecond)
{ {
_semaphore = new SemaphoreSlim(tokensPerSecond, tokensPerSecond); _semaphore = new SemaphoreSlim(tokensPerSecond, tokensPerSecond);
_intervalMs = 1000 / tokensPerSecond; _intervalMs = 1000 / tokensPerSecond;
} }
/// <summary>
/// 等待获取一个令牌。如果当前没有可用令牌,阻塞直到有令牌被释放。
/// </summary>
/// <param name="ct">取消令牌</param>
public async Task WaitAsync(CancellationToken ct = default) public async Task WaitAsync(CancellationToken ct = default)
{ {
await _semaphore.WaitAsync(ct); await _semaphore.WaitAsync(ct);
// 在后台任务中延迟补充令牌
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(_intervalMs, ct); await Task.Delay(_intervalMs, ct);

View File

@@ -1,14 +1,27 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 适配器能力声明。每个适配器在注册时声明自己实现了哪些能力接口。
/// 网关通过此声明判断适配器支持的操作,将请求路由到正确的适配器。
/// </summary>
public class AdapterCapabilities public class AdapterCapabilities
{ {
/// <summary>是否支持自有对象树(如 MC4.0 的区域→设备层级树)</summary>
public bool HasObjectTree { get; set; } public bool HasObjectTree { get; set; }
/// <summary>是否支持扁平设备列表(如 Owl 的 NVR 列表)</summary>
public bool HasFlatDevices { get; set; } public bool HasFlatDevices { get; set; }
/// <summary>是否支持实时点位值读取</summary>
public bool HasPoints { get; set; } public bool HasPoints { get; set; }
/// <summary>是否支持视频取流</summary>
public bool HasStreams { get; set; } public bool HasStreams { get; set; }
/// <summary>是否支持云台控制PTZ</summary>
public bool HasPtz { get; set; } public bool HasPtz { get; set; }
/// <summary>是否支持录像回放</summary>
public bool HasRecordings { get; set; } public bool HasRecordings { get; set; }
/// <summary>是否支持告警查询与处理</summary>
public bool HasAlarms { get; set; } public bool HasAlarms { get; set; }
/// <summary>是否接受反向控制(点位写值)</summary>
public bool AcceptsControl { get; set; } public bool AcceptsControl { get; set; }
/// <summary>是否接受元数据回写(如设备改名)</summary>
public bool AcceptsMetadataPush { get; set; } public bool AcceptsMetadataPush { get; set; }
} }

View File

@@ -1,13 +1,26 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 设备树节点。用于 MC4.0 等具有层级对象树的子系统。
/// Type=1 表示区域节点Type=2 表示设备节点。
/// Option 字典承载节点扩展属性。
/// </summary>
public class DeviceTreeNode public class DeviceTreeNode
{ {
/// <summary>子系统原始 ID</summary>
public int Id { get; set; } public int Id { get; set; }
/// <summary>字符串形式的源 ID</summary>
public string SourceId { get; set; } = ""; public string SourceId { get; set; } = "";
/// <summary>节点名称</summary>
public string Name { get; set; } = ""; public string Name { get; set; } = "";
/// <summary>节点类型1=区域2=设备</summary>
public int Type { get; set; } public int Type { get; set; }
/// <summary>MC4.0 对象类型编码</summary>
public int ObjectType { get; set; } public int ObjectType { get; set; }
/// <summary>节点标签(如 温湿度/烟雾/门磁)</summary>
public string? Tag { get; set; } public string? Tag { get; set; }
/// <summary>节点扩展属性</summary>
public Dictionary<string, object?>? Option { get; set; } public Dictionary<string, object?>? Option { get; set; }
/// <summary>子节点列表</summary>
public List<DeviceTreeNode> Children { get; set; } = new(); public List<DeviceTreeNode> Children { get; set; } = new();
} }

View File

@@ -1,11 +1,21 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 元数据变更集。用于管理端向子系统回写设备元数据。
/// 仅非 null 的字段会被更新到子系统。
/// </summary>
public class MetadataChangeSet public class MetadataChangeSet
{ {
/// <summary>新设备名称</summary>
public string? Name { get; set; } public string? Name { get; set; }
/// <summary>新设备种类</summary>
public string? Category { get; set; } public string? Category { get; set; }
/// <summary>新设备分组</summary>
public string? Group { get; set; } public string? Group { get; set; }
/// <summary>新 IP 地址</summary>
public string? IpAddress { get; set; } public string? IpAddress { get; set; }
/// <summary>新端口</summary>
public int? Port { get; set; } public int? Port { get; set; }
/// <summary>扩展属性变更</summary>
public Dictionary<string, object?>? Extra { get; set; } public Dictionary<string, object?>? Extra { get; set; }
} }

View File

@@ -1,7 +1,12 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 元数据回写结果。
/// </summary>
public class MetadataPushResult public class MetadataPushResult
{ {
/// <summary>操作是否成功</summary>
public bool Success { get; set; } public bool Success { get; set; }
/// <summary>失败时的错误信息</summary>
public string? Message { get; set; } public string? Message { get; set; }
} }

View File

@@ -1,7 +1,14 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 统一分页容器,所有适配器返回分页数据时使用。
/// 适配器内部完成 skip/limit 到 page/size 的语义转换。
/// </summary>
/// <typeparam name="T">分页条目的类型</typeparam>
public class PagedResult<T> public class PagedResult<T>
{ {
/// <summary>当前页数据列表</summary>
public List<T> Items { get; set; } = new(); public List<T> Items { get; set; } = new();
/// <summary>总记录数(用于前端分页组件计算总页数)</summary>
public int Total { get; set; } public int Total { get; set; }
} }

View File

@@ -1,10 +1,18 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 设备实时点位值。描述 IoT 设备的一个测点当前读数。
/// </summary>
public class PointValue public class PointValue
{ {
/// <summary>设备在子系统中的原始 ID</summary>
public string SourceDeviceId { get; set; } = ""; public string SourceDeviceId { get; set; } = "";
/// <summary>点位索引(同一设备可能有多个测点)</summary>
public int PointIndex { get; set; } public int PointIndex { get; set; }
/// <summary>当前数值</summary>
public double Value { get; set; } public double Value { get; set; }
/// <summary>数据更新时间</summary>
public DateTime? UpdateTime { get; set; } public DateTime? UpdateTime { get; set; }
/// <summary>数据上报间隔(秒)</summary>
public int Interval { get; set; } public int Interval { get; set; }
} }

View File

@@ -1,15 +1,29 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 统一告警模型。所有适配器的告警数据统一映射为此格式。
/// 告警等级和状态使用中文字符串,方便前端直接展示。
/// </summary>
public class StandardAlarm public class StandardAlarm
{ {
/// <summary>告警在子系统中的唯一 ID</summary>
public string AlarmId { get; set; } = ""; public string AlarmId { get; set; } = "";
/// <summary>关联的设备 Vol.Pro DeviceId同步后解析</summary>
public string? DeviceId { get; set; } public string? DeviceId { get; set; }
/// <summary>来源适配器标识</summary>
public string AdapterCode { get; set; } = ""; public string AdapterCode { get; set; } = "";
/// <summary>告警等级:提示 / 普通 / 重要 / 紧急</summary>
public string Level { get; set; } = "提示"; public string Level { get; set; } = "提示";
/// <summary>告警标题(简短描述)</summary>
public string Title { get; set; } = ""; public string Title { get; set; } = "";
/// <summary>告警详细内容</summary>
public string? Content { get; set; } public string? Content { get; set; }
/// <summary>告警发生时间</summary>
public DateTime OccurTime { get; set; } public DateTime OccurTime { get; set; }
/// <summary>告警状态:未确认 / 已确认 / 已结束</summary>
public string Status { get; set; } = "未确认"; public string Status { get; set; } = "未确认";
/// <summary>告警触发时的实际值(如温度超标时的实际温度)</summary>
public double? ActualValue { get; set; } public double? ActualValue { get; set; }
/// <summary>告警阈值</summary>
public double? ThresholdValue { get; set; } public double? ThresholdValue { get; set; }
} }

View File

@@ -1,17 +1,38 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 统一设备模型,网关与 Vol.Pro 之间传输设备数据的标准格式。
/// AdapterCode + SourceId 联合唯一标识一个设备。
/// Extra 字典承载适配器特有属性,避免污染核心字段。
/// </summary>
public class StandardDevice public class StandardDevice
{ {
/// <summary>Vol.Pro 侧主键(同步后由 Vol.Pro 回填)</summary>
public int DeviceId { get; set; } public int DeviceId { get; set; }
/// <summary>来源适配器标识,格式 "类型:实例",如 "Owl:main"</summary>
public string AdapterCode { get; set; } = ""; public string AdapterCode { get; set; } = "";
/// <summary>子系统原始设备 IDGB28181 编码 / MC4 sid</summary>
public string SourceId { get; set; } = ""; public string SourceId { get; set; } = "";
/// <summary>设备名称(管理员可修改字段)</summary>
public string Name { get; set; } = ""; public string Name { get; set; } = "";
/// <summary>设备种类,如 摄像机/温湿度变送器(管理员可修改字段)</summary>
public string Category { get; set; } = ""; public string Category { get; set; } = "";
/// <summary>设备分组,如 视频设备/IoT设备管理员可修改字段</summary>
public string Group { get; set; } = ""; public string Group { get; set; } = "";
/// <summary>是否为父设备(有子设备)</summary>
public bool IsParent { get; set; } public bool IsParent { get; set; }
/// <summary>父设备在子系统中的原始 ID用于构建层级关系</summary>
public string? ParentSourceId { get; set; } public string? ParentSourceId { get; set; }
/// <summary>在线状态</summary>
public bool IsOnline { get; set; } public bool IsOnline { get; set; }
/// <summary>设备 IP 地址</summary>
public string? IpAddress { get; set; } public string? IpAddress { get; set; }
/// <summary>设备端口号</summary>
public int? Port { get; set; } public int? Port { get; set; }
/// <summary>
/// 适配器扩展属性 JSON。
/// 示例:摄像机 {"owlDeviceId":"gb_xxx","protocol":"GB28181"}
/// IoT设备 {"mc4DeviceId":1001,"pointIndex":0,"unit":"℃"}
/// </summary>
public Dictionary<string, object?>? Extra { get; set; } public Dictionary<string, object?>? Extra { get; set; }
} }

View File

@@ -1,12 +1,22 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 统一录像记录模型。描述一段视频录像的元信息。
/// </summary>
public class StandardRecording public class StandardRecording
{ {
/// <summary>录像记录 ID</summary>
public int Id { get; set; } public int Id { get; set; }
/// <summary>所属通道 ID子系统中的通道标识</summary>
public string? ChannelId { get; set; } public string? ChannelId { get; set; }
/// <summary>录像开始时间</summary>
public DateTime StartedAt { get; set; } public DateTime StartedAt { get; set; }
/// <summary>录像结束时间</summary>
public DateTime EndedAt { get; set; } public DateTime EndedAt { get; set; }
/// <summary>录像时长(秒)</summary>
public double Duration { get; set; } public double Duration { get; set; }
/// <summary>录像文件路径或 URL</summary>
public string? FilePath { get; set; } public string? FilePath { get; set; }
/// <summary>录像文件大小(字节)</summary>
public long Size { get; set; } public long Size { get; set; }
} }

View File

@@ -1,11 +1,21 @@
namespace IntegrationGateway.Core.Models; namespace IntegrationGateway.Core.Models;
/// <summary>
/// 视频流地址集合。包含多种协议格式的流地址,
/// 前端根据浏览器能力选择合适的协议播放。
/// </summary>
public class StreamUrls public class StreamUrls
{ {
/// <summary>WebSocket-FLV 地址(低延迟,推荐)</summary>
public string? WsFlv { get; set; } public string? WsFlv { get; set; }
/// <summary>HTTP-FLV 地址</summary>
public string? HttpFlv { get; set; } public string? HttpFlv { get; set; }
/// <summary>HLS 地址(兼容性好,延迟较高)</summary>
public string? Hls { get; set; } public string? Hls { get; set; }
/// <summary>WebRTC 地址(超低延迟)</summary>
public string? WebRtc { get; set; } public string? WebRtc { get; set; }
/// <summary>RTMP 地址</summary>
public string? Rtmp { get; set; } public string? Rtmp { get; set; }
/// <summary>RTSP 地址</summary>
public string? Rtsp { get; set; } public string? Rtsp { get; set; }
} }

View File

@@ -2,9 +2,21 @@ using IntegrationGateway.Core.Abstractions;
using IntegrationGateway.Core.Infrastructure; using IntegrationGateway.Core.Infrastructure;
using IntegrationGateway.Core.Models; using IntegrationGateway.Core.Models;
// ═══════════════════════════════════════════════════════════════
// IntegrationGateway 宿主启动程序
//
// 职责:
// 1. 注册 IHttpClientFactory连接池复用
// 2. 创建并注册 OwlAdapter + MC4Adapter
// 3. 并行初始化所有适配器
// 4. 注册 14 个 B 组 REST 端点
// ═══════════════════════════════════════════════════════════════
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// 注册 IHttpClientFactory // ── 注册 HttpClient 工厂 ──
// 命名客户端 "VolPro":用于调用 Vol.Pro A 组接口和适配器内部 HTTP 请求
// 连接池:最多 10 个并发连接5 分钟生命周期
builder.Services.AddHttpClient("VolPro", c => builder.Services.AddHttpClient("VolPro", c =>
{ {
c.Timeout = TimeSpan.FromSeconds(30); c.Timeout = TimeSpan.FromSeconds(30);
@@ -17,20 +29,20 @@ builder.Services.AddHttpClient("VolPro", c =>
var app = builder.Build(); var app = builder.Build();
// 读取配置 // ── 读取配置 ──
var gwCfg = app.Configuration.GetSection("Gateway"); var gwCfg = app.Configuration.GetSection("Gateway");
var owlCfg = app.Configuration.GetSection("Owl"); var owlCfg = app.Configuration.GetSection("Owl");
var mc4Cfg = app.Configuration.GetSection("MC4"); var mc4Cfg = app.Configuration.GetSection("MC4");
// 创建适配器注册中心 // ── 创建适配器注册中心 ──
var registry = new AdapterRegistry(); var registry = new AdapterRegistry();
// 创建 VolPro 客户端工厂 // ── 创建 Vol.Pro 客户端工厂(用于 A1-A4 回调) ──
var volProUrl = gwCfg["VolProBaseUrl"] ?? "http://localhost:9100"; var volProUrl = gwCfg["VolProBaseUrl"] ?? "http://localhost:9100";
var httpFactory = app.Services.GetRequiredService<IHttpClientFactory>(); var httpFactory = app.Services.GetRequiredService<IHttpClientFactory>();
var clientFactory = new GatewayClientFactory(httpFactory, volProUrl); var clientFactory = new GatewayClientFactory(httpFactory, volProUrl);
// 注册 OwlAdapter // ── 注册 OwlAdapter ──
var owlHttp = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro"); var owlHttp = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro");
var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter( var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter(
"Owl:main", owlHttp, "Owl:main", owlHttp,
@@ -40,7 +52,7 @@ var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter(
); );
registry.Register(owlAdapter); registry.Register(owlAdapter);
// 注册 MC4Adapter // ── 注册 MC4Adapter ──
var mc4Http = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro"); var mc4Http = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro");
var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter( var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter(
"MC4:31ku", mc4Http, "MC4:31ku", mc4Http,
@@ -48,13 +60,16 @@ var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter(
); );
registry.Register(mc4Adapter); registry.Register(mc4Adapter);
// 并行初始化适配器 // ── 并行初始化所有适配器 ──
await registry.InitializeAllAsync(); 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 () => app.MapGet("/api/gateway/health", async () =>
{ {
var results = new List<object>(); var results = new List<object>();
@@ -67,34 +82,34 @@ app.MapGet("/api/gateway/health", async () =>
return Results.Ok(results); return Results.Ok(results);
}); });
// B2: 设备列表 // B2: 设备列表 — 分页获取扁平设备列表Owl/门禁/道闸)
app.MapGet("/api/gateway/devices", async (string adapter, int page, int size, string? keyword) => app.MapGet("/api/gateway/devices", async (string adapter, int page, int size, string? keyword) =>
{ {
var a = registry.FindByCode<IHasFlatDevices>(adapter); var a = registry.FindByCode<IHasFlatDevices>(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)); return Results.Ok(await a.GetDevicesAsync(page, size, keyword));
}); });
// B3: 对象树 // B3: 对象树 — 获取层级对象树MC4.0
app.MapGet("/api/gateway/tree", async (string adapter) => app.MapGet("/api/gateway/tree", async (string adapter) =>
{ {
var a = registry.FindByCode<IHasOwnDeviceTree>(adapter); var a = registry.FindByCode<IHasOwnDeviceTree>(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()); return Results.Ok(await a.GetObjectTreeAsync());
}); });
// B6a: 实时 // B6a: 实时取流 — 获取视频通道的实时流地址
app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/live", async (string adapter, string deviceId) => app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/live", async (string adapter, string deviceId) =>
{ {
var a = registry.FindByCode<IHasStreams>(adapter); var a = registry.FindByCode<IHasStreams>(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); var result = await a.GetLiveUrlAsync(deviceId);
return result.WsFlv == null && result.Hls == null return result.WsFlv == null && result.Hls == null
? Results.Problem("No stream URL returned", statusCode: 502) ? Results.Problem("未获取到流地址", statusCode: 502)
: Results.Ok(result); : Results.Ok(result);
}); });
// B6b: 回放 // B6b: 录像回放 — 获取历史录像 HLS 地址
app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string adapter, string deviceId, DateTime start, DateTime end) => app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string adapter, string deviceId, DateTime start, DateTime end) =>
{ {
var a = registry.FindByCode<IHasStreams>(adapter); var a = registry.FindByCode<IHasStreams>(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)); return Results.Ok(await a.GetPlaybackUrlAsync(deviceId, start, end));
}); });
// 截图 // 截图 — 获取通道实时截图
app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string adapter, string deviceId) => app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string adapter, string deviceId) =>
{ {
var a = registry.FindByCode<IHasStreams>(adapter); var a = registry.FindByCode<IHasStreams>(adapter);
@@ -110,7 +125,7 @@ app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string
return Results.Ok(await a.GetSnapshotAsync(deviceId)); 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) => app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapter, string deviceId, PtzRequest req) =>
{ {
var a = registry.FindByCode<IHasStreams>(adapter); var a = registry.FindByCode<IHasStreams>(adapter);
@@ -120,7 +135,7 @@ app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapt
return Results.Ok(); return Results.Ok();
}); });
// B4: 实时点 // B4: 实时点位值 — 获取 IoT 设备测点当前读数
app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, string deviceId) => app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, string deviceId) =>
{ {
var a = registry.FindByCode<IHasPoints>(adapter); var a = registry.FindByCode<IHasPoints>(adapter);
@@ -128,7 +143,7 @@ app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter,
return Results.Ok(await a.GetRealtimeValuesAsync(deviceId)); return Results.Ok(await a.GetRealtimeValuesAsync(deviceId));
}); });
// B5: 控制 // B5: 设备控制 — 向 IoT 设备下发控制指令
app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) => app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) =>
{ {
var a = registry.FindByCode<IHasPoints>(adapter); var a = registry.FindByCode<IHasPoints>(adapter);
@@ -137,7 +152,7 @@ app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, Co
return Results.Ok(); 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) => 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<IHasAlarms>(adapter); var a = registry.FindByCode<IHasAlarms>(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)); 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) => app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string adapter, string alarmId) =>
{ {
var a = registry.FindByCode<IHasAlarms>(adapter); var a = registry.FindByCode<IHasAlarms>(adapter);
@@ -154,7 +169,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string ada
return Results.Ok(); return Results.Ok();
}); });
// 告警结束 // 告警结束 — 结束告警并写回子系统
app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter, string alarmId) => app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter, string alarmId) =>
{ {
var a = registry.FindByCode<IHasAlarms>(adapter); var a = registry.FindByCode<IHasAlarms>(adapter);
@@ -163,7 +178,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter
return Results.Ok(); return Results.Ok();
}); });
// 录像 // 录像查询 — 分页获取录像文件列表
app.MapGet("/api/gateway/recordings/{adapter}/{deviceId}", async (string adapter, string deviceId, DateTime start, DateTime end, int page, int size) => app.MapGet("/api/gateway/recordings/{adapter}/{deviceId}", async (string adapter, string deviceId, DateTime start, DateTime end, int page, int size) =>
{ {
var a = registry.FindByCode<IHasRecordings>(adapter); var a = registry.FindByCode<IHasRecordings>(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)); return Results.Ok(await a.GetRecordingsAsync(deviceId, start, end, page, size));
}); });
// B3: 手动同步 // B3: 手动同步 — 触发适配器全量设备同步
app.MapPost("/api/gateway/devices/sync", async (string adapter) => app.MapPost("/api/gateway/devices/sync", async (string adapter) =>
{ {
var a = registry.FindByCode<IGatewayAdapter>(adapter); var a = registry.FindByCode<IGatewayAdapter>(adapter);
if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND" }); if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND" });
// 根据适配器能力触发对应同步 // 根据适配器能力触发对应同步逻辑
if (a is IHasOwnDeviceTree tree) if (a is IHasOwnDeviceTree tree)
{ {
var obj = await tree.GetObjectTreeAsync(); 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) if (a is IHasFlatDevices flat)
{ {
var dev = await flat.GetDevicesAsync(1, 1000); 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(); app.Run();
// 请求 DTO // ═══════════════════════════════════════════════
// B 组请求 DTO
// ═══════════════════════════════════════════════
/// <summary>云台控制请求</summary>
/// <param name="Direction">方向up/down/left/right/zoom_in/zoom_out/stop</param>
/// <param name="Action">动作类型continuous 或 stop</param>
/// <param name="Speed">速度 0.0-1.0</param>
record PtzRequest(string? Direction, string Action, float Speed); record PtzRequest(string? Direction, string Action, float Speed);
/// <summary>设备控制请求</summary>
/// <param name="DeviceId">目标设备 SourceId</param>
/// <param name="PointIndex">点位索引</param>
/// <param name="Value">目标值</param>
record ControlRequest(string? DeviceId, int PointIndex, double Value); record ControlRequest(string? DeviceId, int PointIndex, double Value);