using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Entity; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.SmartOps; /// 维度看板解析结果(供 Dashboard 受管码筛选取数;绝不回落旧 Atomic)。 public sealed class KpiDimensionDashboardResult { /// SUCCESS / NO_DATA / NOT_CONFIGURED / VERSION_MISMATCH。 public string Status { get; set; } = "NO_DATA"; /// 前端每指标 filterMode:configurable_dimension / dimension_no_data / filter_not_configured / version_mismatch。 public string FilterMode { get; set; } = "dimension_no_data"; public decimal? MetricValue { get; set; } public List<(string Date, decimal? Value)> Trend { get; set; } = new(); public int? SummaryConfigVersion { get; set; } public int? DimensionConfigVersion { get; set; } } /// /// 维度看板取数服务:为受管 KPI(本批 S7_L1_001)在有筛选时从 ado_smart_ops_kpi_dimension_value_day /// 读当前激活维度版本结果,按 AggregationType 复算筛选值 + 逐日趋势。校验绑定汇总版本一致; /// 无配置/版本不匹配/无数据均返回明确状态,**不回落旧 Atomic**。 /// public sealed class KpiDimensionDashboardService : ITransient { public const string FilterModeConfigured = "configurable_dimension"; public const string FilterModeNoData = "dimension_no_data"; public const string FilterModeNotConfigured = "filter_not_configured"; public const string FilterModeVersionMismatch = "version_mismatch"; private readonly ISqlSugarClient _db; private readonly AdoSmartOpsKpiDimensionConfigService _dimensionConfig; private readonly AdoSmartOpsKpiCalcConfigService _summaryConfig; public KpiDimensionDashboardService( ISqlSugarClient db, AdoSmartOpsKpiDimensionConfigService dimensionConfig, AdoSmartOpsKpiCalcConfigService summaryConfig) { _db = db; _dimensionConfig = dimensionConfig; _summaryConfig = summaryConfig; } public async Task ResolveAsync( long tenantId, long factoryId, string metricCode, string moduleCode, SmartOpsDashboardFilter filter) { // 维度配置/结果落在 KPI 数据租户(S5/S6/S7→pbxfxp/AIDOP),与看板会话租户可能不同,须显式解析 var kpiTenantId = AdoSmartOpsKpiCalcConfigService.ResolveKpiTenantId(moduleCode); var active = await _dimensionConfig.GetActiveAsync(kpiTenantId, metricCode); if (active == null) return new KpiDimensionDashboardResult { Status = "NOT_CONFIGURED", FilterMode = FilterModeNotConfigured }; var summary = await _summaryConfig.GetActiveAsync(kpiTenantId, metricCode); if (summary == null || summary.Id != active.SummaryConfigId || summary.VersionNo != active.SummaryConfigVersion) return new KpiDimensionDashboardResult { Status = "VERSION_MISMATCH", FilterMode = FilterModeVersionMismatch, SummaryConfigVersion = active.SummaryConfigVersion, DimensionConfigVersion = active.DimensionConfigVersion, }; var result = new KpiDimensionDashboardResult { SummaryConfigVersion = active.SummaryConfigVersion, DimensionConfigVersion = active.DimensionConfigVersion, }; var ds = filter.DateStart?.Date; var de = filter.DateEnd?.Date; // 业务维度值统一落在 DimensionCode(各 KPI 声明维度不同:物料/工单/订单/品类), // 同时兼容标准列匹配。前端只发该 KPI SupportedDimensions 内的维度。 var orderNo = string.IsNullOrWhiteSpace(filter.OrderNo) ? null : filter.OrderNo!.Trim(); var material = string.IsNullOrWhiteSpace(filter.Material) ? null : filter.Material!.Trim(); var workOrder = string.IsNullOrWhiteSpace(filter.WorkOrder) ? null : filter.WorkOrder!.Trim(); ISugarQueryable Build() => _db.Queryable().ClearFilter() .Where(x => x.TenantId == kpiTenantId && x.MetricCode == metricCode && x.DimensionConfigVersion == active.DimensionConfigVersion) .WhereIF(ds.HasValue, x => x.ValueDate >= ds!.Value) .WhereIF(de.HasValue, x => x.ValueDate <= de!.Value) .WhereIF(orderNo != null, x => x.OrderNo == orderNo || x.DimensionCode == orderNo) .WhereIF(material != null, x => x.MaterialCode == material || x.DimensionCode == material) .WhereIF(workOrder != null, x => x.WorkOrderNo == workOrder || x.DimensionCode == workOrder); result.MetricValue = await AggregateAsync(active.AggregationType, Build); // 逐日趋势(同 AggregationType 复算) var grouped = await Build().GroupBy(x => x.ValueDate) .Select(x => new DimTrendRow { D = x.ValueDate, SumSum = SqlFunc.AggregateSum(x.SumValue), SumCnt = SqlFunc.AggregateSum(x.SampleCount), SumNum = SqlFunc.AggregateSum(x.Numerator), SumDen = SqlFunc.AggregateSum(x.Denominator), AvgMv = SqlFunc.AggregateAvg(x.MetricValue), }) .OrderBy(x => x.D).ToListAsync(); result.Trend = grouped .Select(g => (g.D.ToString("yyyy-MM-dd"), ComputeAggregate(active.AggregationType, g.SumSum, g.SumCnt, g.SumNum, g.SumDen, g.AvgMv))) .ToList(); if (result.MetricValue == null) { result.Status = "NO_DATA"; result.FilterMode = FilterModeNoData; } else { result.Status = "SUCCESS"; result.FilterMode = FilterModeConfigured; } return result; } private static async Task AggregateAsync(string aggregationType, Func> build) { return aggregationType switch { "AVERAGE_OF_SUMS" => Div(await build().SumAsync(x => x.SumValue), (decimal?)await build().SumAsync(x => x.SampleCount)), "RATIO_OF_SUMS" => Div(await build().SumAsync(x => x.Numerator), await build().SumAsync(x => x.Denominator)), "DIRECT_VALUE" => await build().AvgAsync(x => x.MetricValue), _ => null, }; } private static decimal? ComputeAggregate(string aggregationType, decimal? sumSum, decimal? sumCnt, decimal? sumNum, decimal? sumDen, decimal? avgMv) => aggregationType switch { "AVERAGE_OF_SUMS" => Div(sumSum, sumCnt), "RATIO_OF_SUMS" => Div(sumNum, sumDen), "DIRECT_VALUE" => avgMv, _ => null, }; private static decimal? Div(decimal? a, decimal? b) => b.HasValue && b.Value != 0 ? (a ?? 0m) / b.Value : null; private sealed class DimTrendRow { public DateTime D { get; set; } public decimal? SumSum { get; set; } public decimal? SumCnt { get; set; } public decimal? SumNum { get; set; } public decimal? SumDen { get; set; } public decimal? AvgMv { get; set; } } }