using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Entity; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.SmartOps; /// 分发结果:引擎、是否应写 KPI 值、指标值、分母状态。 public sealed class KpiCalcDispatchResult { public string EngineType { get; set; } = "LEGACY_CODE"; /// 是否写 KPI 日值。FAILED 时 false → 不写、保留上一成功值。 public bool ShouldUpsert { get; set; } = true; public decimal? MetricValue { get; set; } /// 分母/结果状态:OK / NO_DATA / NO_NUMERATOR / FAILED。 public string DenominatorStatus { get; set; } = "OK"; } /// /// KPI 计算运行时分发器:解析当前生效配置 → LEGACY_CODE(用旧代码值) / CONFIG_SQL(只读执行 SQL) / LEGACY_TVF(用旧代码值)。 /// 无生效配置 → 回落 LEGACY_CODE(向后兼容)。CONFIG_SQL 失败**不 fallback**、不写值、返回 FAILED(保留上一成功值)。 /// 每次执行落 ado_smart_ops_kpi_calc_run_log(含引擎/版本/SQL 指纹/分子分母/耗时/状态)。 /// public sealed class KpiCalcDispatcher : ITransient { private readonly ISqlSugarClient _db; private readonly AdoSmartOpsKpiCalcConfigService _configService; private readonly KpiSqlReadOnlyExecutor _executor; public KpiCalcDispatcher(ISqlSugarClient db, AdoSmartOpsKpiCalcConfigService configService, KpiSqlReadOnlyExecutor executor) { _db = db; _configService = configService; _executor = executor; } public async Task DispatchAsync( string metricCode, string moduleCode, long tenantId, long factoryId, DateTime bizDate, DateTime periodStart, DateTime periodEnd, string sourceZtid, string batchId, string triggerType, decimal? legacyValue, string legacyDenomStatus, CancellationToken ct) { var startedAt = DateTime.Now; var config = await _configService.GetActiveAsync(tenantId, metricCode); var engine = config?.CalcEngineType ?? "LEGACY_CODE"; var result = new KpiCalcDispatchResult { EngineType = engine }; // 无配置 / LEGACY_CODE / LEGACY_TVF → 用旧代码算出的值(旧 Build 已计算并传入) if (config == null || engine == "LEGACY_CODE" || engine == "LEGACY_TVF") { result.MetricValue = legacyValue; result.DenominatorStatus = legacyDenomStatus; result.ShouldUpsert = true; await WriteRunLogAsync(config, metricCode, moduleCode, tenantId, batchId, triggerType, engine, bizDate, startedAt, legacyValue == null ? "NO_DATA" : "SUCCESS", legacyValue, null, null, 0, null, null, null); return result; } // CONFIG_SQL:只读执行已发布 SQL var pars = new KpiSqlRunParams { TenantId = tenantId, FactoryId = factoryId, ModuleCode = moduleCode, MetricCode = metricCode, BizDate = bizDate, PeriodStart = periodStart, PeriodEnd = periodEnd, SourceZtid = sourceZtid, }; var exec = await _executor.ExecuteAsync(config.DataSourceCode, config.SqlScript ?? "", config.TimeoutSeconds, false, pars, ct); switch (exec.Status) { case "SUCCESS": result.MetricValue = exec.MetricValue; result.DenominatorStatus = "OK"; result.ShouldUpsert = true; break; case "NO_DATA": result.MetricValue = null; result.DenominatorStatus = "NO_DATA"; result.ShouldUpsert = true; // 遵循现有契约:NO_DATA 写 null,不伪造 0 break; default: // FAILED result.MetricValue = null; result.DenominatorStatus = "FAILED"; result.ShouldUpsert = false; // 不 fallback、不写错误新值、保留上一成功值 break; } await WriteRunLogAsync(config, metricCode, moduleCode, tenantId, batchId, triggerType, engine, bizDate, startedAt, exec.Status, exec.MetricValue, exec.NumeratorValue, exec.DenominatorValue, exec.RowCount, exec.ErrorCode, exec.ErrorMessage, exec.SqlHash); return result; } private async Task WriteRunLogAsync( AdoSmartOpsKpiCalcConfig? config, string metricCode, string moduleCode, long tenantId, string batchId, string triggerType, string engine, DateTime bizDate, DateTime startedAt, string status, decimal? metricValue, decimal? numerator, decimal? denominator, int rowCount, string? errorCode, string? errorMessage, string? sqlHash) { try { var finishedAt = DateTime.Now; var log = new AdoSmartOpsKpiCalcRunLog { TenantId = tenantId, BatchId = batchId, ModuleCode = moduleCode, MetricCode = metricCode, ConfigId = config?.Id, VersionNo = config?.VersionNo, EngineType = engine, DataSourceCode = config?.DataSourceCode, BizDate = bizDate.Date, StartedAt = startedAt, FinishedAt = finishedAt, DurationMs = (long)(finishedAt - startedAt).TotalMilliseconds, Status = status, RowCount = rowCount, MetricValue = metricValue, NumeratorValue = numerator, DenominatorValue = denominator, ErrorCode = errorCode, ErrorMessage = errorMessage, SqlHash = sqlHash, ParameterSnapshot = $"{{\"tenant_id\":{tenantId},\"metric_code\":\"{metricCode}\",\"biz_date\":\"{bizDate:yyyy-MM-dd}\"}}", TriggerType = triggerType, CreateTime = finishedAt, }; // 同批次同指标唯一:存在则更新、否则插入(避免撞唯一键 uk_kpi_calc_run_metric_batch) var existing = await _db.Queryable().ClearFilter() .Where(x => x.MetricCode == metricCode && x.BatchId == batchId).FirstAsync(); if (existing != null) { log.Id = existing.Id; await _db.Updateable(log).ExecuteCommandAsync(); } else { await _db.Insertable(log).ExecuteCommandAsync(); } } catch { // 运行日志失败不影响主计算链路 } } }