using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess; namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions; /// 规则目录:回答「这个 rule_code 在当前代码版本里是否被定义、定义成什么」。 public interface IS8RuleCatalog { /// 取定义;未定义返回 null。 S8RuleDefinition? TryGet(string? ruleCode); /// 取定义;未定义抛 ,消息带 reason code。 S8RuleDefinition GetRequired(string? ruleCode); /// 是否已定义。 bool IsDefined(string? ruleCode); /// 全部已定义规则。Batch 2 的 provisioning 将按此为每个租户落 runtime policy 行。 IReadOnlyCollection Definitions { get; } } /// /// 默认目录实现:聚合所有 ,并在构造时做完整性校验。 /// /// 为什么校验放在构造函数而不是运行期:一条定义写错(rule_type 不在词表、 /// TIMEOUT 却没给 Timeout 语义、scene 不是 S1–S7)属于**发布事故**, /// 应该在服务起来的时候就炸掉,而不是等某条规则跑到那一 tick 才报错—— /// 那时错误只出现在后台日志里,页面上一切正常。 /// /// 注册为 Singleton + 启动期强制解析(见 Startup.Configure)。 /// 数据集侧 S8DatasetCatalog 注册为 Transient,其重复定义的失败形态是 /// 「首次解析该服务的那个请求 500」而非启动崩溃 —— 该问题在 /// S8BusinessDatasetDefinitions 的注释里已被记录为已知弱点。规则侧不重复这个弱点。 /// public class S8RuleCatalog : IS8RuleCatalog { /// 规则在当前代码版本中无定义。与「配置错了」严格区分:它意味着该规则不该运行。 public const string ReasonNotFound = "rule_definition_not_found"; private readonly Dictionary _definitions; public S8RuleCatalog(IEnumerable sources) { // rule_code 在 DB 侧是大小写敏感比较(dedup_key 逐字拼接), // 但目录查找用不敏感比较可以把「大小写写错」暴露成重复定义而不是静默两条, // 与 S8DatasetCatalog 的 OrdinalIgnoreCase 一致。 _definitions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var source in sources) { foreach (var definition in source.GetDefinitions()) { Validate(definition, source.GetType().Name); var code = definition.RuleCode.Trim(); if (_definitions.ContainsKey(code)) throw new InvalidOperationException( $"监控规则 {code} 被重复定义(来源包含 {source.GetType().Name})"); _definitions[code] = definition; } } } public IReadOnlyCollection Definitions => _definitions.Values; public bool IsDefined(string? ruleCode) => !string.IsNullOrWhiteSpace(ruleCode) && _definitions.ContainsKey(ruleCode!.Trim()); public S8RuleDefinition? TryGet(string? ruleCode) { if (string.IsNullOrWhiteSpace(ruleCode)) return null; return _definitions.TryGetValue(ruleCode.Trim(), out var definition) ? definition : null; } public S8RuleDefinition GetRequired(string? ruleCode) => TryGet(ruleCode) ?? throw new S8BizException( $"[{ReasonNotFound}] 监控规则 {(string.IsNullOrWhiteSpace(ruleCode) ? "(空)" : ruleCode)} 在当前版本中没有代码定义,无法配置或运行"); /// /// 定义完整性校验。每一条都对应一种「存得下但跑不通」的历史故障形态,不是形式检查。 /// private static void Validate(S8RuleDefinition d, string sourceName) { void Fail(string message) => throw new InvalidOperationException($"监控规则定义非法(来源 {sourceName}):{message}"); if (string.IsNullOrWhiteSpace(d.RuleCode)) Fail("rule_code 不能为空"); if (string.IsNullOrWhiteSpace(d.DisplayName)) Fail($"{d.RuleCode} 缺少 DisplayName(配置页的规则名称没有其他来源)"); if (string.IsNullOrWhiteSpace(d.DatasetCode)) Fail($"{d.RuleCode} 缺少 dataset_code"); if (string.IsNullOrWhiteSpace(d.ExceptionTypeCode)) Fail($"{d.RuleCode} 缺少 exception_type_code"); if (string.IsNullOrWhiteSpace(d.SourceObjectType)) Fail($"{d.RuleCode} 缺少 source_object_type(它参与 dedup_key)"); // rule_type 必须在真实存在 evaluator 的词表内,且**大小写逐字相等**。 // 刻意不用 S8RuleTypeFieldRequirements.IsKnown —— 它内部做 ToUpperInvariant, // 于是 "timeout" 能过校验;而调度器与 evaluator 的 switch 是 ordinal 敏感的, // 那条定义会一路存下来、最后在运行期报 unsupported_rule_type。 // 「存得下但跑不通」正是本 Catalog 要消灭的形态。 if (!S8RuleTypeFieldRequirements.KnownRuleTypes.Contains(d.RuleType, StringComparer.Ordinal)) Fail($"{d.RuleCode} 的 rule_type 非法:{d.RuleType};当前仅支持 " + string.Join(" / ", S8RuleTypeFieldRequirements.KnownRuleTypes)); // scene / stage 必须是严格 S1–S7,拒 legacy 复合场景(与 CreateAsync 既有口径一致)。 if (!S8ModuleCode.IsValid(d.SceneCode)) Fail($"{d.RuleCode} 的 scene_code 非法:{d.SceneCode};当前仅支持 " + string.Join(" / ", S8ModuleCode.All)); if (!S8ModuleCode.IsValid(d.StageCode)) Fail($"{d.RuleCode} 的 stage_code 非法:{d.StageCode}"); // 类型与语义必须配套:声明 TIMEOUT 却不给 Timeout 语义,运行期只会表现为 NullReference。 switch (d.RuleType) { case var t when t == S8TimeoutRuleEvaluator.RuleTypeCode: if (d.Timeout == null) Fail($"{d.RuleCode} 声明 TIMEOUT 但未提供 TIMEOUT 判定语义"); break; case var t when t == S8ShortageRuleEvaluator.RuleTypeCode: if (d.Shortage == null) Fail($"{d.RuleCode} 声明 SHORTAGE 但未提供 SHORTAGE 判定语义"); break; case var t when t == S8OutOfRangeRuleEvaluator.RuleTypeCode: if (d.OutOfRange == null) Fail($"{d.RuleCode} 声明 OUT_OF_RANGE 但未提供 OUT_OF_RANGE 判定语义"); break; } var policy = d.Parameters ?? new S8RuleParameterPolicy(); if (policy.AllowedSeverities == null || policy.AllowedSeverities.Count == 0) Fail($"{d.RuleCode} 的 AllowedSeverities 不能为空"); if (!policy.AllowedSeverities!.Contains(policy.SeverityDefault, StringComparer.Ordinal)) Fail($"{d.RuleCode} 的默认严重度 {policy.SeverityDefault} 不在允许集合内"); } }