diff --git a/doc/设计文档/KMS钥匙柜适配器详细设计文档.md b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md index 66c408b..7773efd 100644 --- a/doc/设计文档/KMS钥匙柜适配器详细设计文档.md +++ b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md @@ -1,10 +1,11 @@ -# KMS 钥匙柜适配器详细设计文档 +# KMS 钥匙柜适配器详细设计文档 v2.0 -> **版本**: 1.0 +> **版本**: 2.0(完整接口版) > **日期**: 2025-05-19 -> **基准**: `doc/整合方案/KMS钥匙柜整合方案_v2.0.md` +> **数据源**: `doc/对接文档/钥匙管理系统软件接口.docx`(KMS API v1.0.4) > **技术栈**: .NET 8 / ASP.NET Core / C# > **架构**: IntegrationGateway 适配器模式 +> **覆盖**: KMS 全部 54 个 REST 接口 --- @@ -14,197 +15,545 @@ 在 IntegrationGateway 中新增 `KmsAdapter`,将智能钥匙管理系统(KMS)作为第三个子系统接入 SecMPS 整合平台。KMS 通过网关的 `IHasFlatDevices` 上报柜体/锁孔设备树,通过 `IHasAlarms` 上报告警记录,由 Vol.Pro 管理端统一展示和管理。 -### 1.2 技术约束 +### 1.2 KMS 系统模型 + +``` +KMS 管理平台 (一个 IP:PORT) +├── 智能钥匙柜 A (locker) +│ ├── 锁孔 1 (lockhole) → 钥匙 (opener) +│ ├── 锁孔 2 +│ └── ... +├── 智能钥匙柜 B +└── ... +``` + +实体关系:**柜体(Locker) 1:N 锁孔(Lockhole) 1:1 钥匙(Opener)** + +### 1.3 技术约束 | 约束 | 说明 | |------|------| -| 不修改 Core 接口 | 复用现有 `IHasFlatDevices` + `IHasAlarms`,不需新增接口 | +| 不修改 Core 接口 | 复用现有 `IHasFlatDevices` + `IHasAlarms` | | 不依赖 KMS 运行时 | `dotnet build` 可在无 KMS 环境下通过 | | 故障隔离 | KMS 离线不影响 Owl/MC4 适配器运行 | -| 限流 | KMS 无明确 QPS 限制,设 5 QPS 保守值 | +| 限流 | 5 QPS 保守值 | --- -## 2. 系统架构 +## 2. KMS 接口完整参考 -``` -┌──────────────┐ HTTP (Bearer Token) ┌────────────────────┐ -│ KMS 服务端 │◄──────────────────────────►│ IntegrationGateway │ -│ :8080 │ /prod-api/* │ :5100 │ -│ (Java) │ │ (NET 8) │ -└──────────────┘ └────────┬───────────┘ - │ B 组接口 - ▼ - ┌────────────────────┐ - │ Vol.Pro 管理端 │ - │ :9100 │ - │ (NET 8) │ - └────────────────────┘ -``` +### 2.0 认证 + +| 项目 | 说明 | +|------|------| +| 接口 | `POST /prod-api/getToken` | +| 参数 | query: `clientId` (必填/string), `clientSecret` (必填/string) | +| 返回 | `{ code: 200, token: "xxx", msg: "操作成功" }` | +| 有效期 | 30 分钟 | +| 使用 | Header: `Authorization: Bearer ` | --- -## 3. 项目结构 +### 2.18 第三方集成接口(Phase 1 — 8 个) -``` -gateway/src/IntegrationGateway.Adapters.Kms/ -├── IntegrationGateway.Adapters.Kms.csproj # 类库, net8.0 -├── KmsAuthHelper.cs # Bearer Token 认证 -├── KmsAdapter.cs # 适配器主体 -└── KmsModels.cs # 请求/响应 DTO -``` +#### 2.18.1 心跳接口 -### 3.1 依赖关系 +| 项目 | 说明 | +|------|------| +| 方法 | `GET` | +| 路径 | `/prod-api/heartBeat` | +| 参数 | 无 | +| 返回 | `{ code: 200, msg: "success" }` | -``` -Host → Adapters.Kms → Core -Host → Core -``` +#### 2.18.2 批量删除员工 -适配器只引用 Core,零外部 NuGet 依赖(除 Microsoft.Extensions.Http 已由 Core 引入)。 +| 项目 | 说明 | +|------|------| +| 方法 | `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 发起时传用户名,网关代理请求。 --- -## 4. KMS 接口详细参考 +### 2.3 交接记录(2 个) -### 4.1 认证 +#### 2.3.1 查询交接记录明细列表 -**POST** `/prod-api/getToken` +| 项目 | 说明 | +|------|------| +| 方法 | `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 }] }` | -| 参数 | 类型 | 说明 | -|------|------|------| -| clientId | query | 由 KMS 分配的客户端ID | -| clientSecret | query | 由 KMS 分配的客户端密钥 | +#### 2.3.2 查询交接记录列表 -响应: -```json -{ "code": 200, "token": "eyJ...", "msg": "操作成功" } -``` +| 项目 | 说明 | +|------|------| +| 方法 | `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, ... }] }` | -### 4.2 第三方接口(Phase 1 实现 4 个) +--- -#### 4.2.1 心跳 — `POST /prod-api/heartBeat` +### 2.4 授权管理(3 个) -请求体:`{}` (空 JSON) +#### 2.4.1 查询授权记录列表 -响应: -```json -{ "code": 200, "msg": "success" } -``` +| 项目 | 说明 | +|------|------| +| 方法 | `GET` | +| 路径 | `/prod-api/kms/permission/list` | +| 参数 | query: `backStaffName` (必填/string), `lendStaffName` (必填/string), `lockerName` (必填/string), `openerCnName` (必填/string), `pageNum` (可选/int), `pageSize` (可选/int) | +| 返回 | `{ code: 200, total, rows: [...] }` | -#### 4.2.2 柜体钥匙列表 — `POST /prod-api/getOpenerList` +#### 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, - "msg": "查询成功", - "rows": [{ - "lockerId": 25, - "lockerName": "10位智能公共钥匙柜", - "lockerCode": "888", - "lockholeList": [{ - "lockholeSort": 1, - "openerId": 2020, - "openerName": "仓库大门钥匙", - "openerType": "永久授权", - "openerState": "在位" - }] - }] + "code": 200, // 200=成功, 0/其他=失败 + "msg": "操作成功", // 消息文本 + "total": 100, // 分页总记录数(列表接口) + "rows": [...], // 数据列表(列表接口) + "data": {...} // 单个对象(详情接口) } ``` -#### 4.2.3 告警列表 — `POST /prod-api/getWarningList` - -请求体:`{}`(所有告警)或带时间范围 - -响应: -```json -{ - "code": 200, - "total": 5, - "rows": [{ - "uuid": "xxx", - "lockerName": "10位公共钥匙柜", - "lockholeSort": 3, - "openerName": "机房钥匙", - "type": 1, - "warningTime": "2025-05-19 10:30:00", - "remark": "超时未归还", - "staffName": "张三" - }] -} -``` - -#### 4.2.4 借还记录 — `POST /prod-api/getRecordList`(Phase 2 参考) - -请求体:`{}` - -响应: -```json -{ - "code": 200, - "total": 10, - "rows": [{ - "uuid": "xxx", - "lockerName": "10位公共钥匙柜", - "lockholeSort": 2, - "openerName": "仓库大门钥匙", - "staffName": "张三", - "borrowTime": "2025-05-19 09:00:00", - "returnTime": "2025-05-19 11:00:00", - "type": "1" - }] -} -``` - -### 4.3 标准业务接口(Phase 2 参考,共 50+) - -见 `doc/整合方案/KMS钥匙柜整合方案_v2.0.md` §1.3。 - --- -## 5. KmsModels 详细设计 +## 4. KmsModels 完整设计 + +### 4.1 响应模型 ```csharp namespace IntegrationGateway.Adapters.Kms; -// ── 认证 ── +// ═══ 认证 ═══ public class KmsTokenResponse { public int Code { get; set; } public string Token { get; set; } = ""; public string? Msg { get; set; } } -// ── 第三方接口响应 ── -public class KmsOpenerListResponse { public int Code { get; set; } public string? Msg { get; set; } public List Rows { get; set; } = new(); } -public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List LockholeList { get; set; } = new(); } +// ═══ 第三方接口 DTO ═══ +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; } } -public class KmsWarningListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List Rows { get; set; } = new(); } +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; } } -public class KmsRecordListResponse { public int Code { get; set; } public int Total { get; set; } public List Rows { get; set; } = new(); } +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; } } + +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; } } +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 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; } } + +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; } } ``` --- -## 6. KmsAuthHelper 详细设计 +## 5. KmsAuthHelper 完整设计 ```csharp /// /// KMS Bearer Token 认证辅助。 -/// -/// 流程: -/// 1. POST /prod-api/getToken?clientId={}&clientSecret={} -/// 2. 返回 { code: 200, token: "xxx" } -/// 3. Token 缓存 25 分钟 (KMS 有效期 30 分钟,留 5 分钟余量) +/// 流程: POST /prod-api/getToken?clientId=x&clientSecret=y → { code:200, token:"xxx" } +/// Token 缓存 25 分钟(KMS 有效期 30 分钟,留 5 分钟余量)。 /// public class KmsAuthHelper { private readonly HttpClient _http; - private readonly string _baseUrl; - private readonly string _clientId; - private readonly string _clientSecret; + private readonly string _baseUrl, _clientId, _clientSecret; private string? _token; private DateTime _tokenExpiry = DateTime.MinValue; @@ -216,19 +565,15 @@ public class KmsAuthHelper public async Task GetTokenAsync() { - if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) - return _token; - + if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token; var resp = await _http.PostAsync( - $"{_baseUrl}/prod-api/getToken?clientId={Uri.EscapeDataString(_clientId)}&clientSecret={Uri.EscapeDataString(_clientSecret)}", - null); + $"{_baseUrl}/prod-api/getToken?clientId={Uri.EscapeDataString(_clientId)}&clientSecret={Uri.EscapeDataString(_clientSecret)}", null); resp.EnsureSuccessStatusCode(); - var result = await resp.Content.ReadFromJsonAsync(); - var code = result.GetProperty("code").GetInt32(); - if (code != 200) throw new Exception($"KMS 认证失败: {code}"); - _token = result.GetProperty("token").GetString(); - _tokenExpiry = DateTime.UtcNow.AddMinutes(25); // 30min TTL, 25min 刷新 - return _token!; + var result = await resp.Content.ReadFromJsonAsync() + ?? throw new Exception("KMS Token 响应为空"); + if (result.Code != 200) throw new Exception($"KMS 认证失败: code={result.Code}"); + _token = result.Token; _tokenExpiry = DateTime.UtcNow.AddMinutes(25); + return _token; } public async Task GetAuthenticatedClientAsync() @@ -245,15 +590,18 @@ public class KmsAuthHelper --- -## 7. KmsAdapter 详细设计 +## 6. KmsAdapter 完整设计 + +### 6.1 类定义与构造函数 ```csharp /// -/// KMS 智能钥匙柜适配器。实现 IHasFlatDevices + IHasAlarms。 +/// KMS 智能钥匙柜适配器。 +/// 实现: IHasFlatDevices + IHasAlarms。 /// -/// 设备模型:柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体ID)。 -/// 限流:5 QPS。 -/// AdapterCode: "KMS:{InstanceName}" +/// 设备模型: 柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体SourceId)。 +/// AdapterCode: "KMS:{InstanceName}"。 +/// 限流: 5 QPS。 /// public class KmsAdapter : IHasFlatDevices, IHasAlarms { @@ -267,27 +615,31 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms public KmsAdapter(string adapterCode, HttpClient http, string baseUrl, string clientId, string clientSecret) { - AdapterCode = adapterCode; - _http = http; + AdapterCode = adapterCode; _http = http; _auth = new KmsAuthHelper(http, baseUrl, clientId, clientSecret); } public async Task InitializeAsync() => await _auth.GetTokenAsync(); +``` - // ── HealthCheck → 2.18.1 心跳 ── +### 6.2 健康检查(2.18.1) + +```csharp public async Task HealthCheckAsync() { try { var client = await _auth.GetAuthenticatedClientAsync(); - var resp = await client.PostAsync("/prod-api/heartBeat", - new StringContent("{}", Encoding.UTF8, "application/json")); + var resp = await client.GetAsync("/prod-api/heartBeat"); return resp.IsSuccessStatusCode; } catch { return false; } } +``` - // ── IHasFlatDevices → 2.18.4 柜体钥匙列表 ── +### 6.3 设备列表(2.18.4) + +```csharp public async Task> GetDevicesAsync(int page, int size, string? keyword = null) { await _limiter.WaitAsync(); @@ -298,45 +650,34 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms var data = await resp.Content.ReadFromJsonAsync()!; var devices = new List(); - foreach (var locker in data.Rows) + foreach (var locker in data.Rows ?? new()) { - // 父设备: 柜体 - devices.Add(new StandardDevice - { - 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 - } - }); - // 子设备: 锁孔 - foreach (var hole in locker.LockholeList) - { - bool isOnline = hole.OpenerState == "在位"; - devices.Add(new StandardDevice - { - SourceId = $"lockhole_{locker.LockerId}_{hole.LockholeSort}", - Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}", - Category = "钥匙位", Group = "门禁设备", - IsParent = false, IsOnline = isOnline, - ParentSourceId = $"locker_{locker.LockerId}", - Extra = new Dictionary - { - ["openerId"] = hole.OpenerId, - ["openerType"] = hole.OpenerType, - ["openerState"] = hole.OpenerState - } - }); - } + devices.Add(MapLockerToDevice(locker)); + if (locker.LockholeList != null) + devices.AddRange(locker.LockholeList.Select(h => MapLockholeToDevice(h, locker.LockerId))); } return new PagedResult { Items = devices, Total = devices.Count }; } - // ── IHasAlarms → 2.18.7 告警列表 ── + 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) + +```csharp public async Task> GetAlarmsAsync( int page, int size, DateTime from, DateTime to, string? level = null, string? state = null) { @@ -347,22 +688,18 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms resp.EnsureSuccessStatusCode(); var data = await resp.Content.ReadFromJsonAsync()!; - var alarms = data.Rows.Select(w => new StandardAlarm + var alarms = (data.Rows ?? new()).Select(w => new StandardAlarm { - AlarmId = w.Uuid ?? "", AdapterCode = AdapterCode, - Level = "普通", // KMS 不区分告警等级,统一"普通" + AlarmId = w.Uuid ?? "", AdapterCode = AdapterCode, Level = "普通", Title = $"{w.LockerName} 锁孔{w.LockholeSort}: {w.OpenerName}", - Content = w.Remark, - OccurTime = DateTime.TryParse(w.WarningTime, out var t) ? t : DateTime.MinValue, + Content = w.Remark, OccurTime = DateTime.TryParse(w.WarningTime, out var t) ? t : DateTime.MinValue, Status = w.Type == 1 ? "未确认" : "已结束" }).ToList(); - return new PagedResult { Items = alarms, Total = data.Total }; } public async Task ConfirmAlarmAsync(string alarmId) { - // KMS 2.18 接口不提供告警确认,调用标准接口 await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); await client.PostAsync($"/prod-api/kms/warning/confirm/{alarmId}", null); @@ -370,67 +707,107 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms public async Task EndAlarmAsync(string alarmId) { - // KMS 2.18 接口不提供告警结束 + // KMS 第三方接口不提供告警结束 API,标准接口联调时补充 + } +``` + +### 6.5 借还记录(2.18.6) + +```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 }; + } +``` + +### 6.6 员工同步(2.18.3 — Phase 2) + +```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(); + } +``` + +### 6.7 远程授权(2.4.3 — Phase 2) + +```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(); } -} ``` --- -## 8. 设备映射逻辑 +## 7. 设备映射逻辑 ``` -POST /prod-api/getOpenerList 响应 +POST /prod-api/getOpenerList │ ▼ -遍历每个 KmsLocker - ├── 生成 1 个父 StandardDevice (SourceId="locker_{lockerId}", IsParent=true) - └── 遍历 lockholeList - └── 每个 lockhole 生成 1 个子 StandardDevice - (SourceId="lockhole_{lockerId}_{lockholeSort}", ParentSourceId="locker_{lockerId}") - (IsOnline = OpenerState=="在位" ? true : false) +遍历 Lockers [ { lockerId, lockerName, lockholeList: [...] } ] + ├── 父设备: SourceId="locker_{lockerId}", IsParent=true, Category="智能钥匙柜" + └── 子设备foreach: SourceId="lockhole_{lockerId}_{lockholeSort}", ParentSourceId="locker_{lockerId}" + Category="钥匙位", IsOnline = (openerState=="在位") ``` -**parentSourceId 解析**(A3 同步时在 gateway_nodesService.SyncDevicesAsync 中处理): +**parentSourceId 解析**(A3 同步时由 `gateway_nodesService.SyncDevicesAsync` 处理): ``` -"locker_{lockerId}" → 查询 base_device WHERE SourceId='locker_{lockerId}' → 获取 DeviceId → 填入子设备的 ParentDeviceId +"locker_{lockerId}" → 查 base_device WHERE SourceId='locker_{lockerId}' → 得到 DeviceId → 填入子设备 ParentDeviceId ``` --- -## 9. 告警映射逻辑 +## 8. 告警映射逻辑 -``` -POST /prod-api/getWarningList 响应 - │ - ▼ -遍历每个 KmsWarning - ├── AlarmId ← uuid - ├── Title ← "{lockerName} 锁孔{lockholeSort}: {openerName}" - ├── Content ← remark - ├── OccurTime ← warningTime - ├── Status ← Type==1 ? "未确认" : "已结束" - └── Level ← "普通" (KMS 不区分告警等级) -``` +| 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 不区分等级) | --- -## 10. 配置 +## 9. 配置 -### 10.1 appsettings.json +### 9.1 appsettings.json ```json { - "KMS": { - "InstanceName": "main", - "BaseUrl": "http://192.168.1.50:8080", - "ClientId": "your_client_id", - "ClientSecret": "your_client_secret" - } + "KMS": [ + { + "InstanceName": "main", + "BaseUrl": "http://192.168.1.50:8080", + "ClientId": "your_client_id", + "ClientSecret": "your_client_secret" + } + ] } ``` -### 10.2 KmsConfig POCO +### 9.2 KmsConfig POCO ```csharp public class KmsConfig @@ -442,7 +819,7 @@ public class KmsConfig } ``` -### 10.3 Program.cs 注册 +### 9.3 Program.cs 注册 ```csharp var kmsList = app.Configuration.GetSection("KMS").Get>() ?? new(); @@ -458,43 +835,31 @@ foreach (var k in kmsList) --- -## 11. 测试策略 +## 10. 与 Vol.Pro 管理端的交互 -### 11.1 单元测试(无 KMS 依赖) +### 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 管理端改动 + +| 项 | 改动 | |------|------| -| KmsModels 序列化 | JSON 往返正确 | -| 设备映射 | locker + lockhole → StandardDevice 正确 | -| 告警映射 | KmsWarning → StandardAlarm 正确 | +| 数据库 | 无 — base_device / iot_alarm 已兼容 | +| 字典 | 新增 "智能钥匙柜" / "钥匙位" 字典项 | +| 后端代码 | 无 — A1-A4 逻辑通用 | +| 前端列表 | 自动显示 KMS 设备(AdapterCode 列区分来源) | +| 前端按钮 | Phase 2: KeyDeviceActions.vue | -### 11.2 集成测试(需 KMS 环境) +### 10.3 钥匙状态展示 -| 场景 | 预期 | -|------|------| -| 认证成功 | GetToken → 返回有效 token | -| 设备同步 | GetOpenerList → 返回柜体+锁孔列表 | -| 告警同步 | GetWarningList → 返回告警列表 | -| 健康检查 | HeartBeat → 200 OK | -| 认证失败 | 错误 clientId → 适配器初始化失败 | -| KMS 离线 | HealthCheck → false, 不影响 Owl/MC4 | +设备列表中每个锁孔(钥匙位)的 `IsOnline` 反映钥匙在位状态,`Extra.openerState` 存储详细状态字符串。管理端可通过 `Extra` 列的 JSON 展示查看钥匙类型和状态。 --- -## 12. 实施计划 - -| 步骤 | 内容 | 文件 | 预计 | -|:---:|------|------|:---:| -| 1 | 创建 `Adapters.Kms` 项目 + 引用 | csproj, sln | 10min | -| 2 | `KmsModels.cs` | 1 文件 | 20min | -| 3 | `KmsAuthHelper.cs` | 1 文件 | 30min | -| 4 | `KmsAdapter.cs` (HealthCheck + GetDevices) | 1 文件 | 1h | -| 5 | `KmsAdapter.cs` (GetAlarms + Confirm/End) | 1 文件 | 30min | -| 6 | appsettings.json + Program.cs 注册 | 2 文件 | 15min | -| 7 | 编译验证 `dotnet build` | 网关 | 5min | -| 8 | 联调 (需 KMS 环境) | — | 2h | - ---- - -> **版本历史**: -> - v1.0 (2025-05-19) — 初版详细设计 +> **接口覆盖**: 54 个 REST 端点全部记录,其中 Phase 1 实现 4 个核心接口,Phase 2 实现 12 个扩展接口,其余 38 个为标准 KMS 管理接口按需对接。 +> **版本历史**: v1.0 (初版) → v2.0 (完整接口版)