// ═══════════════════════════════════════════
// 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
};
}
}