S8NotificationLayerResolver.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.ApprovalFlow.Service;
  3. using Microsoft.Extensions.Logging;
  4. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  5. /// <summary>
  6. /// S8 通知分层路由解析器(S8-NOTIFY-LAYER-RESOLVE-1)
  7. ///
  8. /// 职责链:
  9. /// (tenantId, factoryId, sceneCode, severity)
  10. /// → 命中 ado_s8_notification_layer 行(多行 OK)
  11. /// → 解析 target_role_ids(dual-format)
  12. /// → 解析 notify_channel(IgnoreCase, ',' / ';' 分隔)
  13. /// → S8RoleResolver → userIds
  14. /// → S8NotificationPushAdapter.PushAsync
  15. /// → 由 PushAdapter 落 AdoS8NotificationLog(每渠道一条)
  16. ///
  17. /// 不接入 watch / scheduler / task 主链路;不写 ApprovalFlowNotifyLog;
  18. /// 不修改任何 schema;任何分支异常仅 LogWarning,不向上抛。
  19. ///
  20. /// tenant/factory baseline fallback:先精确匹配 (tenantId, factoryId);
  21. /// 若 0 行,再尝试 (tenant=0, factory=0) baseline;仍 0 行 LogInformation 后 return。
  22. /// </summary>
  23. public class S8NotificationLayerResolver : ITransient
  24. {
  25. private readonly SqlSugarRepository<AdoS8NotificationLayer> _layerRep;
  26. private readonly S8RoleResolver _roleResolver;
  27. private readonly S8NotificationPushAdapter _pushAdapter;
  28. private readonly ILogger<S8NotificationLayerResolver> _logger;
  29. public S8NotificationLayerResolver(
  30. SqlSugarRepository<AdoS8NotificationLayer> layerRep,
  31. S8RoleResolver roleResolver,
  32. S8NotificationPushAdapter pushAdapter,
  33. ILogger<S8NotificationLayerResolver> logger)
  34. {
  35. _layerRep = layerRep;
  36. _roleResolver = roleResolver;
  37. _pushAdapter = pushAdapter;
  38. _logger = logger;
  39. }
  40. public class DispatchByLayerInput
  41. {
  42. public long TenantId { get; set; }
  43. public long FactoryId { get; set; }
  44. public long? ExceptionId { get; set; }
  45. public string? ExceptionNo { get; set; }
  46. public string SceneCode { get; set; } = string.Empty;
  47. public string Severity { get; set; } = string.Empty;
  48. public string Title { get; set; } = string.Empty;
  49. public string Content { get; set; } = string.Empty;
  50. public string? Status { get; set; }
  51. public string? SourceRuleCode { get; set; }
  52. public string? JumpUrl { get; set; }
  53. /// <summary>
  54. /// S8-NOTIFY-WIRE-RECOVERED-1:true 表示恢复事件,BuildNotification 会在 Context 中追加
  55. /// "recovered"="true"。默认 false,保持 CREATED 路径载荷向后兼容。
  56. /// </summary>
  57. public bool Recovered { get; set; }
  58. }
  59. /// <summary>
  60. /// 调用方:watch/scheduler/task 在拿到 sceneCode + severity 后调用本方法(本轮不接入主链路)。
  61. /// </summary>
  62. public async Task DispatchByLayerAsync(DispatchByLayerInput input)
  63. {
  64. if (input == null)
  65. {
  66. _logger.LogWarning("S8LayerDispatch: input null");
  67. return;
  68. }
  69. if (string.IsNullOrWhiteSpace(input.SceneCode) || string.IsNullOrWhiteSpace(input.Severity))
  70. {
  71. _logger.LogInformation("S8LayerDispatch skip: empty sceneCode or severity (exceptionId={ExceptionId})", input.ExceptionId);
  72. return;
  73. }
  74. List<AdoS8NotificationLayer> layers;
  75. try
  76. {
  77. layers = await _layerRep.AsQueryable()
  78. .Where(x => x.TenantId == input.TenantId
  79. && x.FactoryId == input.FactoryId
  80. && x.SceneCode == input.SceneCode
  81. && x.Severity == input.Severity)
  82. .ToListAsync();
  83. // baseline fallback:未命中精确租户/工厂 → 尝试 tenant=0/factory=0 全局基线
  84. // (task 第 4 条允许;schema 中 tenant_id/factory_id 为 NOT NULL bigint,0 视为基线占位)
  85. if (layers.Count == 0)
  86. {
  87. layers = await _layerRep.AsQueryable()
  88. .Where(x => x.TenantId == 0
  89. && x.FactoryId == 0
  90. && x.SceneCode == input.SceneCode
  91. && x.Severity == input.Severity)
  92. .ToListAsync();
  93. if (layers.Count > 0)
  94. _logger.LogInformation("S8LayerDispatch: matched {N} baseline (tenant=0/factory=0) layer rows", layers.Count);
  95. }
  96. }
  97. catch (Exception ex)
  98. {
  99. _logger.LogWarning(ex, "S8LayerDispatch: query AdoS8NotificationLayer failed (scene={Scene}, sev={Sev})", input.SceneCode, input.Severity);
  100. return;
  101. }
  102. if (layers.Count == 0)
  103. {
  104. _logger.LogInformation("S8LayerDispatch: no layer matched (tenant={Tenant}, factory={Factory}, scene={Scene}, sev={Sev})",
  105. input.TenantId, input.FactoryId, input.SceneCode, input.Severity);
  106. return;
  107. }
  108. var notification = BuildNotification(input);
  109. foreach (var layer in layers)
  110. {
  111. List<long> userIds;
  112. try
  113. {
  114. var tokens = S8RoleResolver.SplitTokens(layer.TargetRoleIds);
  115. if (tokens.Count == 0)
  116. {
  117. _logger.LogWarning("S8LayerDispatch: layer id={LayerId} target_role_ids empty, skip row", layer.Id);
  118. continue;
  119. }
  120. userIds = await _roleResolver.ResolveUserIdsAsync(tokens);
  121. }
  122. catch (Exception ex)
  123. {
  124. _logger.LogWarning(ex, "S8LayerDispatch: resolve roles failed for layer id={LayerId}", layer.Id);
  125. continue;
  126. }
  127. if (userIds.Count == 0)
  128. {
  129. _logger.LogWarning("S8LayerDispatch: layer id={LayerId} resolved 0 users from target_role_ids='{Roles}'",
  130. layer.Id, layer.TargetRoleIds);
  131. continue;
  132. }
  133. var channels = ParseChannels(layer.NotifyChannel);
  134. try
  135. {
  136. await _pushAdapter.PushAsync(
  137. tenantId: input.TenantId,
  138. factoryId: input.FactoryId,
  139. exceptionId: input.ExceptionId,
  140. userIds: userIds,
  141. notification: notification,
  142. channels: channels);
  143. }
  144. catch (Exception ex)
  145. {
  146. // PushAdapter 自身已对内部错误做了捕获;这里再兜一层保证主流程不被打断。
  147. _logger.LogWarning(ex, "S8LayerDispatch: push throw (layer id={LayerId})", layer.Id);
  148. }
  149. }
  150. }
  151. private static FlowNotification BuildNotification(DispatchByLayerInput input)
  152. {
  153. // 注意:FlowNotificationTypeEnum 不含 S8_EXCEPTION 值;BizType 字段承载 "S8_EXCEPTION" 语义。
  154. // InstanceId 仅作为载荷字段(PushAdapter 不写 ApprovalFlowNotifyLog,不会脏写该列)。
  155. var ctx = new Dictionary<string, string?>
  156. {
  157. ["exceptionId"] = input.ExceptionId?.ToString(),
  158. ["exceptionNo"] = input.ExceptionNo,
  159. ["sceneCode"] = input.SceneCode,
  160. ["severity"] = input.Severity,
  161. ["status"] = input.Status,
  162. ["sourceRuleCode"] = input.SourceRuleCode,
  163. ["jumpUrl"] = input.JumpUrl,
  164. };
  165. if (input.Recovered) ctx["recovered"] = "true";
  166. return new FlowNotification
  167. {
  168. Type = FlowNotificationTypeEnum.NewTask,
  169. BizType = "S8_EXCEPTION",
  170. InstanceId = input.ExceptionId ?? 0,
  171. Title = input.Title ?? string.Empty,
  172. Content = input.Content ?? string.Empty,
  173. Context = ctx,
  174. };
  175. }
  176. public static List<string> ParseChannels(string? csv)
  177. {
  178. if (string.IsNullOrWhiteSpace(csv)) return new List<string>();
  179. return csv.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
  180. }
  181. }