Files
SecMPS/doc/设计文档/KMS钥匙柜适配器详细设计文档.md

1066 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <token>` |
---
### 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\<string\>, 必填) |
| 返回 | 统一响应状态 |
#### 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 Token1 个)
#### 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<KmsLocker>? Rows { get; set; } }
public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List<KmsLockhole>? 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<KmsWarning>? 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<KmsRecord>? 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<KmsPermission>? 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<KmsStaff>? 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<KmsLockerInfo>? 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<KmsLockhole>? LockholeList { get; set; } }
public class KmsLockholeListResponse { public int Code { get; set; } public int Total { get; set; } public List<KmsLockholeInfo>? 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<KmsOpenerInfo>? 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<KmsStaffOpener>? 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<KmsStaff> Staff { get; set; } = new(); }
// 通用包装
public class KmsApiResponse<T> { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List<T>? Rows { get; set; } public T? Data { get; set; } }
```
---
## 5. KmsAuthHelper 完整设计
```csharp
/// <summary>
/// KMS Bearer Token 认证辅助。
/// 流程: POST /prod-api/getToken?clientId=x&clientSecret=y → { code:200, token:"xxx" }
/// Token 缓存 25 分钟KMS 有效期 30 分钟,留 5 分钟余量)。
/// </summary>
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<string> 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<KmsTokenResponse>()
?? 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<HttpClient> 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
/// <summary>
/// KMS 智能钥匙柜适配器。
/// 实现: IHasFlatDevices + IHasAlarms。
///
/// 设备模型: 柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体SourceId)。
/// AdapterCode: "KMS:{InstanceName}"。
/// 限流: 5 QPS。
/// </summary>
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<bool> 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<PagedResult<StandardDevice>> 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<KmsOpenerListResponse>()!;
var devices = new List<StandardDevice>();
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<StandardDevice> { 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<string, object?> { ["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<string, object?> { ["openerId"] = hole.OpenerId, ["openerType"] = hole.OpenerType, ["openerState"] = hole.OpenerState }
};
```
### 6.4 告警列表2.18.7
```csharp
public async Task<PagedResult<StandardAlarm>> 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<KmsWarningListResponse>()!;
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<StandardAlarm> { 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
/// <summary>查询借还记录Phase 2 — 可作为额外 B 接口暴露)</summary>
public async Task<PagedResult<KmsRecord>> 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<KmsRecordListResponse>()!;
return new PagedResult<KmsRecord> { Items = data.Rows ?? new(), Total = data.Total };
}
```
### 6.6 员工同步2.18.3 — Phase 2
```csharp
/// <summary>从 Vol.Pro 向 KMS 批量同步员工</summary>
public async Task BatchSyncStaffAsync(List<KmsStaff> 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
/// <summary>远程授权开门</summary>
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<List<KmsConfig>>() ?? new();
foreach (var k in kmsList)
{
var code = $"KMS:{k.InstanceName ?? "default"}";
var a = new KmsAdapter(code,
app.Services.GetRequiredService<IHttpClientFactory>().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<T> | ✅ |
| 统一分页 | GetDevices/GetAlarms 返回 PagedResult<T> | ✅ |
| 弹性 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;
/// <summary>
/// 设备反向控制接口。适用于支持远程下发指令的子系统(如 KMS 远程开门、门禁远程开闸、道闸抬杆)。
/// 控制指令为通用键值对字典,适配器内部转换。
/// </summary>
public interface IAcceptsControl : IGatewayAdapter
{
/// <summary>向设备下发控制指令</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="command">指令名,如 "open"/"close"/"authorize"</param>
/// <param name="parameters">指令参数键值对</param>
Task<ControlResult> SendControlAsync(string sourceDeviceId, string command, Dictionary<string, object?> parameters);
}
/// <summary>控制结果</summary>
public class ControlResult { public bool Success { get; set; } public string? Message { get; set; } }
```
```csharp
/// <summary>
/// 业务记录查询接口。适用于具有借还、交接、授权等业务日志的子系统。
/// 不走 StandardAlarm 通道,独立分页查询。
/// </summary>
public interface IHasBusinessLogs : IGatewayAdapter
{
/// <summary>分页查询业务记录</summary>
/// <param name="logType">记录类型: "borrow"/"handover"/"permission"/"event"</param>
/// <param name="from">开始时间</param>
/// <param name="to">结束时间</param>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
/// <param name="filters">额外过滤条件</param>
Task<PagedResult<BusinessLogEntry>> GetBusinessLogsAsync(
string logType, DateTime? from, DateTime? to,
int page, int size, Dictionary<string, string>? filters = null);
}
/// <summary>业务记录条目</summary>
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<string, object?>? Extra { get; set; }
}
```
```csharp
/// <summary>
/// 外部数据同步写入接口。适用于需要从 Vol.Pro 向子系统推送数据的场景(如员工同步)。
/// </summary>
public interface IAcceptsDataSync : IGatewayAdapter
{
/// <summary>批量写入数据到子系统</summary>
/// <param name="dataType">数据类型: "staff"/"department"</param>
/// <param name="items">待同步的数据对象列表JSON 兼容)</param>
Task<SyncResult> SyncDataAsync(string dataType, List<object> items);
/// <summary>批量删除</summary>
Task<SyncResult> DeleteDataAsync(string dataType, List<string> 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