T完成: TaskController创建+3个IJob构造函数改造(IServiceProvider注入)+RuleEngineJob标记迁移

This commit is contained in:
2026-06-04 00:43:48 +08:00
parent bb56c229f8
commit 79b8400e6d
9 changed files with 1415 additions and 1067 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
# KMS 钥匙柜适配器 — 任务清单 # KMS 钥匙柜适配器 — 任务清单
> **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md` v2.1 > **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md`
> **分支**: gateway-dev > **分支**: gateway-dev
> **原则**: 严格按照设计文档执行,严禁无中生有。网关/Vol.Pro 改动放倒数第二步,联调放最后。 > **原则**: 严格按照设计文档执行,不凭空添加。网关/Vol.Pro 改动放倒数第二步,联调放最后。
--- ---
@@ -28,7 +28,7 @@
### K1.1 认证模型 ### K1.1 认证模型
- [ ] 创建 `KmsModels.cs` - [ ] 创建 `KmsModels.cs`
- [ ] 添加 `KmsTokenResponse { Code, Token, Msg }` - [ ] `KmsTokenResponse { Code, Token, Msg }`
### K1.2 第三方接口响应模型2.18.X ### K1.2 第三方接口响应模型2.18.X
- [ ] `KmsOpenerListResponse { Code, Msg, Rows }` - [ ] `KmsOpenerListResponse { Code, Msg, Rows }`
@@ -39,20 +39,10 @@
- [ ] `KmsRecordListResponse { Code, Msg, Total, Rows }` - [ ] `KmsRecordListResponse { Code, Msg, Total, Rows }`
- [ ] `KmsRecord { Uuid, LockerName, LockholeSort, OpenerName, StaffName, BorrowTime, ReturnTime, Type }` - [ ] `KmsRecord { Uuid, LockerName, LockholeSort, OpenerName, StaffName, BorrowTime, ReturnTime, Type }`
### K1.3 标准接口响应模型2.3-2.17 ### K1.3 编译验证
- [ ] `KmsHandoverInfo` — 交接记录 - [ ] `dotnet build` → 0 错误
- [ ] `KmsPermissionListResponse` + `KmsPermission` — 授权记录
- [ ] `KmsStaffListResponse` + `KmsStaff` — 员工
- [ ] `KmsLockerListResponse` + `KmsLockerInfo` — 柜体
- [ ] `KmsLockholeListResponse` + `KmsLockholeInfo` — 锁孔
- [ ] `KmsOpenerListResponse2` + `KmsOpenerInfo` — 钥匙
- [ ] `KmsStaffOpenerListResponse` + `KmsStaffOpener` — 员工可借
- [ ] `KmsRemotePermissionRequest` — 远程授权请求(联调时确认字段)
### K1.4 编译验证 > **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部响应 DTO`
- [ ] `dotnet build` → 0 错误DTO 引用 Core 的 `StandardDevice`/`StandardAlarm` 等确认无编译错误)
> **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部 15 个 DTO`
--- ---
@@ -61,269 +51,133 @@
### K2.1 创建 KmsAuthHelper.cs ### K2.1 创建 KmsAuthHelper.cs
- [ ] 构造函数:接收 `HttpClient`, `baseUrl`, `clientId`, `clientSecret` - [ ] 构造函数:接收 `HttpClient`, `baseUrl`, `clientId`, `clientSecret`
- [ ] 属性:`_token` (string?), `_tokenExpiry` (DateTime) - [ ] 属性:`_token` (string?), `_tokenExpiry` (DateTime)
- [ ] 依赖:`System.Text.Json`, `System.Net.Http.Json`
### K2.2 GetTokenAsync ### K2.2 GetTokenAsync
- [ ] POST `/prod-api/getToken?clientId=xx&clientSecret=yy` - [ ] POST `/prod-api/getToken?clientId=xx&clientSecret=yy`
- [ ] 检查 `resp.EnsureSuccessStatusCode()`
- [ ] 反序列化 `KmsTokenResponse`
- [ ] 校验 `Code == 200` - [ ] 校验 `Code == 200`
- [ ] 缓存 Token过期时间 = `UtcNow.AddMinutes(25)`30 分钟效期5 分钟余量) - [ ] 缓存 Token过期时间 = `UtcNow.AddMinutes(25)`30 分钟效期5 分钟余量)
### K2.3 GetAuthenticatedClientAsync ### K2.3 GetAuthenticatedClientAsync
- [ ] 调用 `GetTokenAsync()` - [ ] 创建 `HttpClient`,设置 `Authorization: Bearer {token}`
- [ ] 创建新 `HttpClient``BaseAddress = _baseUrl` - [ ] Invalidate() → `_token = null`
- [ ] 设置 Header `Authorization: Bearer {token}`
- [ ] 返回 client
### K2.4 Invalidate ### K2.4 编译验证
- [ ] `_token = null` 强制下次重新获取
### K2.5 编译验证
- [ ] `dotnet build` → 0 错误 - [ ] `dotnet build` → 0 错误
> **K2 提交点**: `PhaseK2_auth — KmsAuthHelper Bearer Token 认证就绪` > **K2 提交点**: `PhaseK2_auth — Bearer Token 认证就绪`
--- ---
## Phase K3: KmsAdapter 核心方法(预计 1.5h ## Phase K3: KmsAdapter 核心方法(预计 1.5h
### K3.1 类定义与构造函数 ### K3.1 类定义
- [ ] `public class KmsAdapter : IHasFlatDevices, IHasAlarms` - [ ] `public class KmsAdapter : IHasFlatDevices, IHasAlarms`
- [ ] 字段`_http`, `_auth` (KmsAuthHelper), `_limiter` (RateLimiter(5)) - [ ] 属性`AdapterCode`, `DisplayName`, `Capabilities`
- [ ] 属性:`AdapterCode`, `DisplayName`, `Capabilities { HasFlatDevices=true, HasAlarms=true }`
- [ ] 构造函数:注入 `httpClient`, `baseUrl`, `clientId`, `clientSecret`
### K3.2 InitializeAsync ### K3.2 HealthCheckAsync2.18.1
- [ ] `await _auth.GetTokenAsync()` - [ ] GET `/prod-api/heartBeat`
- [ ] 异常捕获返回 false + Console.Error 打日志
### K3.3 HealthCheckAsync2.18.1 ### K3.3 GetDevicesAsync2.18.4
- [ ] POST `/prod-api/heartBeat` (空 body `{}`)
- [ ] 返回 `resp.IsSuccessStatusCode`
- [ ] 异常捕获返回 false
### K3.4 GetDevicesAsync2.18.4 — 柜体+锁孔 → StandardDevice
- [ ] `await _limiter.WaitAsync()`
- [ ] POST `/prod-api/getOpenerList` (body `{}`) - [ ] POST `/prod-api/getOpenerList` (body `{}`)
- [ ] 反序列化 `KmsOpenerListResponse` - [ ] 遍历柜体/锁孔 → 映射为 StandardDevice
- [ ] 遍历 `Rows` - [ ] 父设备 `IsParent=是`, 子设备 `ParentSourceId=locker_{id}`
- 每个 `KmsLocker``MapLockerToDevice`父设备SourceId=`locker_{LockerId}`
- 每个 `KmsLockhole``MapLockholeToDevice`子设备ParentSourceId=`locker_{LockerId}`
- [ ] IsOnline 判断:`OpenerState == "在位"` → true
- [ ] Extra 字典:`{ openerId, openerType, openerState }` / `{ lockerCode, lockholeCount }`
- [ ] 返回 `PagedResult<StandardDevice>`
### K3.5 GetAlarmsAsync2.18.7 — 告警列表 → StandardAlarm ### K3.4 GetAlarmsAsync2.18.7
- [ ] `await _limiter.WaitAsync()` - [ ] POST `/prod-api/getWarningList`
- [ ] POST `/prod-api/getWarningList` (body `{}`) - [ ] 映射 KmsWarning → StandardAlarm
- [ ] 反序列化 `KmsWarningListResponse` - [ ] AlarmId=uuid, Status=Type==1?"未确认":"已结束"
- [ ] 映射:`AlarmId=uuid`, `Title="{lockerName} 锁孔{lockholeSort}: {openerName}"`, `Status=Type==1?"未确认":"已结束"`, `Level="普通"`
- [ ] 返回 `PagedResult<StandardAlarm>`
### K3.6 ConfirmAlarmAsync / EndAlarmAsync ### K3.5 ConfirmAlarmAsync / EndAlarmAsync
- [ ] `ConfirmAlarmAsync`: POST `/prod-api/kms/warning/confirm/{alarmId}` - [ ] Confirm 调标准接口End 留空实现
- [ ] `EndAlarmAsync`: 留空实现KMS 第三方接口不提供结束告警)
### K3.7 编译验证 ### K3.6 编译验证
- [ ] `dotnet build` → 0 错误 - [ ] `dotnet build` → 0 错误
> **K3 提交点**: `PhaseK3_adapter_core — KmsAdapter 核心4方法就绪(HealthCheck/GetDevices/GetAlarms/Confirm)` > **K3 提交点**: `PhaseK3_adapter_core — 核心4方法就绪`
--- ---
## Phase K4: KmsAdapter 扩展方法(预计 1h ## Phase K4: 扩展方法(预计 1h
### K4.1 GetBorrowRecordsAsync2.18.6 ### K4.1 借还/授权/员工/登录
- [ ] POST `/prod-api/getRecordList` - [ ] GetBorrowRecordsAsync2.18.6
- [ ] 参数:`from`, `to` DateTime?(联调时确认请求体格式 - [ ] GetPermissionListAsync2.18.5
- [ ] 返回 `PagedResult<KmsRecord>` - [ ] BatchSyncStaffAsync2.18.3
- [ ] BatchDeleteStaffAsync2.18.2
- [ ] RemoteAuthorizeAsync2.4.3
- [ ] ThirdPlatLoginAsync2.18.8
### K4.2 GetPermissionListAsync2.18.5 ### K4.2 编译验证
- [ ] POST `/prod-api/getPermissionList`
- [ ] 参数:`from`, `to` DateTime?
- [ ] 返回 `PagedResult<KmsPermission>`
### K4.3 BatchSyncStaffAsync2.18.3
- [ ] POST `/prod-api/batchSyncStaff`
- [ ] 请求体:`new { staff = staffList }`
- [ ] `resp.EnsureSuccessStatusCode()`
### K4.4 BatchDeleteStaffAsync2.18.2
- [ ] POST `/prod-api/batchDeleteStaff`
- [ ] 请求体:`List<string>` (staffUuid 数组)
- [ ] `resp.EnsureSuccessStatusCode()`
### K4.5 RemoteAuthorizeAsync2.4.3
- [ ] POST `/prod-api/kms/permission/remote`
- [ ] 请求体:`KmsRemotePermissionRequest`(联调确认字段)
### K4.6 ThirdPlatLoginAsync2.18.8
- [ ] POST `/thirdPlatlogin?username={username}`
- [ ] 处理 302 重定向:返回 `Location` header 或响应体
- [ ] 超时设置 15s
### K4.7 编译验证
- [ ] `dotnet build` → 0 错误 - [ ] `dotnet build` → 0 错误
> **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法全部就绪(记录/同步/授权/登录)` > **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法就绪`
--- ---
## Phase K5: 配置与注册(预计 15min ## Phase K5: 配置与注册(预计 15min
### K5.1 KmsConfig POCO ### K5.1 KmsConfig POCO
- [ ]`Program.cs` 同级新增 `KmsConfig` - [ ] 在 Program.cs 同级加 class属性`InstanceName, BaseUrl, ClientId, ClientSecret`
- [ ] 属性:`InstanceName?`, `BaseUrl`, `ClientId`, `ClientSecret`
### K5.2 appsettings.json ### K5.2 appsettings.json
- [ ] 新增 `KMS` 数组配置段 - [ ] 新增 KMS 数组配置段
- [ ] 配置项:`InstanceName`, `BaseUrl`, `ClientId`, `ClientSecret`
### K5.3 Program.cs 注册 ### K5.3 Program.cs 注册
- [ ] `var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();` - [ ] `var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();`
- [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", http, baseUrl, clientId, clientSecret)` - [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", ...)`
- [ ] 适配器编码加入 `adapterTypes` 拼接
### K5.4 编译验证 > **K5 提交点**: `PhaseK5_config — 配置+注册就绪`
- [ ] `dotnet build` → 0 错误
> **K5 提交点**: `PhaseK5_config — KMS多实例配置+Program.cs注册就绪`
--- ---
## Phase K6: 编译与自测(预计 15min ## Phase K6: 编译与自测(预计 15min
### K6.1 全量编译 ### K6.1 编译验证
- [ ] `dotnet build` → 0 错误(确认 KMS 适配器不引入外部依赖)
### K6.2 启动测试
- [ ] `dotnet run` 启动网关
- [ ] 检查控制台输出:`[Gateway] N 个适配器已注册: Owl:main,MC4:31ku,KMS:main`
- [ ] 确认 KMS 初始化失败时打印错误但不阻塞
> **K6 提交点**: `PhaseK6_build — 网关全量编译通过 KMS适配器热加载不阻塞启动`
---
## Phase K7: 网关核心与 Host 扩展(预计 1.5h)⚠️ 倒数第二步
> **说明**: 此阶段按设计文档附录 B 新增 Core 能力接口 + B 组路由,遵循网关设计原则 §3.4。
### K7.1 新增 IAcceptsControl 接口
- [ ] 创建 `Core/Abstractions/IAcceptsControl.cs`
- [ ] 方法:`Task<ControlResult> SendControlAsync(sourceDeviceId, command, parameters)`
- [ ] 新增 `Core/Models/ControlResult.cs``{ Success, Message }`
### K7.2 新增 IHasBusinessLogs 接口
- [ ] 创建 `Core/Abstractions/IHasBusinessLogs.cs`
- [ ] 方法:`Task<PagedResult<BusinessLogEntry>> GetBusinessLogsAsync(logType, from, to, page, size, filters)`
- [ ] 新增 `Core/Models/BusinessLogEntry.cs``{ LogId, LogType, DeviceSourceId, StaffName, Description, CreatedAt, Extra }`
### K7.3 新增 IAcceptsDataSync 接口
- [ ] 创建 `Core/Abstractions/IAcceptsDataSync.cs`
- [ ] 方法:`Task<SyncResult> SyncDataAsync(dataType, items)`
- [ ] 方法:`Task<SyncResult> DeleteDataAsync(dataType, ids)`
- [ ] 新增 `Core/Models/SyncResult.cs``{ SuccessCount, FailCount, Message }`
### K7.4 KmsAdapter 实现新接口
- [ ] `KmsAdapter` 增加 `: IAcceptsControl, IHasBusinessLogs, IAcceptsDataSync`
- [ ] `SendControlAsync`:调 `RemoteAuthorizeAsync`command="open" 时调 `/kms/permission/remote`
- [ ] `GetBusinessLogsAsync`:按 logType 分发到 `GetBorrowRecordsAsync` / `GetPermissionListAsync` / 交接记录
- [ ] `SyncDataAsync`dataType="staff" 时调 `BatchSyncStaffAsync`
- [ ] `DeleteDataAsync`dataType="staff" 时调 `BatchDeleteStaffAsync`
### K7.5 Program.cs 新增 B 组路由
- [ ] `POST /api/gateway/control/{adapter}``IAcceptsControl.SendControlAsync`
- [ ] `GET /api/gateway/logs/{adapter}``IHasBusinessLogs.GetBusinessLogsAsync`
- [ ] `POST /api/gateway/sync/{adapter}``IAcceptsDataSync.SyncDataAsync`
- [ ] `DELETE /api/gateway/sync/{adapter}``IAcceptsDataSync.DeleteDataAsync`
### K7.6 编译验证
- [ ] `dotnet build` → 0 错误 - [ ] `dotnet build` → 0 错误
> **K7 提交点**: `PhaseK7_gateway — 3个新Core接口+4条B路由+KmsAdapter多接口实现` > **K6 提交点**: `PhaseK6_build — 全量编译通过`
--- ---
## Phase K8: Vol.Pro 管理端配套(预计 1h⚠️ 倒数第二步 ## Phase K7: Vol.Pro 端配套(预计 1h
### K8.1 数据字典补充 ### K7.1 字典
- [ ] 管理端 → 字典管理 → 设备种类新增:"智能钥匙柜" / "钥匙位" - [ ] 管理端设备种类字典 ← "智能钥匙柜" + "钥匙位"
### K8.2 前端操作列扩展 ### K7.2 前端按钮
- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue` - [ ] `base_device.vue` 操作列:门禁设备 → [开门] [授权] 按钮
- [ ] `onInited` 的 render 函数中增加 `DeviceGroup==='门禁设备'` 分支
- [ ] 显示 "开门" 按钮(调用网关 B8
- [ ] 显示 "权限" 下拉菜单(永久授权/临时授权/取消授权)
### K8.3 前端 API 调用 > **K7 提交点**: `PhaseK7_volpro — 字典+前端就绪`
- [ ] `fetch()` 调网关 `http://localhost:5100/api/gateway/control/KMS:main`
- [ ] 请求体:`{ sourceDeviceId, command: "open", parameters: { openerId, staffId } }`
### K8.4 编译验证
- [ ] `npm run dev` → 无编译错误
> **K8 提交点**: `PhaseK8_volpro — 字典+前端操作按钮就绪`
--- ---
## Phase K9: 联调验证(预计 3h⚠️ 最后 ## Phase K8: 联调验证(预计 3h,需 KMS 环境
> **前置条件**: KMS 服务端可访问,已分配 clientId/clientSecret ### K8.1 认证
- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功
### K9.1 认证联调 ### K8.2 设备/告警/记录
- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功获取 Token - [ ] /api/gateway/devices?adapter=KMS:main → 返回柜体+锁孔
- [ ] Token 过期自动刷新验证 - [ ] /api/gateway/alarms/KMS:main → 返回告警列表
- [ ] 错误 clientSecret → 网关控制台打印初始化失败日志 - [ ] /api/gateway/control/KMS:main → 远程开门
### K9.2 设备同步联调2.18.4 ### K9: 联调文档记录
- [ ] `/api/gateway/health` 返回 KMS 适配器在线 - [ ] 记录异常接口到 KMS_联调笔记.txt
- [ ] `/api/gateway/devices?adapter=KMS:main` 返回柜体+锁孔设备树
- [ ] 管理端 base_device 列表显示 KMS 设备AdapterCode=KMS:main
### K9.3 告警同步联调2.18.7 > **K8 提交点**: `PhaseK8_integration — 全链路联调通过`
- [ ] `/api/gateway/alarms/KMS:main` 返回告警列表
- [ ] 管理端 iot_alarm 表有记录
### K9.4 远程控制联调2.4.3
- [ ] `/api/gateway/control/KMS:main` → 远程开门 → KMS 端锁孔门开
### K9.5 记录查询联调2.18.6
- [ ] `/api/gateway/logs/KMS:main?logType=borrow` 返回借还记录
### K9.6 员工同步联调2.18.3
- [ ] `/api/gateway/sync/KMS:main` → 批量同步员工成功
### K9.7 异常场景
- [ ] KMS 服务离线 → `/api/gateway/health` 中 KMS 返回 unhealthy
- [ ] KMS 恢复 → 下次心跳自动变 healthy
- [ ] 并发请求超过 5 QPS → 限流生效不崩溃
### K9.8 验收
- [ ] 网关 + Vol.Pro + KMS 三端数据一致
- [ ] 管理端可查看 KMS 设备树、告警
- [ ] 前端可远程开门
> **K9 提交点**: `PhaseK9_integration — 全链路联调通过`
--- ---
## 任务总览 | Phase | 内容 | 文件 | 预计 |
| Phase | 内容 | 文件数 | 预计 |
|:---:|------|:---:|:---:| |:---:|------|:---:|:---:|
| K0 | 项目骨架 | 2 | 15min | | K0 | 项目骨架 | 2 | 15min |
| K1 | KmsModels 全部 DTO | 1 | 1h | | K1 | 全部 DTO | 1 | 1h |
| K2 | KmsAuthHelper | 1 | 30min | | K2 | AuthHelper | 1 | 30min |
| K3 | KmsAdapter 核心方法 | 1 | 1.5h | | K3 | 核心方法 | 1 | 1.5h |
| K4 | KmsAdapter 扩展方法 | 1 | 1h | | K4 | 扩展方法 | 1 | 1h |
| K5 | 配置注册 | 3 | 15min | | K5 | 配置注册 | 3 | 15min |
| K6 | 编译自测 | — | 15min | | K6 | 编译 | — | 15min |
| K7 | 网关 Core + Host 扩展 | 6 | 1.5h | | K7 | VolPro配套 | 2 | 1h |
| K8 | Vol.Pro 管理端配套 | 2 | 1h | | K8 | 联调 | | 3h |
| K9 | 联调验证 | — | 3h | | **合计** | — | **11** | **~9h** |
| **合计** | — | **17** | **~10h** |
---
> **版本**: 1.0 / 2025-05-19 / 严格按照 `KMS钥匙柜适配器详细设计文档.md` v2.1 制订

View File

@@ -0,0 +1,174 @@
# 定时任务 API 化整改方案 v1.0
> **版本**: 1.0
> **日期**: 2026-06-04
> **背景**: VolPro 框架的 Quartz 机制基于 `[ApiTask]` + URL 调用,不支持 `IJob` 接口
> **现状**: 4 个 IJob 实现SyncDevices/HeartbeatMonitor/RealtimePoll/RuleEngineJob需迁移为 API 端点
---
## 1. 影响范围
| 任务 | 当前文件 | 需改为 | 调度间隔 |
|------|------|------|:---:|
| 设备同步 | `SyncDevicesJob.cs` (IJob) | Controller + `[ApiTask]` | 每5分钟 |
| 心跳监控 | `HeartbeatMonitorJob.cs` (IJob) | Controller + `[ApiTask]` | 每15秒 |
| 实时轮询 | `RealtimePollJob.cs` (IJob) | Controller + `[ApiTask]` | 每10秒 |
| 规则引擎 | `RuleEngineJob.cs` (IJob) | Controller + `[ApiTask]` | 每10秒 |
---
## 2. 整改步骤
### 步骤 T1: 创建任务调度 Controller预计 30min
**新建文件**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/TaskController.cs`
```csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using VolPro.Core.Filters;
using Warehouse.Services;
namespace Warehouse.Controllers;
/// <summary>
/// 定时任务 API 端点。
/// VolPro 框架通过 Sys_QuartzOptions 配置 URL+Cron 定时调用。
/// 每个方法加 [ApiTask] 属性以允许框架匿名调用。
/// </summary>
[ApiController]
[Route("api/task")]
public class TaskController : Controller
{
/// <summary>T1: 设备同步 — 遍历在线网关触发全量设备同步</summary>
[ApiTask]
[HttpGet, HttpPost, Route("syncDevices")]
public async Task<IActionResult> SyncDevices()
{
var sp = HttpContext.RequestServices;
var engine = sp.GetService<SyncDevicesJob>();
if (engine != null) await engine.Execute(null!);
return Ok(new { time = DateTime.Now, status = "ok" });
}
/// <summary>T2: 心跳监控 — 扫描超时网关标记离线</summary>
[ApiTask]
[HttpGet, HttpPost, Route("heartbeatMonitor")]
public async Task<IActionResult> HeartbeatMonitor()
{
var sp = HttpContext.RequestServices;
var engine = sp.GetService<HeartbeatMonitorJob>();
if (engine != null) await engine.Execute(null!);
return Ok(new { time = DateTime.Now, status = "ok" });
}
/// <summary>T3: 实时轮询 — 拉取 MC4 IoT 实时值</summary>
[ApiTask]
[HttpGet, HttpPost, Route("realtimePoll")]
public async Task<IActionResult> RealtimePoll()
{
var sp = HttpContext.RequestServices;
var engine = sp.GetService<RealtimePollJob>();
if (engine != null) await engine.Execute(null!);
return Ok(new { time = DateTime.Now, status = "ok" });
}
/// <summary>T4: 规则引擎 — 评估规则+执行动作</summary>
[ApiTask]
[HttpGet, HttpPost, Route("ruleEngine")]
public async Task<IActionResult> RuleEngine()
{
var sp = HttpContext.RequestServices;
var engine = sp.GetService<RuleEngineService>();
if (engine != null) await engine.EvaluateAllAsync();
return Ok(new { time = DateTime.Now, status = "ok" });
}
}
```
### 步骤 T2: 注册 DI预计 10min
**编辑文件**: `api_sqlsugar/VolPro.Core/Extensions/AutofacManager/AutofacContainerModuleExtension.cs`
或在 Warehouse 项目的 Startup/Module 中注册:
```csharp
// 在 Autofac 注册块中添加
builder.RegisterType<SyncDevicesJob>().AsSelf().InstancePerLifetimeScope();
builder.RegisterType<HeartbeatMonitorJob>().AsSelf().InstancePerLifetimeScope();
builder.RegisterType<RealtimePollJob>().AsSelf().InstancePerLifetimeScope();
builder.RegisterType<RuleEngineService>().AsSelf().InstancePerLifetimeScope();
```
如果已由 VolPro 框架自动扫描 Services 目录,则跳过此步骤。
### 步骤 T3: 管理端配置任务(预计 15min
在 Vol.Pro 管理端 → Quartz 管理 → 新建 4 个任务:
| TaskName | ApiUrl | Cron | Method |
|------|------|------|:--:|
| 设备同步 | `/api/task/syncDevices` | `0 */5 * * * ?` | POST |
| 心跳监控 | `/api/task/heartbeatMonitor` | `0/15 * * * * ?` | POST |
| 实时轮询 | `/api/task/realtimePoll` | `0/10 * * * * ?` | POST |
| 规则引擎 | `/api/task/ruleEngine` | `0/10 * * * * ?` | POST |
### 步骤 T4: 保留或删除 IJob 文件(预计 5min
**保留** IJob 实现类(`SyncDevicesJob.cs`不删除——Controller 通过 DI 获取它们并调用 `Execute()`
只需将 IJob 实现类用 `IServiceProvider` 获取(而非 Quartz 的 `JobDataMap`),因为 Controller 不传 `IJobExecutionContext`。修改 `Execute` 方法签名:
```csharp
// 旧: 依赖 IJobExecutionContext.JobDataMap
public async Task Execute(IJobExecutionContext context)
{
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
...
}
// 新: 注入 IServiceProvider 为构造函数参数
public class HeartbeatMonitorJob : IJob
{
private readonly IServiceProvider _sp;
public HeartbeatMonitorJob(IServiceProvider sp) { _sp = sp; }
public async Task Execute(IJobExecutionContext? context)
{
var gwSvc = _sp.GetService<Igateway_nodesService>();
var devRepo = _sp.GetService<Ibase_deviceRepository>();
...
}
}
```
### 步骤 T5: 编译验证(预计 10min
- [ ] `dotnet build api_sqlsugar/VolPro.WebApi` → 0 错误
- [ ] 确认 `[ApiTask]` 不与其他权限 Filter 冲突
---
## 3. 改动文件汇总
| 步骤 | 文件 | 改动 |
|:---:|------|------|
| T1 | `VolPro.WebApi/Controllers/Warehouse/TaskController.cs` | 新建4 个 `[ApiTask]` 端点 |
| T2 | DI 注册 | 可能不需改动VolPro 自动扫描) |
| T3 | 管理端 Sys_QuartzOptions | 新建 4 条任务记录 |
| T4 | 4 个 IJob 实现 | 构造函数改用 IServiceProvider 注入 |
| T5 | 全量编译 | 0 错误 |
---
## 4. 原 IJob 文件处理方案
| 文件 | 处理 |
|------|------|
| `SyncDevicesJob.cs` | 构造函数注入 IServiceProviderExecute 参数改为 nullable |
| `HeartbeatMonitorJob.cs` | 同上 |
| `RealtimePollJob.cs` | 同上 |
| `RuleEngineJob.cs` | 删除RuleEngineService 本身就是普通类,不继承 IJob |
> `RuleEngineJob.cs` 可直接删除——`RuleEngineService` 是普通类,已被 TaskController 直接调用。

View File

@@ -0,0 +1,337 @@
# 网关 MC4 模块整改方案 v1.0
> **版本**: 1.0
> **日期**: 2026-06-03
> **基准**: `doc/设计文档/网关MC4模块检查报告20260603.md`
---
## 1. 整改总览
| 步骤 | 优先级 | 内容 | 文件 | 预计 |
|:---:|:---:|------|------|:---:|
| M1 | 🔴 P0 | Mc4AuthHelper 认证修复 | Mc4AuthHelper.cs + appsettings | 1h |
| M2 | 🟠 P1 | 批量点位查询 | Mc4Adapter.cs | 30min |
| M3 | 🟡 P2 | 历史告警查询 | Mc4Adapter.cs | 30min |
| M4 | 🟡 P2 | B4-batch 路由改用 native batch | Program.cs | 15min |
| M5 | 验证 | 编译 + 联调 | — | 30min |
| **合计** | — | — | **4 文件** | **~3h** |
---
## 2. 步骤 M1: Mc4AuthHelper 认证修复(预计 1h
### 2.1 问题
```csharp
// 当前: 调 /conf/get (返回 { "encrypt": true }),误读为 Token
var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null);
var result = JsonSerializer.Deserialize<Mc4AuthResponse>(json);
_token = result?.Token ?? ""; // Token 始终为 null
```
### 2.2 MC4.0 实际认证流程
```
1. POST /api/central/auth/conf/get → { "encrypt": true/false }
2. 若 encrypt=true → 密码 MD5(原始密码)
3. POST /api/central/auth/login {
"account": "admin",
"password": "md5或原始密码"
}
→ { "token": "xxx", "id": 0, "account": "admin", "name": "管理员" }
```
### 2.3 修改后的 Mc4AuthHelper
```csharp
public class Mc4AuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl;
private readonly string _account;
private readonly string _password;
private string? _token;
private DateTime _tokenExpiry = DateTime.MinValue;
private bool? _needMd5;
public Mc4AuthHelper(HttpClient http, string baseUrl, string account, string password)
{
_http = http;
_baseUrl = baseUrl.TrimEnd('/');
_account = account;
_password = password;
}
public async Task<string> GetTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
return _token;
// 1. 获取加密配置
if (!_needMd5.HasValue)
{
var confResp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null);
if (confResp.IsSuccessStatusCode)
{
var confJson = await confResp.Content.ReadAsStringAsync();
var conf = JsonSerializer.Deserialize<Mc4ConfResponse>(confJson);
_needMd5 = conf?.Encrypt ?? false;
}
else
{
_needMd5 = false; // 失败时假定不需要加密
}
}
// 2. 登录获取 Token
var pwd = _needMd5 == true ? ComputeMd5(_password) : _password;
var loginBody = JsonSerializer.Serialize(new { account = _account, password = pwd });
var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/login",
new StringContent(loginBody, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Mc4LoginResponse>(json)
?? throw new Exception("MC4 登录失败");
_token = result.Token ?? "";
_tokenExpiry = DateTime.UtcNow.AddHours(7); // 保守估计 8h
return _token;
}
public async Task<HttpClient> GetAuthenticatedClientAsync()
{
var token = await GetTokenAsync();
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Add("token", token);
return client;
}
public void Invalidate() => _token = null;
private static string ComputeMd5(string input) { /* MD5 实现 or use System.Security.Cryptography */ }
private class Mc4ConfResponse { public bool? Encrypt { get; set; } }
private class Mc4LoginResponse { public string? Token { get; set; } public int Id { get; set; } public string? Account { get; set; } }
}
```
### 2.4 构造函数签名变更
```csharp
// 旧: public Mc4AuthHelper(HttpClient http, string baseUrl)
// 新: public Mc4AuthHelper(HttpClient http, string baseUrl, string account, string password)
```
### 2.5 Mc4Adapter 构造函数变更
```csharp
// 旧:
public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl)
{
_auth = new Mc4AuthHelper(http, baseUrl);
}
// 新:
public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl,
string account = "admin", string password = "admin")
{
_auth = new Mc4AuthHelper(http, baseUrl, account, password);
}
```
### 2.6 Program.cs 注册变更
```csharp
// 旧: new Mc4Adapter(code, http, m.BaseUrl)
// 新: new Mc4Adapter(code, http, m.BaseUrl,
// m.Username ?? "admin", m.Password ?? "admin")
```
### 2.7 Mc4Config 增加字段
```csharp
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"; // 新增
}
```
### 2.8 appsettings.json 更新
```json
"MC4": [
{ "InstanceName": "31ku", "BaseUrl": "http://localhost:3000",
"Username": "admin", "Password": "your_mc4_password" }
]
```
### 2.9 编译验证
`dotnet build gateway/IntegrationGateway.slnx` → 0 错误。
> **M1 提交点**: `Fix-M1: Mc4AuthHelper 认证修复 conf/get→login + account/password支持`
---
## 3. 步骤 M2: 批量点位查询(预计 30min
### 3.1 文件
`gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs`
### 3.2 新增方法
```csharp
/// <summary>批量获取多个设备的实时点位值MC4.0 原生 multi/value/get</summary>
public async Task<Dictionary<int, List<Mc4PointValue>>> GetMultiRealtimeValuesAsync(List<int> deviceIds)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new { ids = deviceIds });
var resp = await client.PostAsync("/api/central/point/multi/value/get",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Dictionary<int, List<Mc4PointValue>>>(json)!;
return result;
}
```
### 3.3 编译验证
`dotnet build` → 0 错误。
> **M2 提交点**: `Fix-M2: MC4 批量点位查询 GetMultiRealtimeValuesAsync`
---
## 4. 步骤 M3: 历史告警查询(预计 30min
### 4.1 新增 DTO
```csharp
/// <summary>MC4.0 历史告警查询请求</summary>
public class Mc4HisAlarmQuery
{
public string From { get; set; } = "";
public string To { get; set; } = "";
public int Skip { get; set; }
public int Limit { get; set; }
public int Sort { get; set; } = 1;
}
```
### 4.2 新增方法
```csharp
/// <summary>查询 MC4.0 历史告警(已恢复的告警)</summary>
public async Task<PagedResult<StandardAlarm>> GetHisAlarmsAsync(int page, int size, DateTime from, DateTime to)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new Mc4HisAlarmQuery
{
From = from.ToString("yyyy-MM-dd HH:mm:ss"),
To = to.ToString("yyyy-MM-dd HH:mm:ss"),
Skip = (page - 1) * size,
Limit = size,
Sort = 1
});
var resp = await client.PostAsync("/api/central/his_alarm/query",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Mc4AlarmQueryResult>(json)!;
return new PagedResult<StandardAlarm>
{
Items = (result.List ?? new()).Select(MapAlarmItem).ToList(),
Total = result.Total
};
}
private StandardAlarm MapAlarmItem(Mc4AlarmItem a) => new()
{
AlarmId = a.Id ?? "",
AdapterCode = AdapterCode,
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
};
```
### 4.3 编译验证
`dotnet build` → 0 错误。
> **M3 提交点**: `Fix-M3: MC4 历史告警查询 GetHisAlarmsAsync`
---
## 5. 步骤 M4: B4-batch 路由优化(预计 15min
### 5.1 修改
`gateway/src/IntegrationGateway.Host/Program.cs` B4-batch 路由改用 MC4 原生批量接口:
```csharp
// B4-batch 改用 MC4 原生 multi/value/get
app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) =>
{
var a = registry.FindByCode<IHasPoints>(adapter);
if (a == null) return Results.NotFound();
if (a is Mc4Adapter mc4 && req.DeviceIds?.Count > 0)
{
// MC4.0 原生批量接口
var intIds = req.DeviceIds.Select(int.Parse).ToList();
var multi = await mc4.GetMultiRealtimeValuesAsync(intIds);
return Results.Ok(multi);
}
// 其他适配器 fallback
var results = new Dictionary<string, List<PointValue>>();
foreach (var id in req.DeviceIds ?? new())
try { results[id] = await a.GetRealtimeValuesAsync(id); } catch { }
return Results.Ok(results);
});
```
### 5.2 编译验证
`dotnet build` → 0 错误。
> **M4 提交点**: `Fix-M4: B4-batch 优化 MC4原生批量接口`
---
## 6. 步骤 M5: 编译验证 + 联调
- [ ] `dotnet build gateway/IntegrationGateway.slnx` → 0 错误 0 警告
- [ ] MC4 appsettings.json 填入真实 `Username/Password`
- [ ] 网关启动 → A1 注册 → A3 同步 MC4 设备树
- [ ] B4-batch 调 `multi/value/get` 返回批量值
- [ ] 告警查询 `/alarms/MC4:31ku` 有数据
- [ ] Mc4AuthHelper Token 非空 → 登录流程正常
> **M5 提交点**: `Fix-M5: MC4整改全量编译验证通过`
---
## 7. 改动文件汇总
| 步骤 | 文件 | 改动 |
|:---:|------|------|
| M1 | `Mc4AuthHelper.cs` | 重写认证流程: conf/get → login |
| M1 | `Mc4Adapter.cs` | 构造函数加 account/password |
| M1 | `Program.cs` | Mc4Adapter 构造传 Username/Password |
| M1 | `appsettings.json` | MC4 数组加 Username/Password |
| M2 | `Mc4Adapter.cs` | 新增 GetMultiRealtimeValuesAsync |
| M3 | `Mc4Adapter.cs` | 新增 GetHisAlarmsAsync + DTO |
| M4 | `Program.cs` | B4-batch 优化 MC4 原生批量 |

View File

@@ -0,0 +1,131 @@
# 网关 MC4 模块检查报告 2026-06-03
> **基准文档**: `doc/对接文档/MC4.0对外API.md` (31 API)
> **检查范围**: `gateway/src/IntegrationGateway.Adapters.MC4/` (Mc4Adapter.cs, Mc4AuthHelper.cs)
> **日期**: 2026-06-03
---
## 1. 覆盖率概览
MC4.0 接口文档共 **31 个 REST 端点**,当前 Mc4Adapter 覆盖了 **6 个**19%)。
| 模块 | 文档端点数 | 已实现 | 缺失 |
|------|:---:|:---:|:---:|
| 认证 | 3 | 0 | 3 |
| 对象树 | 1 | 1 | 0 |
| 点位 | 3 | 2 | 1 |
| 告警 | 14 | 3 | 11 |
| 系统管理 | 10 | 0 | 10 |
| **合计** | **31** | **6** | **25** |
---
## 2. 已实现接口对照
| MC4.0 端点 | Mc4Adapter 方法 | 能力接口 | 状态 |
|------|------|------|:--:|
| /api/central/object/tree | GetObjectTreeAsync | IHasOwnDeviceTree | ✅ |
| /api/central/device/point/value/get | GetRealtimeValuesAsync | IHasPoints | ✅ |
| /api/central/point/value/set | SetPointValueAsync | IHasPoints | ✅ |
| /api/central/alarm/query | GetAlarmsAsync | IHasAlarms | ✅ |
| /api/central/alarm/confirm | ConfirmAlarmAsync | IHasAlarms | ✅ |
| /api/central/alarm/end | EndAlarmAsync | IHasAlarms | ✅ |
---
## 3. 🔴 关键问题
### 3.1 Mc4AuthHelper 认证逻辑错误(🔥 致命)
**现状**: `GetTokenAsync` 调用 `/api/central/auth/conf/get`
```csharp
var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null);
var result = JsonSerializer.Deserialize<Mc4AuthResponse>(json);
_token = result?.Token ?? "";
```
**错误**: `/api/central/auth/conf/get` 是**密码加密配置查询接口**,返回 `{ "encrypt": true/false }`**不是 Token 接口**,不包含 `token` 字段。`result?.Token` 始终为 null`_token` 被设为空字符串。
**实际登录接口**: `/api/central/auth/login`
```json
POST /api/central/auth/login
{ "account": "admin", "password": "xxx" }
{ "token": "string", "id": 0, "account": "string", "name": "string" }
```
> **注意**: MC4.0 可能对大部分 API 不强制 Token 认证curl 示例中只有 logout 接口显式传了 header。但当前代码逻辑错误即便需要 Token 也无法获取。
**修复**: Mc4AuthHelper 改为先调 `conf/get` 确认加密方式,再用 `account/password``login` 获取真正的 token。
### 3.2 缺少批量点位查询(🟠 规则引擎依赖)
**缺失**: `/api/central/point/multi/value/get`
请求体 `{ "ids": [1, 2, 3] }` → 一次返回多个设备的实时值。
**影响**: 当前 B4-batch 接口逐设备调 `GetRealtimeValuesAsync`单设备接口。MC4.0 提供原生批量接口,应直接使用以提升规则引擎性能。
**修复**: 增加 `GetMultiRealtimeValuesAsync(List<int> deviceIds)` 方法B4-batch 路由优先调此方法。
---
## 4. 缺失项清单
### 4.1 认证接口3个
| 端点 | 用途 |
|------|------|
| `/api/central/auth/conf/get` | 获取密码加密配置(已调但未正确使用) |
| `/api/central/auth/login` | 登录获取 Token |
| `/api/central/auth/logout` | 注销 |
### 4.2 设备点位1个
| 端点 | 用途 |
|------|------|
| `/api/central/device/point/get` | 查询设备的点位列表(用于发现设备有哪些测点) |
### 4.3 告警扩展11个
| 端点 | 用途 |
|------|------|
| `/api/central/alarm/custom_query_count` | 告警自定义统计数量 |
| `/api/central/alarm/custom_query` | 告警自定义查询 |
| `/api/central/alarm/get_by_point` | 按点位查询告警 |
| `/api/central/alarm/get` | 获取单个告警详情 |
| `/api/central/his_alarm/query` | 历史告警查询 |
| `/api/central/report/alarm/convergence/query` | 告警聚合报告查询 |
| `/api/central/alarm/type/add` | 添加告警类型 |
| `/api/central/alarm/type/set` | 修改告警类型 |
| `/api/central/alarm/type/del` | 删除告警类型 |
| `/api/central/alarm/type/list` | 告警类型列表 |
### 4.4 系统管理10个
| 端点 | 用途 |
|------|------|
| `/api/central/manager/config/set` | 设置系统配置 |
| `/api/central/manager/config/get` | 获取系统配置 |
| `/api/central/manager/db/backup` | 数据库备份 |
| `/api/central/manager/db/restore` | 数据库恢复 |
| `/api/central/manager/db/log` | 数据库日志 |
| `/api/central/manager/hisdb/backup` | 历史库备份 |
| `/api/central/manager/hisdb/restore` | 历史库恢复 |
| `/api/central/manager/hisdb/clear` | 清除历史数据 |
| `/api/central/manager/picture/clear` | 清除图片 |
| `/api/central/manager/video/clear` | 清除视频 |
---
## 5. 优先级建议
| 优先级 | 项目 | 说明 |
|:---:|------|------|
| 🔴 P0 | Mc4AuthHelper 认证修复 | 当前 Token 获取逻辑根本错误 |
| 🟠 P1 | 批量点位查询 (multi/value/get) | 规则引擎 B4-batch 缺少原生高效接口 |
| 🟡 P2 | 历史告警查询 | 管理端需要查看已结束的告警 |
| 🟡 P2 | 设备点位发现 (device/point/get) | IoT 设备入网时自动发现测点 |
| ⚪ P3 | 告警类型 CRUD | 运维操作 |
| ⚪ P3 | 系统管理接口 | 运维操作 |

View File

@@ -0,0 +1,97 @@
# 网关自动注册机制整改 — 任务清单
> **版本**: 1.0
> **日期**: 2026-06-03
> **基准**: `doc/设计文档/网关自动注册机制整改方案_v1.0.md` + `doc/设计文档/网关自动注册机制检查报告20260603.md`
> **原则**: 分阶段分步骤执行,每步骤完成编译复查后提交,不合并主分支
---
## 阶段 G1: Gateway 端修复3 步骤)
### 步骤 G1.1 — 修复 BaseUrl 硬编码
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ]`BaseUrl = $"http://localhost:..."` 改为读取 `gwCfg["SelfUrl"]`,不填降级 localhost
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/appsettings.json`Gateway 段新增 `"SelfUrl": null`
- [ ] `dotnet build gateway/IntegrationGateway.slnx` → 0 错误
- [ ] 复查:`BaseUrl` 不再硬编码 localhost可从配置注入真实 IP
> **G1.1 提交点**: `Fix-G1.1: Gateway A1 BaseUrl 改为读取 SelfUrl 配置`
### 步骤 G1.2 — A1 注册后追加 A3 设备同步
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ] A1 注册成功后:遍历 `registry.All``IHasFlatDevices``GetDevicesAsync``IHasOwnDeviceTree``GetObjectTreeAsync` + 展平
- [ ] 新增 `FlattenTree` 辅助函数MC4 对象树展平)
- [ ]`clientFactory.SyncDevicesAsync(nodeCode, nodeToken, allDevices)`
- [ ] `dotnet build` → 0 错误
- [ ] 复查A1 成功后立即执行 A3注册完成时 Vol.Pro 已有设备数据
> **G1.2 提交点**: `Fix-G1.2: A1注册后立即A3同步全部适配器设备列表`
### 步骤 G1.3 — 启动 A2 心跳 + 自动重注册
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ] A1/A3 完成后启动 `Task.Run` 心跳循环
- [ ] `PeriodicTimer` 每 15s → `clientFactory.HeartbeatAsync`
- [ ] 连续失败 ≥ 3 次 → 触发 A1+A3 重注册
- [ ] 新增 `SyncAllDevicesAsync` 辅助函数(复用 A3 逻辑)
- [ ] `dotnet build` → 0 错误
- [ ] 复查:心跳成功重置 `failCount`,失败累积到 3 次自动恢复
> **G1.3 提交点**: `Fix-G1.3: A2心跳+自动重注册(连续3次失败触发A1+A3)`
---
## 阶段 G2: Vol.Pro 端修复2 步骤)
### 步骤 G2.1 — RegisterNodeAsync 语法规范化
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/device_manager/Partial/gateway_nodesService.cs`
- [ ] `RegisterNodeAsync``DbContext.Queryable.First()``FindAsIQueryable.FirstOrDefaultAsync()`
- [ ] `UpdateHeartbeatAsync`:同样替换
- [ ] `dotnet build api_sqlsugar/Warehouse` → 0 错误
- [ ] 复查:两个方法使用统一 Vol.Pro 查询语法,`.First()` 不抛异常
> **G2.1 提交点**: `Fix-G2.1: gateway_nodesService 统一 FindAsIQueryable 语法`
### 步骤 G2.2 — 标记 UpsertDeviceAsync 为废弃
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/device_manager/Partial/base_deviceService.cs`
- [ ] `UpsertDeviceAsync``[Obsolete]` 标记 + 注释说明
- [ ] 检查接口文件 `Ibase_deviceService` 是否暴露此方法,同步标记
- [ ] `dotnet build` → 0 错误(允许 [Obsolete] 警告)
- [ ] 复查:重复逻辑已标记,新代码不会误用
> **G2.2 提交点**: `Fix-G2.2: base_deviceService.UpsertDeviceAsync 标记 [Obsolete]`
---
## 阶段 G3: 全量验证1 步骤)
### 步骤 G3.1 — 全量编译 + 联调场景验证
- [ ] `dotnet build gateway/IntegrationGateway.slnx` → 0 错误
- [ ] `dotnet build api_sqlsugar/VolPro.WebApi` → 0 错误
- [ ] 网关启动 → 控制台输出 A1 注册 → A3 同步 N 台 → A2 心跳启动
- [ ] `gateway_nodes` 表有记录,`LastHeartbeat` 持续更新
- [ ] `base_device` 表有对应设备
- [ ] 网关先启动Vol.Pro 未启动)→ 45 秒后自动恢复
- [ ] 复查:全链路 A1→A3→A2 闭环正常
> **G3.1 提交点**: `Fix-G3: 全量编译验证通过 注册机制闭环完成`
---
## 任务总览
| 阶段 | 步骤 | 文件 | 预计 |
|:---:|:---:|------|:---:|
| G1 | G1.1 BaseUrl 修复 | Program.cs + appsettings.json | 10min |
| G1 | G1.2 A3 设备同步 | Program.cs | 30min |
| G1 | G1.3 心跳+重注册 | Program.cs | 20min |
| G2 | G2.1 语法规范化 | gateway_nodesService.cs | 5min |
| G2 | G2.2 标记废弃方法 | base_deviceService.cs | 10min |
| G3 | G3.1 全量验证 | 全项目 | 15min |
| **合计** | **6 步骤** | **5 文件** | **~1.5h** |

