using Admin.NET.Plugin.AiDOP.Entity.S8; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// S8-TIMEOUT-AUTO-ESCALATION-JOB-1(P4-1):扫描 sla_deadline 已超时且未关闭/未已升级的异常, /// 通过 启动 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(与 允许 ESCALATED 的转移一致); /// NEW / PENDING_VERIFICATION / REJECTED 等状态不通过本 Job 自动升级。 /// - 已有 active_flow_instance_id 的异常跳过(防重);UpgradeAsync 内部还会再校验一次,双层保险。 /// - exception_type.escalate_role_code 为空 / 非法时跳过并 LogInformation;不写脏数据,不补默认。 /// - 状态 / Timeline 由 .OnFlowStarted 写入;本服务不重复维护。 /// - 通知层走 ;当前 baseline notify_channel="log",无外部副作用。 /// public class S8TimeoutAutoEscalationService : ITransient { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _typeRep; private readonly S8TaskFlowService _taskFlow; private readonly S8NotificationLayerResolver _layerResolver; private readonly ILogger _logger; public S8TimeoutAutoEscalationService( SqlSugarRepository rep, SqlSugarRepository typeRep, S8TaskFlowService taskFlow, S8NotificationLayerResolver layerResolver, ILogger logger) { _rep = rep; _typeRep = typeRep; _taskFlow = taskFlow; _layerResolver = layerResolver; _logger = logger; } /// /// 扫描一次。返回成功触发升级的异常数量;调用方负责调度 / 限流。 /// public async Task 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 && (x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS") && (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() : (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); } } }