修复: Owl取流JSON反序列化(JsonOpts+PascalCase属性)+前端字段兼容+规则引擎SqlSugar语法
This commit is contained in:
@@ -87,6 +87,41 @@ namespace VolPro.Entity.DomainModels
|
||||
[Editable(true)]
|
||||
public int? RuleID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///数值型恢复阈值(如>28℃触发,≤26℃恢复)
|
||||
/// </summary>
|
||||
[Display(Name ="数值型恢复阈值(如>28℃触发,≤26℃恢复)")]
|
||||
[DisplayFormat(DataFormatString="18,2")]
|
||||
[Column(TypeName="decimal")]
|
||||
[Editable(true)]
|
||||
public decimal? RecoveryThreshold_Numeric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///开关型恢复阈值(如开触发,关恢复)
|
||||
/// </summary>
|
||||
[Display(Name ="开关型恢复阈值(如开触发,关恢复)")]
|
||||
[MaxLength(50)]
|
||||
[Column(TypeName="nvarchar(50)")]
|
||||
[Editable(true)]
|
||||
public string RecoveryThreshold_Switch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///该条件上次触发的时间
|
||||
/// </summary>
|
||||
[Display(Name ="该条件上次触发的时间")]
|
||||
[Column(TypeName="datetime")]
|
||||
[Editable(true)]
|
||||
public DateTime? LastTriggered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///该条件上次触发时的实际值
|
||||
/// </summary>
|
||||
[Display(Name ="该条件上次触发时的实际值")]
|
||||
[DisplayFormat(DataFormatString="18,2")]
|
||||
[Column(TypeName="decimal")]
|
||||
[Editable(true)]
|
||||
public decimal? LastTriggerValue { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace VolPro.WebApi.Controllers.Warehouse;
|
||||
|
||||
/// <summary>
|
||||
/// 文件服务。对外暴露 VolPro 文件系统中的静态文件(截图、导出等)。
|
||||
/// 不走 VolPro JWT 认证体系——网关 B 组接口直接调用。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
public class FileServiceController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取截图文件。
|
||||
/// 文件存放于 VolPro.WebApi/Download/Screenshots/ 目录。
|
||||
/// </summary>
|
||||
/// <param name="filename">文件名(含扩展名,如 abc.png)</param>
|
||||
[HttpGet("api/gateway/screenshots/{filename}")]
|
||||
public IActionResult GetScreenshot(string filename)
|
||||
{
|
||||
// 安全检查:禁止路径穿越(.., /, \)
|
||||
if (string.IsNullOrWhiteSpace(filename) ||
|
||||
filename.Contains("..") ||
|
||||
filename.Contains('/') ||
|
||||
filename.Contains('\\'))
|
||||
return BadRequest(new { error = "非法文件名" });
|
||||
|
||||
var folder = Path.Combine(AppContext.BaseDirectory, "Download", "Screenshots");
|
||||
var filePath = Path.Combine(folder, filename);
|
||||
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
return NotFound(new { error = "文件不存在" });
|
||||
|
||||
var ext = Path.GetExtension(filename).ToLowerInvariant();
|
||||
var contentType = ext switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
return PhysicalFile(filePath, contentType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
中文提示 : 检测到你没有开启文件,AllowLoadLocalInfile=true加到自符串上,已自动执行 SET GLOBAL local_infile=1 在试一次
|
||||
English Message : Loading local data is disabled; this must be enabled on both the client and server sides at SqlSugar.Check.ExceptionEasy(String enMessage, String cnMessage)
|
||||
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
|
||||
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
|
||||
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
|
||||
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
|
||||
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar
|
||||
@@ -21,6 +21,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Warehouse.Services
|
||||
{
|
||||
@@ -31,16 +32,19 @@ namespace Warehouse.Services
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly Igateway_nodesRepository _repository;
|
||||
private readonly ILogger<gateway_nodesService> _logger;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public gateway_nodesService(
|
||||
Igateway_nodesRepository dbRepository,
|
||||
IHttpContextAccessor httpContextAccessor
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<gateway_nodesService> logger
|
||||
)
|
||||
: base(dbRepository)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_repository = dbRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,39 +55,53 @@ namespace Warehouse.Services
|
||||
[Obsolete("由 A1 API Controller 自动调用,不建议手动调用")]
|
||||
public async Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl)
|
||||
{
|
||||
var existingList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode).ToListAsync();
|
||||
var existing = existingList.FirstOrDefault();
|
||||
|
||||
gateway_nodes entity;
|
||||
if (existing != null)
|
||||
_logger.LogInformation("[A1] 网关注册: NodeCode={Node}, Adapters={Adapters}", nodeCode, adapterTypes);
|
||||
try
|
||||
{
|
||||
if (existing.NodeToken != token)
|
||||
throw new UnauthorizedAccessException("NodeToken 不匹配");
|
||||
var existingList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode).ToListAsync();
|
||||
var existing = existingList.FirstOrDefault();
|
||||
|
||||
existing.AdapterTypes = adapterTypes;
|
||||
existing.BaseUrl = baseUrl;
|
||||
existing.IsOnline = "在线";
|
||||
existing.LastHeartbeat = DateTime.Now;
|
||||
_repository.DbContext.Updateable(existing).ExecuteCommand();
|
||||
entity = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new gateway_nodes
|
||||
gateway_nodes entity;
|
||||
if (existing != null)
|
||||
{
|
||||
NodeCode = nodeCode,
|
||||
NodeName = nodeCode,
|
||||
NodeToken = token,
|
||||
AdapterTypes = adapterTypes,
|
||||
BaseUrl = baseUrl,
|
||||
IsOnline = "在线",
|
||||
Enable = "启用",
|
||||
LastHeartbeat = DateTime.Now,
|
||||
CreateDate = DateTime.Now
|
||||
};
|
||||
_repository.DbContext.Insertable(entity).ExecuteCommand();
|
||||
if (existing.NodeToken != token)
|
||||
{
|
||||
_logger.LogWarning("[A1] 注册失败: NodeCode={Node} Token不匹配", nodeCode);
|
||||
throw new UnauthorizedAccessException("NodeToken 不匹配");
|
||||
}
|
||||
|
||||
existing.AdapterTypes = adapterTypes;
|
||||
existing.BaseUrl = baseUrl;
|
||||
existing.IsOnline = "在线";
|
||||
existing.LastHeartbeat = DateTime.Now;
|
||||
_repository.DbContext.Updateable(existing).ExecuteCommand();
|
||||
entity = existing;
|
||||
_logger.LogInformation("[A1] 网关注册(更新): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new gateway_nodes
|
||||
{
|
||||
NodeCode = nodeCode,
|
||||
NodeName = nodeCode,
|
||||
NodeToken = token,
|
||||
AdapterTypes = adapterTypes,
|
||||
BaseUrl = baseUrl,
|
||||
IsOnline = "在线",
|
||||
Enable = "启用",
|
||||
LastHeartbeat = DateTime.Now,
|
||||
CreateDate = DateTime.Now
|
||||
};
|
||||
_repository.DbContext.Insertable(entity).ExecuteCommand();
|
||||
_logger.LogInformation("[A1] 网关注册(新增): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[A1] 注册异常: NodeCode={Node}", nodeCode);
|
||||
throw;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,14 +110,27 @@ namespace Warehouse.Services
|
||||
[Obsolete("由 A2 API Controller 自动调用,不建议手动调用")]
|
||||
public async Task UpdateHeartbeatAsync(string nodeCode, string token)
|
||||
{
|
||||
var entityList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode && x.NodeToken == token).ToListAsync();
|
||||
var entity = entityList.FirstOrDefault();
|
||||
if (entity == null)
|
||||
throw new UnauthorizedAccessException("认证失败:NodeCode 或 Token 无效");
|
||||
try
|
||||
{
|
||||
var entityList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode && x.NodeToken == token).ToListAsync();
|
||||
var entity = entityList.FirstOrDefault();
|
||||
if (entity == null)
|
||||
{
|
||||
_logger.LogWarning("[A2] 心跳认证失败: NodeCode={Node}", nodeCode);
|
||||
throw new UnauthorizedAccessException("认证失败:NodeCode 或 Token 无效");
|
||||
}
|
||||
|
||||
entity.IsOnline = "在线";
|
||||
entity.LastHeartbeat = DateTime.Now;
|
||||
_repository.DbContext.Updateable(entity).ExecuteCommand();
|
||||
entity.IsOnline = "在线";
|
||||
entity.LastHeartbeat = DateTime.Now;
|
||||
_repository.DbContext.Updateable(entity).ExecuteCommand();
|
||||
_logger.LogDebug("[A2] 心跳更新: NodeCode={Node}", nodeCode);
|
||||
}
|
||||
catch (UnauthorizedAccessException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[A2] 心跳异常: NodeCode={Node}", nodeCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,69 +141,83 @@ namespace Warehouse.Services
|
||||
[Obsolete("由 A3 API Controller 自动调用,不建议手动调用")]
|
||||
public async Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<SyncDeviceItem> devices)
|
||||
{
|
||||
var db = _repository.DbContext;
|
||||
|
||||
var adapterCodes = devices.Select(d => d.AdapterCode).Distinct().ToList();
|
||||
var existingIds = db.Queryable<base_device>()
|
||||
.Where(x => x.NodeId == gatewayNodeId && adapterCodes.Contains(x.AdapterCode))
|
||||
.ToList()
|
||||
.ToDictionary(x => (x.AdapterCode, x.SourceId), x => x.DeviceId);
|
||||
|
||||
int added = 0, updated = 0;
|
||||
foreach (var d in devices)
|
||||
_logger.LogInformation("[A3] 设备同步开始: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
|
||||
try
|
||||
{
|
||||
var key = (d.AdapterCode, d.SourceId);
|
||||
existingIds.TryGetValue(key, out var existingId);
|
||||
bool isNew = existingId == 0;
|
||||
var db = _repository.DbContext;
|
||||
|
||||
int? parentDeviceId = null;
|
||||
if (!string.IsNullOrEmpty(d.ParentSourceId))
|
||||
{
|
||||
existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid);
|
||||
if (pid > 0) parentDeviceId = pid;
|
||||
}
|
||||
var adapterCodes = devices.Select(d => d.AdapterCode).Distinct().ToList();
|
||||
// 全局去重——不限定 NodeId,防止网关重启后 NodeId 变化导致重复插入
|
||||
var existingIds = db.Queryable<base_device>()
|
||||
.Where(x => adapterCodes.Contains(x.AdapterCode))
|
||||
.ToList()
|
||||
.ToDictionary(x => (x.AdapterCode, x.SourceId), x => x.DeviceId);
|
||||
|
||||
if (isNew)
|
||||
int added = 0, updated = 0;
|
||||
foreach (var d in devices)
|
||||
{
|
||||
var entity = new base_device
|
||||
var key = (d.AdapterCode, d.SourceId);
|
||||
existingIds.TryGetValue(key, out var existingId);
|
||||
bool isNew = existingId == 0;
|
||||
|
||||
int? parentDeviceId = null;
|
||||
if (!string.IsNullOrEmpty(d.ParentSourceId))
|
||||
{
|
||||
DeviceName = d.Name ?? $"DEV_{d.SourceId}",
|
||||
AdapterCode = d.AdapterCode,
|
||||
SourceId = d.SourceId,
|
||||
DeviceCategory = d.Category,
|
||||
DeviceGroup = d.Group,
|
||||
NodeId = gatewayNodeId,
|
||||
IsParent = d.IsParent ? "是" : "否",
|
||||
ParentDeviceId = parentDeviceId,
|
||||
IsOnline = d.IsOnline ? "在线" : "离线",
|
||||
IpAddress = d.IpAddress,
|
||||
Port = d.Port,
|
||||
ExtraData = d.ExtraDataJson,
|
||||
Enable = "启用",
|
||||
LastSyncTime = DateTime.Now,
|
||||
CreateDate = DateTime.Now
|
||||
};
|
||||
db.Insertable(entity).ExecuteCommand();
|
||||
added++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var entity = db.Queryable<base_device>().InSingle(existingId);
|
||||
if (entity != null)
|
||||
existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid);
|
||||
if (pid > 0) parentDeviceId = pid;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
entity.IsOnline = d.IsOnline ? "在线" : "离线";
|
||||
entity.IsParent = d.IsParent ? "是" : "否";
|
||||
entity.ParentDeviceId = parentDeviceId ?? entity.ParentDeviceId;
|
||||
entity.IpAddress = d.IpAddress;
|
||||
entity.Port = d.Port;
|
||||
entity.ExtraData = d.ExtraDataJson ?? entity.ExtraData;
|
||||
entity.LastSyncTime = DateTime.Now;
|
||||
db.Updateable(entity).ExecuteCommand();
|
||||
updated++;
|
||||
var entity = new base_device
|
||||
{
|
||||
DeviceName = d.Name ?? $"DEV_{d.SourceId}",
|
||||
AdapterCode = d.AdapterCode,
|
||||
SourceId = d.SourceId,
|
||||
DeviceCategory = d.Category,
|
||||
DeviceGroup = d.Group,
|
||||
NodeId = gatewayNodeId,
|
||||
IsParent = d.IsParent ? "是" : "否",
|
||||
ParentDeviceId = parentDeviceId,
|
||||
IsOnline = d.IsOnline ? "在线" : "离线",
|
||||
IpAddress = d.IpAddress,
|
||||
Port = d.Port,
|
||||
ExtraData = d.ExtraDataJson,
|
||||
Enable = "启用",
|
||||
LastSyncTime = DateTime.Now,
|
||||
CreateDate = DateTime.Now
|
||||
};
|
||||
var newId = db.Insertable(entity).ExecuteReturnIdentity();
|
||||
// 补入去重字典,同批次子设备可查到父设备
|
||||
existingIds[(d.AdapterCode, d.SourceId)] = Convert.ToInt32(newId);
|
||||
added++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var entity = db.Queryable<base_device>().InSingle(existingId);
|
||||
if (entity != null)
|
||||
{
|
||||
entity.NodeId = gatewayNodeId; // 重新归属到当前网关
|
||||
entity.IsOnline = d.IsOnline ? "在线" : "离线";
|
||||
entity.IsParent = d.IsParent ? "是" : "否";
|
||||
entity.ParentDeviceId = parentDeviceId ?? entity.ParentDeviceId;
|
||||
entity.IpAddress = d.IpAddress;
|
||||
entity.Port = d.Port;
|
||||
entity.ExtraData = d.ExtraDataJson ?? entity.ExtraData;
|
||||
entity.LastSyncTime = DateTime.Now;
|
||||
db.Updateable(entity).ExecuteCommand();
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("[A3] 设备同步完成: 新增{Added}台, 更新{Updated}台", added, updated);
|
||||
return (added, updated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[A3] 设备同步异常: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
|
||||
throw;
|
||||
}
|
||||
return (added, updated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ CREATE TABLE base_device (
|
||||
INDEX IX_Sync (AdapterCode, SourceId),
|
||||
INDEX IX_Point (PointId),
|
||||
INDEX IX_Parent (ParentDeviceId),
|
||||
INDEX IX_Gateway (GatewayNodeId),
|
||||
INDEX IX_Gateway (NodeId),
|
||||
INDEX IX_Group (DeviceGroup)
|
||||
) COMMENT '统一设备主表';
|
||||
|
||||
@@ -169,25 +169,27 @@ CREATE TABLE gateway_nodes (
|
||||
-- 规则条件/动作的 ValueId 绑定到此表的 VariableId
|
||||
-- DeviceId 关联 base_device.DeviceId
|
||||
-- =================================================
|
||||
|
||||
DROP TABLE IF EXISTS warehouse_variable;
|
||||
CREATE TABLE warehouse_variable (
|
||||
VariableId INT IDENTITY(1,1) PRIMARY KEY,
|
||||
DeviceId INT NOT NULL,
|
||||
VariableName NVARCHAR(255) NOT NULL, -- 温度/湿度/人数
|
||||
PointIndex INT DEFAULT 0, -- MC4 pointIndex / Owl 统计量编码
|
||||
Unit NVARCHAR(50) NULL, -- ℃/%/人
|
||||
SortOrder INT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IX_warehouse_variable_DeviceId ON warehouse_variable (DeviceId);
|
||||
VariableId INT AUTO_INCREMENT COMMENT '变量ID(自增主键)',
|
||||
DeviceId INT NOT NULL COMMENT '关联设备ID(base_device.DeviceId)',
|
||||
VariableName VARCHAR(255) NOT NULL COMMENT '变量名称(温度/湿度/人数等)',
|
||||
PointIndex INT DEFAULT 0 COMMENT '点位索引(MC4 pointIndex / Owl统计量编码)',
|
||||
Unit VARCHAR(50) NULL COMMENT '单位(℃/%/人)',
|
||||
SortOrder INT DEFAULT 0 COMMENT '排序顺序',
|
||||
PRIMARY KEY (VariableId),
|
||||
INDEX IX_warehouse_variable_DeviceId (DeviceId)
|
||||
) COMMENT '规则引擎变量定义表';
|
||||
|
||||
|
||||
-- F3.2 规则引擎滞后窗 (hysteresis)
|
||||
ALTER TABLE warehouse_rulecondition ADD
|
||||
RecoveryThreshold_Numeric DECIMAL(18,2) NULL,
|
||||
RecoveryThreshold_Switch NVARCHAR(50) NULL;
|
||||
-- 触发阈值与恢复阈值之间留缓冲区间,防止阈值附近反复抖动
|
||||
ALTER TABLE warehouse_rulecondition
|
||||
ADD COLUMN RecoveryThreshold_Numeric DECIMAL(18,2) NULL COMMENT '数值型恢复阈值(如>28℃触发,≤26℃恢复)',
|
||||
ADD COLUMN RecoveryThreshold_Switch VARCHAR(50) NULL COMMENT '开关型恢复阈值(如开触发,关恢复)';
|
||||
|
||||
-- F3.3 条件级冷却
|
||||
ALTER TABLE warehouse_rulecondition ADD
|
||||
LastTriggered DATETIME NULL,
|
||||
LastTriggerValue DECIMAL(18,2) NULL;
|
||||
-- F3.3 条件级冷却 (cooldown)
|
||||
-- 冷却期内条件再次命中不重复执行动作,防止告警轰炸
|
||||
ALTER TABLE warehouse_rulecondition
|
||||
ADD COLUMN LastTriggered DATETIME NULL COMMENT '该条件上次触发的时间',
|
||||
ADD COLUMN LastTriggerValue DECIMAL(18,2) NULL COMMENT '该条件上次触发时的实际值';
|
||||
|
||||
@@ -23,6 +23,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
private readonly HttpClient _http;
|
||||
private readonly KmsAuthHelper _auth;
|
||||
private readonly RateLimiter _limiter = new(5);
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
||||
private readonly ILogger<KmsAdapter> _logger;
|
||||
|
||||
/// <summary>适配器编码,格式 "KMS:{实例名}"</summary>
|
||||
@@ -62,9 +63,11 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
{
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var resp = await client.GetAsync("/prod-api/heartBeat");
|
||||
return resp.IsSuccessStatusCode;
|
||||
var ok = resp.IsSuccessStatusCode;
|
||||
_logger.LogDebug("[{Code}] 健康检查完成,状态码={Status}", AdapterCode, ok ? 200 : resp.StatusCode);
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] 健康检查失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -79,56 +82,83 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
{
|
||||
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>()!;
|
||||
_logger.LogDebug("[{Code}] POST /prod-api/getOpenerList → {Count} lockers", AdapterCode, data.Rows?.Count ?? 0);
|
||||
|
||||
var devices = new List<StandardDevice>();
|
||||
foreach (var locker in data.Rows ?? new())
|
||||
|
||||
// ① 获取锁柜列表(父设备)
|
||||
var lockerJson = await client.GetStringAsync(
|
||||
$"/prod-api/kms/locker/list?pageNum=1&pageSize=100");
|
||||
var lockerData = JsonSerializer.Deserialize<KmsLockerListResponse>(lockerJson, JsonOpts);
|
||||
_logger.LogDebug("[{Code}] 获取锁柜列表,共{Count}个", AdapterCode, lockerData?.rows?.Count ?? 0);
|
||||
|
||||
// ② 获取锁芯列表(子设备)
|
||||
var lockholeJson = await client.GetStringAsync(
|
||||
$"/prod-api/kms/lockhole/list?state=1&pageNum=1&pageSize=1000");
|
||||
var lockholeData = JsonSerializer.Deserialize<KmsLockholeListResponse>(lockholeJson, JsonOpts);
|
||||
_logger.LogDebug("[{Code}] 获取锁芯列表,共{Count}个", AdapterCode, lockholeData?.Rows?.Count ?? 0);
|
||||
|
||||
// ③ 映射:锁柜 → 父设备
|
||||
var lockerDict = new Dictionary<int, string>(); // lockerId → lockerName
|
||||
if (lockerData?.rows != null)
|
||||
{
|
||||
devices.Add(MapLockerToDevice(locker));
|
||||
if (locker.LockholeList != null)
|
||||
devices.AddRange(locker.LockholeList.Select(h => MapLockholeToDevice(h, locker.LockerId)));
|
||||
foreach (var locker in lockerData.rows)
|
||||
{
|
||||
var lockerSourceId = $"locker_{locker.id}";
|
||||
lockerDict[locker.id] = locker.name ?? $"锁柜{locker.id}";
|
||||
devices.Add(new StandardDevice
|
||||
{
|
||||
AdapterCode = AdapterCode,
|
||||
SourceId = lockerSourceId,
|
||||
Name = locker.name ?? $"锁柜{locker.id}",
|
||||
Category = "智能钥匙柜",
|
||||
Group = "门禁设备",
|
||||
IsParent = true,
|
||||
IsOnline = locker.state == 1,
|
||||
Extra = new Dictionary<string, object?>
|
||||
{
|
||||
["lockerCode"] = locker.code,
|
||||
["lockholeCount"] = locker.num ?? 0
|
||||
//["inCount"] = locker.inNum ?? 0,
|
||||
//["outCount"] = locker.outNum ?? 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ④ 映射:锁芯 → 子设备
|
||||
if (lockholeData?.Rows != null)
|
||||
{
|
||||
foreach (var hole in lockholeData.Rows)
|
||||
{
|
||||
var lockerId = hole.Locker?.Id ?? hole.LockerId;
|
||||
var lockerName = hole.Locker?.Name ?? (lockerId > 0 ? lockerDict.GetValueOrDefault(lockerId) : null);
|
||||
devices.Add(new StandardDevice
|
||||
{
|
||||
AdapterCode = AdapterCode,
|
||||
SourceId = $"lockhole_{hole.Id}",
|
||||
Name = hole.Opener?.CnName ?? $"锁芯{hole.Sort ?? hole.Id}",
|
||||
Category = "钥匙位",
|
||||
Group = "门禁设备",
|
||||
IsParent = false,
|
||||
IsOnline = hole.State == 1,
|
||||
ParentSourceId = lockerId > 0 ? $"locker_{lockerId}" : null,
|
||||
Extra = new Dictionary<string, object?>
|
||||
{
|
||||
["lockholeSort"] = hole.Sort,
|
||||
["lockerName"] = lockerName,
|
||||
["openerId"] = hole.Opener?.Id,
|
||||
["lockholeState"] = hole.State,
|
||||
["openerName"] = hole.Opener?.CnName,
|
||||
["openerNumber"] = hole.Opener?.Number
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("[{Code}] 设备映射完成,共{Total}台(父{Parent}/子{Child})",
|
||||
AdapterCode, devices.Count, devices.Count(d => d.IsParent), devices.Count(d => !d.IsParent));
|
||||
return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
|
||||
}
|
||||
|
||||
/// <summary>KMS 柜体 → StandardDevice(父设备)</summary>
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>KMS 锁孔 → StandardDevice(子设备)</summary>
|
||||
private static StandardDevice MapLockholeToDevice(KmsLockhole hole, int lockerId) => new()
|
||||
{
|
||||
SourceId = $"lockhole_{lockerId}_{hole.LockholeSort}",
|
||||
Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}",
|
||||
Category = "钥匙位",
|
||||
Group = "门禁设备",
|
||||
IsParent = false,
|
||||
// KMS openerState: 1=在柜, 2=借出, 3=录入, 10=丢失 (数值编码或中文)
|
||||
IsOnline = hole.OpenerState == "1" || hole.OpenerState == "在柜", // KMS: 1=在柜/2=借出/3=录入/10=丢失
|
||||
ParentSourceId = $"locker_{lockerId}",
|
||||
Extra = new Dictionary<string, object?>
|
||||
{
|
||||
["openerId"] = hole.OpenerId,
|
||||
["openerType"] = hole.OpenerType,
|
||||
["openerState"] = hole.OpenerState
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// IHasAlarms — 告警(2.18.7 告警列表)
|
||||
@@ -151,7 +181,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var data = await resp.Content.ReadFromJsonAsync<KmsWarningListResponse>()!;
|
||||
_logger.LogDebug("[{Code}] POST /prod-api/getWarningList → {Count} warnings", AdapterCode, data.Rows?.Count ?? 0);
|
||||
_logger.LogDebug("[{Code}] 获取告警列表,共{Count}条", AdapterCode, data.Rows?.Count ?? 0);
|
||||
|
||||
var alarms = (data.Rows ?? new()).Select(w => new StandardAlarm
|
||||
{
|
||||
@@ -212,7 +242,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var data = await resp.Content.ReadFromJsonAsync<KmsRecordListResponse>()!;
|
||||
_logger.LogDebug("[{Code}] POST /prod-api/getRecordList → {Count} records", AdapterCode, data.Rows?.Count ?? 0);
|
||||
_logger.LogDebug("[{Code}] 获取借还记录,共{Count}条", AdapterCode, data.Rows?.Count ?? 0);
|
||||
return new PagedResult<KmsRecord> { Items = data.Rows ?? new(), Total = data.Total };
|
||||
}
|
||||
|
||||
@@ -240,7 +270,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var data = await resp.Content.ReadFromJsonAsync<KmsPermissionListResponse>()!;
|
||||
_logger.LogDebug("[{Code}] POST /prod-api/getPermissionList → {Count} permissions", AdapterCode, data.Rows?.Count ?? 0);
|
||||
_logger.LogDebug("[{Code}] 获取授权记录,共{Count}条", AdapterCode, data.Rows?.Count ?? 0);
|
||||
return new PagedResult<KmsPermission> { Items = data.Rows ?? new(), Total = data.Total };
|
||||
}
|
||||
|
||||
@@ -261,6 +291,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var resp = await client.PostAsJsonAsync("/prod-api/batchDeleteStaff", staffUuids);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
_logger.LogDebug("[{Code}] 批量删除员工({Count}人),状态={Status}", AdapterCode, staffUuids.Count, resp.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>2.4.3 远程授权开门</summary>
|
||||
@@ -278,6 +309,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var resp = await client.PostAsync($"/thirdPlatlogin?username={Uri.EscapeDataString(username)}", null);
|
||||
_logger.LogDebug("[{Code}] 第三方登录({User}),状态={Status}", AdapterCode, username, resp.StatusCode);
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Redirect)
|
||||
return resp.Headers.Location?.ToString();
|
||||
resp.EnsureSuccessStatusCode();
|
||||
@@ -305,10 +337,12 @@ OpenerIds = parameters.TryGetValue("lockholeSort", out var lh) ? new List<int> {
|
||||
};
|
||||
await RemoteAuthorizeAsync(req);
|
||||
}
|
||||
_logger.LogDebug("[{Code}] 设备控制完成 {Id} cmd={Cmd}", AdapterCode, sourceDeviceId, command);
|
||||
return new ControlResult { Success = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug("[{Code}] 设备控制失败: {Msg}", AdapterCode, ex.Message);
|
||||
return new ControlResult { Success = false, Message = ex.Message };
|
||||
}
|
||||
}
|
||||
@@ -367,7 +401,7 @@ OpenerIds = parameters.TryGetValue("lockholeSort", out var lh) ? new List<int> {
|
||||
await BatchSyncStaffAsync(staffList);
|
||||
return new SyncResult { SuccessCount = staffList.Count };
|
||||
}
|
||||
catch (Exception ex) { return new SyncResult { FailCount = items.Count, Message = ex.Message }; }
|
||||
catch (Exception ex) { _logger.LogDebug("[{Code}] 员工数据同步失败: {Msg}", AdapterCode, ex.Message); return new SyncResult { FailCount = items.Count, Message = ex.Message }; }
|
||||
}
|
||||
|
||||
/// <summary>从 KMS 批量删除数据</summary>
|
||||
@@ -379,6 +413,48 @@ OpenerIds = parameters.TryGetValue("lockholeSort", out var lh) ? new List<int> {
|
||||
await BatchDeleteStaffAsync(ids);
|
||||
return new SyncResult { SuccessCount = ids.Count };
|
||||
}
|
||||
catch (Exception ex) { return new SyncResult { FailCount = ids.Count, Message = ex.Message }; }
|
||||
catch (Exception ex) { _logger.LogDebug("[{Code}] 员工数据删除失败: {Msg}", AdapterCode, ex.Message); return new SyncResult { FailCount = ids.Count, Message = ex.Message }; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 标准管理接口 — 详情查询(暂不实现增删改)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>获取锁柜详情 (2.16.5)</summary>
|
||||
public async Task<KmsLockerInfo?> GetLockerDetailAsync(int lockerId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var json = await client.GetStringAsync($"/prod-api/kms/locker/{lockerId}");
|
||||
return JsonSerializer.Deserialize<KmsLockerInfo>(json);
|
||||
}
|
||||
|
||||
/// <summary>获取锁芯详情 (2.17.4)</summary>
|
||||
public async Task<KmsLockholeDetail?> GetLockholeDetailAsync(int lockholeId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var json = await client.GetStringAsync($"/prod-api/kms/lockhole/{lockholeId}");
|
||||
return JsonSerializer.Deserialize<KmsLockholeDetail>(json);
|
||||
}
|
||||
|
||||
/// <summary>获取钥匙详情 (2.14.8)</summary>
|
||||
public async Task<KmsOpenerInfo?> GetOpenerDetailAsync(int openerId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var json = await client.GetStringAsync($"/prod-api/kms/opener/{openerId}");
|
||||
return JsonSerializer.Deserialize<KmsOpenerInfo>(json);
|
||||
}
|
||||
|
||||
/// <summary>获取锁芯列表,按锁柜ID过滤 (2.17.2)</summary>
|
||||
public async Task<List<KmsLockholeInfo>> GetLockholesByLockerAsync(int lockerId)
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var json = await client.GetStringAsync(
|
||||
$"/prod-api/kms/lockhole/list?state=1&pageNum=1&pageSize=1000");
|
||||
var data = JsonSerializer.Deserialize<KmsLockholeListResponse>(json);
|
||||
return data?.Rows?.Where(h => (h.Locker?.Id ?? h.LockerId) == lockerId).ToList() ?? new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace IntegrationGateway.Adapters.Kms;
|
||||
@@ -15,6 +16,7 @@ public class KmsAuthHelper
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _clientId;
|
||||
private readonly string _clientSecret;
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
||||
private readonly ILogger _logger;
|
||||
private string? _token;
|
||||
private DateTime _tokenExpiry = DateTime.MinValue;
|
||||
@@ -33,6 +35,7 @@ public class KmsAuthHelper
|
||||
_clientId = clientId;
|
||||
_clientSecret = clientSecret;
|
||||
_logger = logger;
|
||||
_http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "SecMPS-Gateway/1.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,23 +44,31 @@ public class KmsAuthHelper
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
|
||||
{
|
||||
_logger?.LogDebug("KMS Token缓存命中(过期时间={Exp})", _tokenExpiry);
|
||||
return _token;
|
||||
}
|
||||
|
||||
var url = $"{_baseUrl}/prod-api/getToken?clientId={Uri.EscapeDataString(_clientId)}&clientSecret={Uri.EscapeDataString(_clientSecret)}";
|
||||
var resp = await _http.PostAsync(url, null);
|
||||
var json = JsonSerializer.Serialize(new { clientId = _clientId, clientSecret = _clientSecret });
|
||||
var content = new ByteArrayContent(Encoding.UTF8.GetBytes(json));
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
_logger?.LogDebug("KMS 开始获取Token(clientId={Cid})", _clientId);
|
||||
var resp = await _http.PostAsync($"{_baseUrl}/prod-api/getToken", content);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
_logger?.LogDebug("KMS Token请求完成,状态={Status}", resp.StatusCode);
|
||||
|
||||
var result = await resp.Content.ReadFromJsonAsync<KmsTokenResponse>()
|
||||
?? throw new Exception("KMS Token 响应为空");
|
||||
if (result.Code != 200)
|
||||
throw new Exception($"KMS 认证失败: code={result.Code}");
|
||||
if (result.code != 200)
|
||||
throw new Exception($"KMS 认证失败: code={result.code}, msg={result.msg}");
|
||||
if (string.IsNullOrEmpty(result.data?.token))
|
||||
throw new Exception("KMS Token 响应中 data.token 为空");
|
||||
|
||||
_token = result.Token;
|
||||
_token = result.data.token;
|
||||
_tokenExpiry = DateTime.UtcNow.AddMinutes(25);
|
||||
_logger?.LogDebug("KMS token obtained, expires in 25min");
|
||||
_logger?.LogDebug("KMS Token获取成功,25分钟有效");
|
||||
return _token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个已认证的 HttpClient,自动附带 Authorization: Bearer 头。
|
||||
/// </summary>
|
||||
@@ -66,6 +77,7 @@ public class KmsAuthHelper
|
||||
var token = await GetTokenAsync();
|
||||
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "SecMPS-Gateway/1.0");
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,15 @@ namespace IntegrationGateway.Adapters.Kms;
|
||||
/// <summary>POST /prod-api/getToken 响应</summary>
|
||||
public class KmsTokenResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Token { get; set; } = "";
|
||||
public string? Msg { get; set; }
|
||||
public int code { get; set; }
|
||||
public string? msg { get; set; }
|
||||
public KmsTokenData? data { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Token 数据嵌套对象</summary>
|
||||
public class KmsTokenData
|
||||
{
|
||||
public string token { get; set; } = "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -158,19 +164,29 @@ public class KmsStaff
|
||||
/// <summary>KMS 柜体列表响应</summary>
|
||||
public class KmsLockerListResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public int Total { get; set; }
|
||||
public List<KmsLockerInfo>? Rows { get; set; }
|
||||
public int code { get; set; }
|
||||
public string? msg { get; set; }
|
||||
public int total { get; set; }
|
||||
public List<KmsLockerInfo>? rows { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>KMS 柜体详细信息</summary>
|
||||
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 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 string? remark { get; set; }
|
||||
public string? createBy { get; set; }
|
||||
public string? createTime { get; set; }
|
||||
public string? updateBy { get; set; }
|
||||
public string? updateTime { get; set; }
|
||||
public object? openers { get; set; }
|
||||
public int? num { get; set; }
|
||||
public int? InNum { get; set; }
|
||||
public int? OutNum { get; set; }
|
||||
public List<KmsLockhole>? LockholeList { get; set; }
|
||||
}
|
||||
|
||||
@@ -178,6 +194,7 @@ public class KmsLockerInfo
|
||||
public class KmsLockholeListResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string? Msg { get; set; }
|
||||
public int Total { get; set; }
|
||||
public List<KmsLockholeInfo>? Rows { get; set; }
|
||||
}
|
||||
@@ -187,9 +204,64 @@ public class KmsLockholeInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int LockerId { get; set; }
|
||||
public int LockholeSort { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public int State { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
public string? CreateBy { get; set; }
|
||||
public string? CreateTime { get; set; }
|
||||
public string? UpdateBy { get; set; }
|
||||
public string? UpdateTime { get; set; }
|
||||
public KmsLockerBrief? Locker { get; set; }
|
||||
public KmsLockholeOpener? Opener { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>锁芯列表项中嵌套的钥匙信息</summary>
|
||||
public class KmsLockholeOpener
|
||||
{
|
||||
public long? Id { get; set; }
|
||||
public int LockerId { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public long? LockholeId { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public long? OpenerGroupId { get; set; }
|
||||
public string? CnName { get; set; }
|
||||
public string? CnNamePy { get; set; }
|
||||
public string? Number { get; set; }
|
||||
public int? Type { get; set; }
|
||||
public int? State { get; set; }
|
||||
public int? WarnInterval { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
public string? CreateBy { get; set; }
|
||||
public string? CreateTime { get; set; }
|
||||
public string? UpdateBy { get; set; }
|
||||
public string? UpdateTime { get; set; }
|
||||
public string? DetailImgUrl { get; set; }
|
||||
public int? DeptId { get; set; }
|
||||
public string? LendStaffName { get; set; }
|
||||
public string? BorrowTime { get; set; }
|
||||
public string? BackStaffName { get; set; }
|
||||
public string? BackTime { get; set; }
|
||||
}
|
||||
|
||||
public class KmsLockerBrief
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class KmsLockholeDetail
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int LockerId { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public int State { get; set; }
|
||||
public int? OpenerId { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
public string? CreateTime { get; set; }
|
||||
public string? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>KMS 钥匙列表响应</summary>
|
||||
|
||||
@@ -58,9 +58,11 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
{
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var resp = await client.PostAsync("/api/central/auth/conf/get", null);
|
||||
return resp.IsSuccessStatusCode;
|
||||
var ok = resp.IsSuccessStatusCode;
|
||||
_logger.LogDebug("[{Code}] 健康检查完成,状态码={Status}", AdapterCode, ok ? 200 : resp.StatusCode);
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] 健康检查失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -79,7 +81,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
var tree = JsonSerializer.Deserialize<List<Mc4TreeNode>>(json)!;
|
||||
_logger.LogDebug("[{Code}] POST /api/central/object/tree → {Len} bytes, {Count} nodes", AdapterCode, json.Length, tree.Count);
|
||||
_logger.LogDebug("[{Code}] 获取对象树,响应{Sz}字节,{Ct}个节点", AdapterCode, json.Length, tree.Count);
|
||||
return tree.Select(MapNode).ToList();
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(json)!;
|
||||
_logger.LogDebug("[{Code}] GET device/{Id} → {Len} bytes, {Count} points", AdapterCode, sourceDeviceId, json.Length, values.Count);
|
||||
_logger.LogDebug("[{Code}] 获取实时点位({Id}),响应{Sz}字节,{Ct}个点位", AdapterCode, sourceDeviceId, json.Length, values.Count);
|
||||
return values.Select(v => new PointValue
|
||||
{
|
||||
SourceDeviceId = sourceDeviceId,
|
||||
@@ -128,8 +130,9 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var body = JsonSerializer.Serialize(new { id = int.Parse(sourceDeviceId), index = pointIndex, value });
|
||||
await client.PostAsync("/api/central/point/value/set",
|
||||
var resp = await client.PostAsync("/api/central/point/value/set",
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
_logger.LogDebug("[{Code}] 设备控制写入 {Id}[{Idx}]={Val},状态={Status}", AdapterCode, sourceDeviceId, pointIndex, value, resp.StatusCode);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -158,7 +161,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<Mc4AlarmQueryResult>(json)!;
|
||||
_logger.LogDebug("[{Code}] POST /api/central/alarm/query → {Len} bytes, {Count} alarms", AdapterCode, json.Length, result.List?.Count ?? 0);
|
||||
_logger.LogDebug("[{Code}] 获取当前告警,响应{Sz}字节,{Ct}条", AdapterCode, json.Length, result.List?.Count ?? 0);
|
||||
return new PagedResult<StandardAlarm>
|
||||
{
|
||||
Items = result.List?.Select(a => new StandardAlarm
|
||||
@@ -185,6 +188,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
|
||||
var cresp = await client.PostAsync("/api/central/alarm/confirm",
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
_logger.LogDebug("[{Code}] 确认告警({Id}),状态={Status}", AdapterCode, alarmId, cresp.StatusCode);
|
||||
cresp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
@@ -196,6 +200,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
|
||||
var eresp = await client.PostAsync("/api/central/alarm/end",
|
||||
new StringContent(body, Encoding.UTF8, "application/json"));
|
||||
_logger.LogDebug("[{Code}] 结束告警({Id}),状态={Status}", AdapterCode, alarmId, eresp.StatusCode);
|
||||
eresp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
@@ -250,7 +255,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<Mc4AlarmQueryResult>(json)!;
|
||||
_logger.LogDebug("[{Code}] POST /api/central/alarm/query → {Len} bytes, {Count} alarms", AdapterCode, json.Length, result.List?.Count ?? 0);
|
||||
_logger.LogDebug("[{Code}] 获取当前告警,响应{Sz}字节,{Ct}条", AdapterCode, json.Length, result.List?.Count ?? 0);
|
||||
return new PagedResult<StandardAlarm>
|
||||
{
|
||||
Items = (result.List ?? new()).Select(MapAlarmItem).ToList(),
|
||||
|
||||
@@ -24,7 +24,7 @@ public class Mc4AuthHelper
|
||||
private readonly ILogger _logger;
|
||||
private string? _token;
|
||||
private DateTime _tokenExpiry = DateTime.MinValue;
|
||||
private bool? _needMd5;
|
||||
private bool? _needMd5 = false;
|
||||
|
||||
public Mc4AuthHelper(HttpClient http, string baseUrl, string account = "admin", string password = "admin", ILogger logger = null!)
|
||||
{
|
||||
@@ -38,7 +38,10 @@ public class Mc4AuthHelper
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
|
||||
{
|
||||
_logger?.LogDebug("MC4 Token缓存命中(过期时间={Exp})", _tokenExpiry);
|
||||
return _token;
|
||||
}
|
||||
|
||||
// 1. 获取加密配置
|
||||
if (!_needMd5.HasValue)
|
||||
@@ -51,14 +54,16 @@ public class Mc4AuthHelper
|
||||
var confJson = await confResp.Content.ReadAsStringAsync();
|
||||
var conf = JsonSerializer.Deserialize<Mc4ConfResponse>(confJson);
|
||||
_needMd5 = conf?.Encrypt ?? false;
|
||||
_logger?.LogDebug("MC4 加密配置: encrypt={Enc}", _needMd5);
|
||||
}
|
||||
else { _needMd5 = false; }
|
||||
}
|
||||
catch { _needMd5 = false; }
|
||||
catch (Exception ex) { _needMd5 = false; _logger?.LogDebug("MC4 加密配置查询失败,MD5已关闭: {Msg}", ex.Message); }
|
||||
}
|
||||
|
||||
// 2. 登录获取 Token
|
||||
var pwd = _needMd5 == true ? ComputeMd5(_password) : _password;
|
||||
_logger?.LogDebug("MC4 开始登录: 账号={Acct}, MD5={Md5}", _account, _needMd5);
|
||||
var loginBody = JsonSerializer.Serialize(new { account = _account, password = pwd });
|
||||
var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/login",
|
||||
new StringContent(loginBody, Encoding.UTF8, "application/json"));
|
||||
@@ -70,7 +75,7 @@ public class Mc4AuthHelper
|
||||
throw new Exception("MC4 登录失败: Token 为空");
|
||||
_token = result.Token;
|
||||
_tokenExpiry = DateTime.UtcNow.AddHours(7);
|
||||
_logger?.LogDebug("MC4 token obtained, expires in 7h");
|
||||
_logger?.LogDebug("MC4 登录成功(账号={Acct})", _account);
|
||||
return _token;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
private readonly HttpClient _http;
|
||||
private readonly OwlAuthHelper _auth;
|
||||
private readonly RateLimiter _limiter = new(5);
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
||||
private readonly ILogger<OwlAdapter> _logger;
|
||||
|
||||
public string AdapterCode { get; }
|
||||
@@ -55,9 +56,11 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
{
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var resp = await client.GetAsync("/health");
|
||||
return resp.IsSuccessStatusCode;
|
||||
bool ok = resp.IsSuccessStatusCode;
|
||||
_logger.LogDebug("[{Code}] 健康检查完成,状态码={Status}", AdapterCode, ok ? 200 : resp.StatusCode);
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] 健康检查失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -71,60 +74,68 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var url = $"/devices/channels?page={page}&size={size}";
|
||||
if (!string.IsNullOrEmpty(keyword)) url += $"&key={Uri.EscapeDataString(keyword)}";
|
||||
var json = await client.GetStringAsync(url);
|
||||
_logger.LogDebug("[{Code}] GET {Url} → {Len} bytes", AdapterCode, url, json.Length);
|
||||
var result = JsonSerializer.Deserialize<OwlPagedResult<OwlDeviceChannel>>(json)!;
|
||||
_logger.LogDebug("[{Code}] 获取设备列表({Url}),响应{Size}字节", AdapterCode, url, json.Length);
|
||||
var result = JsonSerializer.Deserialize<OwlDevicesResponse>(json, JsonOpts)!;
|
||||
|
||||
var devices = new List<StandardDevice>();
|
||||
var deviceItems = result.Items.Where(x => x.Type == "DEVICE").ToList();
|
||||
var channelItems = result.Items.Where(x => x.Type == "CHANNEL").ToList();
|
||||
|
||||
foreach (var d in deviceItems)
|
||||
foreach (var d in result.Items)
|
||||
{
|
||||
var childChannels = channelItems.Where(c => c.Did == d.Id).ToList();
|
||||
devices.Add(MapDevice(d, childChannels));
|
||||
foreach (var ch in childChannels)
|
||||
devices.Add(MapChannel(ch, d.Id));
|
||||
var dev = MapDevice(d);
|
||||
dev.AdapterCode = AdapterCode;
|
||||
devices.Add(dev);
|
||||
|
||||
if (d.Children != null)
|
||||
{
|
||||
foreach (var ch in d.Children)
|
||||
{
|
||||
var chDev = MapChannel(ch, d.Id);
|
||||
chDev.AdapterCode = AdapterCode;
|
||||
devices.Add(chDev);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
|
||||
}
|
||||
|
||||
private static StandardDevice MapDevice(OwlDeviceChannel d, List<OwlDeviceChannel> channels) => new()
|
||||
private StandardDevice MapDevice(OwlDeviceItem d) => new()
|
||||
{
|
||||
SourceId = d.Id ?? "",
|
||||
Name = d.Name ?? d.Id ?? "",
|
||||
SourceId = d.Id,
|
||||
Name = d.Name,
|
||||
Category = "硬盘录像机",
|
||||
Group = "视频设备",
|
||||
IsOnline = d.IsOnline == "1",
|
||||
IsOnline = d.IsOnline,
|
||||
IsParent = true,
|
||||
IpAddress = d.Address,
|
||||
Port = int.TryParse(d.Port, out var p) ? p : null,
|
||||
Port = d.Port > 0 ? d.Port : null,
|
||||
Extra = new Dictionary<string, object?>
|
||||
{
|
||||
["manufacturer"] = d.Manufacturer,
|
||||
["model"] = d.Model,
|
||||
["firmware"] = d.Firmware,
|
||||
["longitude"] = d.Longitude,
|
||||
["latitude"] = d.Latitude,
|
||||
["protocol"] = d.Protocol ?? "GB28181",
|
||||
["manufacturer"] = d.Ext?.Manufacturer,
|
||||
["model"] = d.Ext?.Model,
|
||||
["firmware"] = d.Ext?.Firmware,
|
||||
["protocol"] = "GB28181",
|
||||
["transport"] = d.Transport,
|
||||
["channelCount"] = d.ChannelCount ?? channels.Count
|
||||
["channelCount"] = d.Children?.Count ?? d.Channels,
|
||||
["deviceId"] = d.DeviceId,
|
||||
["registeredAt"] = d.RegisteredAt
|
||||
}
|
||||
};
|
||||
|
||||
private static StandardDevice MapChannel(OwlDeviceChannel ch, string? parentDeviceId) => new()
|
||||
private StandardDevice MapChannel(OwlChannelItem ch, string parentDeviceId) => new()
|
||||
{
|
||||
SourceId = ch.Id ?? "",
|
||||
Name = ch.Name ?? $"通道{ch.Id}",
|
||||
SourceId = ch.Id,
|
||||
Name = ch.Name,
|
||||
Category = "摄像机",
|
||||
Group = "视频设备",
|
||||
IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",
|
||||
IsOnline = ch.IsOnline,
|
||||
IsParent = false,
|
||||
ParentSourceId = parentDeviceId,
|
||||
Extra = new Dictionary<string, object?>
|
||||
{
|
||||
["hasPtz"] = (ch.Ptztype ?? 0) > 0 ? "1" : "0",
|
||||
["hasPtz"] = ch.Ptztype > 0 ? "1" : "0",
|
||||
["app"] = ch.App,
|
||||
["streamId"] = ch.StreamId
|
||||
["streamId"] = ch.Stream,
|
||||
["channelId"] = ch.ChannelId,
|
||||
["hasRecording"] = ch.HasRecording ? "1" : "0"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,8 +150,8 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var resp = await client.PostAsync($"/channels/{channelId}/play", null);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
_logger.LogDebug("[{Code}] POST /channels/{Id}/play → {Len} bytes", AdapterCode, channelId, json.Length);
|
||||
var play = JsonSerializer.Deserialize<OwlPlayResponse>(json)!;
|
||||
_logger.LogDebug("[{Code}] 获取实时流地址({Id}),响应{Sz}字节", AdapterCode, channelId, json.Length);
|
||||
var play = JsonSerializer.Deserialize<OwlPlayResponse>(json, JsonOpts)!;
|
||||
return MapStreamUrls(play);
|
||||
}
|
||||
|
||||
@@ -152,6 +163,8 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
|
||||
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
|
||||
var baseUrl = (client.BaseAddress?.ToString() ?? "").TrimEnd('/');
|
||||
var playbackUrl = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token=***";
|
||||
_logger.LogDebug("[{Code}] 获取回放地址: {Url}", AdapterCode, playbackUrl);
|
||||
return new StreamUrls
|
||||
{
|
||||
Hls = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
|
||||
@@ -162,6 +175,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
_logger.LogDebug("[{Code}] 云台控制({Ch}): 方向={Dir}, 速度={Spd}", AdapterCode, channelId, direction, speed);
|
||||
if (direction.StartsWith("preset_"))
|
||||
{
|
||||
var idx = int.Parse(direction.Replace("preset_", ""));
|
||||
@@ -178,6 +192,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
{
|
||||
await _limiter.WaitAsync();
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
_logger.LogDebug("[{Code}] 云台停止({Ch})", AdapterCode, channelId);
|
||||
await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control", new { action = "stop" });
|
||||
}
|
||||
|
||||
@@ -188,6 +203,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var resp = await client.PostAsync($"/channels/{channelId}/snapshot",
|
||||
new StringContent("{}", System.Text.Encoding.UTF8, "application/json"));
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
_logger.LogDebug("[{Code}] 实时截图({Ch}),响应{Sz}字节", AdapterCode, channelId, json.Length);
|
||||
var snap = JsonSerializer.Deserialize<OwlSnapshotResponse>(json)!;
|
||||
return new StreamUrls { Hls = snap.Link };
|
||||
}
|
||||
@@ -205,6 +221,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
|
||||
var json = await client.GetStringAsync(
|
||||
$"/recordings?cid={channelId}&start_ms={startMs}&end_ms={endMs}&page={page}&size={size}");
|
||||
_logger.LogDebug("[{Code}] 录像查询({Ch}, {S}-{E}),响应{Sz}字节", AdapterCode, channelId, start, end, json.Length);
|
||||
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlRecording>>(json)!;
|
||||
return new PagedResult<StandardRecording>
|
||||
{
|
||||
@@ -227,6 +244,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var client = await _auth.GetAuthenticatedClientAsync();
|
||||
var body = new Dictionary<string, object>();
|
||||
if (changes.Name != null) body["name"] = changes.Name;
|
||||
_logger.LogDebug("[{Code}] 元数据推送({Id}): 名称={Name}", AdapterCode, sourceDeviceId, changes.Name);
|
||||
await client.PutAsJsonAsync($"/devices/{sourceDeviceId}", body);
|
||||
return new MetadataPushResult { Success = true };
|
||||
}
|
||||
@@ -245,6 +263,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var json = await client.GetStringAsync(
|
||||
$"/events?page={page}&size={size}&start_ms={fromMs}&end_ms={toMs}");
|
||||
var result = JsonSerializer.Deserialize<OwlPagedResult<OwlAiEvent>>(json)!;
|
||||
_logger.LogDebug("[{Code}] AI事件查询(第{Pg}页,每页{Sz}条),响应{Bl}字节,共{Ct}条", AdapterCode, page, size, json.Length, result.Items?.Count ?? 0);
|
||||
return new PagedResult<StandardAlarm>
|
||||
{
|
||||
Items = result.Items.Select(MapEventToAlarm).ToList(),
|
||||
@@ -281,12 +300,15 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
switch (command)
|
||||
{
|
||||
case "ai-enable":
|
||||
_logger.LogDebug("[{Code}] 设备控制 {Id}: 启用AI检测", AdapterCode, sourceDeviceId);
|
||||
await client.PostAsync($"/channels/{sourceDeviceId}/ai/enable", null);
|
||||
break;
|
||||
case "ai-disable":
|
||||
_logger.LogDebug("[{Code}] 设备控制 {Id}: 禁用AI检测", AdapterCode, sourceDeviceId);
|
||||
await client.PostAsync($"/channels/{sourceDeviceId}/ai/disable", null);
|
||||
break;
|
||||
case "zone-add":
|
||||
_logger.LogDebug("[{Code}] 设备控制 {Id}: 添加AI检测区域", AdapterCode, sourceDeviceId);
|
||||
await client.PostAsJsonAsync($"/channels/{sourceDeviceId}/zones", parameters!);
|
||||
break;
|
||||
default:
|
||||
@@ -294,7 +316,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
}
|
||||
return new ControlResult { Success = true };
|
||||
}
|
||||
catch (Exception ex) { return new ControlResult { Success = false, Message = ex.Message }; }
|
||||
catch (Exception ex) { _logger.LogDebug("[{Code}] 设备控制失败: {Msg}", AdapterCode, ex.Message); return new ControlResult { Success = false, Message = ex.Message }; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
@@ -46,28 +46,36 @@ public class OwlAuthHelper
|
||||
/// </summary>
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
|
||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
|
||||
{
|
||||
_logger.LogDebug("Owl Token缓存命中(过期时间={Exp})", _tokenExpiry);
|
||||
return _token;
|
||||
}
|
||||
|
||||
// 第一步:获取 RSA 公钥
|
||||
var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key");
|
||||
_logger.LogDebug("Owl 获取登录公钥,响应{Len}字节", keyResp.Length);
|
||||
var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(keyResp);
|
||||
var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!));
|
||||
var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.key!));
|
||||
_logger.LogDebug("Owl RSA公钥解码成功({Len}字符)", publicKey.Length);
|
||||
|
||||
// 第二步:RSA 加密凭据
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKey);
|
||||
var plain = JsonSerializer.Serialize(new { username = _username, password = _password });
|
||||
var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1);
|
||||
var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.OaepSHA256);
|
||||
var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) });
|
||||
_logger.LogDebug("Owl 凭据RSA-OAEP加密完成({Len}字节)", encrypted.Length);
|
||||
|
||||
// 第三步:登录换取 Token
|
||||
var resp = await _http.PostAsync($"{_baseUrl}/login",
|
||||
new StringContent(payload, Encoding.UTF8, "application/json"));
|
||||
_logger.LogDebug("Owl 登录请求完成,状态={Status}", resp.StatusCode);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var loginResult = await resp.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
_token = loginResult!.Token;
|
||||
_tokenExpiry = DateTime.UtcNow.AddDays(2.5);
|
||||
_logger.LogDebug("Owl token obtained, expires in 2.5 days"); // 保守设置,Owl 默认 3 天
|
||||
_logger.LogDebug("Owl Token获取成功(用户={User}, 2.5天有效)", loginResult.User ?? _username); // 保守设置,Owl 默认 3 天
|
||||
return _token;
|
||||
}
|
||||
|
||||
@@ -87,7 +95,7 @@ public class OwlAuthHelper
|
||||
}
|
||||
|
||||
/// <summary>登录密钥响应</summary>
|
||||
public class LoginKeyResponse { public string? Key { get; set; } }
|
||||
public class LoginKeyResponse { public string? key { get; set; } }
|
||||
/// <summary>登录响应</summary>
|
||||
public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace IntegrationGateway.Adapters.Owl;
|
||||
// 通用
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl API 分页响应</summary>
|
||||
/// <summary>Owl API 通用分页响应(录像/AI事件用)</summary>
|
||||
public class OwlPagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
@@ -16,50 +16,115 @@ public class OwlPagedResult<T>
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 设备+通道联合模型 (GET /devices/channels)
|
||||
// 设备+通道联合接口 (GET /devices/channels) — 2026-06-07 实际返回结构
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 设备或通道(联合接口返回)</summary>
|
||||
public class OwlDeviceChannel
|
||||
/// <summary>/devices/channels 响应</summary>
|
||||
public class OwlDevicesResponse
|
||||
{
|
||||
public List<OwlDeviceItem> Items { get; set; } = new();
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>设备项(含子通道列表)</summary>
|
||||
public class OwlDeviceItem
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public string DeviceId { get; set; } = ""; // device_id
|
||||
public string Name { get; set; } = "";
|
||||
public string Transport { get; set; } = "";
|
||||
public int StreamMode { get; set; } // stream_mode
|
||||
public string? Ip { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool IsOnline { get; set; } // is_online — bool 非 string!
|
||||
public string? RegisteredAt { get; set; }
|
||||
public string? KeepaliveAt { get; set; }
|
||||
public int Keepalives { get; set; }
|
||||
public int Expires { get; set; }
|
||||
public int Channels { get; set; }
|
||||
public string? CreatedAt { get; set; }
|
||||
public string? UpdatedAt { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public OwlDeviceExt? Ext { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public List<OwlChannelItem>? Children { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>通道项(嵌套在设备 children 数组中)</summary>
|
||||
public class OwlChannelItem
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Did { get; set; } = "";
|
||||
public string DeviceId { get; set; } = ""; // device_id
|
||||
public string ChannelId { get; set; } = ""; // channel_id
|
||||
public string Name { get; set; } = "";
|
||||
public int Ptztype { get; set; }
|
||||
public bool IsOnline { get; set; }
|
||||
public bool IsPlaying { get; set; } // is_playing
|
||||
public OwlDeviceExt? Ext { get; set; }
|
||||
public string? CreatedAt { get; set; }
|
||||
public string? UpdatedAt { get; set; }
|
||||
public string Type { get; set; } = "";
|
||||
public string? App { get; set; }
|
||||
public string? Stream { get; set; }
|
||||
public OwlChannelConfig? Config { get; set; }
|
||||
public bool HasRecording { get; set; } // has_recording
|
||||
}
|
||||
|
||||
/// <summary>设备/通道扩展信息(嵌套在 ext 字段中)</summary>
|
||||
public class OwlDeviceExt
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; } // "DEVICE" | "CHANNEL"
|
||||
public string? Did { get; set; } // 通道所属设备 ID
|
||||
public string? Name { get; set; }
|
||||
public string? IsOnline { get; set; } // DEVICE: "1"/"0", CHANNEL: true/false
|
||||
public string? Manufacturer { get; set; }
|
||||
public string? Model { get; set; }
|
||||
public string? Firmware { get; set; }
|
||||
public string? Longitude { get; set; }
|
||||
public string? Latitude { get; set; }
|
||||
public int? ChannelCount { get; set; }
|
||||
public int? Ptztype { get; set; } // 0=无云台, 1=方向, 2=预置位
|
||||
public string? Protocol { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? Port { get; set; }
|
||||
public string? Transport { get; set; }
|
||||
public string? App { get; set; } // 流应用名
|
||||
public string? StreamId { get; set; } // 流ID
|
||||
public string? Status { get; set; }
|
||||
public string? RegisterWay { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? GbVersion { get; set; } // gb_version
|
||||
public object? Zones { get; set; }
|
||||
public bool EnabledAi { get; set; } // enabled_ai
|
||||
public string? RecordMode { get; set; } // record_mode
|
||||
}
|
||||
|
||||
/// <summary>通道推送/流配置</summary>
|
||||
public class OwlChannelConfig
|
||||
{
|
||||
public bool IsAuthDisabled { get; set; }
|
||||
public object? PushedAt { get; set; }
|
||||
public object? StoppedAt { get; set; }
|
||||
public string? MediaServerId { get; set; }
|
||||
public string? SourceUrl { get; set; }
|
||||
public int Transport { get; set; }
|
||||
public int TimeoutS { get; set; }
|
||||
public bool EnabledAudio { get; set; }
|
||||
public bool EnabledRemoveNoneReader { get; set; }
|
||||
public bool EnabledDisabledNoneReader { get; set; }
|
||||
public string? StreamKey { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 播放/流
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>Owl 播放响应</summary>
|
||||
/// <summary>Owl 播放响应 (POST /channels/{id}/play)</summary>
|
||||
public class OwlPlayResponse
|
||||
{
|
||||
public string? App { get; set; }
|
||||
public string? Stream { get; set; }
|
||||
public List<OwlPlayItem>? Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Owl 播放流条目</summary>
|
||||
/// <summary>Owl 播放流条目 (snake_case)</summary>
|
||||
public class OwlPlayItem
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("ws_flv")]
|
||||
public string? WsFlv { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("http_flv")]
|
||||
public string? HttpFlv { get; set; }
|
||||
public string? Hls { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("webrtc")]
|
||||
public string? WebRtc { get; set; }
|
||||
public string? Rtmp { get; set; }
|
||||
public string? Rtsp { get; set; }
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace IntegrationGateway.Core.Infrastructure;
|
||||
/// </summary>
|
||||
public class GatewayClientFactory
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly string _volProBaseUrl;
|
||||
|
||||
@@ -48,7 +49,7 @@ public class GatewayClientFactory
|
||||
{
|
||||
var http = CreateClient();
|
||||
var resp = await http.PostAsJsonAsync($"{_volProBaseUrl}/api/gateway/sync/devices",
|
||||
new { nodeCode, token, devices });
|
||||
new { nodeCode, token, devices }, JsonOpts);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||
}
|
||||
@@ -58,7 +59,7 @@ public class GatewayClientFactory
|
||||
{
|
||||
var http = CreateClient();
|
||||
var resp = await http.PostAsJsonAsync($"{_volProBaseUrl}/api/gateway/sync/alarms",
|
||||
new { nodeCode, token, alarms });
|
||||
new { nodeCode, token, alarms }, JsonOpts);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using IntegrationGateway.Core.Abstractions;
|
||||
using IntegrationGateway.Core.Infrastructure;
|
||||
using IntegrationGateway.Core.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// IntegrationGateway 宿主启动程序
|
||||
@@ -96,7 +98,16 @@ Console.WriteLine($"[Gateway] {registry.All.Count} 个适配器已注册: {adapt
|
||||
var nodeCode = gwCfg["NodeCode"] ?? "gw-default";
|
||||
var nodeToken = Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"] ?? "";
|
||||
var port = app.Urls.FirstOrDefault()?.Split(':').LastOrDefault() ?? "5100";
|
||||
var selfUrl = gwCfg["SelfUrl"] ?? $"http://localhost:{port}";
|
||||
var selfUrl = gwCfg["SelfUrl"];
|
||||
if (string.IsNullOrEmpty(selfUrl))
|
||||
{
|
||||
// 自动检测本机局域网 IP,避免注册 localhost 导致前端不可达
|
||||
var host = Dns.GetHostEntry(Dns.GetHostName());
|
||||
var lanIp = host.AddressList
|
||||
.FirstOrDefault(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork
|
||||
&& !IPAddress.IsLoopback(ip));
|
||||
selfUrl = $"http://{(lanIp != null ? lanIp.ToString() : "localhost")}:{port}";
|
||||
}
|
||||
try
|
||||
{
|
||||
var registerReq = new GatewayRegisterRequest
|
||||
@@ -171,8 +182,9 @@ async Task SyncAllDevicesAsync(string nc, string nt, string baseUrl)
|
||||
FlattenTree(allDevices, nodes, adapter.AdapterCode, null);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[Gateway] A3: 适配器 {adapter.AdapterCode} 取设备失败: {ex.Message}"); }
|
||||
}
|
||||
Console.WriteLine($"[Gateway] A3: 收集到 {allDevices.Count} 台设备,开始推送到 Vol.Pro");
|
||||
if (allDevices.Any())
|
||||
await clientFactory.SyncDevicesAsync(nc, nt, allDevices);
|
||||
}
|
||||
@@ -197,7 +209,7 @@ app.MapGet("/api/gateway/health", async () =>
|
||||
foreach (var a in registry.All)
|
||||
{
|
||||
bool healthy = false;
|
||||
try { healthy = await a.HealthCheckAsync(); } catch { }
|
||||
try { healthy = await a.HealthCheckAsync(); } catch (Exception ex) { Console.Error.WriteLine($"[Gateway] B1: HealthCheck {a.AdapterCode} 失败: {ex.Message}"); }
|
||||
results.Add(new { a.AdapterCode, a.DisplayName, Healthy = healthy, a.Capabilities });
|
||||
}
|
||||
return Results.Ok(results);
|
||||
@@ -279,7 +291,7 @@ app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, Batc
|
||||
|
||||
var results = new Dictionary<string, List<PointValue>>();
|
||||
foreach (var deviceId in req.DeviceIds ?? new())
|
||||
try { results[deviceId] = await a.GetRealtimeValuesAsync(deviceId); } catch { }
|
||||
try { results[deviceId] = await a.GetRealtimeValuesAsync(deviceId); } catch (Exception ex) { Console.Error.WriteLine($"[Gateway] B4-batch: {adapter}/{deviceId} 取实时值失败: {ex.Message}"); }
|
||||
return Results.Ok(results);
|
||||
});
|
||||
|
||||
@@ -352,7 +364,7 @@ app.MapPost("/api/gateway/sync/{adapter}", async (string adapter, SyncRequest re
|
||||
});
|
||||
|
||||
// B13: 数据删除 — 从子系统删除数据
|
||||
app.MapDelete("/api/gateway/sync/{adapter}", async (string adapter, SyncDeleteRequest req) =>
|
||||
app.MapDelete("/api/gateway/sync/{adapter}", async (string adapter, [FromBody] SyncDeleteRequest req) =>
|
||||
{
|
||||
var a = registry.FindByCode<IAcceptsDataSync>(adapter);
|
||||
if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" });
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5260",
|
||||
"applicationUrl": "http://0.0.0.0:5100",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -9,21 +9,42 @@
|
||||
"Owl": [
|
||||
{
|
||||
"InstanceName": "main",
|
||||
"BaseUrl": "http://localhost:15123",
|
||||
"BaseUrl": "http://192.168.3.108:15123",
|
||||
"Username": "admin",
|
||||
"Password": "your_owl_password"
|
||||
"Password": "admin"
|
||||
}
|
||||
],
|
||||
"MC4": [
|
||||
{
|
||||
"InstanceName": "31ku",
|
||||
"BaseUrl": "http://localhost:3000"
|
||||
"BaseUrl": "http://192.168.3.90:3000",
|
||||
"Username": "admin",
|
||||
"Password": "@ZLcx8980"
|
||||
},
|
||||
{
|
||||
"InstanceName": "32ku",
|
||||
"BaseUrl": "http://192.168.3.91:3000",
|
||||
"Username": "admin",
|
||||
"Password": "@ZLcx8980"
|
||||
},
|
||||
{
|
||||
"InstanceName": "33ku",
|
||||
"BaseUrl": "http://192.168.3.92:3000/api/central/auth/conf/get",
|
||||
"Username": "admin",
|
||||
"Password": "@ZLcx8980"
|
||||
},
|
||||
{
|
||||
"InstanceName": "34ku",
|
||||
"BaseUrl": "http://192.168.3.93:3000",
|
||||
"Username": "admin",
|
||||
"Password": "@ZLcx8980"
|
||||
}
|
||||
],
|
||||
"Gateway": {
|
||||
"VolProBaseUrl": "http://localhost:9100",
|
||||
"NodeCode": "gw-31ku",
|
||||
"VolProBaseUrl": "http://192.168.3.108:9100",
|
||||
"NodeCode": "gw-test",
|
||||
"NodeToken": "changeme",
|
||||
"SelfUrl": "http://192.168.3.108:5100",
|
||||
"HeartbeatIntervalSec": 15,
|
||||
"AdapterInitTimeoutSec": 30,
|
||||
"GatewayKey": null,
|
||||
@@ -32,9 +53,9 @@
|
||||
"KMS": [
|
||||
{
|
||||
"InstanceName": "main",
|
||||
"BaseUrl": "http://192.168.1.50:8080",
|
||||
"ClientId": "your_client_id",
|
||||
"ClientSecret": "your_client_secret"
|
||||
"BaseUrl": "http://192.168.3.198",
|
||||
"ClientId": "g82tt",
|
||||
"ClientSecret": "2w1q821130@W!Q"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user