全部网关代码添加详细中文注释
This commit is contained in:
@@ -6,19 +6,38 @@ using System.Text.Json;
|
||||
|
||||
namespace IntegrationGateway.Adapters.MC4;
|
||||
|
||||
/// <summary>
|
||||
/// MC4.0 动环监控子系统适配器。
|
||||
///
|
||||
/// 实现的能力接口:
|
||||
/// - IHasOwnDeviceTree:对象树(区域→设备层级)
|
||||
/// - IHasPoints:实时点位值读取 + 反向控制写值
|
||||
/// - IHasAlarms:告警查询、确认、结束
|
||||
///
|
||||
/// 限流:2 QPS(MC4.0 API 推荐值)
|
||||
/// 分页转换:网关 page/size ↔ MC4.0 skip/limit
|
||||
/// </summary>
|
||||
public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly Mc4AuthHelper _auth;
|
||||
/// <summary>令牌桶限流器(2 QPS)</summary>
|
||||
private readonly RateLimiter _limiter = new(2);
|
||||
|
||||
/// <summary>适配器编码,格式 "MC4:实例名"</summary>
|
||||
public string AdapterCode { get; }
|
||||
/// <summary>人类可读的适配器名称</summary>
|
||||
public string DisplayName => $"MC4 ({AdapterCode})";
|
||||
/// <summary>适配器能力声明</summary>
|
||||
public AdapterCapabilities Capabilities => new()
|
||||
{
|
||||
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)
|
||||
{
|
||||
AdapterCode = adapterCode;
|
||||
@@ -26,8 +45,10 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
_auth = new Mc4AuthHelper(http, baseUrl);
|
||||
}
|
||||
|
||||
/// <summary>初始化适配器:获取 MC4.0 Token</summary>
|
||||
public async Task InitializeAsync() => await _auth.GetTokenAsync();
|
||||
|
||||
/// <summary>健康检查:尝试调用 MC4.0 认证接口确认可达性</summary>
|
||||
public async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
@@ -39,7 +60,14 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// ─── IHasOwnDeviceTree ───
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasOwnDeviceTree 实现
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 获取 MC4.0 完整对象树。
|
||||
/// Type=1 的节点为区域,Type=2 的节点为设备。
|
||||
/// </summary>
|
||||
public async Task<List<DeviceTreeNode>> GetObjectTreeAsync()
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
@@ -51,15 +79,24 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
return tree.Select(MapNode).ToList();
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 树节点 → DeviceTreeNode 映射</summary>
|
||||
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<string, object?>(),
|
||||
Children = n.Children?.Select(MapNode).ToList() ?? new()
|
||||
};
|
||||
|
||||
// ─── IHasPoints ───
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasPoints 实现
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>获取指定设备的所有实时点位值</summary>
|
||||
public async Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
@@ -72,11 +109,15 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(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();
|
||||
}
|
||||
|
||||
/// <summary>向指定设备的指定点位写入控制值</summary>
|
||||
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 实现
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询告警列表。
|
||||
/// 内部完成 page/size → skip/limit 转换。
|
||||
/// </summary>
|
||||
public async Task<PagedResult<StandardAlarm>> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>确认告警(同时写回 MC4.0)</summary>
|
||||
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"));
|
||||
}
|
||||
|
||||
/// <summary>结束告警(同时写回 MC4.0)</summary>
|
||||
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 => "已结束", _ => "未确认" };
|
||||
/// <summary>MC4.0 告警等级数字 → 中文映射</summary>
|
||||
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 int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
/// <summary>节点类型:1=区域,2=设备</summary>
|
||||
public int Type { get; set; }
|
||||
public int ObjectType { get; set; }
|
||||
public string? Tag { get; set; }
|
||||
@@ -154,6 +221,7 @@ public class Mc4TreeNode
|
||||
public List<Mc4TreeNode>? Children { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 点位值</summary>
|
||||
public class Mc4PointValue
|
||||
{
|
||||
public int Id { get; set; }
|
||||
@@ -163,25 +231,32 @@ public class Mc4PointValue
|
||||
public int Interval { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 告警查询请求体</summary>
|
||||
public class Mc4AlarmQuery
|
||||
{
|
||||
public string? Sid { get; set; }
|
||||
public string From { get; set; } = "";
|
||||
public string To { get; set; } = "";
|
||||
/// <summary>跳过的记录数(= (page-1)*size)</summary>
|
||||
public int Skip { get; set; }
|
||||
/// <summary>每页条数</summary>
|
||||
public int Limit { get; set; }
|
||||
/// <summary>排序方式:1=时间降序</summary>
|
||||
public int Sort { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 告警查询响应</summary>
|
||||
public class Mc4AlarmQueryResult
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public List<Mc4AlarmItem>? List { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 告警条目</summary>
|
||||
public class Mc4AlarmItem
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
/// <summary>设备 SID</summary>
|
||||
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; }
|
||||
/// <summary>告警触发时阈值信息</summary>
|
||||
public Mc4Option? Soption { get; set; }
|
||||
/// <summary>告警结束时阈值信息</summary>
|
||||
public Mc4Option? Eoption { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>MC4.0 告警阈值信息</summary>
|
||||
public class Mc4Option
|
||||
{
|
||||
public double? Value { get; set; }
|
||||
|
||||
@@ -2,18 +2,34 @@ using System.Text.Json;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _baseUrl;
|
||||
/// <summary>缓存的认证 Token</summary>
|
||||
private string? _token;
|
||||
/// <summary>Token 过期时间(UTC),默认 8 小时</summary>
|
||||
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)
|
||||
{
|
||||
_http = http; _baseUrl = baseUrl.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>获取有效的 Token。缓存有效则直接返回,否则重新获取。</summary>
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
|
||||
@@ -27,6 +43,9 @@ public class Mc4AuthHelper
|
||||
return _token!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个已认证的 HttpClient,自动在 header["token"] 中附带 Token。
|
||||
/// </summary>
|
||||
public async Task<HttpClient> GetAuthenticatedClientAsync()
|
||||
{
|
||||
var token = await GetTokenAsync();
|
||||
@@ -35,7 +54,9 @@ public class Mc4AuthHelper
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>强制清除缓存 Token</summary>
|
||||
public void Invalidate() => _token = null;
|
||||
|
||||
/// <summary>MC4.0 认证响应</summary>
|
||||
public class Mc4AuthResponse { public string? Token { get; set; } }
|
||||
}
|
||||
|
||||
@@ -6,19 +6,41 @@ using System.Net.Http.Json;
|
||||
|
||||
namespace IntegrationGateway.Adapters.Owl;
|
||||
|
||||
/// <summary>
|
||||
/// Owl 视频监控子系统适配器。
|
||||
///
|
||||
/// 实现的能力接口:
|
||||
/// - IHasFlatDevices:设备列表(NVR)和通道列表
|
||||
/// - IHasStreams:实时取流、录像回放、云台控制、截图
|
||||
/// - IHasRecordings:录像文件查询
|
||||
/// - IAcceptsMetadataPush:设备元数据回写(如改名)
|
||||
///
|
||||
/// 限流:5 QPS(Owl 推荐值)
|
||||
/// PTZ 限制:仅支持 continuous 方向移动 + stop,不支持预设位
|
||||
/// </summary>
|
||||
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly OwlAuthHelper _auth;
|
||||
/// <summary>令牌桶限流器(5 QPS)</summary>
|
||||
private readonly RateLimiter _limiter = new(5);
|
||||
|
||||
/// <summary>适配器编码,格式 "Owl:实例名"</summary>
|
||||
public string AdapterCode { get; }
|
||||
/// <summary>人类可读的适配器名称</summary>
|
||||
public string DisplayName => $"Owl ({AdapterCode})";
|
||||
/// <summary>适配器能力声明</summary>
|
||||
public AdapterCapabilities Capabilities => new()
|
||||
{
|
||||
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)
|
||||
{
|
||||
AdapterCode = adapterCode;
|
||||
@@ -26,8 +48,10 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
_auth = new OwlAuthHelper(http, baseUrl, username, password);
|
||||
}
|
||||
|
||||
/// <summary>初始化适配器:获取 Owl JWT Token</summary>
|
||||
public async Task InitializeAsync() => await _auth.GetTokenAsync();
|
||||
|
||||
/// <summary>健康检查:尝试访问 Owl /health 端点</summary>
|
||||
public async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
@@ -39,7 +63,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// ─── IHasFlatDevices ───
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasFlatDevices 实现
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>分页获取 NVR 设备列表</summary>
|
||||
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
@@ -67,6 +99,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
return MapStreamUrls(play);
|
||||
}
|
||||
|
||||
/// <summary>获取历史录像回放地址(HLS VOD 格式)</summary>
|
||||
public async Task<StreamUrls> 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}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>云台方向控制(continuous 模式,仅方向移动)</summary>
|
||||
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 });
|
||||
}
|
||||
|
||||
/// <summary>云台停止</summary>
|
||||
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" });
|
||||
}
|
||||
|
||||
/// <summary>获取通道实时截图</summary>
|
||||
public async Task<StreamUrls> 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<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();
|
||||
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<OwlPagedResult<OwlRecording>>(json)!;
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// ─── IAcceptsMetadataPush ───
|
||||
// ═══════════════════════════════════════════
|
||||
// IAcceptsMetadataPush 实现
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>回写设备元数据(如改名)到 Owl</summary>
|
||||
public async Task<MetadataPushResult> 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 ───
|
||||
// ═══════════════════════════════════════════
|
||||
// 内部映射方法
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 设备 → StandardDevice 映射</summary>
|
||||
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<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)
|
||||
{
|
||||
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; } }
|
||||
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; } }
|
||||
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 反序列化模型(内部使用)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl API 分页响应</summary>
|
||||
public class OwlPagedResult<T>
|
||||
{
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -5,46 +5,74 @@ using System.Net.Http.Json;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
/// <summary>缓存的 JWT Token</summary>
|
||||
private string? _token;
|
||||
/// <summary>Token 过期时间(UTC)</summary>
|
||||
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)
|
||||
{
|
||||
_http = http; _baseUrl = baseUrl.TrimEnd('/');
|
||||
_username = username; _password = password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取有效的 JWT Token。如果缓存有效则直接返回,否则执行完整登录流程。
|
||||
/// </summary>
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
|
||||
|
||||
// 第一步:获取 RSA 公钥
|
||||
var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key");
|
||||
var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(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<LoginResponse>();
|
||||
_token = loginResult!.Token;
|
||||
_tokenExpiry = DateTime.UtcNow.AddDays(2.5);
|
||||
_tokenExpiry = DateTime.UtcNow.AddDays(2.5); // 保守设置,Owl 默认 3 天
|
||||
return _token;
|
||||
}
|
||||
|
||||
/// <summary>强制清除缓存的 Token,下次调用 GetTokenAsync 将重新登录</summary>
|
||||
public void Invalidate() => _token = null;
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个已认证的 HttpClient,自动附带 Authorization: Bearer 头。
|
||||
/// 每次调用都创建一个新实例,避免状态污染。
|
||||
/// </summary>
|
||||
public async Task<HttpClient> GetAuthenticatedClientAsync()
|
||||
{
|
||||
var token = await GetTokenAsync();
|
||||
@@ -53,6 +81,8 @@ public class OwlAuthHelper
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>登录密钥响应</summary>
|
||||
public class LoginKeyResponse { public string? Key { get; set; } }
|
||||
/// <summary>登录响应</summary>
|
||||
public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,13 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>元数据回写(Owl 设备改名等)</summary>
|
||||
/// <summary>
|
||||
/// 元数据回写接口。适用于支持管理端修改设备属性的子系统(如 Owl 设备改名)。
|
||||
/// </summary>
|
||||
public interface IAcceptsMetadataPush : IGatewayAdapter
|
||||
{
|
||||
/// <summary>向子系统回写设备元数据变更</summary>
|
||||
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
|
||||
/// <param name="changes">变更集,仅非 null 字段会被更新</param>
|
||||
Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,20 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>所有适配器必须实现的基础接口</summary>
|
||||
/// <summary>
|
||||
/// 网关适配器基础接口。所有子系统适配器必须实现此接口。
|
||||
/// 定义了适配器的元信息、生命周期和健康检查能力。
|
||||
/// </summary>
|
||||
public interface IGatewayAdapter
|
||||
{
|
||||
/// <summary>适配器编码,格式 "类型:实例",如 "Owl:main"、"MC4:31ku"</summary>
|
||||
string AdapterCode { get; }
|
||||
/// <summary>人类可读的适配器显示名称</summary>
|
||||
string DisplayName { get; }
|
||||
/// <summary>适配器能力声明(声明实现哪些能力接口)</summary>
|
||||
AdapterCapabilities Capabilities { get; }
|
||||
/// <summary>懒加载初始化(建立连接、获取认证 Token 等)。失败不阻塞网关启动。</summary>
|
||||
Task InitializeAsync();
|
||||
/// <summary>健康检查。返回 true 表示适配器及子系统可达。</summary>
|
||||
Task<bool> HealthCheckAsync();
|
||||
}
|
||||
|
||||
@@ -2,11 +2,25 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>告警查询 + 确认 + 结束(MC4.0 / Owl AI 可选)</summary>
|
||||
/// <summary>
|
||||
/// 告警接口。适用于具有告警功能的子系统(如 MC4.0 / Owl AI 事件)。
|
||||
/// 支持告警查询、确认和结束操作。
|
||||
/// </summary>
|
||||
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,
|
||||
string? level = null, string? state = null);
|
||||
/// <summary>确认告警(同时写回子系统)</summary>
|
||||
/// <param name="alarmId">子系统告警 ID</param>
|
||||
Task ConfirmAlarmAsync(string alarmId);
|
||||
/// <summary>结束告警(同时写回子系统)</summary>
|
||||
/// <param name="alarmId">子系统告警 ID</param>
|
||||
Task EndAlarmAsync(string alarmId);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,16 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>扁平设备列表(Owl/门禁/道闸)</summary>
|
||||
/// <summary>
|
||||
/// 扁平设备列表接口。适用于设备无层级关系或层级由网关自行构建的子系统(如 Owl/门禁/道闸)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>自有对象树(MC4.0)</summary>
|
||||
/// <summary>
|
||||
/// 自有对象树接口。适用于具有层级对象树的子系统(如 MC4.0)。
|
||||
/// 返回的 DeviceTreeNode 中 Type=1 为区域节点,Type=2 为设备节点。
|
||||
/// </summary>
|
||||
public interface IHasOwnDeviceTree : IGatewayAdapter
|
||||
{
|
||||
/// <summary>获取子系统的完整对象树</summary>
|
||||
Task<List<DeviceTreeNode>> GetObjectTreeAsync();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,18 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>实时点位值 + 控制(MC4.0 动环)</summary>
|
||||
/// <summary>
|
||||
/// 实时点位值接口。适用于 IoT 动环类子系统(如 MC4.0)。
|
||||
/// 支持读取设备测点实时值和反向控制写值。
|
||||
/// </summary>
|
||||
public interface IHasPoints : IGatewayAdapter
|
||||
{
|
||||
/// <summary>获取指定设备的全部实时点位值</summary>
|
||||
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>录像回放(Owl)</summary>
|
||||
/// <summary>
|
||||
/// 录像回放查询接口。适用于具有录像存储功能的子系统(如 Owl)。
|
||||
/// </summary>
|
||||
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(
|
||||
string channelId, DateTime start, DateTime end, int page, int size);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,27 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Abstractions;
|
||||
|
||||
/// <summary>视频流 + PTZ + 截图(Owl)</summary>
|
||||
/// <summary>
|
||||
/// 视频流接口。适用于视频监控类子系统(如 Owl)。
|
||||
/// 支持实时取流、录像回放、云台控制和截图。
|
||||
/// </summary>
|
||||
public interface IHasStreams : IGatewayAdapter
|
||||
{
|
||||
/// <summary>获取实时视频流地址</summary>
|
||||
/// <param name="channelId">通道 ID</param>
|
||||
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);
|
||||
/// <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);
|
||||
/// <summary>云台停止</summary>
|
||||
Task PtzStopAsync(string channelId);
|
||||
/// <summary>获取通道实时截图</summary>
|
||||
Task<StreamUrls> GetSnapshotAsync(string channelId);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,23 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
namespace IntegrationGateway.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 适配器注册中心。管理所有子系统适配器的生命周期。
|
||||
/// 支持注册、查找、健康检查和并行初始化。
|
||||
/// 单个适配器初始化失败不影响其他适配器。
|
||||
/// </summary>
|
||||
public class AdapterRegistry
|
||||
{
|
||||
/// <summary>已注册的适配器列表</summary>
|
||||
private readonly List<IGatewayAdapter> _adapters = new();
|
||||
|
||||
/// <summary>注册一个适配器实例</summary>
|
||||
public void Register(IGatewayAdapter adapter) => _adapters.Add(adapter);
|
||||
|
||||
/// <summary>
|
||||
/// 并行初始化所有适配器。
|
||||
/// 每个适配器在独立 Task 中初始化,单个失败仅输出错误日志,不抛出异常。
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/// <summary>所有已注册适配器(只读)</summary>
|
||||
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
|
||||
=> _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode && a is T) as T;
|
||||
|
||||
/// <summary>按适配器编码查找(不限定能力类型)</summary>
|
||||
public IGatewayAdapter? FindByCode(string adapterCode)
|
||||
=> _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode);
|
||||
|
||||
/// <summary>获取所有在线适配器</summary>
|
||||
public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters()
|
||||
=> _adapters.AsReadOnly();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace IntegrationGateway.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Vol.Pro HTTP 客户端工厂。封装网关调用 Vol.Pro A 组接口的逻辑。
|
||||
/// 管理 HttpClient 生命周期和连接池复用。
|
||||
/// </summary>
|
||||
public class GatewayClientFactory
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
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)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
_volProBaseUrl = volProBaseUrl.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>创建带连接池复用的 HttpClient</summary>
|
||||
public HttpClient CreateClient() => _httpFactory.CreateClient("VolPro");
|
||||
|
||||
/// <summary>A1: 网关注册。向 Vol.Pro 注册网关节点信息。</summary>
|
||||
public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req)
|
||||
{
|
||||
var http = CreateClient();
|
||||
@@ -25,6 +35,7 @@ public class GatewayClientFactory
|
||||
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||
}
|
||||
|
||||
/// <summary>A2: 心跳上报。每 15 秒调用一次。</summary>
|
||||
public async Task<bool> HeartbeatAsync(GatewayHeartbeatRequest req)
|
||||
{
|
||||
var http = CreateClient();
|
||||
@@ -32,6 +43,7 @@ public class GatewayClientFactory
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
/// <summary>A3: 设备数据同步。向 Vol.Pro 上送设备列表。</summary>
|
||||
public async Task<JsonDocument?> SyncDevicesAsync(string nodeCode, string token, List<object> devices)
|
||||
{
|
||||
var http = CreateClient();
|
||||
@@ -41,6 +53,7 @@ public class GatewayClientFactory
|
||||
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||
}
|
||||
|
||||
/// <summary>A4: 告警同步。向 Vol.Pro 上送告警列表。</summary>
|
||||
public async Task<JsonDocument?> SyncAlarmsAsync(string nodeCode, string token, List<object> alarms)
|
||||
{
|
||||
var http = CreateClient();
|
||||
@@ -51,16 +64,24 @@ public class GatewayClientFactory
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>网关注册请求体</summary>
|
||||
public class GatewayRegisterRequest
|
||||
{
|
||||
/// <summary>网关节点编码</summary>
|
||||
public string NodeCode { get; set; } = "";
|
||||
/// <summary>认证令牌</summary>
|
||||
public string Token { get; set; } = "";
|
||||
/// <summary>适配器类型列表(逗号分隔)</summary>
|
||||
public string AdapterTypes { get; set; } = "";
|
||||
/// <summary>网关自身地址</summary>
|
||||
public string BaseUrl { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>心跳请求体</summary>
|
||||
public class GatewayHeartbeatRequest
|
||||
{
|
||||
/// <summary>网关节点编码</summary>
|
||||
public string NodeCode { get; set; } = "";
|
||||
/// <summary>认证令牌</summary>
|
||||
public string Token { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
namespace IntegrationGateway.Core.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 令牌桶限流器。控制对第三方子系统的请求频率,防止超出 API 配额。
|
||||
/// 每个适配器实例持有独立的限流器。
|
||||
///
|
||||
/// 算法:启动时桶内有 tokensPerSecond 个令牌,每次请求消耗一个令牌,
|
||||
/// 令牌按 (1000/tokensPerSecond) 毫秒的速率补充。
|
||||
/// </summary>
|
||||
public class RateLimiter
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly int _intervalMs;
|
||||
|
||||
/// <summary>
|
||||
/// 创建限流器
|
||||
/// </summary>
|
||||
/// <param name="tokensPerSecond">每秒允许的请求数(QPS)</param>
|
||||
public RateLimiter(int tokensPerSecond)
|
||||
{
|
||||
_semaphore = new SemaphoreSlim(tokensPerSecond, tokensPerSecond);
|
||||
_intervalMs = 1000 / tokensPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待获取一个令牌。如果当前没有可用令牌,阻塞直到有令牌被释放。
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task WaitAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct);
|
||||
// 在后台任务中延迟补充令牌
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(_intervalMs, ct);
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 适配器能力声明。每个适配器在注册时声明自己实现了哪些能力接口。
|
||||
/// 网关通过此声明判断适配器支持的操作,将请求路由到正确的适配器。
|
||||
/// </summary>
|
||||
public class AdapterCapabilities
|
||||
{
|
||||
/// <summary>是否支持自有对象树(如 MC4.0 的区域→设备层级树)</summary>
|
||||
public bool HasObjectTree { get; set; }
|
||||
/// <summary>是否支持扁平设备列表(如 Owl 的 NVR 列表)</summary>
|
||||
public bool HasFlatDevices { get; set; }
|
||||
/// <summary>是否支持实时点位值读取</summary>
|
||||
public bool HasPoints { get; set; }
|
||||
/// <summary>是否支持视频取流</summary>
|
||||
public bool HasStreams { get; set; }
|
||||
/// <summary>是否支持云台控制(PTZ)</summary>
|
||||
public bool HasPtz { get; set; }
|
||||
/// <summary>是否支持录像回放</summary>
|
||||
public bool HasRecordings { get; set; }
|
||||
/// <summary>是否支持告警查询与处理</summary>
|
||||
public bool HasAlarms { get; set; }
|
||||
/// <summary>是否接受反向控制(点位写值)</summary>
|
||||
public bool AcceptsControl { get; set; }
|
||||
/// <summary>是否接受元数据回写(如设备改名)</summary>
|
||||
public bool AcceptsMetadataPush { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 设备树节点。用于 MC4.0 等具有层级对象树的子系统。
|
||||
/// Type=1 表示区域节点,Type=2 表示设备节点。
|
||||
/// Option 字典承载节点扩展属性。
|
||||
/// </summary>
|
||||
public class DeviceTreeNode
|
||||
{
|
||||
/// <summary>子系统原始 ID</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>字符串形式的源 ID</summary>
|
||||
public string SourceId { get; set; } = "";
|
||||
/// <summary>节点名称</summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>节点类型:1=区域,2=设备</summary>
|
||||
public int Type { get; set; }
|
||||
/// <summary>MC4.0 对象类型编码</summary>
|
||||
public int ObjectType { get; set; }
|
||||
/// <summary>节点标签(如 温湿度/烟雾/门磁)</summary>
|
||||
public string? Tag { get; set; }
|
||||
/// <summary>节点扩展属性</summary>
|
||||
public Dictionary<string, object?>? Option { get; set; }
|
||||
/// <summary>子节点列表</summary>
|
||||
public List<DeviceTreeNode> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据变更集。用于管理端向子系统回写设备元数据。
|
||||
/// 仅非 null 的字段会被更新到子系统。
|
||||
/// </summary>
|
||||
public class MetadataChangeSet
|
||||
{
|
||||
/// <summary>新设备名称</summary>
|
||||
public string? Name { get; set; }
|
||||
/// <summary>新设备种类</summary>
|
||||
public string? Category { get; set; }
|
||||
/// <summary>新设备分组</summary>
|
||||
public string? Group { get; set; }
|
||||
/// <summary>新 IP 地址</summary>
|
||||
public string? IpAddress { get; set; }
|
||||
/// <summary>新端口</summary>
|
||||
public int? Port { get; set; }
|
||||
/// <summary>扩展属性变更</summary>
|
||||
public Dictionary<string, object?>? Extra { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据回写结果。
|
||||
/// </summary>
|
||||
public class MetadataPushResult
|
||||
{
|
||||
/// <summary>操作是否成功</summary>
|
||||
public bool Success { get; set; }
|
||||
/// <summary>失败时的错误信息</summary>
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 统一分页容器,所有适配器返回分页数据时使用。
|
||||
/// 适配器内部完成 skip/limit 到 page/size 的语义转换。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">分页条目的类型</typeparam>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
/// <summary>当前页数据列表</summary>
|
||||
public List<T> Items { get; set; } = new();
|
||||
/// <summary>总记录数(用于前端分页组件计算总页数)</summary>
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 设备实时点位值。描述 IoT 设备的一个测点当前读数。
|
||||
/// </summary>
|
||||
public class PointValue
|
||||
{
|
||||
/// <summary>设备在子系统中的原始 ID</summary>
|
||||
public string SourceDeviceId { get; set; } = "";
|
||||
/// <summary>点位索引(同一设备可能有多个测点)</summary>
|
||||
public int PointIndex { get; set; }
|
||||
/// <summary>当前数值</summary>
|
||||
public double Value { get; set; }
|
||||
/// <summary>数据更新时间</summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
/// <summary>数据上报间隔(秒)</summary>
|
||||
public int Interval { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 统一告警模型。所有适配器的告警数据统一映射为此格式。
|
||||
/// 告警等级和状态使用中文字符串,方便前端直接展示。
|
||||
/// </summary>
|
||||
public class StandardAlarm
|
||||
{
|
||||
/// <summary>告警在子系统中的唯一 ID</summary>
|
||||
public string AlarmId { get; set; } = "";
|
||||
/// <summary>关联的设备 Vol.Pro DeviceId(同步后解析)</summary>
|
||||
public string? DeviceId { get; set; }
|
||||
/// <summary>来源适配器标识</summary>
|
||||
public string AdapterCode { get; set; } = "";
|
||||
/// <summary>告警等级:提示 / 普通 / 重要 / 紧急</summary>
|
||||
public string Level { get; set; } = "提示";
|
||||
/// <summary>告警标题(简短描述)</summary>
|
||||
public string Title { get; set; } = "";
|
||||
/// <summary>告警详细内容</summary>
|
||||
public string? Content { get; set; }
|
||||
/// <summary>告警发生时间</summary>
|
||||
public DateTime OccurTime { get; set; }
|
||||
/// <summary>告警状态:未确认 / 已确认 / 已结束</summary>
|
||||
public string Status { get; set; } = "未确认";
|
||||
/// <summary>告警触发时的实际值(如温度超标时的实际温度)</summary>
|
||||
public double? ActualValue { get; set; }
|
||||
/// <summary>告警阈值</summary>
|
||||
public double? ThresholdValue { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 统一设备模型,网关与 Vol.Pro 之间传输设备数据的标准格式。
|
||||
/// AdapterCode + SourceId 联合唯一标识一个设备。
|
||||
/// Extra 字典承载适配器特有属性,避免污染核心字段。
|
||||
/// </summary>
|
||||
public class StandardDevice
|
||||
{
|
||||
/// <summary>Vol.Pro 侧主键(同步后由 Vol.Pro 回填)</summary>
|
||||
public int DeviceId { get; set; }
|
||||
/// <summary>来源适配器标识,格式 "类型:实例",如 "Owl:main"</summary>
|
||||
public string AdapterCode { get; set; } = "";
|
||||
/// <summary>子系统原始设备 ID(GB28181 编码 / MC4 sid)</summary>
|
||||
public string SourceId { get; set; } = "";
|
||||
/// <summary>设备名称(管理员可修改字段)</summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>设备种类,如 摄像机/温湿度变送器(管理员可修改字段)</summary>
|
||||
public string Category { get; set; } = "";
|
||||
/// <summary>设备分组,如 视频设备/IoT设备(管理员可修改字段)</summary>
|
||||
public string Group { get; set; } = "";
|
||||
/// <summary>是否为父设备(有子设备)</summary>
|
||||
public bool IsParent { get; set; }
|
||||
/// <summary>父设备在子系统中的原始 ID,用于构建层级关系</summary>
|
||||
public string? ParentSourceId { get; set; }
|
||||
/// <summary>在线状态</summary>
|
||||
public bool IsOnline { get; set; }
|
||||
/// <summary>设备 IP 地址</summary>
|
||||
public string? IpAddress { get; set; }
|
||||
/// <summary>设备端口号</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 统一录像记录模型。描述一段视频录像的元信息。
|
||||
/// </summary>
|
||||
public class StandardRecording
|
||||
{
|
||||
/// <summary>录像记录 ID</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>所属通道 ID(子系统中的通道标识)</summary>
|
||||
public string? ChannelId { get; set; }
|
||||
/// <summary>录像开始时间</summary>
|
||||
public DateTime StartedAt { get; set; }
|
||||
/// <summary>录像结束时间</summary>
|
||||
public DateTime EndedAt { get; set; }
|
||||
/// <summary>录像时长(秒)</summary>
|
||||
public double Duration { get; set; }
|
||||
/// <summary>录像文件路径或 URL</summary>
|
||||
public string? FilePath { get; set; }
|
||||
/// <summary>录像文件大小(字节)</summary>
|
||||
public long Size { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
namespace IntegrationGateway.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 视频流地址集合。包含多种协议格式的流地址,
|
||||
/// 前端根据浏览器能力选择合适的协议播放。
|
||||
/// </summary>
|
||||
public class StreamUrls
|
||||
{
|
||||
/// <summary>WebSocket-FLV 地址(低延迟,推荐)</summary>
|
||||
public string? WsFlv { get; set; }
|
||||
/// <summary>HTTP-FLV 地址</summary>
|
||||
public string? HttpFlv { get; set; }
|
||||
/// <summary>HLS 地址(兼容性好,延迟较高)</summary>
|
||||
public string? Hls { get; set; }
|
||||
/// <summary>WebRTC 地址(超低延迟)</summary>
|
||||
public string? WebRtc { get; set; }
|
||||
/// <summary>RTMP 地址</summary>
|
||||
public string? Rtmp { get; set; }
|
||||
/// <summary>RTSP 地址</summary>
|
||||
public string? Rtsp { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,21 @@ using IntegrationGateway.Core.Abstractions;
|
||||
using IntegrationGateway.Core.Infrastructure;
|
||||
using IntegrationGateway.Core.Models;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// IntegrationGateway 宿主启动程序
|
||||
//
|
||||
// 职责:
|
||||
// 1. 注册 IHttpClientFactory(连接池复用)
|
||||
// 2. 创建并注册 OwlAdapter + MC4Adapter
|
||||
// 3. 并行初始化所有适配器
|
||||
// 4. 注册 14 个 B 组 REST 端点
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 注册 IHttpClientFactory
|
||||
// ── 注册 HttpClient 工厂 ──
|
||||
// 命名客户端 "VolPro":用于调用 Vol.Pro A 组接口和适配器内部 HTTP 请求
|
||||
// 连接池:最多 10 个并发连接,5 分钟生命周期
|
||||
builder.Services.AddHttpClient("VolPro", c =>
|
||||
{
|
||||
c.Timeout = TimeSpan.FromSeconds(30);
|
||||
@@ -17,20 +29,20 @@ builder.Services.AddHttpClient("VolPro", c =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 读取配置
|
||||
// ── 读取配置 ──
|
||||
var gwCfg = app.Configuration.GetSection("Gateway");
|
||||
var owlCfg = app.Configuration.GetSection("Owl");
|
||||
var mc4Cfg = app.Configuration.GetSection("MC4");
|
||||
|
||||
// 创建适配器注册中心
|
||||
// ── 创建适配器注册中心 ──
|
||||
var registry = new AdapterRegistry();
|
||||
|
||||
// 创建 VolPro 客户端工厂
|
||||
// ── 创建 Vol.Pro 客户端工厂(用于 A1-A4 回调) ──
|
||||
var volProUrl = gwCfg["VolProBaseUrl"] ?? "http://localhost:9100";
|
||||
var httpFactory = app.Services.GetRequiredService<IHttpClientFactory>();
|
||||
var clientFactory = new GatewayClientFactory(httpFactory, volProUrl);
|
||||
|
||||
// 注册 OwlAdapter
|
||||
// ── 注册 OwlAdapter ──
|
||||
var owlHttp = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro");
|
||||
var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter(
|
||||
"Owl:main", owlHttp,
|
||||
@@ -40,7 +52,7 @@ var owlAdapter = new IntegrationGateway.Adapters.Owl.OwlAdapter(
|
||||
);
|
||||
registry.Register(owlAdapter);
|
||||
|
||||
// 注册 MC4Adapter
|
||||
// ── 注册 MC4Adapter ──
|
||||
var mc4Http = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro");
|
||||
var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter(
|
||||
"MC4:31ku", mc4Http,
|
||||
@@ -48,13 +60,16 @@ var mc4Adapter = new IntegrationGateway.Adapters.MC4.Mc4Adapter(
|
||||
);
|
||||
registry.Register(mc4Adapter);
|
||||
|
||||
// 并行初始化适配器
|
||||
// ── 并行初始化所有适配器 ──
|
||||
await registry.InitializeAllAsync();
|
||||
Console.WriteLine($"[Gateway] {registry.All.Count} adapter(s) registered");
|
||||
Console.WriteLine($"[Gateway] {registry.All.Count} 个适配器已注册");
|
||||
|
||||
// ═══ B 组路由 ═══
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B 组路由(管理端 / Vol.Pro → 网关)
|
||||
// 所有路由通过适配器编码查找对应适配器,按能力接口分发请求
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// B1: 健康检查
|
||||
// B1: 健康检查 — 返回所有适配器的健康状态和能力声明
|
||||
app.MapGet("/api/gateway/health", async () =>
|
||||
{
|
||||
var results = new List<object>();
|
||||
@@ -67,34 +82,34 @@ app.MapGet("/api/gateway/health", async () =>
|
||||
return Results.Ok(results);
|
||||
});
|
||||
|
||||
// B2: 设备列表
|
||||
// B2: 设备列表 — 分页获取扁平设备列表(Owl/门禁/道闸)
|
||||
app.MapGet("/api/gateway/devices", async (string adapter, int page, int size, string? keyword) =>
|
||||
{
|
||||
var a = registry.FindByCode<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));
|
||||
});
|
||||
|
||||
// B3: 对象树
|
||||
// B3: 对象树 — 获取层级对象树(MC4.0)
|
||||
app.MapGet("/api/gateway/tree", async (string 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());
|
||||
});
|
||||
|
||||
// B6a: 实时流
|
||||
// B6a: 实时取流 — 获取视频通道的实时流地址
|
||||
app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/live", async (string adapter, string deviceId) =>
|
||||
{
|
||||
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);
|
||||
return result.WsFlv == null && result.Hls == null
|
||||
? Results.Problem("No stream URL returned", statusCode: 502)
|
||||
? Results.Problem("未获取到流地址", statusCode: 502)
|
||||
: Results.Ok(result);
|
||||
});
|
||||
|
||||
// B6b: 回放
|
||||
// B6b: 录像回放 — 获取历史录像 HLS 地址
|
||||
app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/playback", async (string adapter, string deviceId, DateTime start, DateTime end) =>
|
||||
{
|
||||
var a = registry.FindByCode<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));
|
||||
});
|
||||
|
||||
// 截图
|
||||
// 截图 — 获取通道实时截图
|
||||
app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/snapshot", async (string adapter, string deviceId) =>
|
||||
{
|
||||
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));
|
||||
});
|
||||
|
||||
// B7: PTZ
|
||||
// B7: 云台控制 — continuous 方向移动 + stop
|
||||
app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapter, string deviceId, PtzRequest req) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasStreams>(adapter);
|
||||
@@ -120,7 +135,7 @@ app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapt
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// B4: 实时点值
|
||||
// B4: 实时点位值 — 获取 IoT 设备测点当前读数
|
||||
app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter, string deviceId) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasPoints>(adapter);
|
||||
@@ -128,7 +143,7 @@ app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter,
|
||||
return Results.Ok(await a.GetRealtimeValuesAsync(deviceId));
|
||||
});
|
||||
|
||||
// B5: 控制
|
||||
// B5: 设备控制 — 向 IoT 设备下发控制指令
|
||||
app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasPoints>(adapter);
|
||||
@@ -137,7 +152,7 @@ app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, Co
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// B8: 告警查询
|
||||
// B8: 告警查询 — 分页获取告警列表
|
||||
app.MapGet("/api/gateway/alarms/{adapter}", async (string adapter, int page, int size, DateTime from, DateTime to, string? level, string? state) =>
|
||||
{
|
||||
var a = registry.FindByCode<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));
|
||||
});
|
||||
|
||||
// B9: 告警确认
|
||||
// B9: 告警确认 — 确认告警并写回子系统
|
||||
app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string adapter, string alarmId) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasAlarms>(adapter);
|
||||
@@ -154,7 +169,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string ada
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// 告警结束
|
||||
// 告警结束 — 结束告警并写回子系统
|
||||
app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter, string alarmId) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasAlarms>(adapter);
|
||||
@@ -163,7 +178,7 @@ app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/end", async (string adapter
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// 录像
|
||||
// 录像查询 — 分页获取录像文件列表
|
||||
app.MapGet("/api/gateway/recordings/{adapter}/{deviceId}", async (string adapter, string deviceId, DateTime start, DateTime end, int page, int size) =>
|
||||
{
|
||||
var a = registry.FindByCode<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));
|
||||
});
|
||||
|
||||
// B3: 手动同步
|
||||
// B3: 手动同步 — 触发适配器全量设备同步
|
||||
app.MapPost("/api/gateway/devices/sync", async (string adapter) =>
|
||||
{
|
||||
var a = registry.FindByCode<IGatewayAdapter>(adapter);
|
||||
if (a == null) return Results.NotFound(new { error = "ADAPTER_NOT_FOUND" });
|
||||
// 根据适配器能力触发对应同步
|
||||
// 根据适配器能力触发对应同步逻辑
|
||||
if (a is IHasOwnDeviceTree tree)
|
||||
{
|
||||
var obj = await tree.GetObjectTreeAsync();
|
||||
return Results.Ok(new { nodeCount = obj.Count, message = "tree synced" });
|
||||
return Results.Ok(new { nodeCount = obj.Count, message = "对象树同步完成" });
|
||||
}
|
||||
if (a is IHasFlatDevices flat)
|
||||
{
|
||||
var dev = await flat.GetDevicesAsync(1, 1000);
|
||||
return Results.Ok(new { deviceCount = dev.Total, message = "devices synced" });
|
||||
return Results.Ok(new { deviceCount = dev.Total, message = "设备列表同步完成" });
|
||||
}
|
||||
return Results.Ok(new { message = "no sync needed" });
|
||||
return Results.Ok(new { message = "无需同步" });
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
// 请求 DTO
|
||||
// ═══════════════════════════════════════════════
|
||||
// B 组请求 DTO
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>设备控制请求</summary>
|
||||
/// <param name="DeviceId">目标设备 SourceId</param>
|
||||
/// <param name="PointIndex">点位索引</param>
|
||||
/// <param name="Value">目标值</param>
|
||||
record ControlRequest(string? DeviceId, int PointIndex, double Value);
|
||||
|
||||
Reference in New Issue
Block a user