S8NotificationCatalog.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. namespace Admin.NET.Plugin.AiDOP.Const.S8;
  2. /// <summary>
  3. /// S8-NOTIFY-RECIPIENT-1:通知事件码。
  4. ///
  5. /// <para><b>每一条都对应一个真实存在的派发点</b>(见 <c>TriggerHint</c>)。
  6. /// 刻意不造没有业务入口的事件 —— 配了却永远不触发的通知,比没有通知更难排查。</para>
  7. /// </summary>
  8. public static class S8NotifyEventCode
  9. {
  10. /// <summary>规则命中建单 / 人工提报建单。</summary>
  11. public const string ExceptionCreated = "EXCEPTION_CREATED";
  12. /// <summary>被认领。</summary>
  13. public const string ExceptionClaimed = "EXCEPTION_CLAIMED";
  14. /// <summary>被转派。</summary>
  15. public const string ExceptionTransferred = "EXCEPTION_TRANSFERRED";
  16. /// <summary>提交复核(等待审核)。</summary>
  17. public const string VerificationSubmitted = "VERIFICATION_SUBMITTED";
  18. /// <summary>超时自动升级。</summary>
  19. public const string EscalationTriggered = "ESCALATION_TRIGGERED";
  20. /// <summary>检测到恢复。</summary>
  21. public const string ExceptionRecovered = "EXCEPTION_RECOVERED";
  22. /// <summary>超时关闭(闭环及时性回顾)。</summary>
  23. public const string ExceptionOverdueClosed = "EXCEPTION_OVERDUE_CLOSED";
  24. }
  25. /// <summary>
  26. /// 收件人类型。<b>第一版只有四类,全部能真实解析出 SysUserId</b>。
  27. ///
  28. /// <para><b>Supervisor 刻意缺席</b>:系统里不存在组织主管关系
  29. /// (<c>SysOrg.DirectorId</c> 与 <c>SysUser.ManagerUserId</c> 实测填充率均为 0)。
  30. /// 给一个永远解析为空的选项,等于让管理员配一条永远发不出去的通知。</para>
  31. /// </summary>
  32. public static class S8RecipientType
  33. {
  34. /// <summary>该规则的处理账号池(复用 P1-C 的 Rule Handler Pool,不复制第二份人员配置)。</summary>
  35. public const string HandlerPool = "HANDLER_POOL";
  36. /// <summary>当前处理人(<c>assignee_user_id</c>)。</summary>
  37. public const string Assignee = "ASSIGNEE";
  38. /// <summary>
  39. /// 当前实际审核人。
  40. /// <para><b>不是新建的 Reviewer Pool</b> —— 它解析的是<b>当前事实</b>:
  41. /// 优先取单据上的 <c>verifier_user_id</c>(提交复核时手选、真正能点通过/退回的人);
  42. /// 尚未提交复核时回落到审批流节点解析出的审批人。</para>
  43. /// </summary>
  44. public const string Reviewer = "REVIEWER";
  45. /// <summary>
  46. /// 该规则的<b>升级账号池</b>(S8-RESPONSIBILITY-POOL-1)。
  47. /// <para>取代 <c>exception_type.escalate_role_code</c> 成为升级通知的收件来源:
  48. /// 旧字段按异常类型跨租户查,实测会把 B 租户的配置用到 A 租户的异常上。</para>
  49. /// </summary>
  50. public const string EscalationPool = "ESCALATION_POOL";
  51. /// <summary>指定账号(多选 SysUser)。</summary>
  52. public const string SpecificUser = "SPECIFIC_USER";
  53. }
  54. /// <summary>
  55. /// 事件元信息。
  56. /// <para><b><paramref name="TriggerHint"/> 是给业务用户看的「什么时候会发生」</b>,
  57. /// 不是给开发看的派发点标注 —— 通知设置页会直接展示它。
  58. /// 原先几个事件写的是 <c>POST {id}/claim</c> 这样的接口路径,
  59. /// 与「业务用户不需要理解技术实现」相悖。</para>
  60. /// </summary>
  61. public sealed record S8NotifyEventDefinition(string Code, string DisplayName, string TriggerHint, int OrderNo);
  62. /// <summary>
  63. /// 事件与收件人类型目录。后端派发、配置校验、前端 UI 三方共用同一份,
  64. /// 避免出现「页面能配、后端根本不派发」或反之。
  65. /// </summary>
  66. public static class S8NotificationCatalog
  67. {
  68. public static readonly IReadOnlyList<S8NotifyEventDefinition> Events = new List<S8NotifyEventDefinition>
  69. {
  70. new(S8NotifyEventCode.ExceptionCreated, "异常产生", "规则命中建单 / 人工提报", 10),
  71. new(S8NotifyEventCode.ExceptionClaimed, "认领成功", "处理人认领异常时", 20),
  72. new(S8NotifyEventCode.ExceptionTransferred, "转派", "异常被转派给其他处理人时", 30),
  73. new(S8NotifyEventCode.VerificationSubmitted, "提交复核", "处理人提交复核时", 40),
  74. new(S8NotifyEventCode.EscalationTriggered, "超时升级", "超时自动升级作业", 50),
  75. new(S8NotifyEventCode.ExceptionRecovered, "异常恢复", "调度器检测到恢复", 60),
  76. new(S8NotifyEventCode.ExceptionOverdueClosed, "超时关闭", "检验通过后闭环及时性回顾", 70),
  77. };
  78. public static readonly IReadOnlyList<string> RecipientTypes = new[]
  79. {
  80. S8RecipientType.HandlerPool,
  81. S8RecipientType.Assignee,
  82. S8RecipientType.Reviewer,
  83. S8RecipientType.EscalationPool,
  84. S8RecipientType.SpecificUser,
  85. };
  86. /// <summary>
  87. /// S8-RESPONSIBILITY-POOL-1:事件 → 收件人的<b>默认推导</b>。
  88. ///
  89. /// <para><b>为什么是推导而不是让人配</b>:规则已经知道处理池、复核池、升级池、
  90. /// 当前处理人、选定复核人是谁。再让业务用户去选 <c>HANDLER_POOL</c> / <c>ASSIGNEE</c>
  91. /// 这类技术枚举,等于把内部实现搬到页面上,还多出一处会与责任池不一致的配置。
  92. /// 业务用户只需要回答「这件事要不要提醒」,对象由责任关系自动决定。</para>
  93. ///
  94. /// <para><b>每一条都对应真实派发点</b>:<c>EXCEPTION_CREATED</c>(调度器建单)·
  95. /// <c>EXCEPTION_CLAIMED</c> / <c>EXCEPTION_TRANSFERRED</c> / <c>VERIFICATION_SUBMITTED</c>
  96. /// (<c>S8TaskFlowService</c>)· <c>ESCALATION_TRIGGERED</c>(超时升级作业)·
  97. /// <c>EXCEPTION_RECOVERED</c>(调度器判定恢复)· <c>EXCEPTION_OVERDUE_CLOSED</c>(闭环及时性回顾)。
  98. /// <b>「复核通过」「复核退回」刻意缺席</b>:代码里没有这两个派发点,
  99. /// 凭空加进目录只会配出永远不触发的通知 —— 比没有通知更难排查。</para>
  100. ///
  101. /// <para><b>转派通知的对象是「新处理人」</b>:转派完成时 <c>assignee_user_id</c>
  102. /// 已经是新的人,因此 <c>ASSIGNEE</c> 解析出来的就是他,不需要额外的收件人类型。</para>
  103. /// </summary>
  104. public static readonly IReadOnlyDictionary<string, string> DefaultRecipientByEvent =
  105. new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  106. {
  107. [S8NotifyEventCode.ExceptionCreated] = S8RecipientType.HandlerPool,
  108. [S8NotifyEventCode.ExceptionClaimed] = S8RecipientType.Assignee,
  109. [S8NotifyEventCode.ExceptionTransferred] = S8RecipientType.Assignee,
  110. [S8NotifyEventCode.VerificationSubmitted] = S8RecipientType.Reviewer,
  111. [S8NotifyEventCode.EscalationTriggered] = S8RecipientType.EscalationPool,
  112. [S8NotifyEventCode.ExceptionRecovered] = S8RecipientType.Assignee,
  113. [S8NotifyEventCode.ExceptionOverdueClosed] = S8RecipientType.Assignee,
  114. };
  115. /// <summary>事件的默认收件人类型;未知事件返回 <c>null</c>(调用方不得猜)。</summary>
  116. public static string? ResolveDefaultRecipientType(string? eventCode) =>
  117. !string.IsNullOrWhiteSpace(eventCode)
  118. && DefaultRecipientByEvent.TryGetValue(eventCode!, out var t) ? t : null;
  119. /// <summary>收件人类型的业务文案(页面只读展示,不出现技术枚举)。</summary>
  120. public static string DescribeRecipientType(string? type) => type switch
  121. {
  122. S8RecipientType.HandlerPool => "处理人员",
  123. S8RecipientType.Assignee => "当前处理人",
  124. S8RecipientType.Reviewer => "当前复核人",
  125. S8RecipientType.EscalationPool => "升级人员",
  126. S8RecipientType.SpecificUser => "指定账号",
  127. _ => "未配置",
  128. };
  129. /// <summary>
  130. /// 租户级默认配置的 <c>rule_code</c> 哨兵值。
  131. /// <para>人工提报的异常没有来源规则,它的通知只能落在租户级默认上。
  132. /// 用显式常量而不是 NULL:NULL 在唯一索引里的比较语义容易让人配出两条"默认"。</para>
  133. /// </summary>
  134. public const string TenantDefaultRuleCode = "*";
  135. private static readonly HashSet<string> EventSet =
  136. Events.Select(e => e.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
  137. private static readonly HashSet<string> TypeSet =
  138. RecipientTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
  139. public static bool IsKnownEvent(string? code) => !string.IsNullOrWhiteSpace(code) && EventSet.Contains(code!);
  140. public static bool IsKnownRecipientType(string? t) => !string.IsNullOrWhiteSpace(t) && TypeSet.Contains(t!);
  141. }