# KMS 钥匙柜适配器详细设计文档 v2.1 > **版本**: 2.1(完整接口版 + 适配原则审查 + 缺口分析) > **日期**: 2025-05-19 > **数据源**: `doc/对接文档/钥匙管理系统软件接口.docx`(KMS API v1.0.4) > **技术栈**: .NET 8 / ASP.NET Core / C# > **架构**: IntegrationGateway 适配器模式 > **覆盖**: KMS 全部 54 个 REST 接口 --- ## 1. 概述 ### 1.1 设计目标 在 IntegrationGateway 中新增 `KmsAdapter`,将智能钥匙管理系统(KMS)作为第三个子系统接入 SecMPS 整合平台。KMS 通过网关的 `IHasFlatDevices` 上报柜体/锁孔设备树,通过 `IHasAlarms` 上报告警记录,由 Vol.Pro 管理端统一展示和管理。 ### 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` | | 不依赖 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": {...} // 单个对象(详情接口) } ``` --- ## 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; } } // ═══ 第三方接口 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; } } 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 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; } } ``` --- ## 5. KmsAuthHelper 完整设计 ```csharp /// /// KMS Bearer Token 认证辅助。 /// 流程: 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, _clientId, _clientSecret; private string? _token; private DateTime _tokenExpiry = DateTime.MinValue; public KmsAuthHelper(HttpClient http, string baseUrl, string clientId, string clientSecret) { _http = http; _baseUrl = baseUrl.TrimEnd('/'); _clientId = clientId; _clientSecret = clientSecret; } public async Task GetTokenAsync() { 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); resp.EnsureSuccessStatusCode(); 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() { var token = await GetTokenAsync(); var client = new HttpClient { BaseAddress = new Uri(_baseUrl) }; client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); return client; } public void Invalidate() => _token = null; } ``` --- ## 6. KmsAdapter 完整设计 ### 6.1 类定义与构造函数 ```csharp /// /// KMS 智能钥匙柜适配器。 /// 实现: IHasFlatDevices + IHasAlarms。 /// /// 设备模型: 柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体SourceId)。 /// AdapterCode: "KMS:{InstanceName}"。 /// 限流: 5 QPS。 /// public class KmsAdapter : IHasFlatDevices, IHasAlarms { private readonly HttpClient _http; private readonly KmsAuthHelper _auth; private readonly RateLimiter _limiter = new(5); public string AdapterCode { get; } public string DisplayName => $"KMS ({AdapterCode})"; public AdapterCapabilities Capabilities => new() { HasFlatDevices = true, HasAlarms = true }; public KmsAdapter(string adapterCode, HttpClient http, string baseUrl, string clientId, string clientSecret) { AdapterCode = adapterCode; _http = http; _auth = new KmsAuthHelper(http, baseUrl, clientId, clientSecret); } public async Task InitializeAsync() => await _auth.GetTokenAsync(); ``` ### 6.2 健康检查(2.18.1) ```csharp public async Task HealthCheckAsync() { try { var client = await _auth.GetAuthenticatedClientAsync(); var resp = await client.GetAsync("/prod-api/heartBeat"); return resp.IsSuccessStatusCode; } catch { return false; } } ``` ### 6.3 设备列表(2.18.4) ```csharp public async Task> GetDevicesAsync(int page, int size, string? keyword = null) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var resp = await client.PostAsync("/prod-api/getOpenerList", new StringContent("{}", Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var data = await resp.Content.ReadFromJsonAsync()!; 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))); } 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) ```csharp public async Task> GetAlarmsAsync( int page, int size, DateTime from, DateTime to, string? level = null, string? state = null) { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); var resp = await client.PostAsync("/prod-api/getWarningList", new StringContent("{}", Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var data = await resp.Content.ReadFromJsonAsync()!; var alarms = (data.Rows ?? new()).Select(w => new StandardAlarm { 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, Status = w.Type == 1 ? "未确认" : "已结束" }).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) ```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(); } ``` --- ## 7. 设备映射逻辑 ``` 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=="在位") ``` **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 ```json { "KMS": [ { "InstanceName": "main", "BaseUrl": "http://192.168.1.50:8080", "ClientId": "your_client_id", "ClientSecret": "your_client_secret" } ] } ``` ### 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 注册 ```csharp var kmsList = app.Configuration.GetSection("KMS").Get>() ?? new(); foreach (var k in kmsList) { var code = $"KMS:{k.InstanceName ?? "default"}"; var a = new KmsAdapter(code, app.Services.GetRequiredService().CreateClient("VolPro"), k.BaseUrl, k.ClientId, k.ClientSecret); registry.Register(a); } ``` --- ## 10. 与 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 展示查看钥匙类型和状态。 --- > **接口覆盖**: 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)。