Compare commits
2 Commits
07dd459705
...
943bab875a
| Author | SHA1 | Date | |
|---|---|---|---|
| 943bab875a | |||
| 5790d3e8bf |
@@ -8,39 +8,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 网关代码兼容性评估
|
|
||||||
|
|
||||||
> 基于 v3.0 方案对 Phase 0 已生成的 gateway/ 代码进行评估
|
|
||||||
|
|
||||||
| 组件 | 状态 | 说明 |
|
|
||||||
|------|:---:|------|
|
|
||||||
| Core/Abstractions (7接口) | ✅ 可用 | 与 v3.0 完全兼容 |
|
|
||||||
| Core/Models (10模型) | ✅ 可用 | 与 v3.0 完全兼容 |
|
|
||||||
| Core/Infrastructure (AdapterRegistry/TokenManager/RateLimiter) | ✅ 可用 | 无变更 |
|
|
||||||
| Host/Program.cs | ⚠️ 需微调 | 增加 HttpClient 注册 |
|
|
||||||
| Host/HealthController | ✅ 可用 | B1 接口无变更 |
|
|
||||||
| Host/DevicesController | ✅ 可用 | B2 接口无变更 |
|
|
||||||
| Host/PointsController | ✅ 可用 | B4/B5 接口无变更 |
|
|
||||||
| Host/AlarmsController | ✅ 可用 | B8/B9 接口无变更 |
|
|
||||||
| Host/SyncController | ✅ 可用 | B3 接口无变更 |
|
|
||||||
| Host/StreamsController | ⚠️ 需增加 | 缺少 B6b playback 端点 |
|
|
||||||
| Host/RegisterController | ❌ 缺失 | 需新建 A1/A2 接口 |
|
|
||||||
| appsettings.json | ❌ 需重写 | 需改为 v3.0 格式 |
|
|
||||||
| IGatewayClient (调用Vol.Pro) | ❌ 缺失 | 需新建 |
|
|
||||||
|
|
||||||
### 结论
|
|
||||||
|
|
||||||
**现有代码 70% 可用**,需 5 处改动:
|
|
||||||
1. 重写 `appsettings.json`(删除适配器硬编码,改为 VolProUrl/NodeCode/NodeToken)
|
|
||||||
2. 新增 `RegisterController.cs`(A1 注册 + A2 心跳)
|
|
||||||
3. `StreamsController.cs` 增加 B6b playback 端点
|
|
||||||
4. `Program.cs` 增加 `HttpClient` 注册 + 网关启动时调 Vol.Pro 注册
|
|
||||||
5. 新增 `GatewayClient.cs`(网关调用 Vol.Pro API 的 HTTP 封装)
|
|
||||||
|
|
||||||
预计改动量:约 200 行新增代码,不改动现有接口和模型。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 分支管理策略
|
## 分支管理策略
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -78,11 +45,56 @@ git commit -m "Phase {n}: {标题}" && git push && git tag phase-{n}-done
|
|||||||
|
|
||||||
## Phase 0:基础设施(Day 1-2)
|
## Phase 0:基础设施(Day 1-2)
|
||||||
|
|
||||||
### Day 1 — 网关代码修正 + 数据库
|
### Day 1 — 从头搭建网关 + 数据库
|
||||||
|
|
||||||
**任务 1.1**: 修正 gateway/ 代码(5 处改动,约 200 行)
|
**任务 1.1 — 创建 Gateway 解决方案**
|
||||||
|
|
||||||
1. 重写 `appsettings.json`:
|
在 `gateway/` 目录下:
|
||||||
|
```
|
||||||
|
dotnet new sln -n IntegrationGateway
|
||||||
|
dotnet new webapi -n IntegrationGateway.Host -f net8.0
|
||||||
|
dotnet new classlib -n IntegrationGateway.Core -f net8.0
|
||||||
|
dotnet sln add src/IntegrationGateway.Host src/IntegrationGateway.Core
|
||||||
|
dotnet add src/IntegrationGateway.Host reference src/IntegrationGateway.Core
|
||||||
|
dotnet add src/IntegrationGateway.Core package Microsoft.Extensions.Caching.Memory
|
||||||
|
```
|
||||||
|
验证: `dotnet build` 零错误。
|
||||||
|
|
||||||
|
**任务 1.2 — Core 层(接口 + 模型 + 基础设施)**
|
||||||
|
|
||||||
|
`Abstractions/` — 7 个分型接口:
|
||||||
|
- `IIntegrationAdapter` (基础: AdapterCode/HealthCheck/Initialize)
|
||||||
|
- `IHasOwnDeviceTree` (MC4)、`IHasFlatDevices` (Owl)、`IHasPoints` (MC4)
|
||||||
|
- `IHasStreams` (Owl)、`IHasAlarms` (通用)、`IAcceptsMetadataPush` (Owl)
|
||||||
|
|
||||||
|
`Models/` — 10 个标准化模型:
|
||||||
|
- `StandardDevice/DeviceTreeNode/StandardPoint/PointValue/StandardAlarm`
|
||||||
|
- `StreamUrls/SyncReport/PagedResult/AdapterCapabilities/StandardRecording`
|
||||||
|
|
||||||
|
`Infrastructure/` — 3 个基础设施:
|
||||||
|
- `AdapterRegistry` (适配器注册与发现)
|
||||||
|
- `TokenManager` (通用内存 Token 缓存)
|
||||||
|
- `RateLimiter` (SemaphoreSlim 频率控制)
|
||||||
|
|
||||||
|
验证: `dotnet build` 零错误 + 单元测试 mock 适配器通过。
|
||||||
|
|
||||||
|
**任务 1.3 — Host 层 Controller(7 个)**
|
||||||
|
|
||||||
|
| Controller | 对应 API | 说明 |
|
||||||
|
|------------|----------|------|
|
||||||
|
| `HealthController` | B1 | 健康检查 |
|
||||||
|
| `DevicesController` | B2 | 设备列表 |
|
||||||
|
| `PointsController` | B4/B5 | 实时数据+控制 |
|
||||||
|
| `StreamsController` | B6a/B6b/B7 | 取流+回放+云台 |
|
||||||
|
| `AlarmsController` | B8/B9 | 告警查询+确认 |
|
||||||
|
| `SyncController` | B3 | 手动同步 |
|
||||||
|
| `RegisterController` | A1/A2 | 网关注册+心跳 |
|
||||||
|
|
||||||
|
验证: 启动 Host,`GET /api/gateway/health` → 200。
|
||||||
|
|
||||||
|
**任务 1.4 — 网关配置 + 启动注册**
|
||||||
|
|
||||||
|
`appsettings.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"VolProBaseUrl": "http://localhost:9100",
|
"VolProBaseUrl": "http://localhost:9100",
|
||||||
@@ -92,49 +104,80 @@ git commit -m "Phase {n}: {标题}" && git push && git tag phase-{n}-done
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 新增 `RegisterController.cs`:
|
`Program.cs` 启动逻辑:
|
||||||
- `POST /api/gateway/register` — 网关启动注册(Upsert 数据库)
|
|
||||||
- `POST /api/gateway/heartbeat` — 网关每 15s 心跳
|
|
||||||
|
|
||||||
3. `StreamsController.cs` 增加:
|
|
||||||
- `GET {adapter}/{channelId}/playback?start=&end=` — 回放取流
|
|
||||||
|
|
||||||
4. `Program.cs` 增加:
|
|
||||||
```csharp
|
```csharp
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHttpClient("VolPro", c => {
|
builder.Services.AddHttpClient("VolPro", c => {
|
||||||
c.BaseAddress = new Uri(builder.Configuration["VolProBaseUrl"]);
|
c.BaseAddress = new Uri(builder.Configuration["VolProBaseUrl"]);
|
||||||
});
|
});
|
||||||
// 启动后自动注册
|
builder.Services.AddSingleton<AdapterRegistry>();
|
||||||
var registry = app.Services.GetRequiredService<AdapterRegistry>();
|
builder.Services.AddSingleton<TokenManager>();
|
||||||
var http = app.Services.GetRequiredService<IHttpClientFactory>();
|
// 启动后自动调 Vol.Pro 注册
|
||||||
await RegisterWithVolPro(registry, http, app.Configuration);
|
app.Lifetime.ApplicationStarted.Register(() => {
|
||||||
|
Task.Run(() => RegisterWithVolPro(app));
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
5. 新增 `GatewayClient.cs`:
|
新增 `GatewayClient.cs` — 封装网关→Vol.Pro 的 HTTP 调用(A1注册/A2心跳/A3设备同步/A4告警同步)。
|
||||||
网关调用 Vol.Pro API 的封装类(注册/心跳/同步设备/同步告警)
|
|
||||||
|
|
||||||
验证: `dotnet build` 零错误 + Gateway `/health` 200
|
验证: 网关启动后调 Vol.Pro 的 A1 端点成功,返回 nodeId。
|
||||||
|
|
||||||
**任务 1.2**: 执行 db_init.sql(6张表)。验证: 唯一索引存在。
|
**任务 1.5 — 执行 db_init.sql(6张表)**
|
||||||
|
|
||||||
**任务 1.3**: Vol.Pro 侧 GatewayClient 实现 + `GatewayNodeController.cs`(A1/A2/A3/A4 服务端)。验证: 可成功注册并心跳。
|
验证: `SELECT COUNT(*) FROM base_device` → 0,唯一索引 `(AdapterCode, SourceId)` 存在。
|
||||||
|
|
||||||
### Day 2 — 代码生成 + 字典初始化
|
### Day 2 — Vol.Pro 侧集成 + 代码生成 + 字典
|
||||||
|
|
||||||
**任务 2.1**: 代码生成器跑 6 张表。
|
**任务 2.1 — Vol.Pro GatewayClient** (后端调网关的 HTTP 封装)
|
||||||
|
|
||||||
**任务 2.2**: `DeviceManagerController.cs` (Partial/)。
|
新增 `api_sqlsugar/VolPro.Core/Infrastructure/GatewayClient.cs` (IDependency)。
|
||||||
|
封装: 注册/心跳/设备同步/告警同步/实时数据/取流/云台/告警确认。
|
||||||
|
|
||||||
**任务 2.3**: 字典初始化(8个)。
|
**任务 2.2 — Vol.Pro GatewayNodeController** (A组接口服务端)
|
||||||
|
|
||||||
**任务 2.4**: `SyncDevicesJob` + 心跳超时检测 Job。
|
新增 `Controllers/Warehouse/Partial/GatewayNodeController.cs`:
|
||||||
|
- `POST /api/gateway/register` (A1) — Upsert gateway_nodes, 返回设备列表
|
||||||
|
- `POST /api/gateway/heartbeat` (A2) — 更新 LastHeartbeat
|
||||||
|
- `POST /api/gateway/sync/devices` (A3) — 设备同步(字段分治+parentSourceId映射)
|
||||||
|
- `POST /api/gateway/sync/alarms` (A4) — 告警同步
|
||||||
|
|
||||||
|
验证: Postman 模拟网关调 A1 注册,返回 nodeId+设备列表。
|
||||||
|
|
||||||
|
**任务 2.3 — DeviceManagerController**
|
||||||
|
|
||||||
|
新增 `Controllers/Warehouse/Partial/DeviceManagerController.cs`:
|
||||||
|
- `GET GetRegionTree` — 区域→点位→设备树
|
||||||
|
- `GET GetDevicesByPoint?pointId=` — 点位下设备列表(含子设备)
|
||||||
|
- `PUT {deviceId}` — 更新设备(含地图绑定)
|
||||||
|
|
||||||
|
验证: Postman `GET /api/DeviceManager/GetRegionTree` → JSON。
|
||||||
|
|
||||||
|
**任务 2.4 — 代码生成器**
|
||||||
|
|
||||||
|
对 6 张表运行 Vol.Pro 代码生成器。
|
||||||
|
|
||||||
|
验证: 管理端 6 个新菜单可 CRUD。
|
||||||
|
|
||||||
|
**任务 2.5 — 字典初始化**
|
||||||
|
|
||||||
|
在 Vol.Pro 管理端创建 8 个数据字典(设备种类/设备分组/是否父设备/在线状态/启用状态/是否控制点/告警等级/告警状态)。
|
||||||
|
|
||||||
|
验证: base_device 编辑表单中 DeviceCategory 下拉可选到字典值。
|
||||||
|
|
||||||
|
**任务 2.6 — Quartz Job**
|
||||||
|
|
||||||
|
- `SyncDevicesJob` — 定时调网关同步
|
||||||
|
- `HeartbeatMonitorJob` — 超 30s 无心跳级联设备离线
|
||||||
|
|
||||||
|
验证: Quartz 面板看到已注册 Job。
|
||||||
|
|
||||||
### 合并
|
### 合并
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add -A && git commit -m "Phase 0 完成"
|
git add -A && git commit -m "Phase 0 完成"
|
||||||
git checkout master && git merge --squash phase/0-infrastructure
|
git checkout master && git merge --squash phase/0-infrastructure
|
||||||
git commit -m "Phase 0: 网关修正 + 6张表 + 代码生成 + 字典"
|
git commit -m "Phase 0: 网关骨架 + 6张表 + 代码生成 + 字典 + API"
|
||||||
git push && git tag phase-0-done
|
git push && git tag phase-0-done
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
54
gateway/IntegrationGateway.sln
Normal file
54
gateway/IntegrationGateway.sln
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Host", "src\IntegrationGateway.Host\IntegrationGateway.Host.csproj", "{387BD4FC-725B-4948-B413-F50BC6BD605D}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "src\IntegrationGateway.Core\IntegrationGateway.Core.csproj", "{2055B7A5-418F-456C-8642-A99A68282561}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{387BD4FC-725B-4948-B413-F50BC6BD605D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
|
{2055B7A5-418F-456C-8642-A99A68282561} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IAcceptsMetadataPush : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MetadataChangeSet
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public int? StreamMode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MetadataPushResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public List<string> RejectedFields { get; set; } = new();
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IHasAlarms : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
|
||||||
|
int? confirmState = null, int? endState = null, List<int>? levels = null);
|
||||||
|
Task ConfirmAlarmAsync(string alarmId);
|
||||||
|
Task EndAlarmAsync(string alarmId);
|
||||||
|
Task<int> GetPendingAlarmCountAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IHasFlatDevices : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null);
|
||||||
|
Task<StandardDevice?> GetDeviceAsync(string sourceDeviceId);
|
||||||
|
Task<List<StandardDevice>> GetAllDevicesAsync();
|
||||||
|
Task<PagedResult<StandardDevice>> GetChannelsAsync(int page, int size, string? parentDeviceId = null);
|
||||||
|
Task<List<StandardDevice>> GetAllChannelsAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IHasOwnDeviceTree : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<List<DeviceTreeNode>> GetObjectTreeAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IHasPoints : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId);
|
||||||
|
Task<List<PointValue>> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points);
|
||||||
|
Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IHasStreams : IIntegrationAdapter
|
||||||
|
{
|
||||||
|
Task<StreamUrls> GetLiveUrlAsync(string channelId);
|
||||||
|
Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end);
|
||||||
|
Task StopPlayAsync(string channelId);
|
||||||
|
Task<StreamUrls> GetSnapshotAsync(string channelId);
|
||||||
|
Task PtzControlAsync(string channelId, string direction, float speed);
|
||||||
|
Task PtzStopAsync(string channelId);
|
||||||
|
Task<PagedResult<StandardRecording>> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IIntegrationAdapter
|
||||||
|
{
|
||||||
|
string AdapterCode { get; }
|
||||||
|
string DisplayName { get; }
|
||||||
|
AdapterCapabilities Capabilities { get; }
|
||||||
|
Task<bool> HealthCheckAsync();
|
||||||
|
Task InitializeAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Infrastructure;
|
||||||
|
|
||||||
|
public class AdapterRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IIntegrationAdapter> _adapters = new();
|
||||||
|
|
||||||
|
public void Register(IIntegrationAdapter adapter)
|
||||||
|
{
|
||||||
|
_adapters[adapter.AdapterCode] = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IIntegrationAdapter? Get(string adapterCode)
|
||||||
|
{
|
||||||
|
_adapters.TryGetValue(adapterCode, out var adapter);
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IIntegrationAdapter> GetAll() => _adapters.Values;
|
||||||
|
|
||||||
|
public async Task InitializeAllAsync()
|
||||||
|
{
|
||||||
|
foreach (var adapter in _adapters.Values)
|
||||||
|
await adapter.InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace IntegrationGateway.Core.Infrastructure;
|
||||||
|
|
||||||
|
public class RateLimiter
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private readonly int _minIntervalMs;
|
||||||
|
private DateTime _lastRequest = DateTime.MinValue;
|
||||||
|
|
||||||
|
public RateLimiter(int maxCallsPerSecond)
|
||||||
|
{
|
||||||
|
_semaphore = new SemaphoreSlim(maxCallsPerSecond, maxCallsPerSecond);
|
||||||
|
_minIntervalMs = 1000 / maxCallsPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WaitAsync()
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var elapsed = (int)(DateTime.UtcNow - _lastRequest).TotalMilliseconds;
|
||||||
|
if (elapsed < _minIntervalMs)
|
||||||
|
await Task.Delay(_minIntervalMs - elapsed);
|
||||||
|
_lastRequest = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
finally { _semaphore.Release(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Core.Infrastructure;
|
||||||
|
|
||||||
|
public class TokenManager
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private static readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
|
||||||
|
public TokenManager(IMemoryCache cache) => _cache = cache;
|
||||||
|
|
||||||
|
public async Task<string?> GetAsync(string key)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue($"token_{key}", out string? token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAsync(string key, string token, TimeSpan expiresIn)
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try { _cache.Set($"token_{key}", token, expiresIn * 0.9); }
|
||||||
|
finally { _semaphore.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key) => _cache.Remove($"token_{key}");
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class AdapterCapabilities
|
||||||
|
{
|
||||||
|
public bool HasObjectTree { get; set; }
|
||||||
|
public bool HasFlatDevices { get; set; }
|
||||||
|
public bool HasPoints { get; set; }
|
||||||
|
public bool HasStreams { get; set; }
|
||||||
|
public bool HasAlarms { get; set; }
|
||||||
|
public bool HasRecordings { get; set; }
|
||||||
|
public bool HasPtz { get; set; }
|
||||||
|
public bool AcceptsControl { get; set; }
|
||||||
|
public bool AcceptsMetadataPush { get; set; }
|
||||||
|
}
|
||||||
13
gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs
Normal file
13
gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class DeviceTreeNode
|
||||||
|
{
|
||||||
|
public int SourceId { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int NodeType { get; set; }
|
||||||
|
public int ObjectType { get; set; }
|
||||||
|
public string? Tag { get; set; }
|
||||||
|
public Dictionary<string, object?> Option { get; set; } = new();
|
||||||
|
public List<DeviceTreeNode> Children { get; set; } = new();
|
||||||
|
public string? ParentPath { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class PagedResult<T>
|
||||||
|
{
|
||||||
|
public List<T> Items { get; set; } = new();
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
11
gateway/src/IntegrationGateway.Core/Models/PointValue.cs
Normal file
11
gateway/src/IntegrationGateway.Core/Models/PointValue.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class PointValue
|
||||||
|
{
|
||||||
|
public string SourceDeviceId { get; set; } = "";
|
||||||
|
public int PointIndex { get; set; }
|
||||||
|
public double Value { get; set; }
|
||||||
|
public string? UpdateTime { get; set; }
|
||||||
|
public int Interval { get; set; }
|
||||||
|
public bool IsValid { get; set; } = true;
|
||||||
|
}
|
||||||
15
gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs
Normal file
15
gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class StandardAlarm
|
||||||
|
{
|
||||||
|
public string AlarmId { get; set; } = "";
|
||||||
|
public string? DeviceId { get; set; }
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public string Level { get; set; } = "";
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string? Content { get; set; }
|
||||||
|
public DateTime OccurTime { get; set; }
|
||||||
|
public string Status { get; set; } = "Active";
|
||||||
|
public double? ThresholdValue { get; set; }
|
||||||
|
public double? ActualValue { get; set; }
|
||||||
|
}
|
||||||
17
gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs
Normal file
17
gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class StandardDevice
|
||||||
|
{
|
||||||
|
public string SourceId { get; set; } = "";
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Category { get; set; } = "";
|
||||||
|
public string Group { get; set; } = "";
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool IsOnline { get; set; }
|
||||||
|
public bool IsParent { get; set; }
|
||||||
|
public string? ParentSourceId { get; set; }
|
||||||
|
public Dictionary<string, object?> Extra { get; set; } = new();
|
||||||
|
public DateTime LastSyncTime { get; set; }
|
||||||
|
}
|
||||||
13
gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs
Normal file
13
gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class StandardPoint
|
||||||
|
{
|
||||||
|
public string SourceDeviceId { get; set; } = "";
|
||||||
|
public int PointIndex { get; set; }
|
||||||
|
public int PointType { get; set; }
|
||||||
|
public string? PointTag { get; set; }
|
||||||
|
public string PointName { get; set; } = "";
|
||||||
|
public string? PointDesc { get; set; }
|
||||||
|
public string? Unit { get; set; }
|
||||||
|
public bool IsControlPoint { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class StandardRecording
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string ChannelId { get; set; } = "";
|
||||||
|
public DateTime StartedAt { get; set; }
|
||||||
|
public DateTime EndedAt { get; set; }
|
||||||
|
public double Duration { get; set; }
|
||||||
|
public string? FilePath { get; set; }
|
||||||
|
public long Size { get; set; }
|
||||||
|
}
|
||||||
11
gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs
Normal file
11
gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class StreamUrls
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
13
gateway/src/IntegrationGateway.Core/Models/SyncReport.cs
Normal file
13
gateway/src/IntegrationGateway.Core/Models/SyncReport.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace IntegrationGateway.Core.Models;
|
||||||
|
|
||||||
|
public class SyncReport
|
||||||
|
{
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public int Added { get; set; }
|
||||||
|
public int Updated { get; set; }
|
||||||
|
public int Skipped { get; set; }
|
||||||
|
public int Removed { get; set; }
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway/alarms")]
|
||||||
|
public class AlarmsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public AlarmsController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpGet("{adapter}")]
|
||||||
|
public async Task<IActionResult> GetAlarms(string adapter,
|
||||||
|
[FromQuery] DateTime from, [FromQuery] DateTime to,
|
||||||
|
[FromQuery] int page = 1, [FromQuery] int size = 50)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasAlarms al) return NotFound();
|
||||||
|
return Ok(await al.GetAlarmsAsync(page, size, from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{adapter}/{alarmId}/confirm")]
|
||||||
|
public async Task<IActionResult> Confirm(string adapter, string alarmId)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasAlarms al) return NotFound();
|
||||||
|
await al.ConfirmAlarmAsync(alarmId);
|
||||||
|
return Ok(new { status = "confirmed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway/devices")]
|
||||||
|
public class DevicesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public DevicesController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetDevices([FromQuery] string adapter, [FromQuery] int page = 1, [FromQuery] int size = 50)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasFlatDevices f) return NotFound();
|
||||||
|
return Ok(await f.GetDevicesAsync(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{adapter}/{deviceId}")]
|
||||||
|
public async Task<IActionResult> GetDevice(string adapter, string deviceId)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasFlatDevices f) return NotFound();
|
||||||
|
var d = await f.GetDeviceAsync(deviceId);
|
||||||
|
return d is null ? NotFound() : Ok(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway/health")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public HealthController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get()
|
||||||
|
{
|
||||||
|
var status = new Dictionary<string, bool>();
|
||||||
|
foreach (var a in _registry.GetAll())
|
||||||
|
status[a.AdapterCode] = await a.HealthCheckAsync();
|
||||||
|
return Ok(new { gateway = "ok", adapters = status });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway/realtime")]
|
||||||
|
public class PointsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public PointsController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpGet("{adapter}/{deviceId}")]
|
||||||
|
public async Task<IActionResult> GetRealtime(string adapter, string deviceId)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasPoints p) return NotFound();
|
||||||
|
return Ok(await p.GetRealtimeValuesAsync(deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{adapter}/control")]
|
||||||
|
public async Task<IActionResult> Control(string adapter, [FromBody] ControlRequest req)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasPoints p) return NotFound();
|
||||||
|
await p.SetPointValueAsync(req.DeviceSourceId, req.PointIndex, req.Value);
|
||||||
|
return Ok(new { status = "sent" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ControlRequest
|
||||||
|
{
|
||||||
|
public string DeviceSourceId { get; set; } = "";
|
||||||
|
public int PointIndex { get; set; }
|
||||||
|
public double Value { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway")]
|
||||||
|
public class RegisterController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
|
||||||
|
public RegisterController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<IActionResult> Register([FromBody] RegisterRequest req)
|
||||||
|
{
|
||||||
|
// 网关向 Vol.Pro 注册,由外部 GatewayClient 调用
|
||||||
|
return Ok(new { status = "ok" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("heartbeat")]
|
||||||
|
public async Task<IActionResult> Heartbeat([FromBody] HeartbeatRequest req)
|
||||||
|
{
|
||||||
|
return Ok(new { status = "ok" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterRequest
|
||||||
|
{
|
||||||
|
public string NodeCode { get; set; } = "";
|
||||||
|
public string Token { get; set; } = "";
|
||||||
|
public string AdapterTypes { get; set; } = "";
|
||||||
|
public string BaseUrl { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeartbeatRequest
|
||||||
|
{
|
||||||
|
public string NodeCode { get; set; } = "";
|
||||||
|
public string Token { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway/streams")]
|
||||||
|
public class StreamsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public StreamsController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpGet("{adapter}/{channelId}/live")]
|
||||||
|
public async Task<IActionResult> GetLive(string adapter, string channelId)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasStreams s) return NotFound();
|
||||||
|
return Ok(await s.GetLiveUrlAsync(channelId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{adapter}/{channelId}/playback")]
|
||||||
|
public async Task<IActionResult> GetPlayback(string adapter, string channelId,
|
||||||
|
[FromQuery] DateTime start, [FromQuery] DateTime end)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasStreams s) return NotFound();
|
||||||
|
return Ok(await s.GetPlaybackUrlAsync(channelId, start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{adapter}/{channelId}/ptz")]
|
||||||
|
public async Task<IActionResult> Ptz(string adapter, string channelId, [FromBody] PtzRequest req)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter);
|
||||||
|
if (a is not IHasStreams s) return NotFound();
|
||||||
|
if (req.Direction == "stop")
|
||||||
|
await s.PtzStopAsync(channelId);
|
||||||
|
else
|
||||||
|
await s.PtzControlAsync(channelId, req.Direction, req.Speed);
|
||||||
|
return Ok(new { status = "ok" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PtzRequest
|
||||||
|
{
|
||||||
|
public string Direction { get; set; } = "stop";
|
||||||
|
public float Speed { get; set; } = 0.5f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using IntegrationGateway.Core.Abstractions;
|
||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using IntegrationGateway.Core.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/gateway")]
|
||||||
|
public class SyncController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AdapterRegistry _registry;
|
||||||
|
public SyncController(AdapterRegistry registry) => _registry = registry;
|
||||||
|
|
||||||
|
[HttpPost("devices/sync")]
|
||||||
|
public async Task<IActionResult> SyncDevices([FromQuery] string adapter)
|
||||||
|
{
|
||||||
|
var a = _registry.Get(adapter)
|
||||||
|
?? throw new InvalidOperationException($"Adapter '{adapter}' not found");
|
||||||
|
|
||||||
|
var report = new SyncReport { AdapterCode = adapter, StartTime = DateTime.UtcNow };
|
||||||
|
|
||||||
|
if (a is IHasFlatDevices f)
|
||||||
|
{
|
||||||
|
var devices = await f.GetAllDevicesAsync();
|
||||||
|
report.Added = devices.Count;
|
||||||
|
}
|
||||||
|
else if (a is IHasOwnDeviceTree t)
|
||||||
|
{
|
||||||
|
var tree = await t.GetObjectTreeAsync();
|
||||||
|
report.Added = CountDeviceNodes(tree);
|
||||||
|
}
|
||||||
|
else return BadRequest("Adapter does not support device sync");
|
||||||
|
|
||||||
|
report.EndTime = DateTime.UtcNow;
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CountDeviceNodes(List<DeviceTreeNode> nodes)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (var n in nodes)
|
||||||
|
{
|
||||||
|
if (n.NodeType == 2) count++;
|
||||||
|
count += CountDeviceNodes(n.Children);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
gateway/src/IntegrationGateway.Host/GatewayClient.cs
Normal file
118
gateway/src/IntegrationGateway.Host/GatewayClient.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace IntegrationGateway.Host;
|
||||||
|
|
||||||
|
public class GatewayClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public GatewayClient(IHttpClientFactory factory, IConfiguration config)
|
||||||
|
{
|
||||||
|
_http = factory.CreateClient("VolPro");
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A1: 网关注册</summary>
|
||||||
|
public async Task<RegisterResponse?> RegisterAsync()
|
||||||
|
{
|
||||||
|
var resp = await _http.PostAsJsonAsync("/api/gateway/register", new
|
||||||
|
{
|
||||||
|
nodeCode = _config["NodeCode"],
|
||||||
|
token = _config["NodeToken"],
|
||||||
|
adapterTypes = GetAdapterTypes(),
|
||||||
|
baseUrl = _config["Urls"]?.Replace("http://*:", $"http://localhost:")
|
||||||
|
});
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
return await resp.Content.ReadFromJsonAsync<RegisterResponse>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A2: 心跳</summary>
|
||||||
|
public async Task HeartbeatAsync()
|
||||||
|
{
|
||||||
|
await _http.PostAsJsonAsync("/api/gateway/heartbeat", new
|
||||||
|
{
|
||||||
|
nodeCode = _config["NodeCode"],
|
||||||
|
token = _config["NodeToken"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A3: 设备同步</summary>
|
||||||
|
public async Task<SyncResult> SyncDevicesAsync(List<DeviceSyncItem> devices)
|
||||||
|
{
|
||||||
|
var resp = await _http.PostAsJsonAsync("/api/gateway/sync/devices", new
|
||||||
|
{
|
||||||
|
nodeCode = _config["NodeCode"],
|
||||||
|
token = _config["NodeToken"],
|
||||||
|
devices
|
||||||
|
});
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
return await resp.Content.ReadFromJsonAsync<SyncResult>() ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A4: 告警同步</summary>
|
||||||
|
public async Task<SyncResult> SyncAlarmsAsync(List<AlarmSyncItem> alarms)
|
||||||
|
{
|
||||||
|
var resp = await _http.PostAsJsonAsync("/api/gateway/sync/alarms", new
|
||||||
|
{
|
||||||
|
nodeCode = _config["NodeCode"],
|
||||||
|
token = _config["NodeToken"],
|
||||||
|
alarms
|
||||||
|
});
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
return await resp.Content.ReadFromJsonAsync<SyncResult>() ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetAdapterTypes() => "MC4,Owl";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterResponse
|
||||||
|
{
|
||||||
|
public int NodeId { get; set; }
|
||||||
|
public List<DeviceItem> Devices { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceItem
|
||||||
|
{
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public string DeviceName { get; set; } = "";
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public string SourceId { get; set; } = "";
|
||||||
|
public string DeviceCategory { get; set; } = "";
|
||||||
|
public string DeviceGroup { get; set; } = "";
|
||||||
|
public string IsParent { get; set; } = "";
|
||||||
|
public string IsOnline { get; set; } = "";
|
||||||
|
public JsonElement? ExtraData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceSyncItem
|
||||||
|
{
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public string SourceId { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Category { get; set; } = "";
|
||||||
|
public string Group { get; set; } = "";
|
||||||
|
public bool IsParent { get; set; }
|
||||||
|
public string? ParentSourceId { get; set; }
|
||||||
|
public bool IsOnline { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public Dictionary<string, object?>? ExtraData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlarmSyncItem
|
||||||
|
{
|
||||||
|
public string SourceAlarmId { get; set; } = "";
|
||||||
|
public string DeviceSourceId { get; set; } = "";
|
||||||
|
public string AdapterCode { get; set; } = "";
|
||||||
|
public string Level { get; set; } = "";
|
||||||
|
public string Desc { get; set; } = "";
|
||||||
|
public double? Value { get; set; }
|
||||||
|
public string StartTime { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncResult
|
||||||
|
{
|
||||||
|
public int Added { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@IntegrationGateway.Host_HostAddress = http://localhost:5169
|
||||||
|
|
||||||
|
GET {{IntegrationGateway.Host_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
38
gateway/src/IntegrationGateway.Host/Program.cs
Normal file
38
gateway/src/IntegrationGateway.Host/Program.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using IntegrationGateway.Core.Infrastructure;
|
||||||
|
using IntegrationGateway.Host;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddSingleton<AdapterRegistry>();
|
||||||
|
builder.Services.AddSingleton<TokenManager>();
|
||||||
|
builder.Services.AddHttpClient("VolPro", c =>
|
||||||
|
{
|
||||||
|
c.BaseAddress = new Uri(builder.Configuration["VolProBaseUrl"] ?? "http://localhost:9100");
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<GatewayClient>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// 启动时自动向 Vol.Pro 注册
|
||||||
|
app.Lifetime.ApplicationStarted.Register(() =>
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var gw = app.Services.GetRequiredService<GatewayClient>();
|
||||||
|
var registry = app.Services.GetRequiredService<AdapterRegistry>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await gw.RegisterAsync();
|
||||||
|
Console.WriteLine($"[Gateway] Registered as NodeId={result?.NodeId}, Devices={result?.Devices.Count ?? 0}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Gateway] Registration failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:4117",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5169",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
gateway/src/IntegrationGateway.Host/appsettings.json
Normal file
13
gateway/src/IntegrationGateway.Host/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Urls": "http://*:5100",
|
||||||
|
"VolProBaseUrl": "http://localhost:9100",
|
||||||
|
"NodeCode": "gw-31ku",
|
||||||
|
"NodeToken": "xxxxxxxxxx"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user