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; }
}
}