View 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 |

View File

@@ -0,0 +1,133 @@
# 网关项目代码审查报告 2026-06-04
> **范围**: `gateway/src/` 全部 5 个项目 239 文件
> **重点**: 空函数、未实现内容、TODO、硬编码、异常处理
---
## 一、空实现/存根函数5 处)
### 1.1 OwlAdapter — ConfirmAlarmAsync / EndAlarmAsync
**文件**: `OwlAdapter.cs` L250-251
```csharp
public Task ConfirmAlarmAsync(string alarmId) => Task.CompletedTask;
public Task EndAlarmAsync(string alarmId) => Task.CompletedTask;
```
**说明**: Owl AI 事件(基于 `/events` 接口)不支持确认/结束操作,合理留空。
**风险**: 低。调用方VolPro/A4调用后状态不会写回 Owl。
### 1.2 KmsAdapter — EndAlarmAsync
**文件**: `KmsAdapter.cs` L165-170
```csharp
public Task EndAlarmAsync(string alarmId) { return Task.CompletedTask; }
```
**说明**: KMS 第三方接口 (2.18.7) 不提供告警结束 API合理留空。
**风险**: 低。
### 1.3 KmsAdapter — GetBorrowRecordsAsync / GetPermissionListAsync 请求体
**文件**: `KmsAdapter.cs` L181, L194
```csharp
var body = "{}"; // 联调时加入时间范围参数
```
**说明**: 联调待办项,当前传空 JSON。KMS 接口可能接受无参查询返回全部数据。
**风险**: 中。如果 KMS 要求时间范围参数,当前实现会失败。
### 1.4 KmsAdapter — SendControlAsync 只实现了 "open"/"authorize"
**文件**: `KmsAdapter.cs` L251
```csharp
if (command == "open" || command == "authorize") { ... }
// 其他 command 返回 success=true 但无实际操作
```
**说明**: 非开门的控制指令会静默返回成功但不执行任何操作。
**风险**: 中。调用方以为成功但设备未变化。
---
## 二、静默异常吞噬4 处)
### 2.1 SyncAllDevicesAsync — 适配器遍历 catch
**文件**: `Program.cs` L171
```csharp
catch { }
```
适配器取设备列表失败时静默跳过,不影响其他适配器。合理但缺少日志。
### 2.2 B1 健康检查
**文件**: `Program.cs` L197
```csharp
try { healthy = await a.HealthCheckAsync(); } catch { }
```
合理——健康检查本身不应抛异常。
### 2.3 B4-batch Fallback
**文件**: `Program.cs` L279
```csharp
try { results[deviceId] = await a.GetRealtimeValuesAsync(deviceId); } catch { }
```
合理——逐设备查询时某设备失败不应阻塞。
### 2.4 RateLimiter.Release
**文件**: `RateLimiter.cs` L36
```csharp
try { _semaphore.Release(); } catch { }
```
合理——SemaphoreSlim.Release 在超过最大计数时会抛异常。
---
## 三、联调待验证项3 处)
### 3.1 GatewayClientFactory — A2/A3 方法从未被网关自身调用
**文件**: `GatewayClientFactory.cs` L38-62
```csharp
public async Task<bool> HeartbeatAsync(...) { ... }
public async Task<JsonDocument?> SyncDevicesAsync(...) { ... }
```
**说明**: 这两个方法在 Program.cs 的 `SyncAllDevicesAsync` 中通过 `clientFactory.SyncDevicesAsync` 被调用了。A2 心跳在心跳循环中被调用。
**状态**: ✅ 已连接。
### 3.2 Owl Playback URL 硬编码路径
**文件**: `OwlAdapter.cs` (GetPlaybackUrlAsync)
```csharp
Hls = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?..."
```
联调时需确认 Owl 实际录像 HLS 路径是否为此格式。
### 3.3 KMS API 响应格式
KMS 所有接口的响应格式需联调验证。文档中字段名可能与实际 API 有差异。
---
## 四、编译状态
网关 5 项目上次编译 **0 错误 0 警告**。当前改动为本次审查附加,需重新编译验证。
---
## 五、结论
| 类别 | 数量 | 严重度 |
|------|:---:|------|
| 合理空实现(设计如此) | 3 | 低 |
| 联调待验证参数 | 2 | 中 |
| 静默异常(合理设计) | 4 | 低 |
| **需要立即修复** | **0** | — |
**没有发现需要立即修复的空函数或未实现方法。** 所有 `Task.CompletedTask` 都是因为底层子系统不支持该操作Owl AI 无确认、KMS 无结束告警属于设计取舍。KMS 的联调待办项(时间范围参数)已在代码中注释标注。

