|
|
@@ -1,6 +1,7 @@
|
|
|
using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
|
|
|
+using Microsoft.Extensions.Logging;
|
|
|
using SqlSugar;
|
|
|
using System.Data;
|
|
|
using System.Globalization;
|
|
|
@@ -25,6 +26,7 @@ public class S8WatchSchedulerService : ITransient
|
|
|
private readonly S8TimeoutRuleEvaluator _timeoutEvaluator;
|
|
|
private readonly S8ShortageRuleEvaluator _shortageEvaluator;
|
|
|
private readonly S8OutOfRangeRuleEvaluator _outOfRangeEvaluator;
|
|
|
+ private readonly ILogger<S8WatchSchedulerService> _logger;
|
|
|
|
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
|
private const string SqlDataSourceType = "SQL";
|
|
|
@@ -45,7 +47,8 @@ public class S8WatchSchedulerService : ITransient
|
|
|
S8ManualReportService manualReportService,
|
|
|
S8TimeoutRuleEvaluator timeoutEvaluator,
|
|
|
S8ShortageRuleEvaluator shortageEvaluator,
|
|
|
- S8OutOfRangeRuleEvaluator outOfRangeEvaluator)
|
|
|
+ S8OutOfRangeRuleEvaluator outOfRangeEvaluator,
|
|
|
+ ILogger<S8WatchSchedulerService> logger)
|
|
|
{
|
|
|
_ruleRep = ruleRep;
|
|
|
_alertRuleRep = alertRuleRep;
|
|
|
@@ -57,6 +60,7 @@ public class S8WatchSchedulerService : ITransient
|
|
|
_timeoutEvaluator = timeoutEvaluator;
|
|
|
_shortageEvaluator = shortageEvaluator;
|
|
|
_outOfRangeEvaluator = outOfRangeEvaluator;
|
|
|
+ _logger = logger;
|
|
|
}
|
|
|
|
|
|
public async Task<List<S8WatchExecutionRule>> LoadExecutionRulesAsync(long tenantId, long factoryId)
|
|
|
@@ -454,6 +458,18 @@ public class S8WatchSchedulerService : ITransient
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
+ // R5-RECOVERY-MINIMAL-1:evaluator 成功执行后调和 recovered_at。
|
|
|
+ // 仅在 evaluator 成功(throw 不会到这里)时才能判定"未命中"。仅写 recovered_at + updated_at,
|
|
|
+ // 绝不动 status / assignee / verifier / source_payload / last_detected_at。
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await ReconcileRecoveriesForRuleAsync(tenantId, factoryId, rule, ruleType, hits);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogWarning(ex, "recovery_reconcile_failed ruleCode={RuleCode} ruleType={RuleType}", rule.RuleCode, ruleType);
|
|
|
+ }
|
|
|
+
|
|
|
foreach (var hit in hits)
|
|
|
{
|
|
|
if (string.IsNullOrWhiteSpace(hit.DedupKey))
|
|
|
@@ -609,6 +625,56 @@ public class S8WatchSchedulerService : ITransient
|
|
|
.ExecuteCommandAsync();
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// R5 恢复时间最小闭环:对当前 rule 下未关闭、有 dedup_key、recovered_at 仍为 NULL 的异常,
|
|
|
+ /// 凡不在本轮 hits.dedup_key 集合内的,写入 recovered_at = now、updated_at = now。
|
|
|
+ /// 仅写这 2 列;不动 status / assignee / verifier / source_payload / last_detected_at;
|
|
|
+ /// recovered_at 一旦写入,本轮不做复发清空。
|
|
|
+ /// </summary>
|
|
|
+ private async Task ReconcileRecoveriesForRuleAsync(
|
|
|
+ long tenantId, long factoryId, AdoS8WatchRule rule, string ruleType, List<S8RuleHit> hits)
|
|
|
+ {
|
|
|
+ var hitDedupKeys = hits
|
|
|
+ .Where(h => !string.IsNullOrWhiteSpace(h.DedupKey))
|
|
|
+ .Select(h => h.DedupKey)
|
|
|
+ .ToHashSet(StringComparer.Ordinal);
|
|
|
+
|
|
|
+ var candidates = await _exceptionRep.AsQueryable()
|
|
|
+ .Where(x => x.TenantId == tenantId
|
|
|
+ && x.FactoryId == factoryId
|
|
|
+ && !x.IsDeleted
|
|
|
+ && x.Status != "CLOSED"
|
|
|
+ && x.SourceRuleCode == rule.RuleCode
|
|
|
+ && x.DedupKey != null
|
|
|
+ && x.RecoveredAt == null)
|
|
|
+ .Select(x => new { x.Id, x.DedupKey })
|
|
|
+ .ToListAsync();
|
|
|
+ if (candidates.Count == 0) return;
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var recoveredIds = new List<long>();
|
|
|
+ foreach (var c in candidates)
|
|
|
+ {
|
|
|
+ if (hitDedupKeys.Contains(c.DedupKey!)) continue;
|
|
|
+ await _exceptionRep.Context.Updateable<AdoS8Exception>()
|
|
|
+ .SetColumns(x => new AdoS8Exception
|
|
|
+ {
|
|
|
+ RecoveredAt = now,
|
|
|
+ UpdatedAt = now
|
|
|
+ })
|
|
|
+ .Where(x => x.Id == c.Id)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ recoveredIds.Add(c.Id);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (recoveredIds.Count > 0)
|
|
|
+ {
|
|
|
+ _logger.LogInformation(
|
|
|
+ "rule_recovered ruleCode={RuleCode} ruleType={RuleType} recoveredCount={Count} recoveredIds={Ids}",
|
|
|
+ rule.RuleCode, ruleType, recoveredIds.Count, string.Join(",", recoveredIds));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private async Task RefreshDetectionAsync(long exceptionId, S8RuleHit hit)
|
|
|
{
|
|
|
await _exceptionRep.Context.Updateable<AdoS8Exception>()
|