namespace Admin.NET.Plugin.AiDOP.SmartOps; public sealed record KpiResultDecision(string Status, string? Reason, decimal? Value); /// KPI 日值写入 result_status / result_reason 的共用片段。 public static class KpiResultWrite { public const string SetClause = "result_status=@ResultStatus, result_reason=@ResultReason, "; public const string InsertCols = "result_status, result_reason, "; public const string InsertVals = "@ResultStatus, @ResultReason, "; public const string FromMetricSet = "result_status=IF(@MetricValue IS NULL,'NO_DATA','OK'), result_reason=IF(@MetricValue IS NULL,'窗口内无业务数据',NULL), "; public const string FromMetricCols = "result_status, result_reason, "; public const string FromMetricVals = "IF(@MetricValue IS NULL,'NO_DATA','OK'), IF(@MetricValue IS NULL,'窗口内无业务数据',NULL), "; public static SugarParameter Status(string status) => new("@ResultStatus", status); public static SugarParameter Reason(string? reason) => new("@ResultReason", (object?)reason ?? DBNull.Value); } /// /// 有值 → OK;无业务 → NO_DATA;依赖语义被登记来源标明不支持 → SOURCE_UNSUPPORTED,值置空。 /// 对象类语义未登记 → FAILED。 /// public static class KpiResultStatusResolver { public static KpiResultDecision Resolve( string metricCode, decimal? value, IReadOnlyDictionary unsupported, IReadOnlySet registeredObjects) { var reasons = new List(); foreach (var dep in KpiSemanticDependency.For(metricCode)) { if (dep is "无") continue; if (dep is "S6_REPORT" or "FQC") { var obj = dep == "FQC" ? "FQC_TASK" : "S6_REPORT"; if (!registeredObjects.Contains(obj)) reasons.Add($"租户未登记 {obj} 来源"); continue; } if (unsupported.TryGetValue(dep, out var reason)) reasons.Add(string.IsNullOrWhiteSpace(reason) ? dep : reason); } if (reasons.Count > 0) { var failed = reasons.Any(r => r.Contains("未登记", StringComparison.Ordinal)); var status = failed && reasons.All(r => r.Contains("未登记", StringComparison.Ordinal)) ? "FAILED" : "SOURCE_UNSUPPORTED"; return new KpiResultDecision(status, string.Join(";", reasons), null); } return value is null ? new KpiResultDecision("NO_DATA", "窗口内无业务数据", null) : new KpiResultDecision("OK", null, value); } } public sealed class KpiResultStatusService : ITransient { private readonly ISqlSugarClient _db; public KpiResultStatusService(ISqlSugarClient db) => _db = db; public async Task ResolveAsync(long tenantId, string metricCode, decimal? value, CancellationToken cancellationToken = default) { var unsupportedRows = await _db.Ado.SqlQueryAsync( """ SELECT c.semantic_code AS SemanticCode, c.reason AS Reason FROM mdp_tenant_std_source s JOIN mdp_source_capability c ON c.source_system = s.source_system AND c.supported = 0 WHERE s.tenant_id = @tenant """, new { tenant = tenantId }); cancellationToken.ThrowIfCancellationRequested(); var unsupported = unsupportedRows .GroupBy(r => r.SemanticCode, StringComparer.Ordinal) .ToDictionary(g => g.Key, g => g.First().Reason ?? "", StringComparer.Ordinal); var objects = await _db.Ado.SqlQueryAsync( "SELECT std_object FROM mdp_tenant_std_source WHERE tenant_id=@tenant", new { tenant = tenantId }); return KpiResultStatusResolver.Resolve(metricCode, value, unsupported, objects.ToHashSet(StringComparer.Ordinal)); } private sealed class CapRow { public string SemanticCode { get; set; } = ""; public string? Reason { get; set; } } }