| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.ApprovalFlow.Service;
- using Microsoft.Extensions.Logging;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- /// <summary>
- /// S8 通知推送适配器(S8-NOTIFY-PUSH-INTEGRATION-1)
- ///
- /// 直接注入框架已有的 <see cref="INotifyPusher"/> 集合,按 channels 过滤后调用各 pusher。
- /// 不调用 <see cref="FlowNotifyService.NotifyUsers"/>,避免脏写 ApprovalFlowNotifyLog
- /// (该表 InstanceId 非空且无 BizType 列,不适合承载 S8 业务诊断日志)。
- ///
- /// 每个渠道一次推送对应一条 <see cref="AdoS8NotificationLog"/>,作为 S8 自有诊断日志。
- /// 任何渠道失败仅 LogWarning,不抛出,确保通知失败不会阻断异常建单 / watch / scheduler。
- /// </summary>
- public class S8NotificationPushAdapter : ITransient
- {
- private readonly IEnumerable<INotifyPusher> _pushers;
- private readonly FlowNotifyConfigService _cfgService;
- private readonly SqlSugarRepository<AdoS8NotificationLog> _logRep;
- private readonly ILogger<S8NotificationPushAdapter> _logger;
- public S8NotificationPushAdapter(
- IEnumerable<INotifyPusher> pushers,
- FlowNotifyConfigService cfgService,
- SqlSugarRepository<AdoS8NotificationLog> logRep,
- ILogger<S8NotificationPushAdapter> logger)
- {
- _pushers = pushers;
- _cfgService = cfgService;
- _logRep = logRep;
- _logger = logger;
- }
- /// <summary>
- /// 推送一次通知到指定 userIds + channels。
- /// channels 大小写不敏感,与 <see cref="INotifyPusher.Channel"/> 匹配(SignalR/Email/Sms/DingTalk/WorkWeixin)。
- /// notification.Context 推荐写入:exceptionId / exceptionNo / sceneCode / severity / status / sourceRuleCode / jumpUrl。
- /// </summary>
- public async Task PushAsync(
- long tenantId,
- long factoryId,
- long? exceptionId,
- List<long>? userIds,
- FlowNotification notification,
- List<string>? channels)
- {
- if (notification == null)
- {
- _logger.LogWarning("S8Push skipped: notification is null (exceptionId={ExceptionId})", exceptionId);
- return;
- }
- var distinctUserIds = (userIds ?? new List<long>())
- .Where(x => x > 0).Distinct().ToList();
- if (distinctUserIds.Count == 0)
- {
- _logger.LogWarning("S8Push skipped: empty userIds (exceptionId={ExceptionId})", exceptionId);
- return;
- }
- var wanted = new HashSet<string>(
- (channels ?? new List<string>())
- .Where(c => !string.IsNullOrWhiteSpace(c))
- .Select(c => c.Trim()),
- StringComparer.OrdinalIgnoreCase);
- if (wanted.Count == 0)
- {
- _logger.LogInformation("S8Push skipped: empty channels (exceptionId={ExceptionId})", exceptionId);
- return;
- }
- NotifyChannelConfig? cfg = null;
- try { cfg = await _cfgService.GetEffectiveAsync(); }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "S8Push: read NotifyChannelConfig failed; skip-disabled-check 将放行已注册 pusher");
- }
- var registered = _pushers.ToList();
- var registeredChannels = new HashSet<string>(
- registered.Select(p => p.Channel),
- StringComparer.OrdinalIgnoreCase);
- foreach (var pusher in registered)
- {
- if (!wanted.Contains(pusher.Channel)) continue;
- if (cfg != null && !pusher.IsEnabled(cfg))
- {
- _logger.LogInformation("S8Push skip {Channel}: disabled by NotifyChannelConfig", pusher.Channel);
- await SafeWriteLogAsync(tenantId, factoryId, exceptionId, pusher.Channel,
- notification, distinctUserIds.Count, success: false, error: "channel_disabled_by_config");
- continue;
- }
- FlowNotifyPushResult result;
- try
- {
- result = await pusher.PushAsync(distinctUserIds, notification);
- }
- catch (Exception ex)
- {
- result = FlowNotifyPushResult.Fail(ex.Message);
- _logger.LogWarning(ex, "S8Push pusher {Channel} threw", pusher.Channel);
- }
- if (!result.Success)
- _logger.LogWarning("S8Push {Channel} fail: {Err}", pusher.Channel, result.ErrorMessage);
- await SafeWriteLogAsync(tenantId, factoryId, exceptionId, pusher.Channel,
- notification, result.ActualTargetCount, result.Success, result.ErrorMessage);
- }
- foreach (var w in wanted.Where(w => !registeredChannels.Contains(w)))
- _logger.LogInformation("S8Push channel '{Channel}' has no registered pusher, skip", w);
- }
- private async Task SafeWriteLogAsync(
- long tenantId, long factoryId, long? exceptionId,
- string channel, FlowNotification n,
- int targetCount, bool success, string? error)
- {
- try
- {
- var ctx = n.Context ?? new Dictionary<string, string?>();
- static string? Get(Dictionary<string, string?> d, string k) => d.TryGetValue(k, out var v) ? v : null;
- var summary = new
- {
- title = n.Title,
- type = n.Type.ToString(),
- bizType = n.BizType,
- exceptionId = Get(ctx, "exceptionId"),
- exceptionNo = Get(ctx, "exceptionNo"),
- sceneCode = Get(ctx, "sceneCode"),
- severity = Get(ctx, "severity"),
- status = Get(ctx, "status"),
- sourceRuleCode = Get(ctx, "sourceRuleCode"),
- jumpUrl = Get(ctx, "jumpUrl"),
- recovered = "true".Equals(Get(ctx, "recovered"), StringComparison.OrdinalIgnoreCase),
- targetCount,
- success,
- error = string.IsNullOrEmpty(error)
- ? null
- : (error.Length > 800 ? error.Substring(0, 800) + "..." : error),
- };
- var payload = System.Text.Json.JsonSerializer.Serialize(summary);
- if (payload.Length > 4000) payload = payload.Substring(0, 3996) + "...]";
- await _logRep.InsertAsync(new AdoS8NotificationLog
- {
- TenantId = tenantId,
- FactoryId = factoryId,
- ExceptionId = exceptionId,
- Channel = channel,
- Payload = payload,
- CreatedAt = DateTime.Now,
- });
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "S8Push: write AdoS8NotificationLog failed (channel={Channel})", channel);
- }
- }
- }
|