using Admin.NET.Core;
using Admin.NET.Plugin.AiDOP.Entity;
using Admin.NET.Plugin.AiDOP.Infrastructure;
using SqlSugar;
namespace Admin.NET.Plugin.AiDOP.SmartOps;
///
/// 从 KPI 原子聚合层按筛选条件动态聚合并计算最终 KPI。
///
public class SmartOpsKpiAtomicQueryService : ITransient
{
private readonly ISqlSugarClient _db;
public SmartOpsKpiAtomicQueryService(ISqlSugarClient db)
{
_db = db;
}
public async Task QueryAsync(
long tenantId,
long factoryId,
string moduleCode,
SmartOpsDashboardFilter filter,
IReadOnlyList kpis,
CancellationToken cancellationToken = default)
{
AidopTenantMigration.EnsureKpiAtomicTable(_db);
var result = new FilteredKpiQueryResult
{
FilterContext = filter.ToContextDictionary()
.Where(x => !string.IsNullOrWhiteSpace(x.Value))
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)
};
if (filter.IsEmpty || kpis.Count == 0)
{
result.Scope = "module_summary";
return result;
}
var filteredCount = 0;
var summaryCount = 0;
var partialCount = 0;
foreach (var kpi in kpis)
{
cancellationToken.ThrowIfCancellationRequested();
var metricCode = kpi.MetricCode;
var rule = SmartOpsKpiAggregateRuleRegistry.TryGet(metricCode);
var unsupported = SmartOpsKpiAggregateRuleRegistry.GetUnsupportedFilters(metricCode, filter).ToArray();
var filterMode = SmartOpsKpiAggregateRuleRegistry.ResolveFilterMode(metricCode, filter);
result.MetricFilterModes[metricCode] = filterMode;
if (unsupported.Length > 0)
result.UnsupportedFiltersByMetric[metricCode] = unsupported;
if (rule == null || rule.FilterMode == SmartOpsKpiAggregateRuleRegistry.FilterSummaryOnly ||
!SmartOpsKpiAggregateRuleRegistry.SupportsFilter(metricCode, filter))
{
if (filterMode == SmartOpsKpiAggregateRuleRegistry.FilterPartial)
partialCount++;
else
summaryCount++;
continue;
}
var valueSql = BuildAggregateSql(rule.AggregationType, groupByDate: false);
var whereSql = BuildWhereSql(filter);
var factorySql = BuildFactoryWhereSql(filter);
var sql = $"""
SELECT {valueSql} AS MetricValue
FROM ado_smart_ops_kpi_atomic_day
WHERE tenant_id = @tenantId
{factorySql}
AND domain_code = @domainCode
AND metric_code = @metricCode
AND is_deleted = 0
{whereSql}
""";
var parameters = BuildParameters(tenantId, factoryId, rule.DomainCode, metricCode, filter);
var current = await _db.Ado.SqlQuerySingleAsync(sql, parameters);
if (current?.MetricValue == null)
{
summaryCount++;
result.MetricFilterModes[metricCode] = SmartOpsKpiAggregateRuleRegistry.FilterSummaryOnly;
continue;
}
var trendSql = $"""
SELECT stat_date AS StatDate, {BuildAggregateSql(rule.AggregationType, groupByDate: true)} AS MetricValue
FROM ado_smart_ops_kpi_atomic_day
WHERE tenant_id = @tenantId
{factorySql}
AND domain_code = @domainCode
AND metric_code = @metricCode
AND is_deleted = 0
{whereSql}
GROUP BY stat_date
ORDER BY stat_date
""";
var trendRows = await _db.Ado.SqlQueryAsync(trendSql, parameters);
result.Values[metricCode] = new SmartOpsKpiMetricValue
{
Level = kpi.MetricLevel,
MetricValue = current.MetricValue,
Trend = trendRows
.Select(x => new SmartOpsKpiTrendPoint(x.StatDate.ToString("yyyy-MM-dd"), x.MetricValue))
.ToList()
};
filteredCount++;
}
result.Scope = filteredCount switch
{
0 when summaryCount > 0 && partialCount == 0 => "summary_only_fallback",
0 => "summary_only_fallback",
var n when n == kpis.Count => "filtered_atomic",
var n when n > 0 => "partial_filtered",
_ => "summary_only_fallback"
};
return result;
}
private static string BuildAggregateSql(string aggregationType, bool groupByDate)
{
return aggregationType switch
{
SmartOpsKpiAggregateRuleRegistry.AggSum => "ROUND(SUM(sum_value), 4)",
SmartOpsKpiAggregateRuleRegistry.AggRatio =>
"ROUND(100 * SUM(numerator_value) / NULLIF(SUM(denominator_value), 0), 4)",
SmartOpsKpiAggregateRuleRegistry.AggMin => "ROUND(MIN(min_value), 4)",
SmartOpsKpiAggregateRuleRegistry.AggMax => "ROUND(MAX(max_value), 4)",
_ => "ROUND(SUM(sum_value) / NULLIF(SUM(sample_count), 0), 4)"
};
}
private static string BuildFactoryWhereSql(SmartOpsDashboardFilter filter)
{
// UAT/多工厂租户下 atomic 行的 factory_id 常与汇总层默认 factoryId=1 不一致;
// 有业务维度筛选时按 tenant 聚合,避免筛选命中 0 行后回退模块汇总。
return HasBusinessDimensionFilter(filter) ? string.Empty : "AND factory_id = @factoryId";
}
private static bool HasBusinessDimensionFilter(SmartOpsDashboardFilter filter) =>
!string.IsNullOrWhiteSpace(filter.Customer) ||
!string.IsNullOrWhiteSpace(filter.Product) ||
!string.IsNullOrWhiteSpace(filter.OrderNo) ||
!string.IsNullOrWhiteSpace(filter.ProductionLine) ||
!string.IsNullOrWhiteSpace(filter.Supplier) ||
!string.IsNullOrWhiteSpace(filter.Material) ||
!string.IsNullOrWhiteSpace(filter.PoNo) ||
!string.IsNullOrWhiteSpace(filter.Warehouse) ||
!string.IsNullOrWhiteSpace(filter.WorkOrder) ||
!string.IsNullOrWhiteSpace(filter.Equipment) ||
!string.IsNullOrWhiteSpace(filter.OutboundNo);
private static string BuildWhereSql(SmartOpsDashboardFilter filter)
{
var clauses = new List();
if (filter.DateStart.HasValue) clauses.Add("AND stat_date >= @dateStart");
if (filter.DateEnd.HasValue) clauses.Add("AND stat_date <= @dateEnd");
if (!string.IsNullOrWhiteSpace(filter.Customer)) clauses.Add("AND customer_code = @customer");
if (!string.IsNullOrWhiteSpace(filter.Product)) clauses.Add("AND product_code = @product");
if (!string.IsNullOrWhiteSpace(filter.OrderNo)) clauses.Add("AND order_no = @orderNo");
if (!string.IsNullOrWhiteSpace(filter.ProductionLine)) clauses.Add("AND production_line = @productionLine");
if (!string.IsNullOrWhiteSpace(filter.Supplier)) clauses.Add("AND supplier_code = @supplier");
if (!string.IsNullOrWhiteSpace(filter.Material)) clauses.Add("AND material_code = @material");
if (!string.IsNullOrWhiteSpace(filter.PoNo)) clauses.Add("AND po_no = @poNo");
if (!string.IsNullOrWhiteSpace(filter.Warehouse)) clauses.Add("AND warehouse_code = @warehouse");
if (!string.IsNullOrWhiteSpace(filter.WorkOrder)) clauses.Add("AND work_order_no = @workOrder");
if (!string.IsNullOrWhiteSpace(filter.Equipment)) clauses.Add("AND equipment_code = @equipment");
if (!string.IsNullOrWhiteSpace(filter.OutboundNo)) clauses.Add("AND outbound_no = @outboundNo");
return string.Join(' ', clauses);
}
private static object BuildParameters(
long tenantId,
long factoryId,
string domainCode,
string metricCode,
SmartOpsDashboardFilter filter)
{
return new
{
tenantId,
factoryId,
domainCode,
metricCode,
dateStart = filter.DateStart?.Date,
dateEnd = filter.DateEnd?.Date,
customer = filter.Customer?.Trim(),
product = filter.Product?.Trim(),
orderNo = filter.OrderNo?.Trim(),
productionLine = filter.ProductionLine?.Trim(),
supplier = filter.Supplier?.Trim(),
material = filter.Material?.Trim(),
poNo = filter.PoNo?.Trim(),
warehouse = filter.Warehouse?.Trim(),
workOrder = filter.WorkOrder?.Trim(),
equipment = filter.Equipment?.Trim(),
outboundNo = filter.OutboundNo?.Trim()
};
}
}