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

30 KiB
Raw Blame History

KMS 钥匙柜适配器详细设计文档 v2.0

版本: 2.0(完整接口版) 日期: 2025-05-19 数据源: doc/对接文档/钥匙管理系统软件接口.docxKMS 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 统一响应格式:

{
  "code": 200,         // 200=成功, 0/其他=失败
  "msg": "操作成功",    // 消息文本
  "total": 100,        // 分页总记录数(列表接口)
  "rows": [...],       // 数据列表(列表接口)
  "data": {...}        // 单个对象(详情接口)
}

4. KmsModels 完整设计

4.1 响应模型

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 完整设计

/// <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 类定义与构造函数

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

    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

    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

    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

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

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

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

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

9.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; } = "";
}

9.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);
}

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 (完整接口版)