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

866 lines
30 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.0
> **版本**: 2.0(完整接口版)
> **日期**: 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 (完整接口版)