KpiResultStatusResolver.cs 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  2. public sealed record KpiResultDecision(string Status, string? Reason, decimal? Value);
  3. /// <summary>KPI 日值写入 result_status / result_reason 的共用片段。</summary>
  4. public static class KpiResultWrite
  5. {
  6. public const string SetClause = "result_status=@ResultStatus, result_reason=@ResultReason, ";
  7. public const string InsertCols = "result_status, result_reason, ";
  8. public const string InsertVals = "@ResultStatus, @ResultReason, ";
  9. public const string FromMetricSet =
  10. "result_status=IF(@MetricValue IS NULL,'NO_DATA','OK'), result_reason=IF(@MetricValue IS NULL,'窗口内无业务数据',NULL), ";
  11. public const string FromMetricCols = "result_status, result_reason, ";
  12. public const string FromMetricVals =
  13. "IF(@MetricValue IS NULL,'NO_DATA','OK'), IF(@MetricValue IS NULL,'窗口内无业务数据',NULL), ";
  14. public static SugarParameter Status(string status) => new("@ResultStatus", status);
  15. public static SugarParameter Reason(string? reason) => new("@ResultReason", (object?)reason ?? DBNull.Value);
  16. }
  17. /// <summary>
  18. /// 有值 → OK;无业务 → NO_DATA;依赖语义被登记来源标明不支持 → SOURCE_UNSUPPORTED,值置空。
  19. /// 对象类语义未登记 → FAILED。
  20. /// </summary>
  21. public static class KpiResultStatusResolver
  22. {
  23. public static KpiResultDecision Resolve(
  24. string metricCode,
  25. decimal? value,
  26. IReadOnlyDictionary<string, string> unsupported,
  27. IReadOnlySet<string> registeredObjects)
  28. {
  29. var reasons = new List<string>();
  30. foreach (var dep in KpiSemanticDependency.For(metricCode))
  31. {
  32. if (dep is "无")
  33. continue;
  34. if (dep is "S6_REPORT" or "FQC")
  35. {
  36. var obj = dep == "FQC" ? "FQC_TASK" : "S6_REPORT";
  37. if (!registeredObjects.Contains(obj))
  38. reasons.Add($"租户未登记 {obj} 来源");
  39. continue;
  40. }
  41. if (unsupported.TryGetValue(dep, out var reason))
  42. reasons.Add(string.IsNullOrWhiteSpace(reason) ? dep : reason);
  43. }
  44. if (reasons.Count > 0)
  45. {
  46. var failed = reasons.Any(r => r.Contains("未登记", StringComparison.Ordinal));
  47. var status = failed && reasons.All(r => r.Contains("未登记", StringComparison.Ordinal))
  48. ? "FAILED"
  49. : "SOURCE_UNSUPPORTED";
  50. return new KpiResultDecision(status, string.Join(";", reasons), null);
  51. }
  52. return value is null
  53. ? new KpiResultDecision("NO_DATA", "窗口内无业务数据", null)
  54. : new KpiResultDecision("OK", null, value);
  55. }
  56. }
  57. public sealed class KpiResultStatusService : ITransient
  58. {
  59. private readonly ISqlSugarClient _db;
  60. public KpiResultStatusService(ISqlSugarClient db) => _db = db;
  61. public async Task<KpiResultDecision> ResolveAsync(long tenantId, string metricCode, decimal? value, CancellationToken cancellationToken = default)
  62. {
  63. var unsupportedRows = await _db.Ado.SqlQueryAsync<CapRow>(
  64. """
  65. SELECT c.semantic_code AS SemanticCode, c.reason AS Reason
  66. FROM mdp_tenant_std_source s
  67. JOIN mdp_source_capability c ON c.source_system = s.source_system AND c.supported = 0
  68. WHERE s.tenant_id = @tenant
  69. """,
  70. new { tenant = tenantId });
  71. cancellationToken.ThrowIfCancellationRequested();
  72. var unsupported = unsupportedRows
  73. .GroupBy(r => r.SemanticCode, StringComparer.Ordinal)
  74. .ToDictionary(g => g.Key, g => g.First().Reason ?? "", StringComparer.Ordinal);
  75. var objects = await _db.Ado.SqlQueryAsync<string>(
  76. "SELECT std_object FROM mdp_tenant_std_source WHERE tenant_id=@tenant",
  77. new { tenant = tenantId });
  78. return KpiResultStatusResolver.Resolve(metricCode, value, unsupported, objects.ToHashSet(StringComparer.Ordinal));
  79. }
  80. private sealed class CapRow
  81. {
  82. public string SemanticCode { get; set; } = "";
  83. public string? Reason { get; set; }
  84. }
  85. }