Files
SecMPS/gateway/src/IntegrationGateway.Host/Program.cs

398 lines
19 KiB
C#
Raw Blame History

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