SmartOpsKpiAtomicQueryService.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using SqlSugar;
  5. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  6. /// <summary>
  7. /// 从 KPI 原子聚合层按筛选条件动态聚合并计算最终 KPI。
  8. /// </summary>
  9. public class SmartOpsKpiAtomicQueryService : ITransient
  10. {
  11. private readonly ISqlSugarClient _db;
  12. public SmartOpsKpiAtomicQueryService(ISqlSugarClient db)
  13. {
  14. _db = db;
  15. }
  16. public async Task<FilteredKpiQueryResult> QueryAsync(
  17. long tenantId,
  18. long factoryId,
  19. string moduleCode,
  20. SmartOpsDashboardFilter filter,
  21. IReadOnlyList<AdoSmartOpsKpiMaster> kpis,
  22. CancellationToken cancellationToken = default)
  23. {
  24. AidopTenantMigration.EnsureKpiAtomicTable(_db);
  25. var result = new FilteredKpiQueryResult
  26. {
  27. FilterContext = filter.ToContextDictionary()
  28. .Where(x => !string.IsNullOrWhiteSpace(x.Value))
  29. .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)
  30. };
  31. if (filter.IsEmpty || kpis.Count == 0)
  32. {
  33. result.Scope = "module_summary";
  34. return result;
  35. }
  36. var filteredCount = 0;
  37. var summaryCount = 0;
  38. var partialCount = 0;
  39. foreach (var kpi in kpis)
  40. {
  41. cancellationToken.ThrowIfCancellationRequested();
  42. var metricCode = kpi.MetricCode;
  43. var rule = SmartOpsKpiAggregateRuleRegistry.TryGet(metricCode);
  44. var unsupported = SmartOpsKpiAggregateRuleRegistry.GetUnsupportedFilters(metricCode, filter).ToArray();
  45. var filterMode = SmartOpsKpiAggregateRuleRegistry.ResolveFilterMode(metricCode, filter);
  46. result.MetricFilterModes[metricCode] = filterMode;
  47. if (unsupported.Length > 0)
  48. result.UnsupportedFiltersByMetric[metricCode] = unsupported;
  49. if (rule == null || rule.FilterMode == SmartOpsKpiAggregateRuleRegistry.FilterSummaryOnly ||
  50. !SmartOpsKpiAggregateRuleRegistry.SupportsFilter(metricCode, filter))
  51. {
  52. if (filterMode == SmartOpsKpiAggregateRuleRegistry.FilterPartial)
  53. partialCount++;
  54. else
  55. summaryCount++;
  56. continue;
  57. }
  58. var valueSql = BuildAggregateSql(rule.AggregationType, groupByDate: false);
  59. var whereSql = BuildWhereSql(filter);
  60. var sql = $"""
  61. SELECT {valueSql} AS MetricValue
  62. FROM ado_smart_ops_kpi_atomic_day
  63. WHERE tenant_id = @tenantId
  64. AND factory_id = @factoryId
  65. AND domain_code = @domainCode
  66. AND metric_code = @metricCode
  67. AND is_deleted = 0
  68. {whereSql}
  69. """;
  70. var parameters = BuildParameters(tenantId, factoryId, rule.DomainCode, metricCode, filter);
  71. var current = await _db.Ado.SqlQuerySingleAsync<AtomicAggregateRow>(sql, parameters);
  72. if (current?.MetricValue == null)
  73. {
  74. summaryCount++;
  75. result.MetricFilterModes[metricCode] = SmartOpsKpiAggregateRuleRegistry.FilterSummaryOnly;
  76. continue;
  77. }
  78. var trendSql = $"""
  79. SELECT stat_date AS StatDate, {BuildAggregateSql(rule.AggregationType, groupByDate: true)} AS MetricValue
  80. FROM ado_smart_ops_kpi_atomic_day
  81. WHERE tenant_id = @tenantId
  82. AND factory_id = @factoryId
  83. AND domain_code = @domainCode
  84. AND metric_code = @metricCode
  85. AND is_deleted = 0
  86. {whereSql}
  87. GROUP BY stat_date
  88. ORDER BY stat_date
  89. """;
  90. var trendRows = await _db.Ado.SqlQueryAsync<AtomicTrendRow>(trendSql, parameters);
  91. result.Values[metricCode] = new SmartOpsKpiMetricValue
  92. {
  93. Level = kpi.MetricLevel,
  94. MetricValue = current.MetricValue,
  95. Trend = trendRows
  96. .Select(x => new SmartOpsKpiTrendPoint(x.StatDate.ToString("yyyy-MM-dd"), x.MetricValue))
  97. .ToList()
  98. };
  99. filteredCount++;
  100. }
  101. result.Scope = filteredCount switch
  102. {
  103. 0 when summaryCount > 0 && partialCount == 0 => "summary_only_fallback",
  104. 0 => "summary_only_fallback",
  105. var n when n == kpis.Count => "filtered_atomic",
  106. var n when n > 0 => "partial_filtered",
  107. _ => "summary_only_fallback"
  108. };
  109. return result;
  110. }
  111. private static string BuildAggregateSql(string aggregationType, bool groupByDate)
  112. {
  113. return aggregationType switch
  114. {
  115. SmartOpsKpiAggregateRuleRegistry.AggSum => "ROUND(SUM(sum_value), 4)",
  116. SmartOpsKpiAggregateRuleRegistry.AggRatio =>
  117. "ROUND(100 * SUM(numerator_value) / NULLIF(SUM(denominator_value), 0), 4)",
  118. SmartOpsKpiAggregateRuleRegistry.AggMin => "ROUND(MIN(min_value), 4)",
  119. SmartOpsKpiAggregateRuleRegistry.AggMax => "ROUND(MAX(max_value), 4)",
  120. _ => "ROUND(SUM(sum_value) / NULLIF(SUM(sample_count), 0), 4)"
  121. };
  122. }
  123. private static string BuildWhereSql(SmartOpsDashboardFilter filter)
  124. {
  125. var clauses = new List<string>();
  126. if (filter.DateStart.HasValue) clauses.Add("AND stat_date >= @dateStart");
  127. if (filter.DateEnd.HasValue) clauses.Add("AND stat_date <= @dateEnd");
  128. if (!string.IsNullOrWhiteSpace(filter.Customer)) clauses.Add("AND customer_code = @customer");
  129. if (!string.IsNullOrWhiteSpace(filter.Product)) clauses.Add("AND product_code = @product");
  130. if (!string.IsNullOrWhiteSpace(filter.OrderNo)) clauses.Add("AND order_no = @orderNo");
  131. if (!string.IsNullOrWhiteSpace(filter.ProductionLine)) clauses.Add("AND production_line = @productionLine");
  132. if (!string.IsNullOrWhiteSpace(filter.Supplier)) clauses.Add("AND supplier_code = @supplier");
  133. if (!string.IsNullOrWhiteSpace(filter.Material)) clauses.Add("AND material_code = @material");
  134. if (!string.IsNullOrWhiteSpace(filter.PoNo)) clauses.Add("AND po_no = @poNo");
  135. if (!string.IsNullOrWhiteSpace(filter.Warehouse)) clauses.Add("AND warehouse_code = @warehouse");
  136. if (!string.IsNullOrWhiteSpace(filter.WorkOrder)) clauses.Add("AND work_order_no = @workOrder");
  137. if (!string.IsNullOrWhiteSpace(filter.Equipment)) clauses.Add("AND equipment_code = @equipment");
  138. if (!string.IsNullOrWhiteSpace(filter.OutboundNo)) clauses.Add("AND outbound_no = @outboundNo");
  139. return string.Join(' ', clauses);
  140. }
  141. private static object BuildParameters(
  142. long tenantId,
  143. long factoryId,
  144. string domainCode,
  145. string metricCode,
  146. SmartOpsDashboardFilter filter)
  147. {
  148. return new
  149. {
  150. tenantId,
  151. factoryId,
  152. domainCode,
  153. metricCode,
  154. dateStart = filter.DateStart?.Date,
  155. dateEnd = filter.DateEnd?.Date,
  156. customer = filter.Customer?.Trim(),
  157. product = filter.Product?.Trim(),
  158. orderNo = filter.OrderNo?.Trim(),
  159. productionLine = filter.ProductionLine?.Trim(),
  160. supplier = filter.Supplier?.Trim(),
  161. material = filter.Material?.Trim(),
  162. poNo = filter.PoNo?.Trim(),
  163. warehouse = filter.Warehouse?.Trim(),
  164. workOrder = filter.WorkOrder?.Trim(),
  165. equipment = filter.Equipment?.Trim(),
  166. outboundNo = filter.OutboundNo?.Trim()
  167. };
  168. }
  169. }