using System.Security.Cryptography; using System.Text; using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Entity; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.SmartOps; /// 维度执行结果概要(供调度/接口回执)。 public sealed class KpiDimensionRunResult { public string Status { get; set; } = string.Empty; public int RowCount { get; set; } public string? ErrorCode { get; set; } public string? ErrorMessage { get; set; } } /// /// KPI 维度执行通用入口:读当前生效维度配置 → 校验绑定汇总版本一致 → 只读执行 DIMENSION_SQL /// → 事务 FULL REPLACE 写维度结果表 → 写运行日志。禁止 per-KPI 分支。 /// public sealed class KpiDimensionRunService : ITransient { private readonly ISqlSugarClient _db; private readonly KpiDimensionSqlExecutor _executor; private readonly AdoSmartOpsKpiDimensionConfigService _dimensionConfig; private readonly AdoSmartOpsKpiCalcConfigService _summaryConfig; public KpiDimensionRunService( ISqlSugarClient db, KpiDimensionSqlExecutor executor, AdoSmartOpsKpiDimensionConfigService dimensionConfig, AdoSmartOpsKpiCalcConfigService summaryConfig) { _db = db; _executor = executor; _dimensionConfig = dimensionConfig; _summaryConfig = summaryConfig; } /// /// 执行某 KPI 当前生效维度配置。SUMMARY_ONLY / 无配置 / 版本不匹配均只记日志、不写结果。 /// public async Task RunDimensionAsync( string metricCode, string moduleCode, long tenantId, DateTime valueDate, string batchId, string triggerType, CancellationToken ct) { var startedAt = DateTime.Now; var bizDate = valueDate.Date; var active = await _dimensionConfig.GetActiveAsync(tenantId, metricCode); if (active == null) { await WriteRunLogAsync(null, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, "NOT_CONFIGURED", 0, "NOT_CONFIGURED", "无生效维度配置", null); return new KpiDimensionRunResult { Status = "NOT_CONFIGURED", ErrorCode = "NOT_CONFIGURED" }; } if (active.AggregationType == "SUMMARY_ONLY") { await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, "NOT_CONFIGURED", 0, "SUMMARY_ONLY", "维度配置为 SUMMARY_ONLY,不产生维度明细", null); return new KpiDimensionRunResult { Status = "NOT_CONFIGURED", ErrorCode = "SUMMARY_ONLY" }; } // 绑定汇总版本一致性校验 var summary = await _summaryConfig.GetActiveAsync(tenantId, metricCode); if (summary == null || summary.Id != active.SummaryConfigId || summary.VersionNo != active.SummaryConfigVersion) { await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, "VERSION_MISMATCH", 0, "VERSION_MISMATCH", "维度配置绑定的汇总版本与当前生效汇总版本不一致", null); return new KpiDimensionRunResult { Status = "VERSION_MISMATCH", ErrorCode = "VERSION_MISMATCH" }; } var pars = AdoSmartOpsKpiDimensionConfigService.BuildRunParams(tenantId, moduleCode, metricCode, bizDate); var exec = await _executor.ExecuteAsync(active.DataSourceCode, active.SqlScript ?? "", active.TimeoutSeconds, false, pars, ct); if (exec.Status == "FAILED") { await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, "FAILED", exec.RowCount, exec.ErrorCode, exec.ErrorMessage, exec.SqlHash); return new KpiDimensionRunResult { Status = "FAILED", RowCount = exec.RowCount, ErrorCode = exec.ErrorCode, ErrorMessage = exec.ErrorMessage }; } // FULL REPLACE:同 (tenant, metric, dimVersion) + 本次返回的各 value_date 范围内先删后插,事务提交。 var now = DateTime.Now; var rows = exec.Rows.Select(r => ToEntity(r, active, tenantId, metricCode, moduleCode, batchId, now)).ToList(); var dates = rows.Select(x => x.ValueDate.Date).Distinct().ToList(); if (dates.Count == 0) dates.Add(bizDate); // NO_DATA 也清空当日该版本旧数据 var tran = await _db.AsTenant().UseTranAsync(async () => { await _db.Deleteable() .Where(x => x.TenantId == tenantId && x.MetricCode == metricCode && x.DimensionConfigVersion == active.DimensionConfigVersion && dates.Contains(x.ValueDate.Date)) .ExecuteCommandAsync(); if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync(); }); if (!tran.IsSuccess) { await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, "FAILED", rows.Count, "WRITE_FAILED", tran.ErrorException?.Message, exec.SqlHash); return new KpiDimensionRunResult { Status = "FAILED", ErrorCode = "WRITE_FAILED", ErrorMessage = tran.ErrorException?.Message }; } var status = rows.Count == 0 ? "NO_DATA" : "SUCCESS"; await WriteRunLogAsync(active, metricCode, moduleCode, tenantId, batchId, triggerType, bizDate, startedAt, status, rows.Count, null, null, exec.SqlHash); return new KpiDimensionRunResult { Status = status, RowCount = rows.Count }; } private AdoSmartOpsKpiDimensionValueDay ToEntity( KpiDimensionRow r, AdoSmartOpsKpiDimensionConfig cfg, long tenantId, string metricCode, string moduleCode, string batchId, DateTime now) { var valueDate = r.ValueDate == DateTime.MinValue ? DateTime.Today : r.ValueDate.Date; var e = new AdoSmartOpsKpiDimensionValueDay { TenantId = tenantId, ModuleCode = moduleCode, MetricCode = metricCode, SummaryConfigId = cfg.SummaryConfigId, SummaryConfigVersion = cfg.SummaryConfigVersion, DimensionConfigId = cfg.Id, DimensionConfigVersion = cfg.DimensionConfigVersion, DataSourceCode = cfg.DataSourceCode, ValueDate = valueDate, DimensionType = r.DimensionType, DimensionCode = r.DimensionCode, DimensionName = r.DimensionName, OrgId = r.OrgId, FactoryId = r.FactoryId ?? 1, MaterialCode = r.MaterialCode, WorkOrderNo = r.WorkOrderNo, CategoryCode = r.CategoryCode, WarehouseCode = r.WarehouseCode, OrderNo = r.OrderNo, CustomerCode = r.CustomerCode, ProductCode = r.ProductCode, EquipmentCode = r.EquipmentCode, MetricValue = r.MetricValue, Numerator = r.Numerator, Denominator = r.Denominator, SumValue = r.SumValue, SampleCount = r.SampleCount, SourceKey = r.SourceKey, BatchId = batchId, CalcTime = now, CreatedTime = now, }; e.RowHash = ComputeRowHash(tenantId, metricCode, cfg.DimensionConfigVersion, valueDate, r.DimensionType, r.DimensionCode, r.SourceKey); return e; } private static string ComputeRowHash(long tenantId, string metricCode, int dimVersion, DateTime valueDate, string dimType, string dimCode, string? sourceKey) { var raw = $"{tenantId}|{metricCode}|{dimVersion}|{valueDate:yyyyMMdd}|{dimType}|{dimCode}|{sourceKey}"; return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw))); } private async Task WriteRunLogAsync( AdoSmartOpsKpiDimensionConfig? cfg, string metricCode, string moduleCode, long tenantId, string batchId, string triggerType, DateTime bizDate, DateTime startedAt, string status, int rowCount, string? errorCode, string? errorMessage, string? sqlHash) { try { var finishedAt = DateTime.Now; var log = new AdoSmartOpsKpiDimensionRunLog { TenantId = tenantId, BatchId = batchId, ModuleCode = moduleCode, MetricCode = metricCode, DimensionConfigId = cfg?.Id, DimensionConfigVersion = cfg?.DimensionConfigVersion, SummaryConfigVersion = cfg?.SummaryConfigVersion, DataSourceCode = cfg?.DataSourceCode, BizDate = bizDate.Date, StartedAt = startedAt, FinishedAt = finishedAt, DurationMs = (long)(finishedAt - startedAt).TotalMilliseconds, Status = status, RowCount = rowCount, ErrorCode = errorCode, ErrorMessage = errorMessage == null ? null : (errorMessage.Length > 480 ? errorMessage.Substring(0, 480) : errorMessage), SqlHash = sqlHash, TriggerType = triggerType, CreateTime = finishedAt, }; 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 { // 运行日志失败不影响主执行链路 } } }