From 79b8400e6d3e76846d87d4d5f82529b06faed0f7 Mon Sep 17 00:00:00 2001 From: g82tt Date: Thu, 4 Jun 2026 00:43:48 +0800 Subject: [PATCH] =?UTF-8?q?T=E5=AE=8C=E6=88=90:=20TaskController=E5=88=9B?= =?UTF-8?q?=E5=BB=BA+3=E4=B8=AAIJob=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=94=B9=E9=80=A0(IServiceProvider=E6=B3=A8=E5=85=A5)+RuleEngi?= =?UTF-8?q?neJob=E6=A0=87=E8=AE=B0=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/设计文档/KMS钥匙柜适配器详细设计文档.md | 1005 +++-------------- .../KMS钥匙柜适配器详细设计文档_任务清单.md | 282 ++--- doc/设计文档/定时任务API化整改方案_v1.0.md | 174 +++ doc/设计文档/网关MC4模块整改方案_v1.0.md | 337 ++++++ doc/设计文档/网关MC4模块检查报告20260603.md | 131 +++ doc/设计文档/网关自动注册机制整改_任务清单.md | 97 ++ .../网关自动注册机制检查报告20260603.md | 148 +++ doc/设计文档/网关项目代码审查报告20260604.md | 133 +++ doc/设计文档/规则引擎实施计划_任务清单.md | 175 +++ 9 files changed, 1415 insertions(+), 1067 deletions(-) create mode 100644 doc/设计文档/定时任务API化整改方案_v1.0.md create mode 100644 doc/设计文档/网关MC4模块整改方案_v1.0.md create mode 100644 doc/设计文档/网关MC4模块检查报告20260603.md create mode 100644 doc/设计文档/网关自动注册机制整改_任务清单.md create mode 100644 doc/设计文档/网关自动注册机制检查报告20260603.md create mode 100644 doc/设计文档/网关项目代码审查报告20260604.md create mode 100644 doc/设计文档/规则引擎实施计划_任务清单.md diff --git a/doc/设计文档/KMS钥匙柜适配器详细设计文档.md b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md index aff60bf..6378059 100644 --- a/doc/设计文档/KMS钥匙柜适配器详细设计文档.md +++ b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md @@ -1,554 +1,132 @@ -# KMS 钥匙柜适配器详细设计文档 v2.1 +# KMS 钥匙柜适配器详细设计文档 -> **版本**: 2.1(完整接口版 + 适配原则审查 + 缺口分析) +> **版本**: 1.0 > **日期**: 2025-05-19 -> **数据源**: `doc/对接文档/钥匙管理系统软件接口.docx`(KMS API v1.0.4) +> **基准**: `doc/整合方案/KMS钥匙柜整合方案_v2.0.md` +> **接口文档**: `doc/对接文档/钥匙管理系统软件接口.docx` (KMS API v1.0.4) > **技术栈**: .NET 8 / ASP.NET Core / C# -> **架构**: IntegrationGateway 适配器模式 -> **覆盖**: KMS 全部 54 个 REST 接口 --- -## 1. 概述 +## 1. 接口覆盖设计 -### 1.1 设计目标 +### 1.1 完整 KMS API 概览 -在 IntegrationGateway 中新增 `KmsAdapter`,将智能钥匙管理系统(KMS)作为第三个子系统接入 SecMPS 整合平台。KMS 通过网关的 `IHasFlatDevices` 上报柜体/锁孔设备树,通过 `IHasAlarms` 上报告警记录,由 Vol.Pro 管理端统一展示和管理。 +KMS 系统共有 **38 个 REST 端点**,分 9 大类。按设计原则,适配器只代理 **第三方集成接口**(第 2.18 节),不代理 KMS 自身管理接口。 -### 1.2 KMS 系统模型 +### 1.2 第三方集成接口(Phase 1 — 核心实现) + +以下 8 个接口是 KMS 专为第三方对接设计的扁平化 API: + +| # | 方法 | 路径 | 用途 | 适配器方法 | +|---|:---:|------|------|------| +| 2.18.1 | GET | `/prod-api/heartBeat` | 心跳检测 | `HealthCheckAsync()` | +| 2.18.2 | POST | `/prod-api/batchDeleteStaff` | 批量删除员工 | `BatchDeleteStaffAsync()` | +| 2.18.3 | POST | `/prod-api/batchSyncStaff` | 批量同步员工 | `BatchSyncStaffAsync()` | +| 2.18.4 | POST | `/prod-api/getOpenerList` | 查询柜体+钥匙信息 | `GetDevicesAsync()` | +| 2.18.5 | POST | `/prod-api/getPermissionList` | 查询授权记录 | `GetPermissionListAsync()` | +| 2.18.6 | POST | `/prod-api/getRecordList` | 查询借还记录 | `GetBorrowRecordsAsync()` | +| 2.18.7 | POST | `/prod-api/getWarningList` | 查询告警记录 | `GetAlarmsAsync()` | +| 2.18.8 | POST | `/thirdPlatlogin` | 第三方登录/事件 | `ThirdPlatLoginAsync()` | + +加上认证接口 `POST /prod-api/getToken`,适配器共需对接 **9 个 KMS 接口**。 + +### 1.3 标准 KMS 管理接口(Phase 2 可选) + +以下 29 个接口属于 KMS 自身管理功能(员工 CRUD、柜体 CRUD、钥匙 CRUD 等),KMS 自带 Web 管理端即可操作。Vol.Pro 如需代理这些接口,可在 Phase 2 扩展,但设计文档不列入必需实现。 + +| 模块 | 接口数 | 备注 | +|------|:---:|------| +| 交接记录 | 2 | GET `/kms/handover/*` | +| 授权管理 | 3 | GET `/kms/permission/*` + POST remote | +| 告警记录(标准) | 1 | GET `/kms/warning/list` | +| 员工可借钥匙 | 2 | POST/GET `/kms/staffopener/*` | +| 员工管理 | 5 | CRUD `/kms/staff/*` | +| 员工组管理 | 6 | CRUD `/kms/staffGroup/*` | +| 物品类别 | 6 | CRUD `/kms/openerType/*` | +| 柜体管理 | 5 | CRUD `/kms/locker/*` + statistics | +| 锁孔管理 | 4 | CRUD `/kms/lockhole/*` | +| 钥匙管理 | 7 | CRUD `/kms/opener/*` + selectCanBorrow | +| 钥匙组 | 7 | CRUD `/kms/openerGroup/*` | +| 部门 | 1 | GET `/system/dept/root/{userId}` | +| 授权详情 | 1 | GET `/kms/permissioninfo/getByPermissionId/{uuid}` | +| **合计** | **29** | Phase 2 按需实现 | + +--- + +## 2. 项目结构 ``` -KMS 管理平台 (一个 IP:PORT) -├── 智能钥匙柜 A (locker) -│ ├── 锁孔 1 (lockhole) → 钥匙 (opener) -│ ├── 锁孔 2 -│ └── ... -├── 智能钥匙柜 B -└── ... +gateway/src/IntegrationGateway.Adapters.Kms/ +├── IntegrationGateway.Adapters.Kms.csproj # 类库, net8.0 +├── KmsAdapter.cs # 适配器主体 (IHasFlatDevices + IHasAlarms) +├── KmsAuthHelper.cs # Bearer Token 认证 +└── KmsModels.cs # 请求/响应 DTO ``` -实体关系:**柜体(Locker) 1:N 锁孔(Lockhole) 1:1 钥匙(Opener)** +### 2.1 依赖关系 -### 1.3 技术约束 - -| 约束 | 说明 | -|------|------| -| 不修改 Core 接口 | 复用现有 `IHasFlatDevices` + `IHasAlarms` | -| 不依赖 KMS 运行时 | `dotnet build` 可在无 KMS 环境下通过 | -| 故障隔离 | KMS 离线不影响 Owl/MC4 适配器运行 | -| 限流 | 5 QPS 保守值 | - ---- - -## 2. KMS 接口完整参考 - -### 2.0 认证 - -| 项目 | 说明 | -|------|------| -| 接口 | `POST /prod-api/getToken` | -| 参数 | query: `clientId` (必填/string), `clientSecret` (必填/string) | -| 返回 | `{ code: 200, token: "xxx", msg: "操作成功" }` | -| 有效期 | 30 分钟 | -| 使用 | Header: `Authorization: Bearer ` | - ---- - -### 2.18 第三方集成接口(Phase 1 — 8 个) - -#### 2.18.1 心跳接口 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/heartBeat` | -| 参数 | 无 | -| 返回 | `{ code: 200, msg: "success" }` | - -#### 2.18.2 批量删除员工 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/batchDeleteStaff` | -| 请求体 | `["staffUuid1", "staffUuid2", ...]` (array\, 必填) | -| 返回 | 统一响应状态 | - -#### 2.18.3 批量同步员工 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/batchSyncStaff` | -| 请求体 | `[{ staff data }, ...]` — KMS 员工完整信息的数组 | -| 返回 | 统一响应状态 | - -#### 2.18.4 查询柜体及钥匙信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/getOpenerList` | -| 请求体 | `{}` | -| 返回 | `{ code: 200, rows: [ { lockerId, lockerName, lockerCode, lockholeList: [{ lockholeSort, openerId, openerName, openerType, openerState }] } ] }` | - -#### 2.18.5 查询授权记录列表接口 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/getPermissionList` | -| 请求体 | 查询条件(可选时间范围) | -| 返回 | `{ code: 200, total: N, rows: [ { uuid, lockerName, lockholeSort, openerName, staffName, borrowTime, returnTime, type } ] }` | - -#### 2.18.6 查询借还记录列表接口 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/getRecordList` | -| 请求体 | 查询条件(可选时间范围) | -| 返回 | `{ code: 200, total: N, rows: [ { uuid, lockerName, lockholeSort, openerName, staffName, borrowTime, returnTime, type } ] }` | - -#### 2.18.7 查询报警记录列表接口 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/getWarningList` | -| 请求体 | 告警记录业务对象(可选时间范围/类型) | -| 返回 | `{ code: 200, total: N, rows: [ { uuid, lockerName, lockholeSort, openerName, type, warningTime, remark, staffName } ] }` | - -#### 2.18.8 事件记录接口(第三方登录) - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/thirdPlatlogin?username=zhangsan` | -| 参数 | query: `username` (必填) | -| 返回 | 登录成功后重定向到 KMS 管理首页 | - -**设计决策**:2.18.8 是页面重定向接口,不适合 API 对接。改用 B 组接口从 Vol.Pro 发起时传用户名,网关代理请求。 - ---- - -### 2.3 交接记录(2 个) - -#### 2.3.1 查询交接记录明细列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/handover/handoverInfolist` | -| 参数 | query: `handoverId` (必填/string), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [{ id, handoverId, openerId, openerName, lockerId, lockerName, lockholeSort, openerType, openerState, lendStaffName, borrowTime }] }` | - -#### 2.3.2 查询交接记录列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/handover/list` | -| 参数 | query: `lockerName` (必填/string), `fromUser` (可选/string), `fromUserCard` (可选/string), `toUser` (可选/string), `toUserCard` (可选/string), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [{ id, code, createTime, fromUser, fromUserCard, toUser, toUserCard, ... }] }` | - ---- - -### 2.4 授权管理(3 个) - -#### 2.4.1 查询授权记录列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/permission/list` | -| 参数 | query: `backStaffName` (必填/string), `lendStaffName` (必填/string), `lockerName` (必填/string), `openerCnName` (必填/string), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.4.2 查询授权记录列表(授权人视角) - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/permission/listPer` | -| 参数 | query: `backStaffName` (必填/string), `lendStaffName` (必填/string), `lockerName` (必填/string), `openerCnName` (必填/string), `applyTime` (可选/string), `backTime` (可选/string), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.4.3 远程授权 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/kms/permission/remote` | -| 请求体 | `PermissionCmdData` 对象(必填) | -| 返回 | 统一响应状态 | - -> **远程授权参数对象 (PermissionCmdData)**:包含授权人ID、被授权人ID、钥匙ID、有效期等字段(具体结构需在联调时与 KMS 确认)。 - ---- - -### 2.5 告警记录(1 个) - -#### 2.5.1 查询告警记录列表(标准版) - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/warning/list` | -| 参数 | query: `openerCnName` (必填/string), `staffName` (必填/string), `warningType` (可选/string), `pageNum` (可选/int), `pageSize` (可选/int), `type` (可选/int, 1=当前告警 2=历史告警) | -| 返回 | `{ code: 200, total, rows: [{ uuid, lockerName, lockholeSort, openerName, type, warningTime, remark, staffName }] }` | - ---- - -### 2.6 员工可借/永久授权钥匙(2 个) - -#### 2.6.1 设置员工可借/永久授权钥匙 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/kms/staffopener/available` | -| 请求体 | `{ staffIds: [3], openerIds: [1], type: 1 }` — type: 1=可借钥匙, 2=永久授权钥匙 | -| 返回 | `{ code: 200, msg: "操作成功" }` | - -#### 2.6.2 查询员工可借/永久授权钥匙 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/staffopener/listall` | -| 参数 | query: `staffId` (必填/int64), `type` (必填/int) | -| 返回 | `{ code: 200, data: [{ id, staffId, openerId, type }] }` | - ---- - -### 2.7 员工管理(5 个) - -#### 2.7.1 创建员工 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/kms/staff` | -| 请求体 | 员工业务对象(必填): 包含 name, cardNo, phone, email, deptId, groupId, state 等 | -| 返回 | `{ code: 200, msg: "操作成功" }` | - -#### 2.7.2 修改员工 - -| 项目 | 说明 | -|------|------| -| 方法 | `PUT` | -| 路径 | `/prod-api/kms/staff` | -| 请求体 | 员工业务对象(必填,含 id) | -| 返回 | 统一响应状态 | - -#### 2.7.3 查询员工列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/staff/list` | -| 参数 | query: `cardNo` (必填/string), `name` (必填/string), `state` (必填/int), `type` (必填/int), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.7.4 删除员工(批量) - -| 项目 | 说明 | -|------|------| -| 方法 | `DELETE` | -| 路径 | `/prod-api/kms/staff/{ids}` | -| 参数 | path: `ids` — 逗号分隔的员工ID列表 | -| 返回 | 统一响应状态 | - -#### 2.7.5 获取员工详细信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/staff/{id}` | -| 参数 | path: `id` (员工ID) | -| 返回 | `{ code: 200, data: { 员工完整信息 } }` | - ---- - -### 2.9 Token(1 个) - -#### 2.9.1 获取 Token - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/getToken` | -| 参数 | body: `{ clientId, clientSecret }` | -| 返回 | `{ code: 200, token: "xxx", msg: "操作成功" }` | - ---- - -### 2.11 部门管理(1 个) - -#### 2.11.1 根据用户 ID 获取部门信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/system/dept/root/{userId}` | -| 参数 | path: `userId` (int64) | -| 返回 | `{ code: 200, data: { 部门树 } }` | - ---- - -### 2.12 钥匙柜授权信息(1 个) - -#### 2.12.1 获取钥匙柜授权信息详细信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/permissioninfo/getByPermissionId/{uuid}` | -| 参数 | path: `uuid` (授权记录 UUID) | -| 返回 | `{ code: 200, data: { 授权详细信息 } }` | - ---- - -### 2.14 钥匙管理(6 个) - -#### 2.14.1 创建钥匙 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/kms/opener` | -| 请求体 | 钥匙业务对象(必填): 包含 cnName, number, type, state, lockerId 等 | -| 返回 | `{ code: 200, msg: "操作成功" }` | - -#### 2.14.2 修改钥匙 - -| 项目 | 说明 | -|------|------| -| 方法 | `PUT` | -| 路径 | `/prod-api/kms/opener` | -| 请求体 | 钥匙业务对象(必填,含 id) | -| 返回 | 统一响应状态 | - -#### 2.14.4 查询钥匙列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/opener/list` | -| 参数 | query: `cnName` (必填/string), `lockerId` (必填/int), `number` (必填/string), `state` (必填/int), `type` (必填/int), `pageNum/pageSize` (可选) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.14.5 查询可借钥匙 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/opener/selectCanBorrow` | -| 参数 | query: `userId` (必填/int64), `pageNum` (必填/int), `pageSize` (必填/int), `openerName` (可选/string) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.14.6 查询可借钥匙员工列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/opener/staff` | -| 参数 | query: `id` (可选/int, 钥匙ID) | -| 返回 | `{ code: 200, data: [{ staffId, staffName }] }` | - -#### 2.14.7 删除钥匙(批量) - -| 项目 | 说明 | -|------|------| -| 方法 | `DELETE` | -| 路径 | `/prod-api/kms/opener/{ids}` | -| 参数 | path: `ids` — 逗号分隔的钥匙ID列表 | -| 返回 | 统一响应状态 | - -#### 2.14.8 获取钥匙详细信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/opener/{id}` | -| 参数 | path: `id` (int64, 钥匙ID) | -| 返回 | `{ code: 200, data: { 钥匙完整信息 } }` | - ---- - -### 2.16 柜体管理(6 个) - -#### 2.16.1 创建柜体 - -| 项目 | 说明 | -|------|------| -| 方法 | `POST` | -| 路径 | `/prod-api/kms/locker` | -| 请求体 | 柜体业务对象(必填): 包含 name, code, state, deptId 等 | -| 返回 | `{ code: 200, msg: "操作成功" }` | - -#### 2.16.2 修改柜体 - -| 项目 | 说明 | -|------|------| -| 方法 | `PUT` | -| 路径 | `/prod-api/kms/locker` | -| 请求体 | 柜体业务对象(必填,含 id) | -| 返回 | 统一响应状态 | - -#### 2.16.3 查询柜体列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/locker/list` | -| 参数 | query: `name` (必填/string), `state` (必填/int), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.16.4 删除柜体(批量) - -| 项目 | 说明 | -|------|------| -| 方法 | `DELETE` | -| 路径 | `/prod-api/kms/locker/{ids}` | -| 参数 | path: `ids` — 逗号分隔的柜体ID列表 | -| 返回 | 统一响应状态 | - -#### 2.16.5 获取柜体详细信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/locker/{id}` | -| 参数 | path: `id` (柜体ID) | -| 返回 | `{ code: 200, data: { 柜体完整信息(含锁孔列表) } }` | - -#### 2.16.6 首页统计图表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/locker/statistics` | -| 参数 | 无 | -| 返回 | 统计图表数据 | - ---- - -### 2.17 锁孔管理(4 个) - -#### 2.17.1 修改锁孔 - -| 项目 | 说明 | -|------|------| -| 方法 | `PUT` | -| 路径 | `/prod-api/kms/lockhole` | -| 请求体 | 锁孔业务对象(必填) | -| 返回 | 统一响应状态 | - -#### 2.17.2 查询锁孔列表 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/lockhole/list` | -| 参数 | query: `state` (必填/int), `pageNum` (可选/int), `pageSize` (可选/int) | -| 返回 | `{ code: 200, total, rows: [...] }` | - -#### 2.17.3 删除锁孔(批量) - -| 项目 | 说明 | -|------|------| -| 方法 | `DELETE` | -| 路径 | `/prod-api/kms/lockhole/{ids}` | -| 参数 | path: `ids` — 逗号分隔的锁孔ID列表 | -| 返回 | 统一响应状态 | - -#### 2.17.4 获取锁孔详细信息 - -| 项目 | 说明 | -|------|------| -| 方法 | `GET` | -| 路径 | `/prod-api/kms/lockhole/{id}` | -| 参数 | path: `id` (锁孔ID) | -| 返回 | `{ code: 200, data: { 锁孔完整信息 } }` | - ---- - -## 3. 统一响应状态码 - -KMS 统一响应格式: - -```json -{ - "code": 200, // 200=成功, 0/其他=失败 - "msg": "操作成功", // 消息文本 - "total": 100, // 分页总记录数(列表接口) - "rows": [...], // 数据列表(列表接口) - "data": {...} // 单个对象(详情接口) -} +``` +Host → Adapters.Kms → Core +Host → Core ``` +适配器只引用 Core,零外部 NuGet 依赖。 + --- -## 4. KmsModels 完整设计 +## 3. 数据模型设计 -### 4.1 响应模型 +### 3.1 KMS 响应模型(全部 9 个第三方接口的 DTO) ```csharp -namespace IntegrationGateway.Adapters.Kms; - -// ═══ 认证 ═══ +// ── 认证 ── public class KmsTokenResponse { public int Code { get; set; } public string Token { get; set; } = ""; public string? Msg { get; set; } } -// ═══ 第三方接口 DTO ═══ +// ── 2.18.4 柜体+钥匙 ── public class KmsOpenerListResponse { public int Code { get; set; } public string? Msg { get; set; } public List? Rows { get; set; } } public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List? LockholeList { get; set; } } public class KmsLockhole { public int LockholeSort { get; set; } public int OpenerId { get; set; } public string? OpenerName { get; set; } public string? OpenerType { get; set; } public string? OpenerState { get; set; } } +// ── 2.18.7 告警 ── public class KmsWarningListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List? Rows { get; set; } } public class KmsWarning { public string? Uuid { get; set; } public string? LockerName { get; set; } public int LockholeSort { get; set; } public string? OpenerName { get; set; } public int Type { get; set; } public string? WarningTime { get; set; } public string? Remark { get; set; } public string? StaffName { get; set; } } +// ── 2.18.6 借还记录 ── public class KmsRecordListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List? Rows { get; set; } } public class KmsRecord { public string? Uuid { get; set; } public string? LockerName { get; set; } public int LockholeSort { get; set; } public string? OpenerName { get; set; } public string? StaffName { get; set; } public string? BorrowTime { get; set; } public string? ReturnTime { get; set; } public string? Type { get; set; } } -// ═══ 标准接口 DTO ═══ -public class KmsHandoverInfo { public string? Id { get; set; } public string? HandoverId { get; set; } public int OpenerId { get; set; } public string? OpenerName { get; set; } public int LockerId { get; set; } public string? LockerName { get; set; } public int LockholeSort { get; set; } public string? OpenerType { get; set; } public string? OpenerState { get; set; } public string? LendStaffName { get; set; } public string? BorrowTime { get; set; } } +// ── 2.18.5 授权记录 ── +public class KmsPermissionListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List? Rows { get; set; } } +public class KmsPermission { public string? Uuid { get; set; } public string? LockerName { get; set; } public int LockholeSort { get; set; } public string? OpenerName { get; set; } public string? StaffName { get; set; } public string? ApplyTime { get; set; } public string? BackTime { get; set; } } -public class KmsPermissionListResponse { public int Code { get; set; } public int Total { get; set; } public List? Rows { get; set; } } -public class KmsPermission { public string? Uuid { get; set; } public string? LockerName { get; set; } public string? OpenerCnName { get; set; } public string? LendStaffName { get; set; } public string? BackStaffName { get; set; } public string? ApplyTime { get; set; } public string? BackTime { get; set; } } - -public class KmsStaffListResponse { public int Code { get; set; } public int Total { get; set; } public List? Rows { get; set; } } +// ── 2.18.3 员工同步 ── public class KmsStaff { public string? Uuid { get; set; } public string? Name { get; set; } public string? CardNo { get; set; } public string? Phone { get; set; } public string? Email { get; set; } public int? DeptId { get; set; } public int? GroupId { get; set; } public int State { get; set; } public int Type { get; set; } } -public class KmsLockerListResponse { public int Code { get; set; } public int Total { get; set; } public List? Rows { get; set; } } -public class KmsLockerInfo { public int Id { get; set; } public string? Name { get; set; } public string? Code { get; set; } public int State { get; set; } public int? DeptId { get; set; } public List? LockholeList { get; set; } } +// ── 通用响应 ── +public class KmsApiResponse { public int Code { get; set; } public string? Msg { get; set; } } +``` -public class KmsLockholeListResponse { public int Code { get; set; } public int Total { get; set; } public List? Rows { get; set; } } -public class KmsLockholeInfo { public int Id { get; set; } public int LockerId { get; set; } public int LockholeSort { get; set; } public int State { get; set; } public int? OpenerId { get; set; } } +### 3.2 实体映射关系 -public class KmsOpenerListResponse2 { public int Code { get; set; } public int Total { get; set; } public List? Rows { get; set; } } -public class KmsOpenerInfo { public int Id { get; set; } public string? CnName { get; set; } public string? Number { get; set; } public int Type { get; set; } public int State { get; set; } public int? LockerId { get; set; } } - -public class KmsStaffOpenerListResponse { public int Code { get; set; } public List? Data { get; set; } } -public class KmsStaffOpener { public int Id { get; set; } public int StaffId { get; set; } public int OpenerId { get; set; } public int Type { get; set; } } - -public class KmsRemotePermissionRequest { /* 远程授权参数 — 联调时与KMS确认字段 */ } -public class KmsBatchSyncStaffRequest { public List Staff { get; set; } = new(); } - -// 通用包装 -public class KmsApiResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List? Rows { get; set; } public T? Data { get; set; } } +``` +KMS 物理拓扑 base_device 映射 +─────── ────────────────── +智能钥匙柜A (lockerId=25) SourceId="locker_25", IsParent=是 +├── 锁孔1 "仓库大门" SourceId="lockhole_25_1", ParentSourceId="locker_25" +├── 锁孔2 "机房钥匙" SourceId="lockhole_25_2", ParentSourceId="locker_25" +└── 锁孔N ... ``` --- -## 5. KmsAuthHelper 完整设计 +## 4. KmsAuthHelper 设计 ```csharp /// /// KMS Bearer Token 认证辅助。 -/// 流程: POST /prod-api/getToken?clientId=x&clientSecret=y → { code:200, token:"xxx" } -/// Token 缓存 25 分钟(KMS 有效期 30 分钟,留 5 分钟余量)。 +/// Token 通过 POST /prod-api/getToken 获取,参数 clientId + clientSecret。 +/// 缓存 25 分钟(KMS 有效期 30 分钟,留 5 分钟余量)。 /// public class KmsAuthHelper { @@ -563,6 +141,7 @@ public class KmsAuthHelper _clientId = clientId; _clientSecret = clientSecret; } + /// 获取或刷新 Token(自动缓存) public async Task GetTokenAsync() { if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; @@ -576,6 +155,7 @@ public class KmsAuthHelper return _token; } + /// 创建一个已认证的 HttpClient public async Task GetAuthenticatedClientAsync() { var token = await GetTokenAsync(); @@ -590,18 +170,16 @@ public class KmsAuthHelper --- -## 6. KmsAdapter 完整设计 +## 5. KmsAdapter 设计 -### 6.1 类定义与构造函数 +### 5.1 类声明与核心属性 ```csharp /// /// KMS 智能钥匙柜适配器。 -/// 实现: IHasFlatDevices + IHasAlarms。 -/// -/// 设备模型: 柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体SourceId)。 -/// AdapterCode: "KMS:{InstanceName}"。 -/// 限流: 5 QPS。 +/// 实现 IHasFlatDevices + IHasAlarms。 +/// 通过 8 个第三方接口(2.18.X)对接 KMS 子系统。 +/// AdapterCode: "KMS:{InstanceName}",限流: 5 QPS。 /// public class KmsAdapter : IHasFlatDevices, IHasAlarms { @@ -622,9 +200,10 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms public async Task InitializeAsync() => await _auth.GetTokenAsync(); ``` -### 6.2 健康检查(2.18.1) +### 5.2 IGatewayAdapter — 健康检查 ```csharp + /// 2.18.1: 心跳检测 — GET /prod-api/heartBeat public async Task HealthCheckAsync() { try @@ -637,9 +216,10 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms } ``` -### 6.3 设备列表(2.18.4) +### 5.3 IHasFlatDevices — 设备同步 ```csharp + /// 2.18.4: 柜体+钥匙信息 — POST /prod-api/getOpenerList public async Task> GetDevicesAsync(int page, int size, string? keyword = null) { await _limiter.WaitAsync(); @@ -652,34 +232,35 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms var devices = new List(); foreach (var locker in data.Rows ?? new()) { - devices.Add(MapLockerToDevice(locker)); - if (locker.LockholeList != null) - devices.AddRange(locker.LockholeList.Select(h => MapLockholeToDevice(h, locker.LockerId))); + // 父设备: 柜体 + devices.Add(new StandardDevice + { + SourceId = $"locker_{locker.LockerId}", Name = locker.LockerName ?? $"柜体{locker.LockerId}", + Category = "智能钥匙柜", Group = "门禁设备", IsParent = true, IsOnline = true, + Extra = new() { ["lockerCode"] = locker.LockerCode, ["lockholeCount"] = locker.LockholeList?.Count ?? 0 } + }); + // 子设备: 锁孔(钥匙位) + foreach (var hole in locker.LockholeList ?? new()) + { + devices.Add(new StandardDevice + { + SourceId = $"lockhole_{locker.LockerId}_{hole.LockholeSort}", + Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}", + Category = "钥匙位", Group = "门禁设备", IsParent = false, + IsOnline = hole.OpenerState == "在位", ParentSourceId = $"locker_{locker.LockerId}", + Extra = new() { ["openerId"] = hole.OpenerId, ["openerType"] = hole.OpenerType, ["openerState"] = hole.OpenerState } + }); + } } return new PagedResult { Items = devices, Total = devices.Count }; } - - private static StandardDevice MapLockerToDevice(KmsLocker locker) => new() - { - SourceId = $"locker_{locker.LockerId}", Name = locker.LockerName ?? $"柜体{locker.LockerId}", - Category = "智能钥匙柜", Group = "门禁设备", IsParent = true, IsOnline = true, - Extra = new Dictionary { ["lockerCode"] = locker.LockerCode, ["lockholeCount"] = locker.LockholeList?.Count ?? 0 } - }; - - private static StandardDevice MapLockholeToDevice(KmsLockhole hole, int lockerId) => new() - { - SourceId = $"lockhole_{lockerId}_{hole.LockholeSort}", Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}", - Category = "钥匙位", Group = "门禁设备", IsParent = false, IsOnline = hole.OpenerState == "在位", - ParentSourceId = $"locker_{lockerId}", - Extra = new Dictionary { ["openerId"] = hole.OpenerId, ["openerType"] = hole.OpenerType, ["openerState"] = hole.OpenerState } - }; ``` -### 6.4 告警列表(2.18.7) +### 5.4 IHasAlarms — 告警同步 ```csharp - public async Task> GetAlarmsAsync( - int page, int size, DateTime from, DateTime to, string? level = null, string? state = null) + /// 2.18.7: 告警记录 — POST /prod-api/getWarningList + public async Task> GetAlarmsAsync(...) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); @@ -697,102 +278,47 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms }).ToList(); return new PagedResult { Items = alarms, Total = data.Total }; } - - public async Task ConfirmAlarmAsync(string alarmId) - { - await _limiter.WaitAsync(); - var client = await _auth.GetAuthenticatedClientAsync(); - await client.PostAsync($"/prod-api/kms/warning/confirm/{alarmId}", null); - } - - public async Task EndAlarmAsync(string alarmId) - { - // KMS 第三方接口不提供告警结束 API,标准接口联调时补充 - } ``` -### 6.5 借还记录(2.18.6) +### 5.5 扩展方法(非接口方法,供 B 组路由直接调用) ```csharp - /// 查询借还记录(Phase 2 — 可作为额外 B 接口暴露) - public async Task> GetBorrowRecordsAsync(DateTime? from = null, DateTime? to = null) - { - await _limiter.WaitAsync(); - var client = await _auth.GetAuthenticatedClientAsync(); - var body = "{}"; // 联调时加时间范围参数 - var resp = await client.PostAsync("/prod-api/getRecordList", - new StringContent(body, Encoding.UTF8, "application/json")); - resp.EnsureSuccessStatusCode(); - var data = await resp.Content.ReadFromJsonAsync()!; - return new PagedResult { Items = data.Rows ?? new(), Total = data.Total }; - } -``` + // 2.18.6 借还记录 + public async Task> GetBorrowRecordsAsync(DateTime? from = null, DateTime? to = null) { ... } -### 6.6 员工同步(2.18.3 — Phase 2) + // 2.18.5 授权记录 + public async Task> GetPermissionListAsync(DateTime? from = null, DateTime? to = null) { ... } -```csharp - /// 从 Vol.Pro 向 KMS 批量同步员工 - public async Task BatchSyncStaffAsync(List staffList) - { - await _limiter.WaitAsync(); - var client = await _auth.GetAuthenticatedClientAsync(); - var resp = await client.PostAsJsonAsync("/prod-api/batchSyncStaff", new { staff = staffList }); - resp.EnsureSuccessStatusCode(); - } -``` + // 2.18.3 批量同步员工 + public async Task BatchSyncStaffAsync(List staffList) { ... } -### 6.7 远程授权(2.4.3 — Phase 2) + // 2.18.2 批量删除员工 + public async Task BatchDeleteStaffAsync(List staffUuids) { ... } -```csharp - /// 远程授权开门 - public async Task RemoteAuthorizeAsync(KmsRemotePermissionRequest request) - { - await _limiter.WaitAsync(); - var client = await _auth.GetAuthenticatedClientAsync(); - var resp = await client.PostAsJsonAsync("/prod-api/kms/permission/remote", request); - resp.EnsureSuccessStatusCode(); - } + // 2.4.3 远程授权开门 + public async Task RemoteAuthorizeAsync(KmsRemotePermissionRequest request) { ... } + + // 2.18.8 第三方登录代理 + public async Task ThirdPlatLoginAsync(string username) { ... } ``` --- -## 7. 设备映射逻辑 +## 6. 配置 -``` -POST /prod-api/getOpenerList - │ - ▼ -遍历 Lockers [ { lockerId, lockerName, lockholeList: [...] } ] - ├── 父设备: SourceId="locker_{lockerId}", IsParent=true, Category="智能钥匙柜" - └── 子设备foreach: SourceId="lockhole_{lockerId}_{lockholeSort}", ParentSourceId="locker_{lockerId}" - Category="钥匙位", IsOnline = (openerState=="在位") +### 6.1 KmsConfig POCO + +```csharp +public class KmsConfig +{ + public string? InstanceName { get; set; } + public string BaseUrl { get; set; } = ""; + public string ClientId { get; set; } = ""; + public string ClientSecret { get; set; } = ""; +} ``` -**parentSourceId 解析**(A3 同步时由 `gateway_nodesService.SyncDevicesAsync` 处理): -``` -"locker_{lockerId}" → 查 base_device WHERE SourceId='locker_{lockerId}' → 得到 DeviceId → 填入子设备 ParentDeviceId -``` - ---- - -## 8. 告警映射逻辑 - -| KMS 字段 (2.18.7) | StandardAlarm 字段 | 映射规则 | -|------|------|------| -| uuid | AlarmId | 直接映射 | -| type (1/2) | Status | 1→"未确认", 2→"已结束" | -| warningTime | OccurTime | DateTime.Parse | -| lockerName + lockholeSort | Title | 拼接: "{lockerName} 锁孔{sort}: {openerName}" | -| openerName | — | 用于 Title 拼接 | -| remark | Content | 直接映射 | -| staffName | — | Extra 扩展字段 | -| — | Level | 固定 "普通"(KMS 不区分等级) | - ---- - -## 9. 配置 - -### 9.1 appsettings.json +### 6.2 appsettings.json ```json { @@ -807,19 +333,7 @@ POST /prod-api/getOpenerList } ``` -### 9.2 KmsConfig POCO - -```csharp -public class KmsConfig -{ - public string? InstanceName { get; set; } - public string BaseUrl { get; set; } = ""; - public string ClientId { get; set; } = ""; - public string ClientSecret { get; set; } = ""; -} -``` - -### 9.3 Program.cs 注册 +### 6.3 Program.cs 注册 ```csharp var kmsList = app.Configuration.GetSection("KMS").Get>() ?? new(); @@ -835,231 +349,16 @@ foreach (var k in kmsList) --- -## 10. 与 Vol.Pro 管理端的交互 +## 7. Vol.Pro 端配套 -### 10.1 数据流 - -``` -KMS 硬件柜 ──→ KmsAdapter.GetDevices ──→ A3 Sync → base_device (AdapterCode="KMS:main") -KMS 告警 ──→ KmsAdapter.GetAlarms ──→ A4 Sync → iot_alarm (SourceAlarmId=uuid) -管理端操作 ←── B-interface ←── KmsAdapter.RemoteAuthorize (Phase 2) -``` - -### 10.2 管理端改动 - -| 项 | 改动 | -|------|------| -| 数据库 | 无 — base_device / iot_alarm 已兼容 | -| 字典 | 新增 "智能钥匙柜" / "钥匙位" 字典项 | -| 后端代码 | 无 — A1-A4 逻辑通用 | -| 前端列表 | 自动显示 KMS 设备(AdapterCode 列区分来源) | -| 前端按钮 | Phase 2: KeyDeviceActions.vue | - -### 10.3 钥匙状态展示 - -设备列表中每个锁孔(钥匙位)的 `IsOnline` 反映钥匙在位状态,`Extra.openerState` 存储详细状态字符串。管理端可通过 `Extra` 列的 JSON 展示查看钥匙类型和状态。 +| 项 | 改动 | 说明 | +|------|:---:|------| +| 数据库 | 无 | base_device / iot_alarm 已兼容 | +| 后端 | 无 | A1-A4 同步逻辑通用 | +| 字典 | 新增 2 项 | "智能钥匙柜" / "钥匙位" | +| 前端列表 | 无 | 自动显示 KMS 设备 | +| 前端操作 | Phase 2 | KeyDeviceActions.vue(开门/授权按钮)| --- -> **接口覆盖**: 54 个 REST 端点全部记录,其中 Phase 1 实现 4 个核心接口,Phase 2 实现 12 个扩展接口,其余 38 个为标准 KMS 管理接口按需对接。 -> **版本历史**: v1.0 (初版) → v2.0 (完整接口版) - - ---- - -## 附录A: 接口全覆盖比对 - -### A.1 KMS 38 个接口 vs 设计覆盖度 - -| # | KMS 接口 | 方法 | 适配器方法 | 覆盖 | -|---|------|:---:|------|:--:| -| 1 | `/prod-api/getToken` | POST | KmsAuthHelper.GetTokenAsync | ✅ | -| 2 | `/prod-api/heartBeat` | GET | HealthCheckAsync | ✅ | -| 3 | `/prod-api/batchDeleteStaff` | POST | BatchDeleteStaffAsync | ✅ | -| 4 | `/prod-api/batchSyncStaff` | POST | BatchSyncStaffAsync | ✅ | -| 5 | `/prod-api/getOpenerList` | POST | GetDevicesAsync | ✅ | -| 6 | `/prod-api/getPermissionList` | POST | GetPermissionListAsync | ✅ | -| 7 | `/prod-api/getRecordList` | POST | GetBorrowRecordsAsync | ✅ | -| 8 | `/prod-api/getWarningList` | POST | GetAlarmsAsync | ✅ | -| 9 | `/thirdPlatlogin` | POST | ThirdPlatLoginAsync | ✅ | -| 10 | `/kms/handover/handoverInfolist` | GET | ⏭️ Phase2 | 📋 | -| 11 | `/kms/handover/list` | GET | ⏭️ Phase2 | 📋 | -| 12 | `/kms/permission/list` | GET | ⏭️ Phase2 | 📋 | -| 13 | `/kms/permission/listPer` | GET | ⏭️ Phase2 | 📋 | -| 14 | `/kms/permission/remote` | POST | RemoteAuthorizeAsync | ✅ | -| 15 | `/kms/warning/list` | GET | ⏭️ Phase2 (已有 2.18.7) | 📋 | -| 16 | `/kms/staffopener/available` | POST | ⏭️ Phase2 | 📋 | -| 17 | `/kms/staffopener/listall` | GET | ⏭️ Phase2 | 📋 | -| 18 | `/kms/staff` (create) | POST | ⏭️ Phase2 | 📋 | -| 19 | `/kms/staff` (update) | PUT | ⏭️ Phase2 | 📋 | -| 20 | `/kms/staff/list` | GET | ⏭️ Phase2 | 📋 | -| 21 | `/kms/staff/{ids}` (delete) | DELETE | ⏭️ Phase2 | 📋 | -| 22 | `/kms/staff/{id}` (detail) | GET | ⏭️ Phase2 | 📋 | -| 23 | `/system/dept/root/{userId}` | GET | ⏭️ Phase2 | 📋 | -| 24 | `/kms/permissioninfo/getByPermissionId/{uuid}` | GET | ⏭️ Phase2 | 📋 | -| 25 | `/kms/opener` (create) | POST | ⏭️ Phase2 | 📋 | -| 26 | `/kms/opener` (update) | PUT | ⏭️ Phase2 | 📋 | -| 27 | `/kms/opener/list` | GET | ⏭️ Phase2 | 📋 | -| 28 | `/kms/opener/selectCanBorrow` | GET | ⏭️ Phase2 | 📋 | -| 29 | `/kms/opener/staff` | GET | ⏭️ Phase2 | 📋 | -| 30 | `/kms/opener/{ids}` (delete) | DELETE | ⏭️ Phase2 | 📋 | -| 31 | `/kms/opener/{id}` (detail) | GET | ⏭️ Phase2 | 📋 | -| 32 | `/kms/locker` (create) | POST | ⏭️ Phase2 | 📋 | -| 33 | `/kms/locker` (update) | PUT | ⏭️ Phase2 | 📋 | -| 34 | `/kms/locker/list` | GET | ⏭️ Phase2 | 📋 | -| 35 | `/kms/locker/{ids}` (delete) | DELETE | ⏭️ Phase2 | 📋 | -| 36 | `/kms/locker/{id}` (detail) | GET | ⏭️ Phase2 | 📋 | -| 37 | `/kms/locker/statistics` | GET | ⏭️ Phase2 | 📋 | -| 38 | `/kms/lockhole/*` (4接口) | CRUD | ⏭️ Phase2 | 📋 | - -> ✅ = 已设计 📋 = 标准 KMS 管理接口(非第三方集成接口),KMS 自身管理端即可操作,无需网关代理 - ---- - -## 附录B: 适配器设计原则适配性审查 - -### B.1 遵守的设计原则 - -| 原则 | KMS 适配器 | 合规 | -|------|------|:--:| -| 显式优于隐式 | 通过 IHasFlatDevices + IHasAlarms 显式声明能力 | ✅ | -| 异步优先 | 全部方法返回 Task/Task | ✅ | -| 统一分页 | GetDevices/GetAlarms 返回 PagedResult | ✅ | -| 弹性 Extra | 锁孔状态/类型/ID 存 Extra 字典 | ✅ | -| 故障隔离 | KMS 离线不影响 Owl/MC4 | ✅ | -| 编译独立性 | 零外部依赖,只引用 Core | ✅ | -| 热插拔 | 新增 KMS 不改 Core/Controller 签名 | ✅ | - -### B.2 现有接口不能满足的 KMS 能力 - -以下 KMS 功能**超出了**当前 7 个网关能力接口的范围,需要新增 Core 接口或通过 B 组路由扩展: - -| KMS 功能 | 缺口 | 影响 | -|------|------|------| -| **远程授权开门** (2.4.3/2.18.5) | 无可下发控制指令的通用接口 | 需新增 `IAcceptsControl` 或专用接口 | -| **借还记录查询** (2.18.6) | 无通用事件/记录查询接口 | 需新增接口或 B 路由 | -| **员工批量同步** (2.18.3) | 无外部数据写入适配器的接口 | 需新增接口 | -| **第三方登录代理** (2.18.8) | 无页面代理/SSO 接口 | B 路由直接代理 | -| **标准 CRUD 透传** | 适配器不代理子系统的管理接口 | 可走 KMS 自身管理端 | - -### B.3 对接网关设计原则 3.4 要求的新增接口建议 - -按照"接口扩展规则"第 2 条:**如果现有接口不覆盖 → Core 中新增接口**。 - -以下是为 KMS(以及未来的门禁、道闸等子系统)拟新增的能力接口,写入 Core,不改已有接口签名: - -```csharp -namespace IntegrationGateway.Core.Abstractions; - -/// -/// 设备反向控制接口。适用于支持远程下发指令的子系统(如 KMS 远程开门、门禁远程开闸、道闸抬杆)。 -/// 控制指令为通用键值对字典,适配器内部转换。 -/// -public interface IAcceptsControl : IGatewayAdapter -{ - /// 向设备下发控制指令 - /// 子系统设备原始 ID - /// 指令名,如 "open"/"close"/"authorize" - /// 指令参数键值对 - Task SendControlAsync(string sourceDeviceId, string command, Dictionary parameters); -} - -/// 控制结果 -public class ControlResult { public bool Success { get; set; } public string? Message { get; set; } } -``` - -```csharp -/// -/// 业务记录查询接口。适用于具有借还、交接、授权等业务日志的子系统。 -/// 不走 StandardAlarm 通道,独立分页查询。 -/// -public interface IHasBusinessLogs : IGatewayAdapter -{ - /// 分页查询业务记录 - /// 记录类型: "borrow"/"handover"/"permission"/"event" - /// 开始时间 - /// 结束时间 - /// 页码 - /// 每页条数 - /// 额外过滤条件 - Task> GetBusinessLogsAsync( - string logType, DateTime? from, DateTime? to, - int page, int size, Dictionary? filters = null); -} - -/// 业务记录条目 -public class BusinessLogEntry -{ - public string LogId { get; set; } = ""; - public string LogType { get; set; } = ""; // borrow/handover/permission - public string? DeviceSourceId { get; set; } - public string? StaffName { get; set; } - public string? Description { get; set; } - public DateTime? CreatedAt { get; set; } - public Dictionary? Extra { get; set; } -} -``` - -```csharp -/// -/// 外部数据同步写入接口。适用于需要从 Vol.Pro 向子系统推送数据的场景(如员工同步)。 -/// -public interface IAcceptsDataSync : IGatewayAdapter -{ - /// 批量写入数据到子系统 - /// 数据类型: "staff"/"department" - /// 待同步的数据对象列表(JSON 兼容) - Task SyncDataAsync(string dataType, List items); - - /// 批量删除 - Task DeleteDataAsync(string dataType, List ids); -} - -public class SyncResult { public int SuccessCount { get; set; } public int FailCount { get; set; } public string? Message { get; set; } } -``` - -### B.4 KMS 适配器利用新增接口后的能力矩阵 - -``` - 旧接口 新接口 -IGatewayAdapter ✅ (已有) — -IHasFlatDevices ✅ (已有) — -IHasAlarms ✅ (已有) — -IAcceptsControl — ✅ 远程授权/开门 -IHasBusinessLogs — ✅ 借还/交接/授权记录查询 -IAcceptsDataSync — ✅ 员工批量同步/删除 -``` - -### B.5 需同步修改的网关组件 - -如果上述新接口被采用,以下文件需要增加对应路由: - -| 文件 | 改动 | -|------|------| -| `Core/Abstractions/IAcceptsControl.cs` | 新增接口 + ControlResult | -| `Core/Abstractions/IHasBusinessLogs.cs` | 新增接口 + BusinessLogEntry | -| `Core/Abstractions/IAcceptsDataSync.cs` | 新增接口 + SyncResult | -| `Host/Program.cs` | 新增 3 条 B 组路由 | -| `KmsAdapter.cs` | 实现新接口(3 个方法) | - -**或采用更轻量的方案**(不新增接口,直接在 KmsAdapter 上增加非接口方法 + Host 加专用路由): - -| 文件 | 改动 | -|------|------| -| `Host/Program.cs` | 加 `/api/gateway/kms/authorize`、`/kms/records`、`/kms/sync-staff` | -| `KmsAdapter.cs` | 加对应 public 方法,通过 `FindByCode` 查适配器调用 | - -**推荐**: 新增接口方案(符合设计原则 §3.4),因为 KMS 的远程控制/记录查询/数据同步能力具备通用性,门禁、道闸等未来子系统均可复用。 - -### B.6 Vol.Pro 端需新增的配套改动 - -| 改动项 | 说明 | -|------|------| -| KMS 操作按钮 (KeyDeviceActions.vue) | 显示钥匙状态 + "远程开门"/"远程授权" 按钮 | -| 员工同步入口 | 管理端员工管理页增加"同步到KMS"按钮 | -| 借还记录页 | 管理端新增 KMS 借还/交接记录查询页面 | -| 字典 | 新增 "智能钥匙柜" / "钥匙位" 到设备种类字典 | - ---- - -> **比对结论**: 38 个 KMS 接口全部有对应设计,其中 9 个第三方接口 100% 完成方法设计。KMS 特有的远程控制/记录查询/数据同步能力超出了现有 7 个能力接口的范围,按设计原则 §3.4 需新增 3 个 Core 接口(IAcceptsControl / IHasBusinessLogs / IAcceptsDataSync)。 +> **接口覆盖**: 9 个第三方接口(2.18.X + Token)100% 设计覆盖;29 个标准管理接口留 Phase 2 按需扩展。 diff --git a/doc/设计文档/KMS钥匙柜适配器详细设计文档_任务清单.md b/doc/设计文档/KMS钥匙柜适配器详细设计文档_任务清单.md index 0bafb9a..667eca5 100644 --- a/doc/设计文档/KMS钥匙柜适配器详细设计文档_任务清单.md +++ b/doc/设计文档/KMS钥匙柜适配器详细设计文档_任务清单.md @@ -1,8 +1,8 @@ # KMS 钥匙柜适配器 — 任务清单 -> **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md` v2.1 +> **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md` > **分支**: gateway-dev -> **原则**: 严格按照设计文档执行,严禁无中生有。网关/Vol.Pro 改动放倒数第二步,联调放最后。 +> **原则**: 严格按照设计文档执行,不凭空添加。网关/Vol.Pro 改动放倒数第二步,联调放最后。 --- @@ -28,7 +28,7 @@ ### K1.1 认证模型 - [ ] 创建 `KmsModels.cs` -- [ ] 添加 `KmsTokenResponse { Code, Token, Msg }` +- [ ] `KmsTokenResponse { Code, Token, Msg }` ### K1.2 第三方接口响应模型(2.18.X) - [ ] `KmsOpenerListResponse { Code, Msg, Rows }` @@ -39,20 +39,10 @@ - [ ] `KmsRecordListResponse { Code, Msg, Total, Rows }` - [ ] `KmsRecord { Uuid, LockerName, LockholeSort, OpenerName, StaffName, BorrowTime, ReturnTime, Type }` -### K1.3 标准接口响应模型(2.3-2.17) -- [ ] `KmsHandoverInfo` — 交接记录 -- [ ] `KmsPermissionListResponse` + `KmsPermission` — 授权记录 -- [ ] `KmsStaffListResponse` + `KmsStaff` — 员工 -- [ ] `KmsLockerListResponse` + `KmsLockerInfo` — 柜体 -- [ ] `KmsLockholeListResponse` + `KmsLockholeInfo` — 锁孔 -- [ ] `KmsOpenerListResponse2` + `KmsOpenerInfo` — 钥匙 -- [ ] `KmsStaffOpenerListResponse` + `KmsStaffOpener` — 员工可借 -- [ ] `KmsRemotePermissionRequest` — 远程授权请求(联调时确认字段) +### K1.3 编译验证 +- [ ] `dotnet build` → 0 错误 -### K1.4 编译验证 -- [ ] `dotnet build` → 0 错误(DTO 引用 Core 的 `StandardDevice`/`StandardAlarm` 等确认无编译错误) - -> **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部 15 个 DTO` +> **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部响应 DTO` --- @@ -61,269 +51,133 @@ ### K2.1 创建 KmsAuthHelper.cs - [ ] 构造函数:接收 `HttpClient`, `baseUrl`, `clientId`, `clientSecret` - [ ] 属性:`_token` (string?), `_tokenExpiry` (DateTime) -- [ ] 依赖:`System.Text.Json`, `System.Net.Http.Json` ### K2.2 GetTokenAsync - [ ] POST `/prod-api/getToken?clientId=xx&clientSecret=yy` -- [ ] 检查 `resp.EnsureSuccessStatusCode()` -- [ ] 反序列化 `KmsTokenResponse` - [ ] 校验 `Code == 200` - [ ] 缓存 Token,过期时间 = `UtcNow.AddMinutes(25)`(30 分钟效期,5 分钟余量) ### K2.3 GetAuthenticatedClientAsync -- [ ] 调用 `GetTokenAsync()` -- [ ] 创建新 `HttpClient`,`BaseAddress = _baseUrl` -- [ ] 设置 Header `Authorization: Bearer {token}` -- [ ] 返回 client +- [ ] 创建 `HttpClient`,设置 `Authorization: Bearer {token}` +- [ ] Invalidate() → `_token = null` -### K2.4 Invalidate -- [ ] `_token = null` 强制下次重新获取 - -### K2.5 编译验证 +### K2.4 编译验证 - [ ] `dotnet build` → 0 错误 -> **K2 提交点**: `PhaseK2_auth — KmsAuthHelper Bearer Token 认证就绪` +> **K2 提交点**: `PhaseK2_auth — Bearer Token 认证就绪` --- ## Phase K3: KmsAdapter 核心方法(预计 1.5h) -### K3.1 类定义与构造函数 +### K3.1 类定义 - [ ] `public class KmsAdapter : IHasFlatDevices, IHasAlarms` -- [ ] 字段:`_http`, `_auth` (KmsAuthHelper), `_limiter` (RateLimiter(5)) -- [ ] 属性:`AdapterCode`, `DisplayName`, `Capabilities { HasFlatDevices=true, HasAlarms=true }` -- [ ] 构造函数:注入 `httpClient`, `baseUrl`, `clientId`, `clientSecret` +- [ ] 属性:`AdapterCode`, `DisplayName`, `Capabilities` -### K3.2 InitializeAsync -- [ ] `await _auth.GetTokenAsync()` +### K3.2 HealthCheckAsync(2.18.1) +- [ ] GET `/prod-api/heartBeat` +- [ ] 异常捕获返回 false + Console.Error 打日志 -### K3.3 HealthCheckAsync(2.18.1) -- [ ] POST `/prod-api/heartBeat` (空 body `{}`) -- [ ] 返回 `resp.IsSuccessStatusCode` -- [ ] 异常捕获返回 false - -### K3.4 GetDevicesAsync(2.18.4 — 柜体+锁孔 → StandardDevice) -- [ ] `await _limiter.WaitAsync()` +### K3.3 GetDevicesAsync(2.18.4) - [ ] POST `/prod-api/getOpenerList` (body `{}`) -- [ ] 反序列化 `KmsOpenerListResponse` -- [ ] 遍历 `Rows`: - - 每个 `KmsLocker` → `MapLockerToDevice`(父设备,SourceId=`locker_{LockerId}`) - - 每个 `KmsLockhole` → `MapLockholeToDevice`(子设备,ParentSourceId=`locker_{LockerId}`) -- [ ] IsOnline 判断:`OpenerState == "在位"` → true -- [ ] Extra 字典:`{ openerId, openerType, openerState }` / `{ lockerCode, lockholeCount }` -- [ ] 返回 `PagedResult` +- [ ] 遍历柜体/锁孔 → 映射为 StandardDevice +- [ ] 父设备 `IsParent=是`, 子设备 `ParentSourceId=locker_{id}` -### K3.5 GetAlarmsAsync(2.18.7 — 告警列表 → StandardAlarm) -- [ ] `await _limiter.WaitAsync()` -- [ ] POST `/prod-api/getWarningList` (body `{}`) -- [ ] 反序列化 `KmsWarningListResponse` -- [ ] 映射:`AlarmId=uuid`, `Title="{lockerName} 锁孔{lockholeSort}: {openerName}"`, `Status=Type==1?"未确认":"已结束"`, `Level="普通"` -- [ ] 返回 `PagedResult` +### K3.4 GetAlarmsAsync(2.18.7) +- [ ] POST `/prod-api/getWarningList` +- [ ] 映射 KmsWarning → StandardAlarm +- [ ] AlarmId=uuid, Status=Type==1?"未确认":"已结束" -### K3.6 ConfirmAlarmAsync / EndAlarmAsync -- [ ] `ConfirmAlarmAsync`: POST `/prod-api/kms/warning/confirm/{alarmId}` -- [ ] `EndAlarmAsync`: 留空实现(KMS 第三方接口不提供结束告警) +### K3.5 ConfirmAlarmAsync / EndAlarmAsync +- [ ] Confirm 调标准接口;End 留空实现 -### K3.7 编译验证 +### K3.6 编译验证 - [ ] `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 GetBorrowRecordsAsync(2.18.6) -- [ ] POST `/prod-api/getRecordList` -- [ ] 参数:`from`, `to` DateTime?(联调时确认请求体格式) -- [ ] 返回 `PagedResult` +### K4.1 借还/授权/员工/登录 +- [ ] GetBorrowRecordsAsync(2.18.6) +- [ ] GetPermissionListAsync(2.18.5) +- [ ] BatchSyncStaffAsync(2.18.3) +- [ ] BatchDeleteStaffAsync(2.18.2) +- [ ] RemoteAuthorizeAsync(2.4.3) +- [ ] ThirdPlatLoginAsync(2.18.8) -### K4.2 GetPermissionListAsync(2.18.5) -- [ ] POST `/prod-api/getPermissionList` -- [ ] 参数:`from`, `to` DateTime? -- [ ] 返回 `PagedResult` - -### K4.3 BatchSyncStaffAsync(2.18.3) -- [ ] POST `/prod-api/batchSyncStaff` -- [ ] 请求体:`new { staff = staffList }` -- [ ] `resp.EnsureSuccessStatusCode()` - -### K4.4 BatchDeleteStaffAsync(2.18.2) -- [ ] POST `/prod-api/batchDeleteStaff` -- [ ] 请求体:`List` (staffUuid 数组) -- [ ] `resp.EnsureSuccessStatusCode()` - -### K4.5 RemoteAuthorizeAsync(2.4.3) -- [ ] POST `/prod-api/kms/permission/remote` -- [ ] 请求体:`KmsRemotePermissionRequest`(联调确认字段) - -### K4.6 ThirdPlatLoginAsync(2.18.8) -- [ ] POST `/thirdPlatlogin?username={username}` -- [ ] 处理 302 重定向:返回 `Location` header 或响应体 -- [ ] 超时设置 15s - -### K4.7 编译验证 +### K4.2 编译验证 - [ ] `dotnet build` → 0 错误 -> **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法全部就绪(记录/同步/授权/登录)` +> **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法就绪` --- ## Phase K5: 配置与注册(预计 15min) ### K5.1 KmsConfig POCO -- [ ] 在 `Program.cs` 同级新增 `KmsConfig` 类 -- [ ] 属性:`InstanceName?`, `BaseUrl`, `ClientId`, `ClientSecret` +- [ ] 在 Program.cs 同级加 class,属性:`InstanceName, BaseUrl, ClientId, ClientSecret` ### K5.2 appsettings.json -- [ ] 新增 `KMS` 数组配置段 -- [ ] 配置项:`InstanceName`, `BaseUrl`, `ClientId`, `ClientSecret` +- [ ] 新增 KMS 数组配置段 ### K5.3 Program.cs 注册 - [ ] `var kmsList = app.Configuration.GetSection("KMS").Get>() ?? new();` -- [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", http, baseUrl, clientId, clientSecret)` -- [ ] 适配器编码加入 `adapterTypes` 拼接 +- [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", ...)` -### K5.4 编译验证 -- [ ] `dotnet build` → 0 错误 - -> **K5 提交点**: `PhaseK5_config — KMS多实例配置+Program.cs注册就绪` +> **K5 提交点**: `PhaseK5_config — 配置+注册就绪` --- ## Phase K6: 编译与自测(预计 15min) -### 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 SendControlAsync(sourceDeviceId, command, parameters)` -- [ ] 新增 `Core/Models/ControlResult.cs`:`{ Success, Message }` - -### K7.2 新增 IHasBusinessLogs 接口 -- [ ] 创建 `Core/Abstractions/IHasBusinessLogs.cs` -- [ ] 方法:`Task> 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 SyncDataAsync(dataType, items)` -- [ ] 方法:`Task 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 编译验证 +### K6.1 编译验证 - [ ] `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 前端操作列扩展 -- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue` -- [ ] `onInited` 的 render 函数中增加 `DeviceGroup==='门禁设备'` 分支 -- [ ] 显示 "开门" 按钮(调用网关 B8) -- [ ] 显示 "权限" 下拉菜单(永久授权/临时授权/取消授权) +### K7.2 前端按钮 +- [ ] `base_device.vue` 操作列:门禁设备 → [开门] [授权] 按钮 -### K8.3 前端 API 调用 -- [ ] `fetch()` 调网关 `http://localhost:5100/api/gateway/control/KMS:main` -- [ ] 请求体:`{ sourceDeviceId, command: "open", parameters: { openerId, staffId } }` - -### K8.4 编译验证 -- [ ] `npm run dev` → 无编译错误 - -> **K8 提交点**: `PhaseK8_volpro — 字典+前端操作按钮就绪` +> **K7 提交点**: `PhaseK7_volpro — 字典+前端就绪` --- -## Phase K9: 联调验证(预计 3h)⚠️ 最后 +## Phase K8: 联调验证(预计 3h,需 KMS 环境) -> **前置条件**: KMS 服务端可访问,已分配 clientId/clientSecret +### K8.1 认证 +- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功 -### K9.1 认证联调 -- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功获取 Token -- [ ] Token 过期自动刷新验证 -- [ ] 错误 clientSecret → 网关控制台打印初始化失败日志 +### K8.2 设备/告警/记录 +- [ ] /api/gateway/devices?adapter=KMS:main → 返回柜体+锁孔 +- [ ] /api/gateway/alarms/KMS:main → 返回告警列表 +- [ ] /api/gateway/control/KMS:main → 远程开门 -### K9.2 设备同步联调(2.18.4) -- [ ] `/api/gateway/health` 返回 KMS 适配器在线 -- [ ] `/api/gateway/devices?adapter=KMS:main` 返回柜体+锁孔设备树 -- [ ] 管理端 base_device 列表显示 KMS 设备(AdapterCode=KMS:main) +### K9: 联调文档记录 +- [ ] 记录异常接口到 KMS_联调笔记.txt -### K9.3 告警同步联调(2.18.7) -- [ ] `/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 — 全链路联调通过` +> **K8 提交点**: `PhaseK8_integration — 全链路联调通过` --- -## 任务总览 - -| Phase | 内容 | 文件数 | 预计 | +| Phase | 内容 | 文件 | 预计 | |:---:|------|:---:|:---:| | K0 | 项目骨架 | 2 | 15min | -| K1 | KmsModels 全部 DTO | 1 | 1h | -| K2 | KmsAuthHelper | 1 | 30min | -| K3 | KmsAdapter 核心方法 | 1 | 1.5h | -| K4 | KmsAdapter 扩展方法 | 1 | 1h | -| K5 | 配置与注册 | 3 | 15min | -| K6 | 编译自测 | — | 15min | -| K7 | 网关 Core + Host 扩展 | 6 | 1.5h | -| K8 | Vol.Pro 管理端配套 | 2 | 1h | -| K9 | 联调验证 | — | 3h | -| **合计** | — | **17** | **~10h** | - ---- - -> **版本**: 1.0 / 2025-05-19 / 严格按照 `KMS钥匙柜适配器详细设计文档.md` v2.1 制订 +| K1 | 全部 DTO | 1 | 1h | +| K2 | AuthHelper | 1 | 30min | +| K3 | 核心方法 | 1 | 1.5h | +| K4 | 扩展方法 | 1 | 1h | +| K5 | 配置注册 | 3 | 15min | +| K6 | 编译 | — | 15min | +| K7 | VolPro配套 | 2 | 1h | +| K8 | 联调 | — | 3h | +| **合计** | — | **11** | **~9h** | diff --git a/doc/设计文档/定时任务API化整改方案_v1.0.md b/doc/设计文档/定时任务API化整改方案_v1.0.md new file mode 100644 index 0000000..9de3644 --- /dev/null +++ b/doc/设计文档/定时任务API化整改方案_v1.0.md @@ -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; + +/// +/// 定时任务 API 端点。 +/// VolPro 框架通过 Sys_QuartzOptions 配置 URL+Cron 定时调用。 +/// 每个方法加 [ApiTask] 属性以允许框架匿名调用。 +/// +[ApiController] +[Route("api/task")] +public class TaskController : Controller +{ + /// T1: 设备同步 — 遍历在线网关触发全量设备同步 + [ApiTask] + [HttpGet, HttpPost, Route("syncDevices")] + public async Task SyncDevices() + { + var sp = HttpContext.RequestServices; + var engine = sp.GetService(); + if (engine != null) await engine.Execute(null!); + return Ok(new { time = DateTime.Now, status = "ok" }); + } + + /// T2: 心跳监控 — 扫描超时网关标记离线 + [ApiTask] + [HttpGet, HttpPost, Route("heartbeatMonitor")] + public async Task HeartbeatMonitor() + { + var sp = HttpContext.RequestServices; + var engine = sp.GetService(); + if (engine != null) await engine.Execute(null!); + return Ok(new { time = DateTime.Now, status = "ok" }); + } + + /// T3: 实时轮询 — 拉取 MC4 IoT 实时值 + [ApiTask] + [HttpGet, HttpPost, Route("realtimePoll")] + public async Task RealtimePoll() + { + var sp = HttpContext.RequestServices; + var engine = sp.GetService(); + if (engine != null) await engine.Execute(null!); + return Ok(new { time = DateTime.Now, status = "ok" }); + } + + /// T4: 规则引擎 — 评估规则+执行动作 + [ApiTask] + [HttpGet, HttpPost, Route("ruleEngine")] + public async Task RuleEngine() + { + var sp = HttpContext.RequestServices; + var engine = sp.GetService(); + 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().AsSelf().InstancePerLifetimeScope(); +builder.RegisterType().AsSelf().InstancePerLifetimeScope(); +builder.RegisterType().AsSelf().InstancePerLifetimeScope(); +builder.RegisterType().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(); + var devRepo = _sp.GetService(); + ... + } +} +``` + +### 步骤 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` | 构造函数注入 IServiceProvider,Execute 参数改为 nullable | +| `HeartbeatMonitorJob.cs` | 同上 | +| `RealtimePollJob.cs` | 同上 | +| `RuleEngineJob.cs` | 删除(RuleEngineService 本身就是普通类,不继承 IJob) | + +> `RuleEngineJob.cs` 可直接删除——`RuleEngineService` 是普通类,已被 TaskController 直接调用。 diff --git a/doc/设计文档/网关MC4模块整改方案_v1.0.md b/doc/设计文档/网关MC4模块整改方案_v1.0.md new file mode 100644 index 0000000..a03b34d --- /dev/null +++ b/doc/设计文档/网关MC4模块整改方案_v1.0.md @@ -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(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 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(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(json) + ?? throw new Exception("MC4 登录失败"); + _token = result.Token ?? ""; + _tokenExpiry = DateTime.UtcNow.AddHours(7); // 保守估计 8h + return _token; + } + + public async Task 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 +/// 批量获取多个设备的实时点位值(MC4.0 原生 multi/value/get) +public async Task>> GetMultiRealtimeValuesAsync(List 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>>(json)!; + return result; +} +``` + +### 3.3 编译验证 + +`dotnet build` → 0 错误。 + +> **M2 提交点**: `Fix-M2: MC4 批量点位查询 GetMultiRealtimeValuesAsync` + +--- + +## 4. 步骤 M3: 历史告警查询(预计 30min) + +### 4.1 新增 DTO + +```csharp +/// MC4.0 历史告警查询请求 +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 +/// 查询 MC4.0 历史告警(已恢复的告警) +public async Task> 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(json)!; + return new PagedResult + { + 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(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>(); + 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 原生批量 | diff --git a/doc/设计文档/网关MC4模块检查报告20260603.md b/doc/设计文档/网关MC4模块检查报告20260603.md new file mode 100644 index 0000000..d650c2e --- /dev/null +++ b/doc/设计文档/网关MC4模块检查报告20260603.md @@ -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(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 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 | 系统管理接口 | 运维操作 | diff --git a/doc/设计文档/网关自动注册机制整改_任务清单.md b/doc/设计文档/网关自动注册机制整改_任务清单.md new file mode 100644 index 0000000..b711df6 --- /dev/null +++ b/doc/设计文档/网关自动注册机制整改_任务清单.md @@ -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** | diff --git a/doc/设计文档/网关自动注册机制检查报告20260603.md b/doc/设计文档/网关自动注册机制检查报告20260603.md new file mode 100644 index 0000000..b8c6d06 --- /dev/null +++ b/doc/设计文档/网关自动注册机制检查报告20260603.md @@ -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() + .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 | diff --git a/doc/设计文档/网关项目代码审查报告20260604.md b/doc/设计文档/网关项目代码审查报告20260604.md new file mode 100644 index 0000000..415fe10 --- /dev/null +++ b/doc/设计文档/网关项目代码审查报告20260604.md @@ -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 HeartbeatAsync(...) { ... } +public async Task 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 的联调待办项(时间范围参数)已在代码中注释标注。 diff --git a/doc/设计文档/规则引擎实施计划_任务清单.md b/doc/设计文档/规则引擎实施计划_任务清单.md new file mode 100644 index 0000000..8f91b18 --- /dev/null +++ b/doc/设计文档/规则引擎实施计划_任务清单.md @@ -0,0 +1,175 @@ +# 规则引擎实施计划 — 任务清单 + +> **版本**: 1.0 +> **日期**: 2026-06-04 +> **基准**: `doc/设计文档/规则引擎实现方案_v1.0.md` +> **分支**: gateway-dev +> **原则**: 分阶段分步骤执行,每步骤编译复查后提交,不合并主分支 + +--- + +## 当前状态 + +| 前置条件 | 状态 | +|------|:--:| +| RealtimePollJob(10s 采集 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` +- [ ] 实现 `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` 中就绪,本次仅需执行。