// ═══════════════════════════════════════════ // RuleEngineService — 规则引擎核心服务(V2.0 完整实现) // 端到端流程:加载启用规则 → 分组取实时值 → 评估条件 → 执行动作(控制/告警/通知)→ 写日志 // 设计决策详见 doc/整合方案/SecMPS_规则引擎完整实施_v2.0.md §3 步骤 3 // ═══════════════════════════════════════════ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using SqlSugar; using VolPro.Core.DbSqlSugar; // FirstOrDefaultAsync 扩展方法 using VolPro.Core.SignalR; using VolPro.Entity.DomainModels; using Warehouse.IRepositories; using Warehouse.IServices; using Warehouse.Services; // iot_alarmService / SyncAlarmItem 命名空间 using VolPro.Warehouse.Services; // GatewayClient 命名空间 namespace Warehouse.Services; /// /// 规则引擎核心服务。由 RuleEngineJob 每 10s 调用 EvaluateAllAsync() 一次。 /// /// 设计要点(详见方案 v2.0 §3 步骤 3): /// 1. 用 SqlSugar 的 Includes 加载导航属性(条件/动作) /// 2. 按 AdapterCode 分组批量调网关 B4-batch(避免逐设备 HTTP 调用) /// 3. 规则评估:AND/OR + 数值/开关比对 + 滞后窗(RecoveryThreshold)+ 规则级冷却 /// 4. 动作执行:控制/告警/通知 三种类型在 ExecuteActionsAsync 内 switch 串行执行(MVP 阶段) /// 5. 推送 SignalR:用 IHubContext<MessageHub> 推 "RuleTriggered" 事件(与 DataView.vue 订阅名一致) /// 6. 写执行日志:warehouse_rulelog 记录每次评估的触发/条件摘要/耗时 /// 7. gateway_nodes.default 在每条规则触发前查一次(避免 N+1) /// public class RuleEngineService { private readonly Iwarehouse_ruleRepository _ruleRepo; private readonly Ibase_deviceRepository _deviceRepo; private readonly Iwarehouse_rulelogRepository _logRepo; // V2.0.1 修复 DI:iot_alarmService 由 AutofacContainerModuleExtension.AddModule // 按"第一个接口 Iiot_alarmService"注册到内置 DI(实现类未单独注册), // 构造函数按具体类注入会触发 "Unable to resolve service for type iot_alarmService"。 private readonly Iiot_alarmService _alarmService; private readonly GatewayClient _gatewayClient; private readonly Igateway_nodesService _nodeService; private readonly IHubContext _hub; private readonly ILogger _logger; public RuleEngineService( Iwarehouse_ruleRepository ruleRepo, Ibase_deviceRepository deviceRepo, Iwarehouse_rulelogRepository logRepo, Iiot_alarmService alarmService, GatewayClient gatewayClient, Igateway_nodesService nodeService, IHubContext hub, ILogger logger) { _ruleRepo = ruleRepo; _deviceRepo = deviceRepo; _logRepo = logRepo; _alarmService = alarmService; _gatewayClient = gatewayClient; _nodeService = nodeService; _hub = hub; _logger = logger; } /// /// 评估所有启用规则 + 执行动作 + 写日志。每 10s 由 RuleEngineJob 调用。 /// public async Task EvaluateAllAsync() { var sw = Stopwatch.StartNew(); // 1. 加载启用规则(用 Includes 加载条件/动作导航属性) var rules = await _ruleRepo.FindAsIQueryable(r => r.Enable == "启用") .Includes(r => r.warehouse_rulecondition) .Includes(r => r.warehouse_ruleaction) .ToListAsync(); if (!rules.Any()) { _logger.LogDebug("[RuleEngine] 无启用规则"); return; } // 2. 收集所有涉及设备 ID(按 AdapterCode 分组取实时值用) var allDeviceIds = rules.SelectMany(r => r.warehouse_rulecondition) .Select(c => c.DeviceId).Where(id => id.HasValue) .Distinct().Select(id => id!.Value).ToList(); if (!allDeviceIds.Any()) { _logger.LogDebug("[RuleEngine] 启用规则无有效设备"); return; } // 3. 查设备表 → Dictionary // SqlSugar 没有 ToDictionaryAsync,先 ToListAsync 再 LINQ ToDictionary var deviceList = await _deviceRepo.FindAsIQueryable(d => allDeviceIds.Contains(d.DeviceId)) .ToListAsync(); var devices = deviceList.ToDictionary(d => d.DeviceId); // 5. 一次性查 gateway_nodes.default(避免每条规则触发都查) var defaultNode = await _nodeService.FindAsIQueryable(n => n.NodeCode == "default") .FirstOrDefaultAsync(); var defaultBaseUrl = defaultNode?.BaseUrl ?? ""; // 5b. 按 AdapterCode 分组 → 调 B4-batch 批量取实时值 var realtimeCache = new Dictionary>(); // DeviceId → PointValues var adapterGroups = devices.Values .Where(d => !string.IsNullOrEmpty(d.AdapterCode) && !string.IsNullOrEmpty(d.SourceId)) .GroupBy(d => d.AdapterCode); foreach (var grp in adapterGroups) { try { var adapterCode = grp.Key!; var sourceIds = grp.Select(d => d.SourceId!).ToList(); // V2.0 步骤 5:baseUrl 从 defaultBaseUrl 传入(与方案 §3 步骤 5 一致) var result = await _gatewayClient.GetRealtimeBatchAsync(defaultBaseUrl, adapterCode, sourceIds); foreach (var dev in grp) { if (result.TryGetValue(dev.SourceId!, out var points)) realtimeCache[dev.DeviceId] = points; } } catch (Exception ex) { _logger.LogError(ex, "[RuleEngine] 网关批量取实时值失败: Adapter={Adapter}", grp.Key); } } // 6. 逐规则评估(按 Priority 倒序,数字越大越先评估) foreach (var rule in rules.OrderByDescending(r => r.Priority)) { var ruleSw = Stopwatch.StartNew(); try { // 6a. 全局冷却检查(距离上次触发不足 CooldownSec 秒则跳过) if (rule.LastTriggered.HasValue && (DateTime.Now - rule.LastTriggered.Value).TotalSeconds < rule.CooldownSec) { continue; } // 6b. 评估所有条件 var condResults = new List<(warehouse_rulecondition cond, bool met, decimal? actualValue)>(); foreach (var cond in rule.warehouse_rulecondition) { if (!cond.DeviceId.HasValue) continue; if (!realtimeCache.TryGetValue(cond.DeviceId.Value, out var points)) continue; var point = FindPointByValueId(points, cond.ValueId); bool met = EvaluateCondition(cond, point?.Value); condResults.Add((cond, met, point?.Value)); } // 至少需要 1 个有效条件才评估 if (!condResults.Any()) continue; // AND / OR 评估 bool triggered = rule.JudgmentMode == "OR" ? condResults.Any(c => c.met) : condResults.All(c => c.met); // 6c. 触发 → 执行动作链 if (triggered) { await ExecuteActionsAsync(rule, devices, condResults, defaultBaseUrl); rule.LastTriggered = DateTime.Now; } rule.LastEvaluated = DateTime.Now; // 立即保存(saveChanges=true)确保状态持久化 _ruleRepo.Update(rule, true); // 6d. 写执行日志 var logEntry = new warehouse_rulelog { RuleID = rule.RuleID, Triggered = triggered ? "是" : "否", ConditionSummary = string.Join(";", condResults.Select(c => $"id={c.cond.id}({c.met},val={c.actualValue?.ToString() ?? "null"})")), EvaluatedAt = DateTime.Now, DurationMs = (int)ruleSw.ElapsedMilliseconds }; _logRepo.Add(logEntry, true); } catch (Exception ex) { _logger.LogError(ex, "[RuleEngine] 规则 {RuleID} 评估失败", rule.RuleID); } } sw.Stop(); _logger.LogInformation("[RuleEngine] 评估 {Count} 条规则, 触发 {Triggered} 条, 耗时 {Ms}ms", rules.Count, rules.Count(r => r.LastTriggered == DateTime.Now.Date || (r.LastTriggered.HasValue && (DateTime.Now - r.LastTriggered.Value).TotalSeconds < 1)), sw.ElapsedMilliseconds); } /// /// 执行规则下的所有动作(MVP 阶段:串行执行;并发留待 V2.1 优化)。 /// 串行原因:动作多为"控制+告警+通知"组合,串行可保证告警→通知时序一致。 /// /// 触发的规则 /// DeviceId → 设备映射 /// 条件评估结果 /// gateway_nodes.default.BaseUrl(已查好) private async Task ExecuteActionsAsync( warehouse_rule rule, Dictionary devices, List<(warehouse_rulecondition cond, bool met, decimal? actualValue)> condResults, string defaultBaseUrl) { foreach (var action in rule.warehouse_ruleaction.OrderByDescending(a => a.id)) { try { switch (action.ActionType ?? "控制") { case "控制": if (action.DeviceId.HasValue && devices.TryGetValue(action.DeviceId.Value, out var dev) && !string.IsNullOrEmpty(dev.AdapterCode) && !string.IsNullOrEmpty(dev.SourceId)) { var ok = await _gatewayClient.ControlDeviceAsync( defaultBaseUrl, dev.AdapterCode, dev.SourceId, action.ValueId ?? 0, (double)(action.TargetValue_Number ?? 0)); _logger.LogInformation("[RuleEngine] 规则 {RuleID} 控制 {Adapter}/{SourceId} 点位 {Point} = {Value} -> {Result}", rule.RuleID, dev.AdapterCode, dev.SourceId, action.ValueId, action.TargetValue_Number, ok ? "OK" : "FAIL"); } else { _logger.LogWarning("[RuleEngine] 规则 {RuleID} 动作 {ActionId} 设备缺失或未配置 AdapterCode/SourceId", rule.RuleID, action.id); } break; case "告警": if (action.Alert == "是") { var triggerSource = condResults.FirstOrDefault(c => c.met); string? deviceSourceId = null; if (action.DeviceId.HasValue && devices.TryGetValue(action.DeviceId.Value, out var actionDev)) { deviceSourceId = actionDev.SourceId; } await _alarmService.UpsertAlarmAsync(new SyncAlarmItem { SourceAlarmId = $"rule_{rule.RuleID}_{DateTime.Now:yyyyMMddHHmmssfff}", DeviceSourceId = deviceSourceId ?? "", AdapterCode = "RuleEngine", Level = "重要", Desc = action.AlertMessage ?? rule.Title, Value = triggerSource.actualValue.HasValue ? (double)triggerSource.actualValue.Value : (double?)null, StartTime = DateTime.Now.ToString("o") }, action.DeviceId); _logger.LogInformation("[RuleEngine] 规则 {RuleID} 已生成告警: {Message}", rule.RuleID, action.AlertMessage ?? rule.Title); } break; case "通知": // 推送 SignalR(事件名 RuleTriggered 与 DataView.vue:19 订阅名一致) await _hub.Clients.All.SendAsync("RuleTriggered", new { ruleId = rule.RuleID, title = rule.Title, alertMessage = action.AlertMessage ?? rule.Title, level = "重要", timestamp = DateTime.Now.ToString("o"), triggeredDevices = condResults.Where(c => c.met) .Select(c => c.cond.DeviceId).ToList() }); _logger.LogInformation("[RuleEngine] 规则 {RuleID} 已推送 SignalR 通知", rule.RuleID); break; default: _logger.LogWarning("[RuleEngine] 规则 {RuleID} 动作 {ActionId} 未知类型: {Type}", rule.RuleID, action.id, action.ActionType); break; } } catch (Exception ex) { // 单条动作失败不阻塞后续动作 _logger.LogError(ex, "[RuleEngine] 规则 {RuleID} 动作 {ActionId} 执行失败: {Type}", rule.RuleID, action.id, action.ActionType); } } } /// /// 在设备实时点位列表里按 ValueId(点位索引)查找。 /// /// 设备的实时点位列表 /// 点位索引(warehouse_rulecondition.ValueId) /// 匹配的点位,未找到返回 null private PointValue? FindPointByValueId(List points, int? valueId) { if (!valueId.HasValue) return null; return points.FirstOrDefault(p => p.PointIndex == valueId.Value); } /// /// 评估单条条件是否满足(数值/开关 + 比较运算 + 滞后窗)。 /// /// 规则条件 /// 实际实时值(点位 Value) /// true=满足条件 private bool EvaluateCondition(warehouse_rulecondition cond, decimal? actualValue) { // 没有实际值(如设备离线/点位无数据)→ 不满足 if (!actualValue.HasValue) return false; bool isSwitch = cond.Type == "开关状态"; bool isNumeric = cond.Type == "数值"; if (isSwitch) { // 开关型:actualValue 非 0 = "开",0 = "关" var actualSwitch = actualValue.Value != 0 ? "开" : "关"; var targetSwitch = cond.TargetValue_Switch ?? "开"; bool met = CompareByOperator(cond.CompareOperator, actualSwitch, targetSwitch); // 滞后窗:如果当前未满足且配置了恢复阈值,则用恢复阈值再判一次 if (!met && !string.IsNullOrEmpty(cond.RecoveryThreshold_Switch)) { met = actualSwitch == cond.RecoveryThreshold_Switch; } return met; } else { // 数值型(默认走数值分支) var actual = actualValue.Value; var target = cond.TargetValue_Number ?? 0; bool met = CompareByOperator(cond.CompareOperator, (double)actual, (double)target); // 滞后窗 if (!met && cond.RecoveryThreshold_Numeric.HasValue) { met = CompareByOperator(cond.CompareOperator, (double)actual, (double)cond.RecoveryThreshold_Numeric.Value); } return met; } } /// /// 通用比较器(支持 > < = ≥ ≤ ≠)。 /// private bool CompareByOperator(string? op, double actual, double target) { return (op ?? "=") switch { ">" => actual > target, "<" => actual < target, "=" => Math.Abs(actual - target) < 0.0001, "≥" or ">=" => actual >= target, "≤" or "<=" => actual <= target, "≠" or "!=" => Math.Abs(actual - target) >= 0.0001, _ => false }; } /// /// 字符串比较器(开关型专用)。 /// private bool CompareByOperator(string? op, string actual, string target) { return (op ?? "=") switch { "=" => actual == target, "≠" or "!=" => actual != target, _ => false }; } }