398 lines
19 KiB
C#
398 lines
19 KiB
C#
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; } = ""; }
|