| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- 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;
- 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
- && (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<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);
- }
- }
- }
|