S8ConfigScope.cs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  2. /// <summary>
  3. /// S8-CONFIG-GLOBAL-ROW-SEMANTICS-AND-KPI-TARGET-1:S8 配置对象的「平台默认 / 工厂覆盖」统一语义。
  4. ///
  5. /// 生效读取顺序:工厂覆盖 &gt; 平台默认。
  6. ///
  7. /// 平台默认(tenant_id = 0 且 factory_id = 0):
  8. /// 租户 / 工厂管理员 <b>只读</b>——不允许 Update / Delete,也不允许在页面上给出最终只会 404 的编辑动作;
  9. /// 需要调整时走「自定义本工厂」:复制平台默认的业务字段 → 由服务端盖章可信租户 / 工厂 → 存为工厂覆盖行。
  10. ///
  11. /// 工厂覆盖(tenant_id / factory_id = 当前可信作用域):
  12. /// 可编辑、可删除;删除即「恢复平台默认」,平台默认行本身不受影响。
  13. ///
  14. /// 平台默认行的维护能力与租户侧维护分离;在没有明确的平台级权限模型之前,
  15. /// 本层只保证租户侧只读,<b>不为了方便重新开放 global write</b>。
  16. /// </summary>
  17. public static class S8ConfigScope
  18. {
  19. /// <summary>平台默认行的租户 Id。</summary>
  20. public const long GlobalTenantId = 0L;
  21. /// <summary>平台默认行的工厂 Id。</summary>
  22. public const long GlobalFactoryId = 0L;
  23. /// <summary>作用域标识:平台默认。</summary>
  24. public const string Global = "GLOBAL";
  25. /// <summary>作用域标识:当前工厂覆盖。</summary>
  26. public const string Factory = "FACTORY";
  27. /// <summary>是否为平台默认行。</summary>
  28. public static bool IsGlobal(long tenantId, long factoryId) =>
  29. tenantId == GlobalTenantId && factoryId == GlobalFactoryId;
  30. /// <summary>把归属列翻译成作用域标识,供前端按业务语言展示(不暴露 tenantId / factoryId 本身)。</summary>
  31. public static string Resolve(long tenantId, long factoryId) =>
  32. IsGlobal(tenantId, factoryId) ? Global : Factory;
  33. /// <summary>
  34. /// S8-STEP6C-CFG-NOTIFY-FIX-AND-CERT-1:「平台默认 + 工厂覆盖」的**唯一** merge 实现。
  35. ///
  36. /// 语义:按 <paramref name="keyOf"/> 分组;组内存在工厂行 → 只取工厂行;否则回落该组的平台默认行。
  37. /// 覆盖粒度**完全等于** key 的粒度——调用方通过选择 key 决定「一行覆盖一行」还是「整组接管」。
  38. ///
  39. /// ⚠️ 本方法是 runtime resolver 与配置页 List 的共同实现,**不得各自复制一份相似 GroupBy**:
  40. /// 两处一旦分叉,就会出现「页面显示一种生效配置、运行时用另一种」的静默背离
  41. /// (S8-STEP6C 已实测过一次:resolver 用 (0,0) 基线派发,而配置页只查精确租户 → 页面空表、通知照发)。
  42. ///
  43. /// 返回组内**全部**入选行(不是单行),因为 DB 侧无唯一索引(唯一性仅由 service 层 AnyAsync 前置检查保证),
  44. /// 同 key 可能存在重复行;丢弃它们会让 runtime 与页面再次不一致。调用方需自行决定最终排序。
  45. /// </summary>
  46. public static IEnumerable<T> EffectiveRows<T, TKey>(
  47. IEnumerable<T> all,
  48. Func<T, long> tenantOf,
  49. Func<T, long> factoryOf,
  50. Func<T, TKey> keyOf) =>
  51. all.GroupBy(keyOf).SelectMany(g =>
  52. {
  53. var own = g.Where(x => !IsGlobal(tenantOf(x), factoryOf(x))).ToList();
  54. return own.Count > 0 ? own : g.Where(x => IsGlobal(tenantOf(x), factoryOf(x)));
  55. });
  56. }