|
|
@@ -39,6 +39,11 @@ public class S8WatchSchedulerService : ITransient
|
|
|
private const string DetectResultNoHit = "NO_HIT";
|
|
|
private const string DetectResultEvaluateFailed = "EVALUATE_FAILED";
|
|
|
|
|
|
+ // S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细写入限频窗口。
|
|
|
+ // 同 (tenant, factory, rule_code, dedup_key) 在 10 分钟内最多写一条 REFRESHED detection_log;
|
|
|
+ // 业务侧 RefreshDetectionAsync / BackfillLegacyExceptionAsync 不受限频影响。
|
|
|
+ private const int RefreshedRateLimitMinutes = 10;
|
|
|
+
|
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
|
private const string SqlDataSourceType = "SQL";
|
|
|
|
|
|
@@ -849,6 +854,41 @@ public class S8WatchSchedulerService : ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细限频判定。
|
|
|
+ /// 查询 cutoff = detectedAt - RefreshedRateLimitMinutes 之后,
|
|
|
+ /// 是否已有同 (tenant_id, factory_id, rule_code, dedup_key, DetectResult=REFRESHED) 的 detection_log;
|
|
|
+ /// 命中既有索引 idx_s8_detection_log_dedup_time。查询失败仅 LogWarning 后返回 false,
|
|
|
+ /// 宁可多写一条 REFRESHED 明细,也不阻断调度主链路;ruleCode / dedupKey 空白时同样返回 false(保留写入)。
|
|
|
+ /// 业务侧 RefreshDetectionAsync / BackfillLegacyExceptionAsync 不在限频范围。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<bool> HasRecentRefreshedDetectionLogAsync(
|
|
|
+ long tenantId, long factoryId, string ruleCode, string dedupKey, DateTime detectedAt)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(ruleCode) || string.IsNullOrWhiteSpace(dedupKey))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ var cutoff = detectedAt.AddMinutes(-RefreshedRateLimitMinutes);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _detectionLogRep.AsQueryable()
|
|
|
+ .Where(x => x.TenantId == tenantId
|
|
|
+ && x.FactoryId == factoryId
|
|
|
+ && x.DedupKey == dedupKey
|
|
|
+ && x.RuleCode == ruleCode
|
|
|
+ && x.DetectResult == DetectResultRefreshed
|
|
|
+ && x.DetectedAt >= cutoff)
|
|
|
+ .AnyAsync();
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogWarning(ex,
|
|
|
+ "detection_log_refreshed_ratelimit_check_failed ruleCode={RuleCode} dedupKey={DedupKey}",
|
|
|
+ ruleCode, dedupKey);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private static string Truncate(string? s, int max) =>
|
|
|
string.IsNullOrEmpty(s) ? string.Empty : (s.Length <= max ? s : s.Substring(0, max));
|
|
|
|
|
|
@@ -1287,18 +1327,10 @@ public class S8WatchSchedulerService : ITransient
|
|
|
recoveredIds = new();
|
|
|
}
|
|
|
|
|
|
- if (hits.Count == 0 && recoveredIds.Count == 0)
|
|
|
- {
|
|
|
- await WriteDetectionLogAsync(new AdoS8DetectionLog
|
|
|
- {
|
|
|
- TenantId = tenantId, FactoryId = factoryId,
|
|
|
- RuleId = rule.Id, RuleCode = rule.RuleCode, RuleType = ruleType, SceneCode = rule.SceneCode,
|
|
|
- SourceObjectType = rule.SourceObjectType,
|
|
|
- DetectResult = DetectResultNoHit,
|
|
|
- DetectedAt = DateTime.Now,
|
|
|
- RunId = runId, TriggerSource = DetectionTriggerSource
|
|
|
- });
|
|
|
- }
|
|
|
+ // S8-DETECTION-LOG-WRITE-REDUCE-P1-1:NO_HIT 不再写 detection_log 明细。
|
|
|
+ // 规则级"被定期评估"信号由 ado_s8_watch_rule.LastRunAt / LastStatus / LastRunId / LastDurationMs
|
|
|
+ // 在 OnRuleCompletedAsync / ApplyRunOnceCompletionAsync 中承接,无需 detection_log 冗余记录。
|
|
|
+ // DetectResultNoHit 常量保留(历史数据反查 / 未来如需恢复明细写入)。
|
|
|
|
|
|
foreach (var hit in hits)
|
|
|
{
|
|
|
@@ -1324,7 +1356,10 @@ public class S8WatchSchedulerService : ITransient
|
|
|
try
|
|
|
{
|
|
|
await RefreshDetectionAsync(matchedId, hit);
|
|
|
- await WriteDetectionLogAsync(BuildHitLog(tenantId, factoryId, rule, ruleType, hit, DetectResultRefreshed, matchedId, runId));
|
|
|
+ if (!await HasRecentRefreshedDetectionLogAsync(tenantId, factoryId, rule.RuleCode, hit.DedupKey, hit.DetectedAt))
|
|
|
+ {
|
|
|
+ await WriteDetectionLogAsync(BuildHitLog(tenantId, factoryId, rule, ruleType, hit, DetectResultRefreshed, matchedId, runId));
|
|
|
+ }
|
|
|
results.Add(BuildSkippedDuplicate(rule, hit, matchedId));
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
@@ -1351,7 +1386,10 @@ public class S8WatchSchedulerService : ITransient
|
|
|
try
|
|
|
{
|
|
|
await BackfillLegacyExceptionAsync(compatId, hit);
|
|
|
- await WriteDetectionLogAsync(BuildHitLog(tenantId, factoryId, rule, ruleType, hit, DetectResultRefreshed, compatId, runId));
|
|
|
+ if (!await HasRecentRefreshedDetectionLogAsync(tenantId, factoryId, rule.RuleCode, hit.DedupKey, hit.DetectedAt))
|
|
|
+ {
|
|
|
+ await WriteDetectionLogAsync(BuildHitLog(tenantId, factoryId, rule, ruleType, hit, DetectResultRefreshed, compatId, runId));
|
|
|
+ }
|
|
|
results.Add(BuildSkippedDuplicate(rule, hit, compatId));
|
|
|
}
|
|
|
catch (Exception ex)
|