1、每10分钟,主动判断一下遥信全体 2、程序启动时,提取遥测遥信全体命令,每个命令闹间隔1秒 3、提取遥测遥信全体命令完成后(10分钟超时),再执行巡检和定位发送提取遥测信息 4、每30秒请求一次虚点 5、其他一系列修改
903 lines
38 KiB
C#
903 lines
38 KiB
C#
using DynamicExpresso;
|
||
using Microsoft.Extensions.Logging;
|
||
using Newtonsoft.Json.Linq;
|
||
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using ToolLibrary;
|
||
using YunDa.Server.ISMSTcp.Domain;
|
||
using YunDa.Server.ISMSTcp.Interfaces;
|
||
using YunDa.Server.ISMSTcp.Models;
|
||
using YunDa.SOMS.DataTransferObject.ExternalEntities.BeijingYounuo;
|
||
using YunDa.SOMS.DataTransferObject.GeneralInformation.EquipmentLiveDataDto;
|
||
using YunDa.SOMS.DataTransferObject.GeneralInformation.ProtectionDeviceInfoDto;
|
||
using YunDa.SOMS.Entities.GeneralInformation;
|
||
using YunDa.SOMS.Redis.Repositories;
|
||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||
|
||
namespace YunDa.Server.ISMSTcp.Services
|
||
{
|
||
/// <summary>
|
||
/// 遥信数据处理服务
|
||
/// </summary>
|
||
public class TelesignalisationHandle
|
||
{
|
||
private readonly ILogger<TelesignalisationHandle> _logger;
|
||
private readonly IRedisRepository<TelesignalisationModel, string> _telesignalisationModelListRedis;
|
||
private readonly string _telesignalisationModelListRediskey = "telesignalisationModelList";
|
||
public string _telesignalisationInflectionInflectionZZChannelRediskey = "telesignalisationInflection_ZZ_Channel";
|
||
private readonly IWebSocketPushService _webSocketPushService;
|
||
private readonly IApiEndpoints _apiEndpoints;
|
||
// 🔧 新增:保护装置通信信息Redis仓储,用于检查设备检修状态
|
||
private readonly IRedisRepository<ProtectionDeviceCommInfoOutput, string> _protectionDeviceCommInfoRedis;
|
||
private const string PROTECTION_DEVICE_COMM_INFO_REDIS_KEY = "protectionDeviceCommInfo";
|
||
|
||
// 映射字典:YX_ID -> haskey
|
||
private readonly ConcurrentDictionary<string, string> _yxIdToHashKeyMapping = new ConcurrentDictionary<string, string>();
|
||
|
||
// 🔧 新增:网线和光缆逻辑表达式处理相关字段
|
||
private readonly WebApiRequest _webApiRequest;
|
||
private readonly ConcurrentDictionary<string, List<NetworkCableDto>> _telesignalToNetworkCableMapping = new ConcurrentDictionary<string, List<NetworkCableDto>>();
|
||
private readonly ConcurrentDictionary<string, List<OpticalCableDto>> _telesignalToOpticalCableMapping = new ConcurrentDictionary<string, List<OpticalCableDto>>();
|
||
|
||
// 初始化状态
|
||
public volatile bool _isInitialized = false;
|
||
public volatile bool _isExpressionMappingInitialized = false;
|
||
private readonly object _initLock = new object();
|
||
private readonly object _expressionInitLock = new object();
|
||
|
||
//遥信数据缓冲队列
|
||
private ZzDataCacheContainer _zzDataCacheContainer = new ZzDataCacheContainer(ZzDataCacheContainerDataType.eYX, 30); //只保留5分钟的数据
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="logger">日志服务</param>
|
||
/// <param name="telesignalisationModelListRedis">遥信数据Redis仓储</param>
|
||
public TelesignalisationHandle(
|
||
IApiEndpoints apiEndpoints,
|
||
ILogger<TelesignalisationHandle> logger,
|
||
IWebSocketPushService webSocketPushService,
|
||
IRedisRepository<TelesignalisationModel, string> telesignalisationModelListRedis,
|
||
IRedisRepository<ProtectionDeviceCommInfoOutput, string> protectionDeviceCommInfoRedis,
|
||
WebApiRequest webApiRequest,
|
||
ThingService thingService)
|
||
{
|
||
_apiEndpoints = apiEndpoints ?? throw new ArgumentNullException(nameof(apiEndpoints));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
_telesignalisationModelListRedis = telesignalisationModelListRedis ?? throw new ArgumentNullException(nameof(telesignalisationModelListRedis));
|
||
_webSocketPushService = webSocketPushService ?? throw new ArgumentNullException(nameof(webSocketPushService));
|
||
_protectionDeviceCommInfoRedis = protectionDeviceCommInfoRedis ?? throw new ArgumentNullException(nameof(protectionDeviceCommInfoRedis));
|
||
_webApiRequest = webApiRequest ?? throw new ArgumentNullException(nameof(webApiRequest));
|
||
|
||
ZzDataCache.SetDataCache(ZzDataCacheContainerDataType.eYX, _zzDataCacheContainer);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化遥信数据映射
|
||
/// </summary>
|
||
/// <param name="maxRetries">最大重试次数</param>
|
||
/// <param name="retryDelayMs">重试间隔(毫秒)</param>
|
||
/// <returns>初始化任务</returns>
|
||
public async Task<bool> InitAsync(int maxRetries = 3, int retryDelayMs = 2000)
|
||
{
|
||
if (_isInitialized)
|
||
{
|
||
_logger.LogInformation("遥信数据映射已经初始化完成");
|
||
return true;
|
||
}
|
||
|
||
lock (_initLock)
|
||
{
|
||
if (_isInitialized)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
_logger.LogInformation("开始初始化遥信数据映射...");
|
||
}
|
||
|
||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("第 {Attempt}/{MaxRetries} 次尝试查询遥信数据", attempt, maxRetries);
|
||
|
||
// 查询所有遥信数据,使用固定的数据源类别 "Zongzi" (0)
|
||
string redisKey = $"{_telesignalisationModelListRediskey}_Zongzi";
|
||
var telesignalisationModels = await _telesignalisationModelListRedis.HashSetGetAllAsync(redisKey);
|
||
|
||
if (telesignalisationModels == null || telesignalisationModels.Count == 0)
|
||
{
|
||
_logger.LogWarning("第 {Attempt} 次查询遥信数据为空,Redis键: {RedisKey}", attempt, redisKey);
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
_logger.LogInformation("等待 {DelayMs} 毫秒后重试...", retryDelayMs);
|
||
await Task.Delay(retryDelayMs);
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("所有重试都失败,无法获取遥信数据");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 创建映射字典
|
||
int mappingCount = 0;
|
||
foreach (var model in telesignalisationModels)
|
||
{
|
||
if (!string.IsNullOrEmpty(model.ismsbaseYXId))
|
||
{
|
||
// 构造haskey格式:"{dev_addr}_{dev_sector}_{dev_inf}_0"
|
||
// 这里使用模型中的地址信息
|
||
string haskey = $"{model.DeviceAddress}_0_{model.DispatcherAddress}_0";
|
||
|
||
|
||
if (_yxIdToHashKeyMapping.TryAdd(model.ismsbaseYXId, haskey))
|
||
{
|
||
mappingCount++;
|
||
//初始化
|
||
_zzDataCacheContainer.Write(model.ismsbaseYXId, model.ResultValue, model.ResultTime, model.Name, model.ResultValueStr, model.DispatcherAddress);
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("遥信模型 {ModelName} 的 ismsbaseYXId 为空,跳过", model.Name);
|
||
}
|
||
}
|
||
|
||
lock (_initLock)
|
||
{
|
||
_isInitialized = true;
|
||
}
|
||
|
||
_logger.LogInformation("遥信数据映射初始化成功!总计 {TotalModels} 个模型,创建 {MappingCount} 个映射",
|
||
telesignalisationModels.Count, mappingCount);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "第 {Attempt} 次初始化遥信数据映射时发生错误", attempt);
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
_logger.LogInformation("等待 {DelayMs} 毫秒后重试...", retryDelayMs);
|
||
await Task.Delay(retryDelayMs);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("所有重试都失败,初始化遥信数据映射失败");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化网线和光缆逻辑表达式映射
|
||
/// </summary>
|
||
/// <param name="maxRetries">最大重试次数</param>
|
||
/// <param name="retryDelayMs">重试间隔(毫秒)</param>
|
||
/// <returns>初始化任务</returns>
|
||
public async Task<bool> InitExpressionMappingAsync(int maxRetries = 3, int retryDelayMs = 2000)
|
||
{
|
||
if (_isExpressionMappingInitialized)
|
||
{
|
||
_logger.LogInformation("网线和光缆逻辑表达式映射已经初始化完成");
|
||
return true;
|
||
}
|
||
|
||
lock (_expressionInitLock)
|
||
{
|
||
if (_isExpressionMappingInitialized)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
_logger.LogInformation("开始初始化网线和光缆逻辑表达式映射...");
|
||
}
|
||
|
||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("第 {Attempt}/{MaxRetries} 次尝试获取网线和光缆配置数据", attempt, maxRetries);
|
||
|
||
// 1. 获取网线配置数据
|
||
var networkCables = await _webApiRequest.GetNetworkCableListAsync();
|
||
var opticalCables = await _webApiRequest.GetOpticalCableListAsync();
|
||
|
||
if ((networkCables == null || networkCables.Count == 0) &&
|
||
(opticalCables == null || opticalCables.Count == 0))
|
||
{
|
||
_logger.LogWarning("第 {Attempt} 次查询网线和光缆配置数据为空", attempt);
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
_logger.LogInformation("等待 {DelayMs} 毫秒后重试...", retryDelayMs);
|
||
await Task.Delay(retryDelayMs);
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("所有重试都失败,无法获取网线和光缆配置数据");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 2. 处理网线配置,创建遥信地址到网线配置的映射
|
||
int networkCableMappingCount = 0;
|
||
if (networkCables != null)
|
||
{
|
||
foreach (var cable in networkCables.Where(c => c.IsActive && !string.IsNullOrWhiteSpace(c.LogicalExpression)))
|
||
{
|
||
var addresses = ExtractTelesignalAddresses(cable.LogicalExpression);
|
||
foreach (var address in addresses)
|
||
{
|
||
_telesignalToNetworkCableMapping.AddOrUpdate(address,
|
||
new List<NetworkCableDto> { cable },
|
||
(key, existingList) =>
|
||
{
|
||
existingList.Add(cable);
|
||
return existingList;
|
||
});
|
||
networkCableMappingCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 处理光缆配置,创建遥信地址到光缆配置的映射
|
||
int opticalCableMappingCount = 0;
|
||
if (opticalCables != null)
|
||
{
|
||
foreach (var cable in opticalCables.Where(c => c.IsActive && !string.IsNullOrWhiteSpace(c.LogicalExpression)))
|
||
{
|
||
var addresses = ExtractTelesignalAddresses(cable.LogicalExpression);
|
||
foreach (var address in addresses)
|
||
{
|
||
_telesignalToOpticalCableMapping.AddOrUpdate(address,
|
||
new List<OpticalCableDto> { cable },
|
||
(key, existingList) =>
|
||
{
|
||
existingList.Add(cable);
|
||
return existingList;
|
||
});
|
||
opticalCableMappingCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
lock (_expressionInitLock)
|
||
{
|
||
_isExpressionMappingInitialized = true;
|
||
}
|
||
|
||
_logger.LogInformation("网线和光缆逻辑表达式映射初始化成功!网线配置: {NetworkCableCount} 个,光缆配置: {OpticalCableCount} 个,网线映射: {NetworkCableMappingCount} 个,光缆映射: {OpticalCableMappingCount} 个",
|
||
networkCables?.Count ?? 0, opticalCables?.Count ?? 0, networkCableMappingCount, opticalCableMappingCount);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "第 {Attempt} 次初始化网线和光缆逻辑表达式映射时发生错误", attempt);
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
_logger.LogInformation("等待 {DelayMs} 毫秒后重试...", retryDelayMs);
|
||
await Task.Delay(retryDelayMs);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("所有重试都失败,初始化网线和光缆逻辑表达式映射失败");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理遥信数据
|
||
/// </summary>
|
||
/// <param name="yxData">遥信数据JSON令牌</param>
|
||
/// <param name="skipForwarding">是否跳过转发逻辑(仅更新Redis)</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task<TelesignalisationModel?> ProcessYXDataAsync(JToken yxData, bool skipForwarding = false)
|
||
{
|
||
try
|
||
{
|
||
if (yxData == null)
|
||
{
|
||
_logger.LogWarning("遥信数据为空,跳过处理");
|
||
return null;
|
||
}
|
||
if (!_isInitialized)
|
||
{
|
||
_logger.LogWarning("遥信数据映射尚未初始化,跳过处理");
|
||
return null;
|
||
}
|
||
return await ProcessSingleYXDataAsync(yxData, skipForwarding);
|
||
//_logger.LogInformation("遥信数据处理完成");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "处理遥信数据时发生错误: {YXData}", yxData?.ToString());
|
||
throw;
|
||
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理单个遥信数据项
|
||
/// </summary>
|
||
/// <param name="yxItem">单个遥信数据项</param>
|
||
/// <param name="skipForwarding">是否跳过转发逻辑(仅更新Redis)</param>
|
||
/// <returns>处理任务</returns>
|
||
private async Task<TelesignalisationModel?> ProcessSingleYXDataAsync(JToken yxItem, bool skipForwarding = false)
|
||
{
|
||
try
|
||
{
|
||
// 解析YXData结构
|
||
var yxDataModel = yxItem.ToObject<YXData>();
|
||
|
||
if (yxDataModel == null)
|
||
{
|
||
_logger.LogWarning("无法解析遥信数据项: {Item}", yxItem?.ToString());
|
||
return null;
|
||
}
|
||
|
||
//开关状态(合、分、不定),与具体的规约(101、104)无关 TSwitchstate = (swon = 0 swOff = 1,swUncertainty = 2);
|
||
switch (yxDataModel.V)
|
||
{
|
||
case 0:
|
||
yxDataModel.V = 2;
|
||
break;
|
||
|
||
case 2:
|
||
yxDataModel.V = 0;
|
||
break;
|
||
}
|
||
|
||
//if (yxDataModel.YX_ID == "YXB001121065")
|
||
//{
|
||
// _logger.LogWarning($"YXB001121065: {yxDataModel.V}, {yxDataModel.T}");
|
||
// Debug.WriteLine($"YXB001121065: {yxDataModel.V}, {yxDataModel.T}");
|
||
//}
|
||
|
||
// 使用映射字典查找对应的haskey
|
||
if (!_yxIdToHashKeyMapping.TryGetValue(yxDataModel.YX_ID, out string haskey))
|
||
{
|
||
// _logger.LogWarning("未找到YX_ID {YxId} 对应的映射", yxDataModel.YX_ID);
|
||
return null;
|
||
}
|
||
|
||
// 构建Redis键
|
||
string redisKey = $"{_telesignalisationModelListRediskey}_Zongzi";
|
||
|
||
// 从Redis获取现有数据
|
||
var telesignalisationModel = await _telesignalisationModelListRedis.HashSetGetOneAsync(redisKey, haskey);
|
||
if (telesignalisationModel == null)
|
||
{
|
||
_logger.LogWarning("Redis中未找到遥信数据: RedisKey={RedisKey}, HasKey={HasKey}", redisKey, haskey);
|
||
return null;
|
||
}
|
||
|
||
// 解析时间
|
||
DateTime resultTime = DateTime.Now;
|
||
if (!string.IsNullOrEmpty(yxDataModel.T))
|
||
{
|
||
if (DateTime.TryParse(yxDataModel.T, out DateTime parsedTime))
|
||
{
|
||
if (parsedTime.Year>2000)
|
||
{
|
||
resultTime = parsedTime;
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("无法解析时间格式: {TimeStr},使用当前时间", yxDataModel.T);
|
||
}
|
||
}
|
||
|
||
// 更新数据
|
||
telesignalisationModel.ResultTime = resultTime;
|
||
telesignalisationModel.ResultValue = yxDataModel.V;
|
||
|
||
// 保存到Redis
|
||
await _telesignalisationModelListRedis.HashSetUpdateOneAsync(redisKey, haskey, telesignalisationModel);
|
||
string channel = $"{_telesignalisationInflectionInflectionZZChannelRediskey}_Zongzi";
|
||
_telesignalisationModelListRedis.PublishAsync(channel, telesignalisationModel);
|
||
|
||
//缓存遥信数据到内存
|
||
_zzDataCacheContainer.Write(yxDataModel.YX_ID, yxDataModel.V, resultTime, telesignalisationModel.Name, telesignalisationModel.ResultValueStr, telesignalisationModel.DispatcherAddress);
|
||
|
||
// 🔧 优化:如果跳过转发,仅更新Redis后直接返回
|
||
if (skipForwarding)
|
||
{
|
||
_logger.LogDebug("跳过转发逻辑 - YX_ID: {YxId}", yxDataModel.YX_ID);
|
||
return telesignalisationModel;
|
||
}
|
||
|
||
// 提取设备ID并调用数据变位信号API
|
||
var equipmentId = telesignalisationModel.EquipmentInfoId.Value;
|
||
await CallDataChangedAsync(equipmentId);
|
||
|
||
// 🔧 新增:检查设备是否处于检修状态,如果是则跳过数据推送
|
||
var isInMaintenance = await IsEquipmentInMaintenanceAsync(equipmentId);
|
||
if (isInMaintenance)
|
||
{
|
||
_logger.LogDebug("设备处于检修状态,跳过遥信数据推送 - EquipmentInfoId: {EquipmentId}", equipmentId);
|
||
// 设备处于检修状态,跳过数据推送
|
||
return telesignalisationModel;
|
||
}
|
||
|
||
// WebSocket实时推送
|
||
try
|
||
{
|
||
JToken pointToken = JToken.FromObject(telesignalisationModel);
|
||
// 推送到YX分组
|
||
await _webSocketPushService.PushYXDataAsync(pointToken, telesignalisationModel.EquipmentInfoId.ToString());
|
||
}
|
||
catch (Exception pushEx)
|
||
{
|
||
_logger.LogWarning(pushEx, "YX数据WebSocket推送失败,但不影响主要处理流程");
|
||
}
|
||
|
||
// 🔧 新增:处理网线和光缆逻辑表达式
|
||
if (_isExpressionMappingInitialized)
|
||
{
|
||
await ProcessLogicExpressionsAsync(yxDataModel.YX_ID, yxDataModel.V);
|
||
}
|
||
|
||
return telesignalisationModel;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "处理单个遥信数据项时发生错误: {Item}", yxItem?.ToString());
|
||
return null;
|
||
}
|
||
}
|
||
public async Task<List<ZzDataResultModel>> GetYXDataByDataIds(List<string> ids, int times, int timeWindowType, CancellationToken cancellationToken)
|
||
{
|
||
|
||
//检查数据是否为空,如果为空,从Redis里取最新值
|
||
if(!_isInitialized)
|
||
{
|
||
await InitAsync();
|
||
}
|
||
|
||
var finalResult = await _zzDataCacheContainer.Read(ids, times, timeWindowType, cancellationToken);
|
||
|
||
|
||
string redisKey = $"{_telesignalisationModelListRediskey}_Zongzi";
|
||
foreach (var item in finalResult)
|
||
{
|
||
if(item.TimeStamp == DateTime.MinValue)
|
||
{
|
||
if (!_yxIdToHashKeyMapping.TryGetValue(item.Id, out string haskey))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var telesignalisationModel = await _telesignalisationModelListRedis.HashSetGetOneAsync(redisKey, haskey);
|
||
if (telesignalisationModel == null)
|
||
{
|
||
_logger.LogWarning("Redis中未找到遥信数据: RedisKey={RedisKey}, HasKey={HasKey}", redisKey, haskey);
|
||
continue; // 继续处理下一个haskey
|
||
}
|
||
|
||
item.Value = telesignalisationModel.ResultValue;
|
||
item.ValueStr = telesignalisationModel.ResultValueStr;
|
||
item.TimeStamp = telesignalisationModel.ResultTime;
|
||
}
|
||
|
||
}
|
||
|
||
return finalResult;
|
||
}
|
||
|
||
private readonly ConcurrentQueue<DateTime> _callTimestamps = new ConcurrentQueue<DateTime>();
|
||
private readonly SemaphoreSlim _rateLimitLock = new SemaphoreSlim(1, 1);
|
||
private const int MaxCallsPerWindow = 300;
|
||
private const int TimeWindowSeconds = 3;
|
||
|
||
private async Task CallDataChangedAsync(Guid equipmentId)
|
||
{
|
||
try
|
||
{
|
||
// 检查调用频率
|
||
if (!await ShouldAllowCallAsync())
|
||
{
|
||
_logger.LogWarning("数据变位信号调用频率过高,放弃本次调用,设备ID: {EquipmentId}", equipmentId);
|
||
return;
|
||
}
|
||
|
||
var url = $"{_apiEndpoints.DataChangedUri}?equipmentId={equipmentId}";
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
var response = await Task.Run(() => HttpHelper.HttpGetRequest<object>(url));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "数据变位信号API调用失败,设备ID: {EquipmentId}", equipmentId);
|
||
}
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "调用数据变位信号API异常,设备ID: {EquipmentId}", equipmentId);
|
||
}
|
||
}
|
||
|
||
private async Task<bool> ShouldAllowCallAsync()
|
||
{
|
||
await _rateLimitLock.WaitAsync();
|
||
try
|
||
{
|
||
var now = DateTime.UtcNow;
|
||
var windowStart = now.AddSeconds(-TimeWindowSeconds);
|
||
|
||
// 移除时间窗口外的旧记录
|
||
while (_callTimestamps.TryPeek(out var oldestTime) && oldestTime < windowStart)
|
||
{
|
||
_callTimestamps.TryDequeue(out _);
|
||
}
|
||
|
||
// 检查当前窗口内的调用次数
|
||
if (_callTimestamps.Count >= MaxCallsPerWindow)
|
||
{
|
||
return false; // 超过限制,拒绝调用
|
||
}
|
||
|
||
// 记录本次调用时间
|
||
_callTimestamps.Enqueue(now);
|
||
return true;
|
||
}
|
||
finally
|
||
{
|
||
_rateLimitLock.Release();
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 🔧 新增:检查设备是否处于检修状态
|
||
/// </summary>
|
||
/// <param name="equipmentInfoId">设备信息ID</param>
|
||
/// <returns>是否处于检修状态</returns>
|
||
private async Task<bool> IsEquipmentInMaintenanceAsync(Guid equipmentInfoId)
|
||
{
|
||
try
|
||
{
|
||
// 从Redis获取所有保护装置通信信息
|
||
var allDevices = await _protectionDeviceCommInfoRedis.HashSetGetAllAsync(PROTECTION_DEVICE_COMM_INFO_REDIS_KEY);
|
||
|
||
// 查找匹配的设备
|
||
var targetDevice = allDevices.FirstOrDefault(d => d.EquipmentInfoId == equipmentInfoId);
|
||
|
||
if (targetDevice != null)
|
||
{
|
||
return targetDevice.IsMaintenance;
|
||
}
|
||
|
||
// 如果未找到设备信息,默认不处于检修状态
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "检查设备检修状态时发生错误 - EquipmentInfoId: {EquipmentInfoId}", equipmentInfoId);
|
||
// 异常情况下默认不处于检修状态,避免影响正常数据传输
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从逻辑表达式中提取遥信地址
|
||
/// </summary>
|
||
/// <param name="expression">逻辑表达式,如 "{1793}==1" 或 "{1800}==1&&{1900}==1"</param>
|
||
/// <returns>遥信地址列表</returns>
|
||
private List<string> ExtractTelesignalAddresses(string expression)
|
||
{
|
||
var addresses = new List<string>();
|
||
if (string.IsNullOrWhiteSpace(expression))
|
||
{
|
||
return addresses;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 使用正则表达式提取花括号中的数字
|
||
var matches = Regex.Matches(expression, @"\{(\d+)\}");
|
||
foreach (Match match in matches)
|
||
{
|
||
if (match.Groups.Count > 1)
|
||
{
|
||
var address = match.Groups[1].Value;
|
||
if (!addresses.Contains(address))
|
||
{
|
||
addresses.Add(address);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "提取遥信地址时发生错误,表达式: {Expression}", expression);
|
||
}
|
||
|
||
return addresses;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理网线和光缆逻辑表达式
|
||
/// </summary>
|
||
/// <param name="yxId">遥信ID</param>
|
||
/// <param name="value">遥信值</param>
|
||
/// <returns></returns>
|
||
private async Task ProcessLogicExpressionsAsync(string yxId, int value)
|
||
{
|
||
try
|
||
{
|
||
// 处理网线逻辑表达式
|
||
if (_telesignalToNetworkCableMapping.TryGetValue(yxId, out var networkCables))
|
||
{
|
||
foreach (var cable in networkCables)
|
||
{
|
||
await EvaluateAndProcessExpressionAsync(cable.LogicalExpression, cable.TwinId, "网线", cable.CiCode);
|
||
}
|
||
}
|
||
|
||
// 处理光缆逻辑表达式
|
||
if (_telesignalToOpticalCableMapping.TryGetValue(yxId, out var opticalCables))
|
||
{
|
||
foreach (var cable in opticalCables)
|
||
{
|
||
await EvaluateAndProcessExpressionAsync(cable.LogicalExpression, cable.TwinId, "光缆", cable.CiCode);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "处理网线和光缆逻辑表达式时发生错误,遥信ID: {YxId}", yxId);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 评估逻辑表达式并处理结果
|
||
/// </summary>
|
||
/// <param name="expression">逻辑表达式</param>
|
||
/// <param name="twinId">孪生体ID</param>
|
||
/// <param name="cableType">线缆类型</param>
|
||
/// <param name="cableName">线缆名称</param>
|
||
/// <returns></returns>
|
||
private async Task EvaluateAndProcessExpressionAsync(string expression, string twinId, string cableType, string cableName)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(expression) || string.IsNullOrWhiteSpace(twinId))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 1. 获取表达式中的所有遥信地址
|
||
var addresses = ExtractTelesignalAddresses(expression);
|
||
if (addresses.Count == 0)
|
||
{
|
||
_logger.LogWarning("表达式中未找到有效的遥信地址: {Expression}", expression);
|
||
return;
|
||
}
|
||
|
||
// 2. 获取当前遥信值并替换表达式中的地址
|
||
var evaluableExpression = await ReplaceAddressesWithCurrentValuesAsync(expression, addresses);
|
||
if (string.IsNullOrWhiteSpace(evaluableExpression))
|
||
{
|
||
_logger.LogWarning("无法构建可评估的表达式: {Expression}", expression);
|
||
return;
|
||
}
|
||
|
||
// 3. 使用NCalc评估表达式
|
||
var result = EvaluateExpressionWithNCalc(evaluableExpression);
|
||
if (!result.HasValue)
|
||
{
|
||
_logger.LogWarning("表达式评估失败: {Expression} -> {EvaluableExpression}", expression, evaluableExpression);
|
||
return;
|
||
}
|
||
|
||
_logger.LogDebug("{CableType} {TwinId} 表达式评估结果: {Result}, 原表达式: {Expression}, 评估表达式: {EvaluableExpression}",
|
||
cableType, twinId, result.Value, expression, evaluableExpression);
|
||
|
||
// 4. 根据结果调用相应的API
|
||
if (result.Value) // True表示异常,需要推送报警
|
||
{
|
||
await PushCableAlarmAsync(twinId, cableType, cableName, expression);
|
||
}
|
||
else // False表示正常,需要删除报警
|
||
{
|
||
await DeleteCableAlarmAsync(twinId, cableType);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "评估和处理{CableType}逻辑表达式时发生错误: {Expression}, TwinId: {TwinId}", cableType, expression, twinId);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将表达式中的地址替换为当前遥信值
|
||
/// </summary>
|
||
/// <param name="expression">原始表达式</param>
|
||
/// <param name="addresses">地址列表</param>
|
||
/// <returns>可评估的表达式</returns>
|
||
private async Task<string> ReplaceAddressesWithCurrentValuesAsync(string expression, List<string> addresses)
|
||
{
|
||
try
|
||
{
|
||
var result = expression;
|
||
string redisKey = $"{_telesignalisationModelListRediskey}_Zongzi";
|
||
|
||
foreach (var address in addresses)
|
||
{
|
||
// 构造haskey格式来查找对应的遥信数据
|
||
// 这里需要根据实际的haskey格式来构造,可能需要调整
|
||
var possibleHashKeys = new[]
|
||
{
|
||
$"{address}_0_{address}_0", // 尝试不同的haskey格式
|
||
$"0_0_{address}_0",
|
||
address
|
||
};
|
||
|
||
int? telesignalValue = null;
|
||
foreach (var hashKey in possibleHashKeys)
|
||
{
|
||
try
|
||
{
|
||
var telesignalModel = await _telesignalisationModelListRedis.HashSetGetOneAsync(redisKey, hashKey);
|
||
if (telesignalModel != null)
|
||
{
|
||
telesignalValue = telesignalModel.ResultValue;
|
||
break;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 继续尝试下一个hashKey格式
|
||
}
|
||
}
|
||
|
||
// 如果找不到对应的遥信值,使用默认值0
|
||
var valueToUse = telesignalValue ?? 0;
|
||
result = result.Replace($"{{{address}}}", valueToUse.ToString());
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "替换表达式地址时发生错误: {Expression}", expression);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用NCalc评估表达式
|
||
/// </summary>
|
||
/// <param name="expression">表达式</param>
|
||
/// <returns>评估结果</returns>
|
||
private bool? EvaluateExpressionWithNCalc(string expression)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(expression))
|
||
{
|
||
return null;
|
||
}
|
||
// ===== DynamicExpresso - 数学/逻辑表达式 =====
|
||
var mathInterpreter = new Interpreter();
|
||
// 简单计算
|
||
var result = mathInterpreter.Eval(expression); // 17
|
||
|
||
if (result is bool boolResult)
|
||
{
|
||
return boolResult;
|
||
}
|
||
else if (result is double doubleResult)
|
||
{
|
||
// 非零为true,零为false
|
||
return Math.Abs(doubleResult) > 0.0001;
|
||
}
|
||
else if (result is int intResult)
|
||
{
|
||
return intResult != 0;
|
||
}
|
||
else
|
||
{
|
||
// 尝试转换为布尔值
|
||
if (bool.TryParse(result?.ToString(), out bool parsedBool))
|
||
{
|
||
return parsedBool;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "使用NCalc评估表达式时发生错误: {Expression}", expression);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 推送线缆报警到3D系统
|
||
/// </summary>
|
||
/// <param name="twinId">孪生体ID</param>
|
||
/// <param name="cableType">线缆类型</param>
|
||
/// <param name="cableName">线缆名称</param>
|
||
/// <param name="expression">触发的表达式</param>
|
||
/// <returns></returns>
|
||
private async Task PushCableAlarmAsync(string twinId, string cableType, string cableName, string expression)
|
||
{
|
||
try
|
||
{
|
||
// 这里需要调用BeijingYounuoApiAppService的PushEquipmentAlarmsAsync方法
|
||
// 由于在ISMSTcp项目中,可能需要通过HTTP API调用
|
||
var alarmData = new YounuoAlert
|
||
{
|
||
TwinID = twinId,
|
||
Severity = 1, // 默认严重级别
|
||
Status = 1, // 活跃报警
|
||
SourceAlertKey = $"{cableType}异常",
|
||
Summary = $"{cableType} {cableName} 异常: {expression}",
|
||
SourceIdentifier = $"{cableType}_{twinId}",
|
||
LastOccurrence = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||
};
|
||
|
||
// 这里应该调用BeijingYounuoApiAppService,但由于架构限制,
|
||
// 可能需要通过HTTP API或消息队列来实现
|
||
_logger.LogInformation("推送{CableType}报警: TwinId={TwinId}, 表达式={Expression}", cableType, twinId, expression);
|
||
|
||
// TODO: 实现实际的API调用
|
||
await _webApiRequest.CallPushAlarmsApiAsync(alarmData);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "推送{CableType}报警时发生错误: TwinId={TwinId}", cableType, twinId);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除线缆报警
|
||
/// </summary>
|
||
/// <param name="twinId">孪生体ID</param>
|
||
/// <param name="cableType">线缆类型</param>
|
||
/// <returns></returns>
|
||
private async Task DeleteCableAlarmAsync(string twinId, string cableType)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("删除{CableType}报警: TwinId={TwinId}", cableType, twinId);
|
||
await _webApiRequest.CallDeleteAlarmsByTwinIdApiAsync(twinId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "删除{CableType}报警时发生错误: TwinId={TwinId}", cableType, twinId);
|
||
}
|
||
}
|
||
}
|
||
}
|