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

501 lines
17 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 钥匙柜适配器详细设计文档
> **版本**: 1.0
> **日期**: 2025-05-19
> **基准**: `doc/整合方案/KMS钥匙柜整合方案_v2.0.md`
> **技术栈**: .NET 8 / ASP.NET Core / C#
> **架构**: IntegrationGateway 适配器模式
---
## 1. 概述
### 1.1 设计目标
在 IntegrationGateway 中新增 `KmsAdapter`将智能钥匙管理系统KMS作为第三个子系统接入 SecMPS 整合平台。KMS 通过网关的 `IHasFlatDevices` 上报柜体/锁孔设备树,通过 `IHasAlarms` 上报告警记录,由 Vol.Pro 管理端统一展示和管理。
### 1.2 技术约束
| 约束 | 说明 |
|------|------|
| 不修改 Core 接口 | 复用现有 `IHasFlatDevices` + `IHasAlarms`,不需新增接口 |
| 不依赖 KMS 运行时 | `dotnet build` 可在无 KMS 环境下通过 |
| 故障隔离 | KMS 离线不影响 Owl/MC4 适配器运行 |
| 限流 | KMS 无明确 QPS 限制,设 5 QPS 保守值 |
---
## 2. 系统架构
```
┌──────────────┐ HTTP (Bearer Token) ┌────────────────────┐
│ KMS 服务端 │◄──────────────────────────►│ IntegrationGateway │
│ :8080 │ /prod-api/* │ :5100 │
│ (Java) │ │ (NET 8) │
└──────────────┘ └────────┬───────────┘
│ B 组接口
┌────────────────────┐
│ Vol.Pro 管理端 │
│ :9100 │
│ (NET 8) │
└────────────────────┘
```
---
## 3. 项目结构
```
gateway/src/IntegrationGateway.Adapters.Kms/
├── IntegrationGateway.Adapters.Kms.csproj # 类库, net8.0
├── KmsAuthHelper.cs # Bearer Token 认证
├── KmsAdapter.cs # 适配器主体
└── KmsModels.cs # 请求/响应 DTO
```
### 3.1 依赖关系
```
Host → Adapters.Kms → Core
Host → Core
```
适配器只引用 Core零外部 NuGet 依赖(除 Microsoft.Extensions.Http 已由 Core 引入)。
---
## 4. KMS 接口详细参考
### 4.1 认证
**POST** `/prod-api/getToken`
| 参数 | 类型 | 说明 |
|------|------|------|
| clientId | query | 由 KMS 分配的客户端ID |
| clientSecret | query | 由 KMS 分配的客户端密钥 |
响应:
```json
{ "code": 200, "token": "eyJ...", "msg": "操作成功" }
```
### 4.2 第三方接口Phase 1 实现 4 个)
#### 4.2.1 心跳 — `POST /prod-api/heartBeat`
请求体:`{}` (空 JSON)
响应:
```json
{ "code": 200, "msg": "success" }
```
#### 4.2.2 柜体钥匙列表 — `POST /prod-api/getOpenerList`
请求体:`{}`
响应(核心字段):
```json
{
"code": 200,
"msg": "查询成功",
"rows": [{
"lockerId": 25,
"lockerName": "10位智能公共钥匙柜",
"lockerCode": "888",
"lockholeList": [{
"lockholeSort": 1,
"openerId": 2020,
"openerName": "仓库大门钥匙",
"openerType": "永久授权",
"openerState": "在位"
}]
}]
}
```
#### 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 详细设计
```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<KmsLocker> Rows { get; set; } = new(); }
public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List<KmsLockhole> LockholeList { get; set; } = new(); }
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; } = new(); }
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<KmsRecord> Rows { get; set; } = new(); }
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; } }
```
---
## 6. KmsAuthHelper 详细设计
```csharp
/// <summary>
/// KMS Bearer Token 认证辅助。
///
/// 流程:
/// 1. POST /prod-api/getToken?clientId={}&clientSecret={}
/// 2. 返回 { code: 200, token: "xxx" }
/// 3. Token 缓存 25 分钟 (KMS 有效期 30 分钟,留 5 分钟余量)
/// </summary>
public class KmsAuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl;
private readonly string _clientId;
private readonly string _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<JsonElement>();
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!;
}
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;
}
```
---
## 7. KmsAdapter 详细设计
```csharp
/// <summary>
/// KMS 智能钥匙柜适配器。实现 IHasFlatDevices + IHasAlarms。
///
/// 设备模型:柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体ID)。
/// 限流5 QPS。
/// AdapterCode: "KMS:{InstanceName}"
/// </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();
// ── HealthCheck → 2.18.1 心跳 ──
public async Task<bool> HealthCheckAsync()
{
try
{
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync("/prod-api/heartBeat",
new StringContent("{}", Encoding.UTF8, "application/json"));
return resp.IsSuccessStatusCode;
}
catch { return false; }
}
// ── IHasFlatDevices → 2.18.4 柜体钥匙列表 ──
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)
{
// 父设备: 柜体
devices.Add(new StandardDevice
{
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
}
});
// 子设备: 锁孔
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<string, object?>
{
["openerId"] = hole.OpenerId,
["openerType"] = hole.OpenerType,
["openerState"] = hole.OpenerState
}
});
}
}
return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
}
// ── IHasAlarms → 2.18.7 告警列表 ──
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.Select(w => new StandardAlarm
{
AlarmId = w.Uuid ?? "", AdapterCode = AdapterCode,
Level = "普通", // KMS 不区分告警等级,统一"普通"
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)
{
// KMS 2.18 接口不提供告警确认,调用标准接口
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 2.18 接口不提供告警结束
}
}
```
---
## 8. 设备映射逻辑
```
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)
```
**parentSourceId 解析**A3 同步时在 gateway_nodesService.SyncDevicesAsync 中处理):
```
"locker_{lockerId}" → 查询 base_device WHERE SourceId='locker_{lockerId}' → 获取 DeviceId → 填入子设备的 ParentDeviceId
```
---
## 9. 告警映射逻辑
```
POST /prod-api/getWarningList 响应
遍历每个 KmsWarning
├── AlarmId ← uuid
├── Title ← "{lockerName} 锁孔{lockholeSort}: {openerName}"
├── Content ← remark
├── OccurTime ← warningTime
├── Status ← Type==1 ? "未确认" : "已结束"
└── Level ← "普通" (KMS 不区分告警等级)
```
---
## 10. 配置
### 10.1 appsettings.json
```json
{
"KMS": {
"InstanceName": "main",
"BaseUrl": "http://192.168.1.50:8080",
"ClientId": "your_client_id",
"ClientSecret": "your_client_secret"
}
}
```
### 10.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; } = "";
}
```
### 10.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);
}
```
---
## 11. 测试策略
### 11.1 单元测试(无 KMS 依赖)
| 测试 | 验证点 |
|------|------|
| KmsModels 序列化 | JSON 往返正确 |
| 设备映射 | locker + lockhole → StandardDevice 正确 |
| 告警映射 | KmsWarning → StandardAlarm 正确 |
### 11.2 集成测试(需 KMS 环境)
| 场景 | 预期 |
|------|------|
| 认证成功 | GetToken → 返回有效 token |
| 设备同步 | GetOpenerList → 返回柜体+锁孔列表 |
| 告警同步 | GetWarningList → 返回告警列表 |
| 健康检查 | HeartBeat → 200 OK |
| 认证失败 | 错误 clientId → 适配器初始化失败 |
| KMS 离线 | HealthCheck → false, 不影响 Owl/MC4 |
---
## 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) — 初版详细设计