using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.ApprovalFlow.Service; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// S8 通知推送适配器(S8-NOTIFY-PUSH-INTEGRATION-1) /// /// 直接注入框架已有的 集合,按 channels 过滤后调用各 pusher。 /// 不调用 ,避免脏写 ApprovalFlowNotifyLog /// (该表 InstanceId 非空且无 BizType 列,不适合承载 S8 业务诊断日志)。 /// /// 每个渠道一次推送对应一条 ,作为 S8 自有诊断日志。 /// 任何渠道失败仅 LogWarning,不抛出,确保通知失败不会阻断异常建单 / watch / scheduler。 /// public class S8NotificationPushAdapter : ITransient { private readonly IEnumerable _pushers; private readonly FlowNotifyConfigService _cfgService; private readonly SqlSugarRepository _logRep; private readonly ILogger _logger; public S8NotificationPushAdapter( IEnumerable pushers, FlowNotifyConfigService cfgService, SqlSugarRepository logRep, ILogger logger) { _pushers = pushers; _cfgService = cfgService; _logRep = logRep; _logger = logger; } /// /// 推送一次通知到指定 userIds + channels。 /// channels 大小写不敏感,与 匹配(SignalR/Email/Sms/DingTalk/WorkWeixin)。 /// notification.Context 推荐写入:exceptionId / exceptionNo / sceneCode / severity / status / sourceRuleCode / jumpUrl。 /// public async Task PushAsync( long tenantId, long factoryId, long? exceptionId, List? userIds, FlowNotification notification, List? channels) { if (notification == null) { _logger.LogWarning("S8Push skipped: notification is null (exceptionId={ExceptionId})", exceptionId); return; } var distinctUserIds = (userIds ?? new List()) .Where(x => x > 0).Distinct().ToList(); if (distinctUserIds.Count == 0) { _logger.LogWarning("S8Push skipped: empty userIds (exceptionId={ExceptionId})", exceptionId); return; } var wanted = new HashSet( (channels ?? new List()) .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( 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(); static string? Get(Dictionary 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); } } }