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);
}
}
}