namespace Admin.NET.Plugin.AiDOP.Const.S8; /// /// S8-NOTIFY-RECIPIENT-1:通知事件码。 /// /// 每一条都对应一个真实存在的派发点(见 TriggerHint)。 /// 刻意不造没有业务入口的事件 —— 配了却永远不触发的通知,比没有通知更难排查。 /// public static class S8NotifyEventCode { /// 规则命中建单 / 人工提报建单。 public const string ExceptionCreated = "EXCEPTION_CREATED"; /// 被认领。 public const string ExceptionClaimed = "EXCEPTION_CLAIMED"; /// 被转派。 public const string ExceptionTransferred = "EXCEPTION_TRANSFERRED"; /// 提交复核(等待审核)。 public const string VerificationSubmitted = "VERIFICATION_SUBMITTED"; /// 超时自动升级。 public const string EscalationTriggered = "ESCALATION_TRIGGERED"; /// 检测到恢复。 public const string ExceptionRecovered = "EXCEPTION_RECOVERED"; /// 超时关闭(闭环及时性回顾)。 public const string ExceptionOverdueClosed = "EXCEPTION_OVERDUE_CLOSED"; } /// /// 收件人类型。第一版只有四类,全部能真实解析出 SysUserId。 /// /// Supervisor 刻意缺席:系统里不存在组织主管关系 /// (SysOrg.DirectorIdSysUser.ManagerUserId 实测填充率均为 0)。 /// 给一个永远解析为空的选项,等于让管理员配一条永远发不出去的通知。 /// public static class S8RecipientType { /// 该规则的处理账号池(复用 P1-C 的 Rule Handler Pool,不复制第二份人员配置)。 public const string HandlerPool = "HANDLER_POOL"; /// 当前处理人(assignee_user_id)。 public const string Assignee = "ASSIGNEE"; /// /// 当前实际审核人。 /// 不是新建的 Reviewer Pool —— 它解析的是当前事实: /// 优先取单据上的 verifier_user_id(提交复核时手选、真正能点通过/退回的人); /// 尚未提交复核时回落到审批流节点解析出的审批人。 /// public const string Reviewer = "REVIEWER"; /// /// 该规则的升级账号池(S8-RESPONSIBILITY-POOL-1)。 /// 取代 exception_type.escalate_role_code 成为升级通知的收件来源: /// 旧字段按异常类型跨租户查,实测会把 B 租户的配置用到 A 租户的异常上。 /// public const string EscalationPool = "ESCALATION_POOL"; /// 指定账号(多选 SysUser)。 public const string SpecificUser = "SPECIFIC_USER"; } /// /// 事件元信息。 /// 是给业务用户看的「什么时候会发生」, /// 不是给开发看的派发点标注 —— 通知设置页会直接展示它。 /// 原先几个事件写的是 POST {id}/claim 这样的接口路径, /// 与「业务用户不需要理解技术实现」相悖。 /// public sealed record S8NotifyEventDefinition(string Code, string DisplayName, string TriggerHint, int OrderNo); /// /// 事件与收件人类型目录。后端派发、配置校验、前端 UI 三方共用同一份, /// 避免出现「页面能配、后端根本不派发」或反之。 /// public static class S8NotificationCatalog { public static readonly IReadOnlyList Events = new List { new(S8NotifyEventCode.ExceptionCreated, "异常产生", "规则命中建单 / 人工提报", 10), new(S8NotifyEventCode.ExceptionClaimed, "认领成功", "处理人认领异常时", 20), new(S8NotifyEventCode.ExceptionTransferred, "转派", "异常被转派给其他处理人时", 30), new(S8NotifyEventCode.VerificationSubmitted, "提交复核", "处理人提交复核时", 40), new(S8NotifyEventCode.EscalationTriggered, "超时升级", "超时自动升级作业", 50), new(S8NotifyEventCode.ExceptionRecovered, "异常恢复", "调度器检测到恢复", 60), new(S8NotifyEventCode.ExceptionOverdueClosed, "超时关闭", "检验通过后闭环及时性回顾", 70), }; public static readonly IReadOnlyList RecipientTypes = new[] { S8RecipientType.HandlerPool, S8RecipientType.Assignee, S8RecipientType.Reviewer, S8RecipientType.EscalationPool, S8RecipientType.SpecificUser, }; /// /// S8-RESPONSIBILITY-POOL-1:事件 → 收件人的默认推导。 /// /// 为什么是推导而不是让人配:规则已经知道处理池、复核池、升级池、 /// 当前处理人、选定复核人是谁。再让业务用户去选 HANDLER_POOL / ASSIGNEE /// 这类技术枚举,等于把内部实现搬到页面上,还多出一处会与责任池不一致的配置。 /// 业务用户只需要回答「这件事要不要提醒」,对象由责任关系自动决定。 /// /// 每一条都对应真实派发点EXCEPTION_CREATED(调度器建单)· /// EXCEPTION_CLAIMED / EXCEPTION_TRANSFERRED / VERIFICATION_SUBMITTED /// (S8TaskFlowService)· ESCALATION_TRIGGERED(超时升级作业)· /// EXCEPTION_RECOVERED(调度器判定恢复)· EXCEPTION_OVERDUE_CLOSED(闭环及时性回顾)。 /// 「复核通过」「复核退回」刻意缺席:代码里没有这两个派发点, /// 凭空加进目录只会配出永远不触发的通知 —— 比没有通知更难排查。 /// /// 转派通知的对象是「新处理人」:转派完成时 assignee_user_id /// 已经是新的人,因此 ASSIGNEE 解析出来的就是他,不需要额外的收件人类型。 /// public static readonly IReadOnlyDictionary DefaultRecipientByEvent = new Dictionary(StringComparer.OrdinalIgnoreCase) { [S8NotifyEventCode.ExceptionCreated] = S8RecipientType.HandlerPool, [S8NotifyEventCode.ExceptionClaimed] = S8RecipientType.Assignee, [S8NotifyEventCode.ExceptionTransferred] = S8RecipientType.Assignee, [S8NotifyEventCode.VerificationSubmitted] = S8RecipientType.Reviewer, [S8NotifyEventCode.EscalationTriggered] = S8RecipientType.EscalationPool, [S8NotifyEventCode.ExceptionRecovered] = S8RecipientType.Assignee, [S8NotifyEventCode.ExceptionOverdueClosed] = S8RecipientType.Assignee, }; /// 事件的默认收件人类型;未知事件返回 null(调用方不得猜)。 public static string? ResolveDefaultRecipientType(string? eventCode) => !string.IsNullOrWhiteSpace(eventCode) && DefaultRecipientByEvent.TryGetValue(eventCode!, out var t) ? t : null; /// 收件人类型的业务文案(页面只读展示,不出现技术枚举)。 public static string DescribeRecipientType(string? type) => type switch { S8RecipientType.HandlerPool => "处理人员", S8RecipientType.Assignee => "当前处理人", S8RecipientType.Reviewer => "当前复核人", S8RecipientType.EscalationPool => "升级人员", S8RecipientType.SpecificUser => "指定账号", _ => "未配置", }; /// /// 租户级默认配置的 rule_code 哨兵值。 /// 人工提报的异常没有来源规则,它的通知只能落在租户级默认上。 /// 用显式常量而不是 NULL:NULL 在唯一索引里的比较语义容易让人配出两条"默认"。 /// public const string TenantDefaultRuleCode = "*"; private static readonly HashSet EventSet = Events.Select(e => e.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); private static readonly HashSet TypeSet = RecipientTypes.ToHashSet(StringComparer.OrdinalIgnoreCase); public static bool IsKnownEvent(string? code) => !string.IsNullOrWhiteSpace(code) && EventSet.Contains(code!); public static bool IsKnownRecipientType(string? t) => !string.IsNullOrWhiteSpace(t) && TypeSet.Contains(t!); }