S8TimeoutAutoEscalationService.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. using Admin.NET.Plugin.AiDOP.Const.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Microsoft.Extensions.Logging;
  5. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  6. /// <summary>
  7. /// S8-TIMEOUT-AUTO-ESCALATION-JOB-1(P4-1):扫描 sla_deadline 已超时且未关闭/未已升级的异常,
  8. /// 通过 <see cref="S8TaskFlowService.UpgradeAsync"/> 启动 EXCEPTION_ESCALATION ApprovalFlow,与人工升级链路 100% 等价。
  9. ///
  10. /// 设计要点:
  11. /// - 不依赖 timeout_flag;扫描公式与读端 IsCurrentlyTimeout 一致:sla_deadline IS NOT NULL AND sla_deadline &lt; now
  12. /// AND status NOT IN ('CLOSED','RECOVERED','ESCALATED')。
  13. /// - status 进一步限制在 ASSIGNED / IN_PROGRESS(与 <see cref="Infrastructure.S8.S8StatusRules"/> 允许 ESCALATED 的转移一致);
  14. /// NEW / PENDING_VERIFICATION / REJECTED 等状态不通过本 Job 自动升级。
  15. /// - 已有 active_flow_instance_id 的异常跳过(防重);UpgradeAsync 内部还会再校验一次,双层保险。
  16. /// - S8-RESPONSIBILITY-POOL-1:升级 authority = **该规则的升级账号池**(ado_s8_rule_responsibility_user)。
  17. /// 池空 / 成员全部失效时跳过并 LogInformation;**绝不回落到任何角色**(fail closed)。
  18. /// exception_type.escalate_role_code 已退居 LEGACY:它是按异常类型跨租户查的,
  19. /// 实测会把 B 租户的配置用到 A 租户的异常上;责任池天然带 tenant_id,不存在这条越界路径。
  20. /// - 人工提报的异常没有来源规则,不参与规则级自动升级(与认领 / 复核同一口径)。
  21. /// - 状态 / Timeline 由 <see cref="ExceptionEscalationBizHandler"/>.OnFlowStarted 写入;本服务不重复维护。
  22. /// - 通知层走 <see cref="S8NotificationLayerResolver"/>;当前 baseline notify_channel="log",无外部副作用。
  23. /// </summary>
  24. public class S8TimeoutAutoEscalationService : ITransient
  25. {
  26. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  27. private readonly IS8RuleResponsibilityReader _pools;
  28. private readonly S8TaskFlowService _taskFlow;
  29. private readonly S8NotificationLayerResolver _layerResolver;
  30. private readonly ILogger<S8TimeoutAutoEscalationService> _logger;
  31. public S8TimeoutAutoEscalationService(
  32. SqlSugarRepository<AdoS8Exception> rep,
  33. IS8RuleResponsibilityReader pools,
  34. S8TaskFlowService taskFlow,
  35. S8NotificationLayerResolver layerResolver,
  36. ILogger<S8TimeoutAutoEscalationService> logger)
  37. {
  38. _rep = rep;
  39. _pools = pools;
  40. _taskFlow = taskFlow;
  41. _layerResolver = layerResolver;
  42. _logger = logger;
  43. }
  44. /// <summary>
  45. /// 扫描一次。返回成功触发升级的异常数量;调用方负责调度 / 限流。
  46. /// </summary>
  47. public async Task<int> RunOnceAsync(int batchSize = 50, CancellationToken ct = default)
  48. {
  49. var now = DateTime.Now;
  50. var candidates = await _rep.AsQueryable()
  51. .Where(x => !x.IsDeleted
  52. && x.SlaDeadline != null
  53. && x.SlaDeadline < now
  54. && (x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS")
  55. && (x.ActiveFlowInstanceId == null || x.ActiveFlowInstanceId == 0))
  56. .OrderBy(x => x.SlaDeadline)
  57. .Take(batchSize)
  58. .ToListAsync();
  59. if (candidates.Count == 0) return 0;
  60. var processed = 0;
  61. foreach (var e in candidates)
  62. {
  63. ct.ThrowIfCancellationRequested();
  64. // S8-RESPONSIBILITY-POOL-1:升级 authority = 该规则的升级账号池。
  65. //
  66. // 人工提报没有来源规则 —— 它不属于任何规则的责任范围,不参与规则级自动升级。
  67. // 一刀切地给它找个升级人,只会把"谁都没配"伪装成"已经有人负责"。
  68. if (string.IsNullOrWhiteSpace(e.SourceRuleCode))
  69. {
  70. _logger.LogInformation(
  71. "s8_timeout_auto_escalate_skip exceptionId={Id} exceptionCode={Code} reason=no_source_rule",
  72. e.Id, e.ExceptionCode);
  73. continue;
  74. }
  75. // 空池 fail closed:不回落 escalate_role_code、不回落任何角色。
  76. // 回落会让「我没给这条规则配升级人」与「有人在跟进」同时成立 —— 最难查的一类错配。
  77. var escalationPool = await _pools.GetValidMemberIdsAsync(
  78. e.TenantId, e.SourceRuleCode!, S8ResponsibilityType.Escalation);
  79. if (escalationPool.Count == 0)
  80. {
  81. _logger.LogInformation(
  82. "s8_timeout_auto_escalate_skip exceptionId={Id} exceptionCode={Code} reason=escalation_pool_empty rule={Rule}",
  83. e.Id, e.ExceptionCode, e.SourceRuleCode);
  84. continue;
  85. }
  86. try
  87. {
  88. // UpgradeAsync 已内置 ActiveFlowInstanceId / IsAllowedTransition 二次校验;
  89. // 与 manual upgrade 100% 等价,状态/timeline 由 OnFlowStarted 写入。
  90. var remark = $"[AUTO] SLA deadline exceeded; auto escalation triggered. sla_deadline={e.SlaDeadline:yyyy-MM-dd HH:mm:ss}; escalation_pool={escalationPool.Count}";
  91. await _taskFlow.UpgradeAsync(e.Id, e.TenantId, remark);
  92. processed++;
  93. _logger.LogInformation(
  94. "s8_timeout_auto_escalate_started exceptionId={Id} exceptionCode={Code} rule={Rule} escalationPoolSize={PoolSize}",
  95. e.Id, e.ExceptionCode, e.SourceRuleCode, escalationPool.Count);
  96. await TryDispatchAsync(e);
  97. }
  98. catch (Exception ex)
  99. {
  100. _logger.LogWarning(ex,
  101. "s8_timeout_auto_escalate_failed exceptionId={Id} status={Status}", e.Id, e.Status);
  102. }
  103. }
  104. if (processed > 0 || candidates.Count > 0)
  105. _logger.LogInformation(
  106. "s8_timeout_auto_escalate_summary processed={Processed} candidates={Total}", processed, candidates.Count);
  107. return processed;
  108. }
  109. private async Task TryDispatchAsync(AdoS8Exception e)
  110. {
  111. try
  112. {
  113. await _layerResolver.DispatchByLayerAsync(new S8NotificationLayerResolver.DispatchByLayerInput
  114. {
  115. TenantId = e.TenantId,
  116. ExceptionId = e.Id,
  117. ExceptionNo = e.ExceptionCode,
  118. // 优先 module_code(S1-S7 严格基线),保持与 NotificationLayer baseline 同口径。
  119. SceneCode = string.IsNullOrWhiteSpace(e.ModuleCode) ? e.SceneCode : e.ModuleCode!,
  120. Severity = e.Severity,
  121. Status = "ESCALATED",
  122. EventCode = S8NotifyEventCode.EscalationTriggered,
  123. ExceptionRef = e,
  124. Title = $"[AUTO] 异常升级 - {e.ExceptionCode}",
  125. Content = "SLA 已超时,系统自动触发升级。",
  126. SourceRuleCode = e.SourceRuleCode,
  127. });
  128. }
  129. catch (Exception ex)
  130. {
  131. _logger.LogWarning(ex, "s8_timeout_auto_escalate_dispatch_failed exceptionId={Id}", e.Id);
  132. }
  133. }
  134. }