# 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 Rows { get; set; } = new(); } public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List LockholeList { get; set; } = new(); } public class KmsLockhole { public int LockholeSort { get; set; } public int OpenerId { get; set; } public string? OpenerName { get; set; } public string? OpenerType { get; set; } public string? OpenerState { get; set; } } public class KmsWarningListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List Rows { get; set; } = new(); } public class KmsWarning { public string? Uuid { get; set; } public string? LockerName { get; set; } public int LockholeSort { get; set; } public string? OpenerName { get; set; } public int Type { get; set; } public string? WarningTime { get; set; } public string? Remark { get; set; } public string? StaffName { get; set; } } public class KmsRecordListResponse { public int Code { get; set; } public int Total { get; set; } public List Rows { get; set; } = new(); } public class 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 /// /// KMS Bearer Token 认证辅助。 /// /// 流程: /// 1. POST /prod-api/getToken?clientId={}&clientSecret={} /// 2. 返回 { code: 200, token: "xxx" } /// 3. Token 缓存 25 分钟 (KMS 有效期 30 分钟,留 5 分钟余量) /// public class KmsAuthHelper { private readonly HttpClient _http; private readonly string _baseUrl; private readonly string _clientId; private readonly string _clientSecret; private 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(); 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 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 /// /// KMS 智能钥匙柜适配器。实现 IHasFlatDevices + IHasAlarms。 /// /// 设备模型:柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体ID)。 /// 限流:5 QPS。 /// AdapterCode: "KMS:{InstanceName}" /// 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 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> 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) { // 父设备: 柜体 devices.Add(new StandardDevice { SourceId = $"locker_{locker.LockerId}", Name = locker.LockerName ?? $"柜体{locker.LockerId}", Category = "智能钥匙柜", Group = "门禁设备", IsParent = true, IsOnline = true, Extra = new Dictionary { ["lockerCode"] = locker.LockerCode, ["lockholeCount"] = locker.LockholeList.Count } }); // 子设备: 锁孔 foreach (var hole in locker.LockholeList) { bool isOnline = hole.OpenerState == "在位"; devices.Add(new StandardDevice { SourceId = $"lockhole_{locker.LockerId}_{hole.LockholeSort}", Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}", Category = "钥匙位", Group = "门禁设备", IsParent = false, IsOnline = isOnline, ParentSourceId = $"locker_{locker.LockerId}", Extra = new Dictionary { ["openerId"] = hole.OpenerId, ["openerType"] = hole.OpenerType, ["openerState"] = hole.OpenerState } }); } } return new PagedResult { Items = devices, Total = devices.Count }; } // ── IHasAlarms → 2.18.7 告警列表 ── 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.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 { 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>() ?? 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); } ``` --- ## 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) — 初版详细设计