SmartOpsKpiAtomicQueryService.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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 factorySql = BuildFactoryWhereSql(filter);
  61. var sql = $"""
  62. SELECT {valueSql} AS MetricValue
  63. FROM ado_smart_ops_kpi_atomic_day
  64. WHERE tenant_id = @tenantId
  65. {factorySql}
  66. AND domain_code = @domainCode
  67. AND metric_code = @metricCode
  68. AND is_deleted = 0
  69. {whereSql}
  70. """;
  71. var parameters = BuildParameters(tenantId, factoryId, rule.DomainCode, metricCode, filter);
  72. var current = await _db.Ado.SqlQuerySingleAsync<AtomicAggregateRow>(sql, parameters);
  73. if (current?.MetricValue == null)
  74. {
  75. summaryCount++;
  76. result.MetricFilterModes[metricCode] = SmartOpsKpiAggregateRuleRegistry.FilterSummaryOnly;
  77. continue;
  78. }
  79. var trendSql = $"""
  80. SELECT stat_date AS StatDate, {BuildAggregateSql(rule.AggregationType, groupByDate: true)} AS MetricValue
  81. FROM ado_smart_ops_kpi_atomic_day
  82. WHERE tenant_id = @tenantId
  83. {factorySql}
  84. AND domain_code = @domainCode
  85. AND metric_code = @metricCode
  86. AND is_deleted = 0
  87. {whereSql}
  88. GROUP BY stat_date
  89. ORDER BY stat_date
  90. """;
  91. var trendRows = await _db.Ado.SqlQueryAsync<AtomicTrendRow>(trendSql, parameters);
  92. result.Values[metricCode] = new SmartOpsKpiMetricValue
  93. {
  94. Level = kpi.MetricLevel,
  95. MetricValue = current.MetricValue,
  96. Trend = trendRows
  97. .Select(x => new SmartOpsKpiTrendPoint(x.StatDate.ToString("yyyy-MM-dd"), x.MetricValue))
  98. .ToList()
  99. };
  100. filteredCount++;
  101. }
  102. result.Scope = filteredCount switch
  103. {
  104. 0 when summaryCount > 0 && partialCount == 0 => "summary_only_fallback",
  105. 0 => "summary_only_fallback",
  106. var n when n == kpis.Count => "filtered_atomic",
  107. var n when n > 0 => "partial_filtered",
  108. _ => "summary_only_fallback"
  109. };
  110. return result;
  111. }
  112. private static string BuildAggregateSql(string aggregationType, bool groupByDate)
  113. {
  114. return aggregationType switch
  115. {
  116. SmartOpsKpiAggregateRuleRegistry.AggSum => "ROUND(SUM(sum_value), 4)",
  117. SmartOpsKpiAggregateRuleRegistry.AggRatio =>
  118. "ROUND(100 * SUM(numerator_value) / NULLIF(SUM(denominator_value), 0), 4)",
  119. SmartOpsKpiAggregateRuleRegistry.AggMin => "ROUND(MIN(min_value), 4)",
  120. SmartOpsKpiAggregateRuleRegistry.AggMax => "ROUND(MAX(max_value), 4)",
  121. _ => "ROUND(SUM(sum_value) / NULLIF(SUM(sample_count), 0), 4)"
  122. };
  123. }
  124. private static string BuildFactoryWhereSql(SmartOpsDashboardFilter filter)
  125. {
  126. // UAT/多工厂租户下 atomic 行的 factory_id 常与汇总层默认 factoryId=1 不一致;
  127. // 有业务维度筛选时按 tenant 聚合,避免筛选命中 0 行后回退模块汇总。
  128. return HasBusinessDimensionFilter(filter) ? string.Empty : "AND factory_id = @factoryId";
  129. }
  130. private static bool HasBusinessDimensionFilter(SmartOpsDashboardFilter filter) =>
  131. !string.IsNullOrWhiteSpace(filter.Customer) ||
  132. !string.IsNullOrWhiteSpace(filter.Product) ||
  133. !string.IsNullOrWhiteSpace(filter.OrderNo) ||
  134. !string.IsNullOrWhiteSpace(filter.ProductionLine) ||
  135. !string.IsNullOrWhiteSpace(filter.Supplier) ||
  136. !string.IsNullOrWhiteSpace(filter.Material) ||
  137. !string.IsNullOrWhiteSpace(filter.PoNo) ||
  138. !string.IsNullOrWhiteSpace(filter.Warehouse) ||
  139. !string.IsNullOrWhiteSpace(filter.WorkOrder) ||
  140. !string.IsNullOrWhiteSpace(filter.Equipment) ||
  141. !string.IsNullOrWhiteSpace(filter.OutboundNo);
  142. private static string BuildWhereSql(SmartOpsDashboardFilter filter)
  143. {
  144. var clauses = new List<string>();
  145. if (filter.DateStart.HasValue) clauses.Add("AND stat_date >= @dateStart");
  146. if (filter.DateEnd.HasValue) clauses.Add("AND stat_date <= @dateEnd");
  147. if (!string.IsNullOrWhiteSpace(filter.Customer)) clauses.Add("AND customer_code = @customer");
  148. if (!string.IsNullOrWhiteSpace(filter.Product)) clauses.Add("AND product_code = @product");
  149. if (!string.IsNullOrWhiteSpace(filter.OrderNo)) clauses.Add("AND order_no = @orderNo");
  150. if (!string.IsNullOrWhiteSpace(filter.ProductionLine)) clauses.Add("AND production_line = @productionLine");
  151. if (!string.IsNullOrWhiteSpace(filter.Supplier)) clauses.Add("AND supplier_code = @supplier");
  152. if (!string.IsNullOrWhiteSpace(filter.Material)) clauses.Add("AND material_code = @material");
  153. if (!string.IsNullOrWhiteSpace(filter.PoNo)) clauses.Add("AND po_no = @poNo");
  154. if (!string.IsNullOrWhiteSpace(filter.Warehouse)) clauses.Add("AND warehouse_code = @warehouse");
  155. if (!string.IsNullOrWhiteSpace(filter.WorkOrder)) clauses.Add("AND work_order_no = @workOrder");
  156. if (!string.IsNullOrWhiteSpace(filter.Equipment)) clauses.Add("AND equipment_code = @equipment");
  157. if (!string.IsNullOrWhiteSpace(filter.OutboundNo)) clauses.Add("AND outbound_no = @outboundNo");
  158. return string.Join(' ', clauses);
  159. }
  160. private static object BuildParameters(
  161. long tenantId,
  162. long factoryId,
  163. string domainCode,
  164. string metricCode,
  165. SmartOpsDashboardFilter filter)
  166. {
  167. return new
  168. {
  169. tenantId,
  170. factoryId,
  171. domainCode,
  172. metricCode,
  173. dateStart = filter.DateStart?.Date,
  174. dateEnd = filter.DateEnd?.Date,
  175. customer = filter.Customer?.Trim(),
  176. product = filter.Product?.Trim(),
  177. orderNo = filter.OrderNo?.Trim(),
  178. productionLine = filter.ProductionLine?.Trim(),
  179. supplier = filter.Supplier?.Trim(),
  180. material = filter.Material?.Trim(),
  181. poNo = filter.PoNo?.Trim(),
  182. warehouse = filter.Warehouse?.Trim(),
  183. workOrder = filter.WorkOrder?.Trim(),
  184. equipment = filter.Equipment?.Trim(),
  185. outboundNo = filter.OutboundNo?.Trim()
  186. };
  187. }
  188. }