using Admin.NET.Plugin.AiDOP.Const.S8;
using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Infrastructure;
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 内部还会再校验一次,双层保险。
/// - S8-RESPONSIBILITY-POOL-1:升级 authority = **该规则的升级账号池**(ado_s8_rule_responsibility_user)。
/// 池空 / 成员全部失效时跳过并 LogInformation;**绝不回落到任何角色**(fail closed)。
/// exception_type.escalate_role_code 已退居 LEGACY:它是按异常类型跨租户查的,
/// 实测会把 B 租户的配置用到 A 租户的异常上;责任池天然带 tenant_id,不存在这条越界路径。
/// - 人工提报的异常没有来源规则,不参与规则级自动升级(与认领 / 复核同一口径)。
/// - 状态 / Timeline 由 .OnFlowStarted 写入;本服务不重复维护。
/// - 通知层走 ;当前 baseline notify_channel="log",无外部副作用。
///
public class S8TimeoutAutoEscalationService : ITransient
{
private readonly SqlSugarRepository _rep;
private readonly IS8RuleResponsibilityReader _pools;
private readonly S8TaskFlowService _taskFlow;
private readonly S8NotificationLayerResolver _layerResolver;
private readonly ILogger _logger;
public S8TimeoutAutoEscalationService(
SqlSugarRepository rep,
IS8RuleResponsibilityReader pools,
S8TaskFlowService taskFlow,
S8NotificationLayerResolver layerResolver,
ILogger logger)
{
_rep = rep;
_pools = pools;
_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;
var processed = 0;
foreach (var e in candidates)
{
ct.ThrowIfCancellationRequested();
// S8-RESPONSIBILITY-POOL-1:升级 authority = 该规则的升级账号池。
//
// 人工提报没有来源规则 —— 它不属于任何规则的责任范围,不参与规则级自动升级。
// 一刀切地给它找个升级人,只会把"谁都没配"伪装成"已经有人负责"。
if (string.IsNullOrWhiteSpace(e.SourceRuleCode))
{
_logger.LogInformation(
"s8_timeout_auto_escalate_skip exceptionId={Id} exceptionCode={Code} reason=no_source_rule",
e.Id, e.ExceptionCode);
continue;
}
// 空池 fail closed:不回落 escalate_role_code、不回落任何角色。
// 回落会让「我没给这条规则配升级人」与「有人在跟进」同时成立 —— 最难查的一类错配。
var escalationPool = await _pools.GetValidMemberIdsAsync(
e.TenantId, e.SourceRuleCode!, S8ResponsibilityType.Escalation);
if (escalationPool.Count == 0)
{
_logger.LogInformation(
"s8_timeout_auto_escalate_skip exceptionId={Id} exceptionCode={Code} reason=escalation_pool_empty rule={Rule}",
e.Id, e.ExceptionCode, e.SourceRuleCode);
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}; escalation_pool={escalationPool.Count}";
await _taskFlow.UpgradeAsync(e.Id, e.TenantId, remark);
processed++;
_logger.LogInformation(
"s8_timeout_auto_escalate_started exceptionId={Id} exceptionCode={Code} rule={Rule} escalationPoolSize={PoolSize}",
e.Id, e.ExceptionCode, e.SourceRuleCode, escalationPool.Count);
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,
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",
EventCode = S8NotifyEventCode.EscalationTriggered,
ExceptionRef = e,
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);
}
}
}