S8NotificationPushAdapter.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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-PUSH-INTEGRATION-1)
  7. ///
  8. /// 直接注入框架已有的 <see cref="INotifyPusher"/> 集合,按 channels 过滤后调用各 pusher。
  9. /// 不调用 <see cref="FlowNotifyService.NotifyUsers"/>,避免脏写 ApprovalFlowNotifyLog
  10. /// (该表 InstanceId 非空且无 BizType 列,不适合承载 S8 业务诊断日志)。
  11. ///
  12. /// 每个渠道一次推送对应一条 <see cref="AdoS8NotificationLog"/>,作为 S8 自有诊断日志。
  13. /// 任何渠道失败仅 LogWarning,不抛出,确保通知失败不会阻断异常建单 / watch / scheduler。
  14. /// </summary>
  15. public class S8NotificationPushAdapter : ITransient
  16. {
  17. private readonly IEnumerable<INotifyPusher> _pushers;
  18. private readonly FlowNotifyConfigService _cfgService;
  19. private readonly SqlSugarRepository<AdoS8NotificationLog> _logRep;
  20. private readonly ILogger<S8NotificationPushAdapter> _logger;
  21. public S8NotificationPushAdapter(
  22. IEnumerable<INotifyPusher> pushers,
  23. FlowNotifyConfigService cfgService,
  24. SqlSugarRepository<AdoS8NotificationLog> logRep,
  25. ILogger<S8NotificationPushAdapter> logger)
  26. {
  27. _pushers = pushers;
  28. _cfgService = cfgService;
  29. _logRep = logRep;
  30. _logger = logger;
  31. }
  32. /// <summary>
  33. /// 推送一次通知到指定 userIds + channels。
  34. /// channels 大小写不敏感,与 <see cref="INotifyPusher.Channel"/> 匹配(SignalR/Email/Sms/DingTalk/WorkWeixin)。
  35. /// notification.Context 推荐写入:exceptionId / exceptionNo / sceneCode / severity / status / sourceRuleCode / jumpUrl。
  36. /// </summary>
  37. public async Task PushAsync(
  38. long tenantId,
  39. long factoryId,
  40. long? exceptionId,
  41. List<long>? userIds,
  42. FlowNotification notification,
  43. List<string>? channels)
  44. {
  45. if (notification == null)
  46. {
  47. _logger.LogWarning("S8Push skipped: notification is null (exceptionId={ExceptionId})", exceptionId);
  48. return;
  49. }
  50. var distinctUserIds = (userIds ?? new List<long>())
  51. .Where(x => x > 0).Distinct().ToList();
  52. if (distinctUserIds.Count == 0)
  53. {
  54. _logger.LogWarning("S8Push skipped: empty userIds (exceptionId={ExceptionId})", exceptionId);
  55. return;
  56. }
  57. var wanted = new HashSet<string>(
  58. (channels ?? new List<string>())
  59. .Where(c => !string.IsNullOrWhiteSpace(c))
  60. .Select(c => c.Trim()),
  61. StringComparer.OrdinalIgnoreCase);
  62. if (wanted.Count == 0)
  63. {
  64. _logger.LogInformation("S8Push skipped: empty channels (exceptionId={ExceptionId})", exceptionId);
  65. return;
  66. }
  67. NotifyChannelConfig? cfg = null;
  68. try { cfg = await _cfgService.GetEffectiveAsync(); }
  69. catch (Exception ex)
  70. {
  71. _logger.LogWarning(ex, "S8Push: read NotifyChannelConfig failed; skip-disabled-check 将放行已注册 pusher");
  72. }
  73. var registered = _pushers.ToList();
  74. var registeredChannels = new HashSet<string>(
  75. registered.Select(p => p.Channel),
  76. StringComparer.OrdinalIgnoreCase);
  77. foreach (var pusher in registered)
  78. {
  79. if (!wanted.Contains(pusher.Channel)) continue;
  80. if (cfg != null && !pusher.IsEnabled(cfg))
  81. {
  82. _logger.LogInformation("S8Push skip {Channel}: disabled by NotifyChannelConfig", pusher.Channel);
  83. await SafeWriteLogAsync(tenantId, factoryId, exceptionId, pusher.Channel,
  84. notification, distinctUserIds.Count, success: false, error: "channel_disabled_by_config");
  85. continue;
  86. }
  87. FlowNotifyPushResult result;
  88. try
  89. {
  90. result = await pusher.PushAsync(distinctUserIds, notification);
  91. }
  92. catch (Exception ex)
  93. {
  94. result = FlowNotifyPushResult.Fail(ex.Message);
  95. _logger.LogWarning(ex, "S8Push pusher {Channel} threw", pusher.Channel);
  96. }
  97. if (!result.Success)
  98. _logger.LogWarning("S8Push {Channel} fail: {Err}", pusher.Channel, result.ErrorMessage);
  99. await SafeWriteLogAsync(tenantId, factoryId, exceptionId, pusher.Channel,
  100. notification, result.ActualTargetCount, result.Success, result.ErrorMessage);
  101. }
  102. foreach (var w in wanted.Where(w => !registeredChannels.Contains(w)))
  103. _logger.LogInformation("S8Push channel '{Channel}' has no registered pusher, skip", w);
  104. }
  105. private async Task SafeWriteLogAsync(
  106. long tenantId, long factoryId, long? exceptionId,
  107. string channel, FlowNotification n,
  108. int targetCount, bool success, string? error)
  109. {
  110. try
  111. {
  112. var ctx = n.Context ?? new Dictionary<string, string?>();
  113. static string? Get(Dictionary<string, string?> d, string k) => d.TryGetValue(k, out var v) ? v : null;
  114. var summary = new
  115. {
  116. title = n.Title,
  117. type = n.Type.ToString(),
  118. bizType = n.BizType,
  119. exceptionId = Get(ctx, "exceptionId"),
  120. exceptionNo = Get(ctx, "exceptionNo"),
  121. sceneCode = Get(ctx, "sceneCode"),
  122. severity = Get(ctx, "severity"),
  123. status = Get(ctx, "status"),
  124. sourceRuleCode = Get(ctx, "sourceRuleCode"),
  125. jumpUrl = Get(ctx, "jumpUrl"),
  126. recovered = "true".Equals(Get(ctx, "recovered"), StringComparison.OrdinalIgnoreCase),
  127. targetCount,
  128. success,
  129. error = string.IsNullOrEmpty(error)
  130. ? null
  131. : (error.Length > 800 ? error.Substring(0, 800) + "..." : error),
  132. };
  133. var payload = System.Text.Json.JsonSerializer.Serialize(summary);
  134. if (payload.Length > 4000) payload = payload.Substring(0, 3996) + "...]";
  135. await _logRep.InsertAsync(new AdoS8NotificationLog
  136. {
  137. TenantId = tenantId,
  138. FactoryId = factoryId,
  139. ExceptionId = exceptionId,
  140. Channel = channel,
  141. Payload = payload,
  142. CreatedAt = DateTime.Now,
  143. });
  144. }
  145. catch (Exception ex)
  146. {
  147. _logger.LogWarning(ex, "S8Push: write AdoS8NotificationLog failed (channel={Channel})", channel);
  148. }
  149. }
  150. }