diff --git a/doc/设计文档/KMS钥匙柜适配器详细设计文档.md b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md new file mode 100644 index 0000000..66c408b --- /dev/null +++ b/doc/设计文档/KMS钥匙柜适配器详细设计文档.md @@ -0,0 +1,500 @@ +# 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) — 初版详细设计