|
|
@@ -0,0 +1,143 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
+using Microsoft.Extensions.Logging;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// S8-TIMEOUT-AUTO-ESCALATION-JOB-1(P4-1):扫描 sla_deadline 已超时且未关闭/未已升级的异常,
|
|
|
+/// 通过 <see cref="S8TaskFlowService.UpgradeAsync"/> 启动 EXCEPTION_ESCALATION ApprovalFlow,与人工升级链路 100% 等价。
|
|
|
+///
|
|
|
+/// 设计要点:
|
|
|
+/// - 不依赖 timeout_flag;扫描公式与读端 IsCurrentlyTimeout 一致:sla_deadline IS NOT NULL AND sla_deadline < now
|
|
|
+/// AND status NOT IN ('CLOSED','RECOVERED','ESCALATED')。
|
|
|
+/// - status 进一步限制在 ASSIGNED / IN_PROGRESS(与 <see cref="Infrastructure.S8.S8StatusRules"/> 允许 ESCALATED 的转移一致);
|
|
|
+/// NEW / PENDING_VERIFICATION / REJECTED 等状态不通过本 Job 自动升级。
|
|
|
+/// - 已有 active_flow_instance_id 的异常跳过(防重);UpgradeAsync 内部还会再校验一次,双层保险。
|
|
|
+/// - exception_type.escalate_role_code 为空 / 非法时跳过并 LogInformation;不写脏数据,不补默认。
|
|
|
+/// - 状态 / Timeline 由 <see cref="ExceptionEscalationBizHandler"/>.OnFlowStarted 写入;本服务不重复维护。
|
|
|
+/// - 通知层走 <see cref="S8NotificationLayerResolver"/>;当前 baseline notify_channel="log",无外部副作用。
|
|
|
+/// </summary>
|
|
|
+public class S8TimeoutAutoEscalationService : ITransient
|
|
|
+{
|
|
|
+ private readonly SqlSugarRepository<AdoS8Exception> _rep;
|
|
|
+ private readonly SqlSugarRepository<AdoS8ExceptionType> _typeRep;
|
|
|
+ private readonly S8TaskFlowService _taskFlow;
|
|
|
+ private readonly S8NotificationLayerResolver _layerResolver;
|
|
|
+ private readonly ILogger<S8TimeoutAutoEscalationService> _logger;
|
|
|
+
|
|
|
+ // S8StatusRules 中 ESCALATED 仅允许从 ASSIGNED / IN_PROGRESS 转入。
|
|
|
+ private static readonly string[] EscalatableStatuses = { "ASSIGNED", "IN_PROGRESS" };
|
|
|
+
|
|
|
+ public S8TimeoutAutoEscalationService(
|
|
|
+ SqlSugarRepository<AdoS8Exception> rep,
|
|
|
+ SqlSugarRepository<AdoS8ExceptionType> typeRep,
|
|
|
+ S8TaskFlowService taskFlow,
|
|
|
+ S8NotificationLayerResolver layerResolver,
|
|
|
+ ILogger<S8TimeoutAutoEscalationService> logger)
|
|
|
+ {
|
|
|
+ _rep = rep;
|
|
|
+ _typeRep = typeRep;
|
|
|
+ _taskFlow = taskFlow;
|
|
|
+ _layerResolver = layerResolver;
|
|
|
+ _logger = logger;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 扫描一次。返回成功触发升级的异常数量;调用方负责调度 / 限流。
|
|
|
+ /// </summary>
|
|
|
+ public async Task<int> RunOnceAsync(int batchSize = 50, CancellationToken ct = default)
|
|
|
+ {
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var candidates = await _rep.AsQueryable()
|
|
|
+ .Where(x => !x.IsDeleted
|
|
|
+ && x.SlaDeadline != null
|
|
|
+ && x.SlaDeadline < now
|
|
|
+ && EscalatableStatuses.Contains(x.Status)
|
|
|
+ && (x.ActiveFlowInstanceId == null || x.ActiveFlowInstanceId == 0))
|
|
|
+ .OrderBy(x => x.SlaDeadline)
|
|
|
+ .Take(batchSize)
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ if (candidates.Count == 0) return 0;
|
|
|
+
|
|
|
+ // exception_type.escalate_role_code 批量映射(factory 优先以与现有 ResolveModuleCodeAsync 一致)
|
|
|
+ var typeCodes = candidates
|
|
|
+ .Select(c => c.ExceptionTypeCode)
|
|
|
+ .Where(c => !string.IsNullOrWhiteSpace(c))
|
|
|
+ .Distinct()
|
|
|
+ .Select(c => c!)
|
|
|
+ .ToList();
|
|
|
+ var typeMap = typeCodes.Count == 0
|
|
|
+ ? new Dictionary<string, AdoS8ExceptionType>()
|
|
|
+ : (await _typeRep.AsQueryable().ClearFilter()
|
|
|
+ .Where(t => typeCodes.Contains(t.TypeCode))
|
|
|
+ .OrderByDescending(t => t.FactoryId)
|
|
|
+ .ToListAsync())
|
|
|
+ .GroupBy(t => t.TypeCode)
|
|
|
+ .ToDictionary(g => g.Key, g => g.First());
|
|
|
+
|
|
|
+ var processed = 0;
|
|
|
+ foreach (var e in candidates)
|
|
|
+ {
|
|
|
+ ct.ThrowIfCancellationRequested();
|
|
|
+ if (string.IsNullOrWhiteSpace(e.ExceptionTypeCode)
|
|
|
+ || !typeMap.TryGetValue(e.ExceptionTypeCode, out var type)
|
|
|
+ || string.IsNullOrWhiteSpace(type.EscalateRoleCode))
|
|
|
+ {
|
|
|
+ _logger.LogInformation(
|
|
|
+ "s8_timeout_auto_escalate_skip exceptionId={Id} exceptionCode={Code} reason=escalate_role_empty typeCode={TypeCode}",
|
|
|
+ e.Id, e.ExceptionCode, e.ExceptionTypeCode);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // UpgradeAsync 已内置 ActiveFlowInstanceId / IsAllowedTransition 二次校验;
|
|
|
+ // 与 manual upgrade 100% 等价,状态/timeline 由 OnFlowStarted 写入。
|
|
|
+ var remark = $"[AUTO] SLA deadline exceeded; auto escalation triggered. sla_deadline={e.SlaDeadline:yyyy-MM-dd HH:mm:ss}; escalate_role_code={type.EscalateRoleCode}";
|
|
|
+ await _taskFlow.UpgradeAsync(e.Id, e.TenantId, e.FactoryId, remark);
|
|
|
+ processed++;
|
|
|
+ _logger.LogInformation(
|
|
|
+ "s8_timeout_auto_escalate_started exceptionId={Id} exceptionCode={Code} typeCode={TypeCode} escalateRoleCode={Role}",
|
|
|
+ e.Id, e.ExceptionCode, e.ExceptionTypeCode, type.EscalateRoleCode);
|
|
|
+
|
|
|
+ await TryDispatchAsync(e);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogWarning(ex,
|
|
|
+ "s8_timeout_auto_escalate_failed exceptionId={Id} status={Status}", e.Id, e.Status);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (processed > 0 || candidates.Count > 0)
|
|
|
+ _logger.LogInformation(
|
|
|
+ "s8_timeout_auto_escalate_summary processed={Processed} candidates={Total}", processed, candidates.Count);
|
|
|
+ return processed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task TryDispatchAsync(AdoS8Exception e)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await _layerResolver.DispatchByLayerAsync(new S8NotificationLayerResolver.DispatchByLayerInput
|
|
|
+ {
|
|
|
+ TenantId = e.TenantId,
|
|
|
+ FactoryId = e.FactoryId,
|
|
|
+ ExceptionId = e.Id,
|
|
|
+ ExceptionNo = e.ExceptionCode,
|
|
|
+ // 优先 module_code(S1-S7 严格基线),保持与 NotificationLayer baseline 同口径。
|
|
|
+ SceneCode = string.IsNullOrWhiteSpace(e.ModuleCode) ? e.SceneCode : e.ModuleCode!,
|
|
|
+ Severity = e.Severity,
|
|
|
+ Status = "ESCALATED",
|
|
|
+ Title = $"[AUTO] 异常升级 - {e.ExceptionCode}",
|
|
|
+ Content = "SLA 已超时,系统自动触发升级。",
|
|
|
+ SourceRuleCode = e.SourceRuleCode,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogWarning(ex, "s8_timeout_auto_escalate_dispatch_failed exceptionId={Id}", e.Id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|