| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- 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;
- /// <summary>规则目录:回答「这个 rule_code 在当前代码版本里是否被定义、定义成什么」。</summary>
- public interface IS8RuleCatalog
- {
- /// <summary>取定义;未定义返回 null。</summary>
- S8RuleDefinition? TryGet(string? ruleCode);
- /// <summary>取定义;未定义抛 <see cref="S8BizException"/>,消息带 reason code。</summary>
- S8RuleDefinition GetRequired(string? ruleCode);
- /// <summary>是否已定义。</summary>
- bool IsDefined(string? ruleCode);
- /// <summary>全部已定义规则。Batch 2 的 provisioning 将按此为每个租户落 runtime policy 行。</summary>
- IReadOnlyCollection<S8RuleDefinition> Definitions { get; }
- }
- /// <summary>
- /// 默认目录实现:聚合所有 <see cref="IS8RuleDefinitionSource"/>,并在构造时做完整性校验。
- ///
- /// <para><b>为什么校验放在构造函数而不是运行期</b>:一条定义写错(rule_type 不在词表、
- /// TIMEOUT 却没给 Timeout 语义、scene 不是 S1–S7)属于**发布事故**,
- /// 应该在服务起来的时候就炸掉,而不是等某条规则跑到那一 tick 才报错——
- /// 那时错误只出现在后台日志里,页面上一切正常。</para>
- ///
- /// <para><b>注册为 Singleton + 启动期强制解析</b>(见 <c>Startup.Configure</c>)。
- /// 数据集侧 <c>S8DatasetCatalog</c> 注册为 Transient,其重复定义的失败形态是
- /// 「首次解析该服务的那个请求 500」而非启动崩溃 —— 该问题在
- /// <c>S8BusinessDatasetDefinitions</c> 的注释里已被记录为已知弱点。规则侧不重复这个弱点。</para>
- /// </summary>
- public class S8RuleCatalog : IS8RuleCatalog
- {
- /// <summary>规则在当前代码版本中无定义。与「配置错了」严格区分:它意味着该规则不该运行。</summary>
- public const string ReasonNotFound = "rule_definition_not_found";
- private readonly Dictionary<string, S8RuleDefinition> _definitions;
- public S8RuleCatalog(IEnumerable<IS8RuleDefinitionSource> sources)
- {
- // rule_code 在 DB 侧是大小写敏感比较(dedup_key 逐字拼接),
- // 但目录查找用不敏感比较可以把「大小写写错」暴露成重复定义而不是静默两条,
- // 与 S8DatasetCatalog 的 OrdinalIgnoreCase 一致。
- _definitions = new Dictionary<string, S8RuleDefinition>(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<S8RuleDefinition> 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)} 在当前版本中没有代码定义,无法配置或运行");
- /// <summary>
- /// 定义完整性校验。每一条都对应一种「存得下但跑不通」的历史故障形态,不是形式检查。
- /// </summary>
- 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} 不在允许集合内");
- }
- }
|