T完成: TaskController创建+3个IJob构造函数改造(IServiceProvider注入)+RuleEngineJob标记迁移
This commit is contained in:
148
doc/设计文档/网关自动注册机制检查报告20260603.md
Normal file
148
doc/设计文档/网关自动注册机制检查报告20260603.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 网关 ↔ Vol.Pro 自动注册机制检查报告 2026-06-03
|
||||
|
||||
> **日期**: 2026-06-03
|
||||
> **检查范围**: `gateway/src/IntegrationGateway.Host/Program.cs` A1 注册段 + `VolPro gateway_nodesController.cs` A1-A4 + 相关 Service
|
||||
> **方法**: 逐行比对网关发送体 ↔ Vol.Pro 接收体 ↔ 数据库 Schema
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据流追踪
|
||||
|
||||
```
|
||||
网关 Program.cs (line 82-105)
|
||||
├─ InitializeAllAsync() ← 适配器初始化
|
||||
├─ RegisterAsync(registerReq) ← A1: POST /api/gateway/register
|
||||
└─ (无后续调用) ← A2/A3 未触发
|
||||
|
||||
Vol.Pro gateway_nodesController
|
||||
├─ [POST] /api/gateway/register ← RegisterGateway → RegisterNodeAsync
|
||||
├─ [POST] /api/gateway/heartbeat ← GatewayHeartbeat → UpdateHeartbeatAsync
|
||||
├─ [POST] /api/gateway/sync/devices ← SyncDevices → SyncDevicesAsync
|
||||
└─ [POST] /api/gateway/sync/alarms ← SyncAlarms → UpsertAlarmAsync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 发现的问题
|
||||
|
||||
### 2.1 🔴 网关不调用 A3 设备同步 — 注册后设备列表为空
|
||||
|
||||
**现状**: 网关 Program.cs 在 A1 注册后**不调用** `clientFactory.SyncDevicesAsync()`。Vol.Pro 的 `RegisterGateway` 返回设备列表时调用 `GetDevicesByGatewayNodeAsync(node.NodeId)`,查询 `WHERE NodeId=xxx AND ParentDeviceId IS NULL`。
|
||||
|
||||
**后果**: 首次注册返回的设备列表永远为空(数据库尚无此网关的设备记录)。设备必须等 Vol.Pro 的 Quartz `SyncDevicesJob`(每 5 分钟触发一次)才有机会同步。
|
||||
|
||||
**修复**: A1 注册后立即遍历适配器同步设备,整体流程改为:
|
||||
|
||||
```csharp
|
||||
// A1 注册
|
||||
var registerResult = await clientFactory.RegisterAsync(registerReq);
|
||||
// A3 同步设备(Owl → GetDevicesAsync, MC4 → GetObjectTreeAsync)
|
||||
foreach (var adapter in registry.All)
|
||||
{
|
||||
var devices = adapter switch
|
||||
{
|
||||
IHasFlatDevices f => (await f.GetDevicesAsync(1, 1000)).Items,
|
||||
IHasOwnDeviceTree t => FlattenTree(await t.GetObjectTreeAsync()),
|
||||
_ => new()
|
||||
};
|
||||
if (devices.Any())
|
||||
await clientFactory.SyncDevicesAsync(nodeCode, nodeToken, devices.Select(d => new { ... }).ToList());
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 🔴 网关 A1 BaseUrl 硬编码 `localhost`
|
||||
|
||||
**现状**:
|
||||
```csharp
|
||||
BaseUrl = $"http://localhost:{app.Urls.FirstOrDefault()?.Split(':').LastOrDefault() ?? "5100"}"
|
||||
```
|
||||
|
||||
**后果**: 网关部署在 `192.168.1.100` 时,Vol.Pro 存的 BaseUrl 仍是 `http://localhost:5100`。Vol.Pro 端的 GatewayClient 和 Quartz Job 用此地址回调网关时全部失败。
|
||||
|
||||
**修复**: 改为读取配置或使用 `app.Configuration["Gateway:SelfUrl"]`,不填时降级为 `localhost`:
|
||||
|
||||
```csharp
|
||||
BaseUrl = gwCfg["SelfUrl"] ?? $"http://localhost:{port}"
|
||||
```
|
||||
|
||||
### 2.3 🟠 网关不调用 A2 心跳 — 无持续在线状态更新
|
||||
|
||||
**现状**: 网关只在 A1 注册时上报一次在线状态,之后不再调 A2 心跳。
|
||||
|
||||
**后果**: Vol.Pro 的 `gateway_nodes.LastHeartbeat` 停留在注册时刻,`HeartbeatMonitorJob`(每 15s)会在注册后 30s 将网关标记离线。
|
||||
|
||||
**修复**: 注册完成后启动后台心跳循环:
|
||||
|
||||
```csharp
|
||||
_ = Task.Run(async () => {
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(15));
|
||||
try { await clientFactory.HeartbeatAsync(new GatewayHeartbeatRequest { NodeCode = nodeCode, Token = nodeToken }); }
|
||||
catch { Console.Error.WriteLine("[Gateway] 心跳失败"); }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2.4 🟠 Vol.Pro — RegisterNodeAsync 使用旧 Queryable 语法
|
||||
|
||||
**现状**:
|
||||
```csharp
|
||||
var existing = _repository.DbContext.Queryable<gateway_nodes>()
|
||||
.First(x => x.NodeCode == nodeCode);
|
||||
```
|
||||
|
||||
`DbContext.Queryable` 是 SqlSugar 原始方式,项目中其他 Service 使用 `FindAsIQueryable`(Vol.Pro 封装)。
|
||||
|
||||
**后果**: 不影响功能但风格不一致。且 `.First()` 可能抛异常(找不到记录时),而 `.FirstOrDefault()` + null 检查更安全。
|
||||
|
||||
**修复**: 改为 `FindAsIQueryable(x => x.NodeCode == nodeCode).FirstOrDefaultAsync()`。
|
||||
|
||||
### 2.5 🟡 A1 返回结果未被网关使用
|
||||
|
||||
**现状**:
|
||||
```csharp
|
||||
var registerResult = await clientFactory.RegisterAsync(registerReq);
|
||||
// registerResult 未使用
|
||||
```
|
||||
|
||||
`RegisterGateway` 返回 `{ nodeId, devices: [...] }`,网关不读取也不使用。
|
||||
|
||||
**后果**: 网关不知道自己的 NodeId,后续 A2/A3 需要 nodeCode + token 而非 nodeId。GatewayClientFactory 的 A2/A3 方法也用的是 nodeCode + token,所以不依赖 nodeId。
|
||||
|
||||
**评估**: **不需要修复** — 当前设计合理(nodeCode 是天然业务主键)。
|
||||
|
||||
### 2.6 🟡 base_deviceService 与 gateway_nodesService 重复实现设备同步
|
||||
|
||||
**现状**:
|
||||
- `gateway_nodesService.SyncDevicesAsync` — 完整的设备同步(新增+更新)
|
||||
- `base_deviceService.UpsertDeviceAsync` — 单设备 Upsert(被 Controller 调用但实际未被使用)
|
||||
|
||||
`gateway_nodesController.SyncDevices` 调的是 `gateway_nodesService.SyncDevicesAsync` 而非 `base_deviceService.UpsertDeviceAsync`。
|
||||
|
||||
**后果**: `base_deviceService.UpsertDeviceAsync` 是死代码。
|
||||
|
||||
**修复**: 保留 `gateway_nodesService.SyncDevicesAsync`(批量处理更高效),移除或标记 `base_deviceService.UpsertDeviceAsync` 为 `[Obsolete]`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 调用链完整性矩阵
|
||||
|
||||
| 接口 | Vol.Pro 端 | 网关调用 | 状态 |
|
||||
|------|:---:|:---:|:--:|
|
||||
| A1 /api/gateway/register | ✅ RegisterGateway | ✅ 已调用 | ⚠️ BaseUrl=localhost / 不返设备 |
|
||||
| A2 /api/gateway/heartbeat | ✅ GatewayHeartbeat | ❌ 未调用 | 🔴 30秒后被标记离线 |
|
||||
| A3 /api/gateway/sync/devices | ✅ SyncDevices | ❌ 未调用 | 🔴 首次注册设备列表为空 |
|
||||
| A4 /api/gateway/sync/alarms | ✅ SyncAlarms | ❌ 未调用 | ⚪ 告警由 Vol.Pro Quartz 拉取 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 修复优先级
|
||||
|
||||
| 编号 | 问题 | 严重度 | 预计 |
|
||||
|:---:|------|:---:|:---:|
|
||||
| 1 | A3 设备同步未触发 | 🔴 | 30min |
|
||||
| 2 | A1 BaseUrl=localhost | 🔴 | 10min |
|
||||
| 3 | A2 心跳未循环 | 🟠 | 15min |
|
||||
| 4 | RegisterNodeAsync 语法不一致 | 🟠 | 5min |
|
||||
| 5 | 重复的设备 Upsert 逻辑 | 🟡 | 10min |
|
||||
Reference in New Issue
Block a user