S8RuleRuntimeParameters.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  4. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
  5. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  6. /// <summary>
  7. /// S8-RULE-GOVERNANCE-BATCH1:一条规则在某租户下的**运行参数**(B 类)。
  8. ///
  9. /// <para><b>与 Definition(A 类)的分界线</b>:改这里的值,规则「判定什么」不变,只是
  10. /// 「多久跑一次、抖动几次算数、宽限多久、报多严重、兜底派给谁」变了。
  11. /// 改 Definition 才会改变业务含义 —— 而 Definition 在代码里,没有 API 能改。</para>
  12. ///
  13. /// <para><b>params_json 的新职责</b>:只承载 <see cref="GraceMinutes"/> /
  14. /// <see cref="DefaultOccurrenceDeptId"/> / <see cref="DefaultResponsibleDeptId"/> 三项。
  15. /// 其余参数是实体列(poll / trigger / recover / severity),不进 JSON。</para>
  16. ///
  17. /// <para><b>历史 JSON 兼容(本批不做 migration)</b>:库里现存的 params_json 可能是
  18. /// 旧的 A+B 全量 JSON、可能只有 B、也可能是 NULL。三种都必须能起来:
  19. /// <list type="bullet">
  20. /// <item>旧 A 字段(dueAtField / statusField / completedStates / objectIdField /
  21. /// objectCodeField / exceptionTypeCode)→ <b>读到也忽略</b>,绝不作为语义来源;</item>
  22. /// <item>B 字段存在 → 正常解析;</item>
  23. /// <item>params_json 为 NULL / 空白 / 非法 JSON → 全部回落 Definition 声明的默认值,
  24. /// <b>不再抛 rule_not_configured</b>。</item>
  25. /// </list>
  26. /// 最后一条是 G1 的直接修复点:本地 Rule 01 的 params_json 已被
  27. /// <c>PUT /params {"enabled":false}</c> 抹成 NULL,在旧代码里这条规则已经永久跑不起来。</para>
  28. /// </summary>
  29. public sealed class S8RuleRuntimeParameters
  30. {
  31. /// <summary>轮询间隔(秒)。真源是实体列 <c>poll_interval_seconds</c>。</summary>
  32. public int PollIntervalSeconds { get; init; }
  33. /// <summary>连续命中 N 次才建单。真源是实体列 <c>trigger_count_required</c>。</summary>
  34. public int TriggerCountRequired { get; init; }
  35. /// <summary>连续未命中 N 次才标 recovered。真源是实体列 <c>recover_count_required</c>。</summary>
  36. public int RecoverCountRequired { get; init; }
  37. /// <summary>严重度。真源是实体列 <c>severity</c>;非法值回落 Definition 默认,不静默降级为 FOLLOW。</summary>
  38. public string Severity { get; init; } = S8SeverityCode.Follow;
  39. /// <summary>宽限分钟。真源是 params_json。</summary>
  40. public int GraceMinutes { get; init; }
  41. /// <summary>发生部门兜底。真源是 params_json;未配置为 null。</summary>
  42. public long? DefaultOccurrenceDeptId { get; init; }
  43. /// <summary>责任部门兜底。真源是 params_json;未配置为 null。</summary>
  44. public long? DefaultResponsibleDeptId { get; init; }
  45. // ─────────────────────────────────────────────────────────────
  46. // params_json 的 B 类键名。写入与读取共用同一批常量,杜绝两处拼写漂移。
  47. // ─────────────────────────────────────────────────────────────
  48. internal const string KeyGraceMinutes = "graceMinutes";
  49. internal const string KeyDefaultOccurrenceDeptId = "defaultOccurrenceDeptId";
  50. internal const string KeyDefaultResponsibleDeptId = "defaultResponsibleDeptId";
  51. /// <summary>
  52. /// 从实体行 + Definition 解析出生效运行参数。
  53. /// <b>本方法不抛异常</b>:任何解析失败都回落默认值。运行参数解析失败不应该让一条
  54. /// 业务语义完全正确的规则停摆 —— 那正是 G1 造成的后果。
  55. /// </summary>
  56. public static S8RuleRuntimeParameters Resolve(AdoS8WatchRule rule, S8RuleDefinition definition)
  57. {
  58. var policy = definition.Parameters ?? new S8RuleParameterPolicy();
  59. var graceMinutes = policy.GraceMinutesDefault;
  60. long? occurrenceDeptId = null;
  61. long? responsibleDeptId = null;
  62. if (!string.IsNullOrWhiteSpace(rule.ParamsJson))
  63. {
  64. try
  65. {
  66. using var doc = JsonDocument.Parse(rule.ParamsJson!);
  67. var root = doc.RootElement;
  68. if (root.ValueKind == JsonValueKind.Object)
  69. {
  70. // 只读 B 类三键。旧 JSON 里的 A 类字段就在同一个对象里,
  71. // 这里**不读**它们 —— 忽略是刻意的,不是遗漏。
  72. if (root.TryGetProperty(KeyGraceMinutes, out var g)
  73. && g.ValueKind == JsonValueKind.Number
  74. && g.TryGetInt32(out var gv))
  75. graceMinutes = gv;
  76. occurrenceDeptId = ReadNullableLong(root, KeyDefaultOccurrenceDeptId);
  77. responsibleDeptId = ReadNullableLong(root, KeyDefaultResponsibleDeptId);
  78. }
  79. }
  80. catch (JsonException)
  81. {
  82. // 非法 JSON:回落默认值。历史脏数据不该把规则打死。
  83. }
  84. }
  85. if (!policy.AllowsDepartmentDefaults)
  86. {
  87. occurrenceDeptId = null;
  88. responsibleDeptId = null;
  89. }
  90. return new S8RuleRuntimeParameters
  91. {
  92. PollIntervalSeconds = Clamp(rule.PollIntervalSeconds, policy.PollIntervalSecondsMin,
  93. policy.PollIntervalSecondsMax, policy.PollIntervalSecondsDefault),
  94. TriggerCountRequired = Clamp(rule.TriggerCountRequired, policy.TriggerCountRequiredMin,
  95. policy.TriggerCountRequiredMax, policy.TriggerCountRequiredDefault),
  96. RecoverCountRequired = Clamp(rule.RecoverCountRequired, policy.RecoverCountRequiredMin,
  97. policy.RecoverCountRequiredMax, policy.RecoverCountRequiredDefault),
  98. Severity = ResolveSeverity(rule.Severity, policy),
  99. GraceMinutes = Clamp(graceMinutes, policy.GraceMinutesMin, policy.GraceMinutesMax,
  100. policy.GraceMinutesDefault),
  101. DefaultOccurrenceDeptId = occurrenceDeptId,
  102. DefaultResponsibleDeptId = responsibleDeptId
  103. };
  104. }
  105. /// <summary>
  106. /// 把 B 类参数序列化回 params_json。
  107. /// <b>只写这三个键</b>:任何旧 A 类字段在这次写入后自然消失,无需 migration 也不会被再次读到。
  108. /// </summary>
  109. public string ToParamsJson()
  110. {
  111. var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
  112. {
  113. [KeyGraceMinutes] = GraceMinutes
  114. };
  115. if (DefaultOccurrenceDeptId.HasValue) payload[KeyDefaultOccurrenceDeptId] = DefaultOccurrenceDeptId.Value;
  116. if (DefaultResponsibleDeptId.HasValue) payload[KeyDefaultResponsibleDeptId] = DefaultResponsibleDeptId.Value;
  117. return JsonSerializer.Serialize(payload);
  118. }
  119. private static long? ReadNullableLong(JsonElement root, string key)
  120. {
  121. if (!root.TryGetProperty(key, out var el)) return null;
  122. if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var v)) return v > 0 ? v : null;
  123. // 历史数据里存在字符串化的 id,容忍解析但不容忍非法值。
  124. if (el.ValueKind == JsonValueKind.String && long.TryParse(el.GetString(), out var sv)) return sv > 0 ? sv : null;
  125. return null;
  126. }
  127. private static int Clamp(int value, int min, int max, int fallback)
  128. {
  129. if (value < min || value > max) return fallback;
  130. return value;
  131. }
  132. /// <summary>
  133. /// 严重度解析。**刻意不复用 <c>S8SeverityCode.Normalize</c>**:它的兜底分支是
  134. /// <c>_ =&gt; Follow</c>,会把 legacy 的 HIGH / 拼写错误静默降级成 FOLLOW;
  135. /// 那正是库里 severity='HIGH' 的行当年混进来的方式。这里回落 Definition 声明的默认值。
  136. /// </summary>
  137. private static string ResolveSeverity(string? severity, S8RuleParameterPolicy policy)
  138. {
  139. if (!string.IsNullOrWhiteSpace(severity)
  140. && policy.AllowedSeverities.Contains(severity!.Trim(), StringComparer.Ordinal))
  141. return severity!.Trim();
  142. return policy.SeverityDefault;
  143. }
  144. }