|
@@ -1,5 +1,6 @@
|
|
|
using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
|
|
|
using SqlSugar;
|
|
using SqlSugar;
|
|
|
using System.Data;
|
|
using System.Data;
|
|
|
using System.Globalization;
|
|
using System.Globalization;
|
|
@@ -18,8 +19,10 @@ public class S8WatchSchedulerService : ITransient
|
|
|
private readonly SqlSugarRepository<AdoS8AlertRule> _alertRuleRep;
|
|
private readonly SqlSugarRepository<AdoS8AlertRule> _alertRuleRep;
|
|
|
private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
|
|
private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
|
|
|
private readonly SqlSugarRepository<AdoS8Exception> _exceptionRep;
|
|
private readonly SqlSugarRepository<AdoS8Exception> _exceptionRep;
|
|
|
|
|
+ private readonly SqlSugarRepository<AdoS8ExceptionType> _exceptionTypeRep;
|
|
|
private readonly S8NotificationService _notificationService;
|
|
private readonly S8NotificationService _notificationService;
|
|
|
private readonly S8ManualReportService _manualReportService;
|
|
private readonly S8ManualReportService _manualReportService;
|
|
|
|
|
+ private readonly S8TimeoutRuleEvaluator _timeoutEvaluator;
|
|
|
|
|
|
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
|
private const string SqlDataSourceType = "SQL";
|
|
private const string SqlDataSourceType = "SQL";
|
|
@@ -35,15 +38,19 @@ public class S8WatchSchedulerService : ITransient
|
|
|
SqlSugarRepository<AdoS8AlertRule> alertRuleRep,
|
|
SqlSugarRepository<AdoS8AlertRule> alertRuleRep,
|
|
|
SqlSugarRepository<AdoS8DataSource> dataSourceRep,
|
|
SqlSugarRepository<AdoS8DataSource> dataSourceRep,
|
|
|
SqlSugarRepository<AdoS8Exception> exceptionRep,
|
|
SqlSugarRepository<AdoS8Exception> exceptionRep,
|
|
|
|
|
+ SqlSugarRepository<AdoS8ExceptionType> exceptionTypeRep,
|
|
|
S8NotificationService notificationService,
|
|
S8NotificationService notificationService,
|
|
|
- S8ManualReportService manualReportService)
|
|
|
|
|
|
|
+ S8ManualReportService manualReportService,
|
|
|
|
|
+ S8TimeoutRuleEvaluator timeoutEvaluator)
|
|
|
{
|
|
{
|
|
|
_ruleRep = ruleRep;
|
|
_ruleRep = ruleRep;
|
|
|
_alertRuleRep = alertRuleRep;
|
|
_alertRuleRep = alertRuleRep;
|
|
|
_dataSourceRep = dataSourceRep;
|
|
_dataSourceRep = dataSourceRep;
|
|
|
_exceptionRep = exceptionRep;
|
|
_exceptionRep = exceptionRep;
|
|
|
|
|
+ _exceptionTypeRep = exceptionTypeRep;
|
|
|
_notificationService = notificationService;
|
|
_notificationService = notificationService;
|
|
|
_manualReportService = manualReportService;
|
|
_manualReportService = manualReportService;
|
|
|
|
|
+ _timeoutEvaluator = timeoutEvaluator;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public async Task<List<S8WatchExecutionRule>> LoadExecutionRulesAsync(long tenantId, long factoryId)
|
|
public async Task<List<S8WatchExecutionRule>> LoadExecutionRulesAsync(long tenantId, long factoryId)
|
|
@@ -370,9 +377,211 @@ public class S8WatchSchedulerService : ITransient
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // R2: TIMEOUT 路径,按 rule_type='TIMEOUT' 分派;OUT_OF_RANGE 走上方既有兼容分支不动。
|
|
|
|
|
+ // 未知 rule_type 与 null 由 LoadExecutionRulesAsync 既有过滤(DEVICE + S2S6_PRODUCTION + AlertRule)天然隔离,
|
|
|
|
|
+ // 不在本路径处理;不抛出全局异常。
|
|
|
|
|
+ var timeoutResults = await ProcessTimeoutRulesAsync(tenantId, factoryId);
|
|
|
|
|
+ results.AddRange(timeoutResults);
|
|
|
|
|
+
|
|
|
|
|
+ return results;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// R2 TIMEOUT 类规则主链:装载 enabled WatchRule.RuleType='TIMEOUT' → evaluator → dedup_key 去重 → 建单/刷新。
|
|
|
|
|
+ /// dedup 命中:UPDATE last_detected_at + source_payload,不重复建单;
|
|
|
|
|
+ /// dedup 未命中:校验 ExceptionTypeCode 是否在 baseline(tenant=0/factory=0 全局或本租户工厂),缺则跳过;
|
|
|
|
|
+ /// 通过 → S8ManualReportService.CreateFromHitAsync 落标准 AdoS8Exception,新列全部回填。
|
|
|
|
|
+ /// 不做 SLA 升级、不做事件触发、不做 RecoveredAt。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task<List<S8WatchCreationResult>> ProcessTimeoutRulesAsync(long tenantId, long factoryId)
|
|
|
|
|
+ {
|
|
|
|
|
+ var results = new List<S8WatchCreationResult>();
|
|
|
|
|
+
|
|
|
|
|
+ var timeoutRules = await _ruleRep.AsQueryable()
|
|
|
|
|
+ .Where(x => x.TenantId == tenantId
|
|
|
|
|
+ && x.FactoryId == factoryId
|
|
|
|
|
+ && x.Enabled
|
|
|
|
|
+ && x.RuleType == S8TimeoutRuleEvaluator.RuleTypeCode)
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+ if (timeoutRules.Count == 0) return results;
|
|
|
|
|
+
|
|
|
|
|
+ var alertRules = (await _alertRuleRep.AsQueryable()
|
|
|
|
|
+ .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId)
|
|
|
|
|
+ .ToListAsync()).AsReadOnly();
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var rule in timeoutRules.OrderBy(x => x.Id))
|
|
|
|
|
+ {
|
|
|
|
|
+ List<S8RuleHit> hits;
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ hits = await _timeoutEvaluator.EvaluateAsync(tenantId, factoryId, rule, alertRules);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "evaluate_failed", ex.Message));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var hit in hits)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(hit.DedupKey))
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "missing_dedup_key", null, hit));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long matchedId;
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ matchedId = await FindOpenExceptionByDedupKeyAsync(tenantId, factoryId, hit.DedupKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "query_failed", ex.Message, hit));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (matchedId > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await RefreshDetectionAsync(matchedId, hit);
|
|
|
|
|
+ results.Add(BuildSkippedDuplicate(rule, hit, matchedId));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "refresh_failed", ex.Message, hit));
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bool typeExists;
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ typeExists = await _exceptionTypeRep.AsQueryable()
|
|
|
|
|
+ .Where(t => t.TypeCode == hit.ExceptionTypeCode
|
|
|
|
|
+ && (t.TenantId == 0 || t.TenantId == tenantId)
|
|
|
|
|
+ && (t.FactoryId == 0 || t.FactoryId == factoryId)
|
|
|
|
|
+ && t.Enabled)
|
|
|
|
|
+ .AnyAsync();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "query_failed", ex.Message, hit));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!typeExists)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "exception_type_missing", null, hit));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ var entity = await _manualReportService.CreateFromHitAsync(hit);
|
|
|
|
|
+ results.Add(BuildCreatedResult(rule, hit, entity.Id));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ results.Add(BuildSkipResult(rule, "create_failed", ex.Message, hit));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return results;
|
|
return results;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private async Task<long> FindOpenExceptionByDedupKeyAsync(long tenantId, long factoryId, string dedupKey)
|
|
|
|
|
+ {
|
|
|
|
|
+ var ids = await _exceptionRep.AsQueryable()
|
|
|
|
|
+ .Where(x => x.TenantId == tenantId
|
|
|
|
|
+ && x.FactoryId == factoryId
|
|
|
|
|
+ && !x.IsDeleted
|
|
|
|
|
+ && x.Status != "CLOSED"
|
|
|
|
|
+ && x.DedupKey == dedupKey)
|
|
|
|
|
+ .Select(x => x.Id)
|
|
|
|
|
+ .Take(1)
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+ return ids.Count > 0 ? ids[0] : 0L;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async Task RefreshDetectionAsync(long exceptionId, S8RuleHit hit)
|
|
|
|
|
+ {
|
|
|
|
|
+ await _exceptionRep.Context.Updateable<AdoS8Exception>()
|
|
|
|
|
+ .SetColumns(x => new AdoS8Exception
|
|
|
|
|
+ {
|
|
|
|
|
+ LastDetectedAt = hit.DetectedAt,
|
|
|
|
|
+ SourcePayload = hit.SourcePayload,
|
|
|
|
|
+ UpdatedAt = DateTime.Now
|
|
|
|
|
+ })
|
|
|
|
|
+ .Where(x => x.Id == exceptionId)
|
|
|
|
|
+ .ExecuteCommandAsync();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static S8WatchCreationResult BuildCreatedResult(AdoS8WatchRule rule, S8RuleHit hit, long exceptionId) =>
|
|
|
|
|
+ new()
|
|
|
|
|
+ {
|
|
|
|
|
+ DedupResult = new S8WatchDedupResult
|
|
|
|
|
+ {
|
|
|
|
|
+ Hit = ToWatchHit(rule, hit),
|
|
|
|
|
+ CanCreate = true,
|
|
|
|
|
+ MatchedExceptionId = null,
|
|
|
|
|
+ Reason = "no_pending"
|
|
|
|
|
+ },
|
|
|
|
|
+ Created = true,
|
|
|
|
|
+ Skipped = false,
|
|
|
|
|
+ CreatedExceptionId = exceptionId,
|
|
|
|
|
+ Reason = "auto_created",
|
|
|
|
|
+ ErrorMessage = null
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static S8WatchCreationResult BuildSkippedDuplicate(AdoS8WatchRule rule, S8RuleHit hit, long matchedId) =>
|
|
|
|
|
+ new()
|
|
|
|
|
+ {
|
|
|
|
|
+ DedupResult = new S8WatchDedupResult
|
|
|
|
|
+ {
|
|
|
|
|
+ Hit = ToWatchHit(rule, hit),
|
|
|
|
|
+ CanCreate = false,
|
|
|
|
|
+ MatchedExceptionId = matchedId,
|
|
|
|
|
+ Reason = "duplicate_pending"
|
|
|
|
|
+ },
|
|
|
|
|
+ Created = false,
|
|
|
|
|
+ Skipped = true,
|
|
|
|
|
+ CreatedExceptionId = null,
|
|
|
|
|
+ Reason = "duplicate_pending",
|
|
|
|
|
+ ErrorMessage = null
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static S8WatchCreationResult BuildSkipResult(AdoS8WatchRule rule, string reason, string? error, S8RuleHit? hit = null) =>
|
|
|
|
|
+ new()
|
|
|
|
|
+ {
|
|
|
|
|
+ DedupResult = new S8WatchDedupResult
|
|
|
|
|
+ {
|
|
|
|
|
+ Hit = hit != null ? ToWatchHit(rule, hit) : new S8WatchHitResult { SourceRuleId = rule.Id, SourceRuleCode = rule.RuleCode },
|
|
|
|
|
+ CanCreate = false,
|
|
|
|
|
+ MatchedExceptionId = null,
|
|
|
|
|
+ Reason = reason
|
|
|
|
|
+ },
|
|
|
|
|
+ Created = false,
|
|
|
|
|
+ Skipped = true,
|
|
|
|
|
+ CreatedExceptionId = null,
|
|
|
|
|
+ Reason = reason,
|
|
|
|
|
+ ErrorMessage = error
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static S8WatchHitResult ToWatchHit(AdoS8WatchRule rule, S8RuleHit hit) => new()
|
|
|
|
|
+ {
|
|
|
|
|
+ SourceRuleId = hit.SourceRuleId == 0 ? rule.Id : hit.SourceRuleId,
|
|
|
|
|
+ SourceRuleCode = string.IsNullOrEmpty(hit.SourceRuleCode) ? rule.RuleCode : hit.SourceRuleCode,
|
|
|
|
|
+ DataSourceId = hit.DataSourceId,
|
|
|
|
|
+ RelatedObjectCode = hit.RelatedObjectCode,
|
|
|
|
|
+ Severity = hit.Severity,
|
|
|
|
|
+ OccurrenceDeptId = hit.OccurrenceDeptId,
|
|
|
|
|
+ ResponsibleDeptId = hit.ResponsibleDeptId,
|
|
|
|
|
+ SourcePayload = hit.SourcePayload
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 单次轮询入口。当前仅完成规则读取与组装,返回可执行规则数量,不做实际数据采集。
|
|
/// 单次轮询入口。当前仅完成规则读取与组装,返回可执行规则数量,不做实际数据采集。
|
|
|
/// </summary>
|
|
/// </summary>
|