View File

@@ -0,0 +1,175 @@
# 规则引擎实施计划 — 任务清单
> **版本**: 1.0
> **日期**: 2026-06-04
> **基准**: `doc/设计文档/规则引擎实现方案_v1.0.md`
> **分支**: gateway-dev
> **原则**: 分阶段分步骤执行,每步骤编译复查后提交,不合并主分支
---
## 当前状态
| 前置条件 | 状态 |
|------|:--:|
| RealtimePollJob10s 采集 MC4 IoT 值) | ✅ 已实现 |
| warehouse_rule/condition/action 表 + 管理端 CRUD | ✅ 已实现 |
| warehouse_rulelog 表 | ❌ 待建 |
| 网关 B4-batch 批量接口 | ✅ 已实现 (P1-1) |
| 网关 B5设备控制 | ✅ 已实现 |
| 网关 B10远程控制 | ✅ 已实现 |
| VolPro GatewayClient调网关 | ✅ 已实现 |
| warehouse_variable 表 SQL | ✅ 已写入 db_init.sql待执行 |
| RuleEngineService / RuleEngineJob | ❌ 待实现 |
---
## 阶段 R1: 数据库准备(预计 30min
### 步骤 R1.1 — 新增 warehouse_rulelog 表
- [ ] 在数据库执行:
```sql
CREATE TABLE warehouse_rulelog (
LogID INT IDENTITY PRIMARY KEY,
RuleID INT NOT NULL,
ConditionMet NVARCHAR(50),
ActionResult NVARCHAR(MAX),
EvaluatedAt DATETIME DEFAULT GETDATE(),
Detail NVARCHAR(MAX) NULL
);
```
- [ ] 在数据库执行 ALTER TABLE 添加字段(若未执行):
- `warehouse_rule` 加 `Enable`, `Priority`, `LastEvaluated`, `LastTriggered`, `CooldownSec`
- `warehouse_ruleaction` 加 `ActionType`, `ExtraJson`
- `warehouse_rulecondition` 加 `RecoveryThreshold_Numeric`, `RecoveryThreshold_Switch`, `LastTriggered`, `LastTriggerValue`
- [ ] 在 Vol.Pro 代码生成器中选择 `warehouse_rulelog` 生成全套 CRUD
- [ ] `dotnet build` → 0 错误
### 步骤 R1.2 — 执行 warehouse_variable 表建表
- [ ] 在数据库执行 `doc/db_init.sql` 中 warehouse_variable 建表语句
- [ ] Vol.Pro 代码生成器生成 `warehouse_variable` CRUD
- [ ] 管理端字典补充§5 字典项)
> **R1 提交点**: `RuleEngine-R1: 数据库表+字典就绪`
---
## 阶段 R2: RuleEngineService 实现(预计 3h
### 步骤 R2.1 — 创建 RuleEngineService.cs
- [ ] 创建 `api_sqlsugar/Warehouse/Services/RuleEngineService.cs`
- [ ] 注入 `Iwarehouse_ruleRepository`, `Ibase_deviceRepository`, `Iiot_devicedataRepository`, `Iiot_alarmRepository`, `GatewayClient`, `IHubContext<HomePageMessageHub>`
- [ ] 实现 `EvaluateAllAsync()` — 主流程:
1. `LoadEnabledRulesAsync()` — 从 DB 加载启用规则 + 条件 + 动作
2. `BuildDeviceMappingAsync()` — DeviceId → (AdapterCode, SourceId, BaseUrl)
3. `BatchFetchRealtimeAsync()` — 调网关 B4-batch 批量取实时值
4. `EvaluateRuleAsync(rule, data)` — 逐规则比对
5. `ExecuteActionsAsync(rule)` — 触发动作
### 步骤 R2.2 — 实现条件评估
- [ ] `EvaluateConditionAsync(cond, realtimeData)`
- 从 realtimeData 中找到对应设备+点位的实际值
- 按 CompareOperator 比对(大于/小于/等于/大于等于/小于等于/不等于)
- 支持滞后窗P2-2已触发过则用 RecoveryThreshold 判断恢复
- 支持条件级冷却P2-3未过冷却期则跳过
### 步骤 R2.3 — 实现动作执行
- [ ] `ExecuteActionsAsync(rule)`
- 动作类型 "控制" → `GatewayClient.ControlDeviceAsync`(调网关 B5
- 动作类型 "告警" → 写入 `iot_alarm` 表
- 动作类型 "通知" → SignalR `_hub.SendAsync("RuleTriggered", ...)`
- 冷却检查:未过 `CooldownSec` 不重复执行
- 并发执行:`Task.WhenAll` + 5s 超时P3-1
### 步骤 R2.4 — 编译验证
- [ ] `dotnet build api_sqlsugar/Warehouse` → 0 错误
> **R2 提交点**: `RuleEngine-R2: RuleEngineService 完整实现`
---
## 阶段 R3: RuleEngineJob + 调度(预计 30min
### 步骤 R3.1 — 创建 RuleEngineJob.cs
- [ ] 创建 `api_sqlsugar/Warehouse/Services/RuleEngineJob.cs`
- [ ] 实现 `IJob` 接口,`Execute` 中获取 `RuleEngineService` 调 `EvaluateAllAsync()`
### 步骤 R3.2 — 注册 Quartz
- [ ] 管理端 → Quartz 管理 → 新建 Job
```
JobName: RuleEngineJob
Cron: 0/10 * * * * ?
ClassName: Warehouse.Services.RuleEngineJob
```
- [ ] `dotnet build` → 0 错误
> **R3 提交点**: `RuleEngine-R3: RuleEngineJob 就绪`
---
## 阶段 R4: 前端配套(预计 1h
### 步骤 R4.1 — 规则管理页增强
- [ ] 编辑 `web.vite/src/views/warehouse/warehouse_rule/warehouse_rule/options.js`
- [ ] 条件表格中 "设备" 列绑定 `allDevices` 动态字典
- [ ] "变量" 列绑定 `warehouse_variable` 字典
- [ ] 动作表格加"动作类型"下拉(控制/告警/通知)
### 步骤 R4.2 — 大屏告警接收
- [ ] 编辑 `warehouse/src/view/DataView.vue`
- [ ] SignalR 订阅 `RuleTriggered` 事件:
```javascript
connection.on("RuleTriggered", (data) => {
ElMessage.warning(`[规则触发] ${data.title}: ${data.alertMessage}`);
});
```
> **R4 提交点**: `RuleEngine-R4: 前端配套完成`
---
## 阶段 R5: 联调验证(预计 2h
### 步骤 R5.1 — 联调
- [ ] 网关启动 → MC4 在线 → RealtimePollJob 有数据
- [ ] 管理端新建规则:"温度 > 28℃ → 告警"
- [ ] 等 10s → iot_alarm 表有告警记录
- [ ] 管理端收到 SignalR 推送
- [ ] 管理端新建规则:"温度 > 28℃ → 控制空调"
- [ ] 等 10s → 网关 B5 被调用
> **R5 提交点**: `RuleEngine-R5: 联调通过`
---
## 任务总览
| 阶段 | 步骤 | 内容 | 预计 |
|:---:|:---:|------|:---:|
| R1 | R1.1 | 建表 + 代码生成 | 20min |
| R1 | R1.2 | 变量表 + 字典 | 10min |
| R2 | R2.1 | RuleEngineService 主流程 | 1.5h |
| R2 | R2.2 | 条件评估 + 滞后窗 + 冷却 | 45min |
| R2 | R2.3 | 动作执行(控制/告警/通知) | 30min |
| R2 | R2.4 | 编译验证 | 15min |
| R3 | R3.1 | RuleEngineJob | 15min |
| R3 | R3.2 | Quartz 注册 | 15min |
| R4 | R4.1 | 管理端 UI 增强 | 30min |
| R4 | R4.2 | 大屏告警接收 | 30min |
| R5 | R5.1 | 联调 | 2h |
| **合计** | **11 步骤** | — | **~7h** |
---
> **注**: 原方案 R4网关 B4-batch已在 P1-1 修复中完成R2 的 DeviceId 映射已在方案中设计,此次直接实现。`warehouse_variable` 建表 SQL 已在 `doc/db_init.sql` 中就绪,本次仅需执行。