using System.Diagnostics; namespace Admin.NET.Plugin.ApprovalFlow.Service; /// /// 流程通知服务(P4-16 重构 + 模板化/DB 配置) /// - 聚合所有 INotifyPusher,按渠道启用状态串行调度 /// - 标题/正文优先走 FlowNotifyTemplateService 模板渲染(支持 {变量} + BizType 覆盖),未命中回退兜底文案 /// - 渠道开关/凭据优先从 DB(ApprovalFlowNotifyConfig)读取,无记录回退 ApprovalFlow.json /// - 每次分发写 ApprovalFlowNotifyLog 便于运维追溯与降级审计 /// public class FlowNotifyService : ITransient { private readonly IEnumerable _pushers; private readonly SqlSugarRepository _logRep; private readonly FlowNotifyTemplateService _tplService; private readonly FlowNotifyConfigService _cfgService; public FlowNotifyService( IEnumerable pushers, SqlSugarRepository logRep, FlowNotifyTemplateService tplService, FlowNotifyConfigService cfgService) { _pushers = pushers; _logRep = logRep; _tplService = tplService; _cfgService = cfgService; } public async Task NotifyUsers(List userIds, FlowNotification notification) { if (userIds.Count == 0) return; // 模板渲染(未命中则用传入的 Title/Content 兜底) var ctx = notification.Context ?? new Dictionary(); var rendered = await _tplService.RenderAsync(notification.Type, notification.BizType, ctx); if (rendered != null) { notification.Title = rendered.Value.title; notification.Content = rendered.Value.content; } var cfg = await _cfgService.GetEffectiveAsync(); foreach (var pusher in _pushers) { if (!pusher.IsEnabled(cfg)) continue; var sw = Stopwatch.StartNew(); FlowNotifyPushResult result; try { result = await pusher.PushAsync(userIds, notification); } catch (Exception ex) { result = FlowNotifyPushResult.Fail(ex.Message); } sw.Stop(); await SafeWriteLog(notification, pusher.Channel, userIds, result, (int)sw.ElapsedMilliseconds); } } // ═══════════════════════════════════════════ // 事件专用便捷方法(FlowEngineService 调用) // 每个方法收 bizType 作为最后一个参数;构造 FlowNotification 时带上变量上下文 // ═══════════════════════════════════════════ public Task NotifyUrge(List userIds, long instanceId, string title, string bizType = "") => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Urge, instanceId, bizType, fallbackTitle: $"【催办】{title}", fallbackContent: "流程发起人催促您尽快审批,请及时处理。", ctx: new() { ["title"] = title })); public Task NotifyNewTask(List userIds, long instanceId, string title, string? nodeName, string bizType = "") => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.NewTask, instanceId, bizType, fallbackTitle: $"【待审批】{title}", fallbackContent: $"您有一条新的审批任务({nodeName}),请及时处理。", ctx: new() { ["title"] = title, ["nodeName"] = nodeName })); public Task NotifyFlowCompleted(long initiatorId, long instanceId, string title, FlowInstanceStatusEnum finalStatus, string bizType = "") { var statusText = finalStatus == FlowInstanceStatusEnum.Approved ? "已通过" : "已拒绝"; return NotifyUsers(new List { initiatorId }, Build(FlowNotificationTypeEnum.FlowCompleted, instanceId, bizType, fallbackTitle: $"【审批{statusText}】{title}", fallbackContent: $"您发起的审批流程已{statusText}。", ctx: new() { ["title"] = title, ["statusText"] = statusText })); } public Task NotifyTransferred(long targetUserId, long instanceId, string title, string? fromName, string bizType = "") => NotifyUsers(new List { targetUserId }, Build(FlowNotificationTypeEnum.Transferred, instanceId, bizType, fallbackTitle: $"【转办】{title}", fallbackContent: $"{fromName} 将一条审批任务转交给您,请及时处理。", ctx: new() { ["title"] = title, ["fromName"] = fromName })); public Task NotifyReturned(List userIds, long instanceId, string title, string? returnedBy, string bizType = "") => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Returned, instanceId, bizType, fallbackTitle: $"【退回】{title}", fallbackContent: $"{returnedBy} 已退回该审批,请重新审核。", ctx: new() { ["title"] = title, ["fromName"] = returnedBy })); public Task NotifyAddSign(long targetUserId, long instanceId, string title, string? fromName, string bizType = "") => NotifyUsers(new List { targetUserId }, Build(FlowNotificationTypeEnum.AddSign, instanceId, bizType, fallbackTitle: $"【加签】{title}", fallbackContent: $"{fromName} 邀请您参与审批,请及时处理。", ctx: new() { ["title"] = title, ["fromName"] = fromName })); public Task NotifyEscalated(List targetUserIds, long instanceId, string title, string? fromName, string? nodeName, string bizType = "") => NotifyUsers(targetUserIds, Build(FlowNotificationTypeEnum.Escalated, instanceId, bizType, fallbackTitle: $"【升级】{title}", fallbackContent: $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。", ctx: new() { ["title"] = title, ["fromName"] = fromName, ["nodeName"] = nodeName })); public Task NotifyTimeout(List userIds, long instanceId, string title, string bizType = "") => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Timeout, instanceId, bizType, fallbackTitle: $"【超时提醒】{title}", fallbackContent: "您有一条审批任务已超时,请尽快处理。", ctx: new() { ["title"] = title })); public Task NotifyWithdrawn(List userIds, long instanceId, string title, string? initiatorName, string bizType = "") => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Withdrawn, instanceId, bizType, fallbackTitle: $"【已撤回】{title}", fallbackContent: $"{initiatorName} 已撤回该审批流程。", ctx: new() { ["title"] = title, ["initiatorName"] = initiatorName })); private static FlowNotification Build(FlowNotificationTypeEnum type, long instanceId, string bizType, string fallbackTitle, string fallbackContent, Dictionary ctx) { return new FlowNotification { Type = type, InstanceId = instanceId, BizType = bizType ?? "", Title = fallbackTitle, Content = fallbackContent, Context = ctx, }; } // ═══════════════════════════════════════════ // 日志写入 // ═══════════════════════════════════════════ private async Task SafeWriteLog(FlowNotification notification, string channel, List userIds, FlowNotifyPushResult result, int elapsedMs) { try { var csv = string.Join(",", userIds); if (csv.Length > 1024) csv = csv.Substring(0, 1020) + "..."; var err = result.ErrorMessage; if (!string.IsNullOrEmpty(err) && err.Length > 1024) err = err.Substring(0, 1020) + "..."; await _logRep.InsertAsync(new ApprovalFlowNotifyLog { InstanceId = notification.InstanceId, NotifyType = notification.Type.ToString(), Channel = channel, Title = notification.Title, TargetUserIds = csv, TargetCount = userIds.Count, Success = result.Success, ErrorMessage = err, ElapsedMs = elapsedMs, }); } catch { // 日志写入失败不能影响主流程 } } } public class FlowNotification { public FlowNotificationTypeEnum Type { get; set; } public long InstanceId { get; set; } /// 业务类型,用于模板 BizType 级覆盖查找 public string BizType { get; set; } = ""; public string Title { get; set; } = ""; public string Content { get; set; } = ""; /// 模板渲染上下文(变量表) public Dictionary? Context { get; set; } } /// /// 通知类型 /// public enum FlowNotificationTypeEnum { NewTask, Urge, FlowCompleted, Transferred, Returned, AddSign, Withdrawn, Escalated, Timeout, } /// /// 通知渠道配置(来自 ApprovalFlow:Notify JSON 或 ApprovalFlowNotifyConfig 表的聚合结果) /// public class NotifyChannelConfig { public bool SignalR { get; set; } = true; public bool DingTalk { get; set; } public bool WorkWeixin { get; set; } public bool Email { get; set; } public bool Sms { get; set; } /// P4-16: 钉钉群机器人 Webhook URL(含 access_token) public string? DingTalkWebhookUrl { get; set; } /// P4-16: 钉钉群机器人加签 Secret(可选,启用加签时必填) public string? DingTalkSecret { get; set; } /// P4-16: 企业微信群机器人 Webhook URL(含 key) public string? WorkWeixinWebhookUrl { get; set; } /// P4-16: 短信运营商侧的通知模板 Id(阿里云/腾讯云/自定义) public string? SmsTemplateId { get; set; } }