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

17 KiB
Raw Blame History

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 分配的客户端密钥

响应:

{ "code": 200, "token": "eyJ...", "msg": "操作成功" }

4.2 第三方接口Phase 1 实现 4 个)

4.2.1 心跳 — POST /prod-api/heartBeat

请求体:{} (空 JSON)

响应:

{ "code": 200, "msg": "success" }

4.2.2 柜体钥匙列表 — POST /prod-api/getOpenerList

请求体:{}

响应(核心字段):

{
  "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

请求体:{}(所有告警)或带时间范围

响应:

{
  "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/getRecordListPhase 2 参考)

请求体:{}

响应:

{
  "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 详细设计

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 详细设计

/// <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 详细设计

/// <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

{
  "KMS": {
    "InstanceName": "main",
    "BaseUrl": "http://192.168.1.50:8080",
    "ClientId": "your_client_id",
    "ClientSecret": "your_client_secret"
  }
}

10.2 KmsConfig POCO

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 注册

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) — 初版详细设计