S8NotificationLayerMerge.cs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  4. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  5. /// <summary>
  6. /// S8-STEP6C-CFG-NOTIFY-FIX-AND-CERT-1:通知分层「平台默认 / 工厂覆盖」的**单一** merge 契约。
  7. ///
  8. /// <para><b>覆盖粒度 = (scene_code, 归一 severity, 归一 level_code)</b>,即逐层覆盖。</para>
  9. ///
  10. /// 修正前(S8-STEP6C-CFG-NOTIFY-CLOSURE-1 及更早)覆盖粒度是整个 (scene, severity) 键:
  11. /// resolver 先按 (tenant,factory,scene,severity) 精确查,**仅当该键 0 行**才回落 (0,0)。
  12. /// 后果是工厂只要为某一层建一行(例如只配 L2_MANAGER),平台默认在该 scene+severity 下的
  13. /// **其余层级(L1/L3)会整体消失**——工厂本意是「补一层」,实际却是「换掉整组」,
  14. /// 且该副作用在页面上不可见。本类把粒度收敛到 level,使「补一层」就只影响那一层。
  15. ///
  16. /// ⚠️ <b>resolver 与配置页 ListAsync 必须都走 <see cref="Effective"/>。</b>
  17. /// 两处一旦各自实现,就会出现「页面显示一种生效配置、运行时按另一种派发」的静默背离
  18. /// (已实测过一次:resolver 用 (0,0) 基线派发通知,而配置页只查精确租户 → 页面空表、通知照发)。
  19. /// </summary>
  20. public static class S8NotificationLayerMerge
  21. {
  22. /// <summary>层级归一:去空白 + 大写。历史脏数据存在 level_code = '' 的行,归一后自成一组,不与合法层级混淆。</summary>
  23. public static string NormalizeLevel(string? levelCode) =>
  24. (levelCode ?? string.Empty).Trim().ToUpperInvariant();
  25. /// <summary>merge 键:(scene_code, 归一 severity, 归一 level_code)。</summary>
  26. public static (string Scene, string Severity, string Level) KeyOf(AdoS8NotificationLayer x) =>
  27. (x.SceneCode ?? string.Empty, S8SeverityCode.Normalize(x.Severity), NormalizeLevel(x.LevelCode));
  28. /// <summary>
  29. /// 从「平台默认行 ∪ 本工厂行」的候选集算出生效集合:逐 (scene, severity, level) 取工厂优先,
  30. /// 该层无工厂行则回落平台默认。
  31. ///
  32. /// 排序为稳定确定序:scene → severity → level → id。
  33. /// 末位 <c>Id</c> 是必要的——DB 侧无唯一索引(唯一性仅由 service 层 AnyAsync 前置检查保证),
  34. /// 同键重复行若依赖 DB 返回序,派发顺序会在不同查询计划下漂移。
  35. /// </summary>
  36. public static List<AdoS8NotificationLayer> Effective(IEnumerable<AdoS8NotificationLayer> candidates) =>
  37. S8ConfigScope.EffectiveRows(candidates, x => x.TenantId, x => x.FactoryId, KeyOf)
  38. .OrderBy(x => x.SceneCode, StringComparer.Ordinal)
  39. .ThenBy(x => S8SeverityCode.Normalize(x.Severity), StringComparer.Ordinal)
  40. .ThenBy(x => NormalizeLevel(x.LevelCode), StringComparer.Ordinal)
  41. .ThenBy(x => x.Id)
  42. .ToList();
  43. }