| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- using System.Text.Json;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
- namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
- /// <summary>
- /// S8-RULE-GOVERNANCE-BATCH1:一条规则在某租户下的**运行参数**(B 类)。
- ///
- /// <para><b>与 Definition(A 类)的分界线</b>:改这里的值,规则「判定什么」不变,只是
- /// 「多久跑一次、抖动几次算数、宽限多久、报多严重、兜底派给谁」变了。
- /// 改 Definition 才会改变业务含义 —— 而 Definition 在代码里,没有 API 能改。</para>
- ///
- /// <para><b>params_json 的新职责</b>:只承载 <see cref="GraceMinutes"/> /
- /// <see cref="DefaultOccurrenceDeptId"/> / <see cref="DefaultResponsibleDeptId"/> 三项。
- /// 其余参数是实体列(poll / trigger / recover / severity),不进 JSON。</para>
- ///
- /// <para><b>历史 JSON 兼容(本批不做 migration)</b>:库里现存的 params_json 可能是
- /// 旧的 A+B 全量 JSON、可能只有 B、也可能是 NULL。三种都必须能起来:
- /// <list type="bullet">
- /// <item>旧 A 字段(dueAtField / statusField / completedStates / objectIdField /
- /// objectCodeField / exceptionTypeCode)→ <b>读到也忽略</b>,绝不作为语义来源;</item>
- /// <item>B 字段存在 → 正常解析;</item>
- /// <item>params_json 为 NULL / 空白 / 非法 JSON → 全部回落 Definition 声明的默认值,
- /// <b>不再抛 rule_not_configured</b>。</item>
- /// </list>
- /// 最后一条是 G1 的直接修复点:本地 Rule 01 的 params_json 已被
- /// <c>PUT /params {"enabled":false}</c> 抹成 NULL,在旧代码里这条规则已经永久跑不起来。</para>
- /// </summary>
- public sealed class S8RuleRuntimeParameters
- {
- /// <summary>轮询间隔(秒)。真源是实体列 <c>poll_interval_seconds</c>。</summary>
- public int PollIntervalSeconds { get; init; }
- /// <summary>连续命中 N 次才建单。真源是实体列 <c>trigger_count_required</c>。</summary>
- public int TriggerCountRequired { get; init; }
- /// <summary>连续未命中 N 次才标 recovered。真源是实体列 <c>recover_count_required</c>。</summary>
- public int RecoverCountRequired { get; init; }
- /// <summary>严重度。真源是实体列 <c>severity</c>;非法值回落 Definition 默认,不静默降级为 FOLLOW。</summary>
- public string Severity { get; init; } = S8SeverityCode.Follow;
- /// <summary>宽限分钟。真源是 params_json。</summary>
- public int GraceMinutes { get; init; }
- /// <summary>发生部门兜底。真源是 params_json;未配置为 null。</summary>
- public long? DefaultOccurrenceDeptId { get; init; }
- /// <summary>责任部门兜底。真源是 params_json;未配置为 null。</summary>
- public long? DefaultResponsibleDeptId { get; init; }
- // ─────────────────────────────────────────────────────────────
- // params_json 的 B 类键名。写入与读取共用同一批常量,杜绝两处拼写漂移。
- // ─────────────────────────────────────────────────────────────
- internal const string KeyGraceMinutes = "graceMinutes";
- internal const string KeyDefaultOccurrenceDeptId = "defaultOccurrenceDeptId";
- internal const string KeyDefaultResponsibleDeptId = "defaultResponsibleDeptId";
- /// <summary>
- /// 从实体行 + Definition 解析出生效运行参数。
- /// <b>本方法不抛异常</b>:任何解析失败都回落默认值。运行参数解析失败不应该让一条
- /// 业务语义完全正确的规则停摆 —— 那正是 G1 造成的后果。
- /// </summary>
- public static S8RuleRuntimeParameters Resolve(AdoS8WatchRule rule, S8RuleDefinition definition)
- {
- var policy = definition.Parameters ?? new S8RuleParameterPolicy();
- var graceMinutes = policy.GraceMinutesDefault;
- long? occurrenceDeptId = null;
- long? responsibleDeptId = null;
- if (!string.IsNullOrWhiteSpace(rule.ParamsJson))
- {
- try
- {
- using var doc = JsonDocument.Parse(rule.ParamsJson!);
- var root = doc.RootElement;
- if (root.ValueKind == JsonValueKind.Object)
- {
- // 只读 B 类三键。旧 JSON 里的 A 类字段就在同一个对象里,
- // 这里**不读**它们 —— 忽略是刻意的,不是遗漏。
- if (root.TryGetProperty(KeyGraceMinutes, out var g)
- && g.ValueKind == JsonValueKind.Number
- && g.TryGetInt32(out var gv))
- graceMinutes = gv;
- occurrenceDeptId = ReadNullableLong(root, KeyDefaultOccurrenceDeptId);
- responsibleDeptId = ReadNullableLong(root, KeyDefaultResponsibleDeptId);
- }
- }
- catch (JsonException)
- {
- // 非法 JSON:回落默认值。历史脏数据不该把规则打死。
- }
- }
- if (!policy.AllowsDepartmentDefaults)
- {
- occurrenceDeptId = null;
- responsibleDeptId = null;
- }
- return new S8RuleRuntimeParameters
- {
- PollIntervalSeconds = Clamp(rule.PollIntervalSeconds, policy.PollIntervalSecondsMin,
- policy.PollIntervalSecondsMax, policy.PollIntervalSecondsDefault),
- TriggerCountRequired = Clamp(rule.TriggerCountRequired, policy.TriggerCountRequiredMin,
- policy.TriggerCountRequiredMax, policy.TriggerCountRequiredDefault),
- RecoverCountRequired = Clamp(rule.RecoverCountRequired, policy.RecoverCountRequiredMin,
- policy.RecoverCountRequiredMax, policy.RecoverCountRequiredDefault),
- Severity = ResolveSeverity(rule.Severity, policy),
- GraceMinutes = Clamp(graceMinutes, policy.GraceMinutesMin, policy.GraceMinutesMax,
- policy.GraceMinutesDefault),
- DefaultOccurrenceDeptId = occurrenceDeptId,
- DefaultResponsibleDeptId = responsibleDeptId
- };
- }
- /// <summary>
- /// 把 B 类参数序列化回 params_json。
- /// <b>只写这三个键</b>:任何旧 A 类字段在这次写入后自然消失,无需 migration 也不会被再次读到。
- /// </summary>
- public string ToParamsJson()
- {
- var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
- {
- [KeyGraceMinutes] = GraceMinutes
- };
- if (DefaultOccurrenceDeptId.HasValue) payload[KeyDefaultOccurrenceDeptId] = DefaultOccurrenceDeptId.Value;
- if (DefaultResponsibleDeptId.HasValue) payload[KeyDefaultResponsibleDeptId] = DefaultResponsibleDeptId.Value;
- return JsonSerializer.Serialize(payload);
- }
- private static long? ReadNullableLong(JsonElement root, string key)
- {
- if (!root.TryGetProperty(key, out var el)) return null;
- if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var v)) return v > 0 ? v : null;
- // 历史数据里存在字符串化的 id,容忍解析但不容忍非法值。
- if (el.ValueKind == JsonValueKind.String && long.TryParse(el.GetString(), out var sv)) return sv > 0 ? sv : null;
- return null;
- }
- private static int Clamp(int value, int min, int max, int fallback)
- {
- if (value < min || value > max) return fallback;
- return value;
- }
- /// <summary>
- /// 严重度解析。**刻意不复用 <c>S8SeverityCode.Normalize</c>**:它的兜底分支是
- /// <c>_ => Follow</c>,会把 legacy 的 HIGH / 拼写错误静默降级成 FOLLOW;
- /// 那正是库里 severity='HIGH' 的行当年混进来的方式。这里回落 Definition 声明的默认值。
- /// </summary>
- private static string ResolveSeverity(string? severity, S8RuleParameterPolicy policy)
- {
- if (!string.IsNullOrWhiteSpace(severity)
- && policy.AllowedSeverities.Contains(severity!.Trim(), StringComparer.Ordinal))
- return severity!.Trim();
- return policy.SeverityDefault;
- }
- }
|