FlowNotifyService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. using System.Diagnostics;
  2. namespace Admin.NET.Plugin.ApprovalFlow.Service;
  3. /// <summary>
  4. /// 流程通知服务(P4-16 重构 + 模板化/DB 配置)
  5. /// - 聚合所有 INotifyPusher,按渠道启用状态串行调度
  6. /// - 标题/正文优先走 FlowNotifyTemplateService 模板渲染(支持 {变量} + BizType 覆盖),未命中回退兜底文案
  7. /// - 渠道开关/凭据优先从 DB(ApprovalFlowNotifyConfig)读取,无记录回退 ApprovalFlow.json
  8. /// - 每次分发写 ApprovalFlowNotifyLog 便于运维追溯与降级审计
  9. /// </summary>
  10. public class FlowNotifyService : ITransient
  11. {
  12. private readonly IEnumerable<INotifyPusher> _pushers;
  13. private readonly SqlSugarRepository<ApprovalFlowNotifyLog> _logRep;
  14. private readonly FlowNotifyTemplateService _tplService;
  15. private readonly FlowNotifyConfigService _cfgService;
  16. public FlowNotifyService(
  17. IEnumerable<INotifyPusher> pushers,
  18. SqlSugarRepository<ApprovalFlowNotifyLog> logRep,
  19. FlowNotifyTemplateService tplService,
  20. FlowNotifyConfigService cfgService)
  21. {
  22. _pushers = pushers;
  23. _logRep = logRep;
  24. _tplService = tplService;
  25. _cfgService = cfgService;
  26. }
  27. public async Task NotifyUsers(List<long> userIds, FlowNotification notification)
  28. {
  29. if (userIds.Count == 0) return;
  30. // 模板渲染(未命中则用传入的 Title/Content 兜底)
  31. var ctx = notification.Context ?? new Dictionary<string, string?>();
  32. var rendered = await _tplService.RenderAsync(notification.Type, notification.BizType, ctx);
  33. if (rendered != null)
  34. {
  35. notification.Title = rendered.Value.title;
  36. notification.Content = rendered.Value.content;
  37. }
  38. var cfg = await _cfgService.GetEffectiveAsync();
  39. foreach (var pusher in _pushers)
  40. {
  41. if (!pusher.IsEnabled(cfg)) continue;
  42. var sw = Stopwatch.StartNew();
  43. FlowNotifyPushResult result;
  44. try
  45. {
  46. result = await pusher.PushAsync(userIds, notification);
  47. }
  48. catch (Exception ex)
  49. {
  50. result = FlowNotifyPushResult.Fail(ex.Message);
  51. }
  52. sw.Stop();
  53. await SafeWriteLog(notification, pusher.Channel, userIds, result, (int)sw.ElapsedMilliseconds);
  54. }
  55. }
  56. // ═══════════════════════════════════════════
  57. // 事件专用便捷方法(FlowEngineService 调用)
  58. // 每个方法收 bizType 作为最后一个参数;构造 FlowNotification 时带上变量上下文
  59. // ═══════════════════════════════════════════
  60. public Task NotifyUrge(List<long> userIds, long instanceId, string title, string bizType = "")
  61. => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Urge, instanceId, bizType,
  62. fallbackTitle: $"【催办】{title}",
  63. fallbackContent: "流程发起人催促您尽快审批,请及时处理。",
  64. ctx: new() { ["title"] = title }));
  65. public Task NotifyNewTask(List<long> userIds, long instanceId, string title, string? nodeName, string bizType = "")
  66. => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.NewTask, instanceId, bizType,
  67. fallbackTitle: $"【待审批】{title}",
  68. fallbackContent: $"您有一条新的审批任务({nodeName}),请及时处理。",
  69. ctx: new() { ["title"] = title, ["nodeName"] = nodeName }));
  70. public Task NotifyFlowCompleted(long initiatorId, long instanceId, string title, FlowInstanceStatusEnum finalStatus, string bizType = "")
  71. {
  72. var statusText = finalStatus == FlowInstanceStatusEnum.Approved ? "已通过" : "已拒绝";
  73. return NotifyUsers(new List<long> { initiatorId }, Build(FlowNotificationTypeEnum.FlowCompleted, instanceId, bizType,
  74. fallbackTitle: $"【审批{statusText}】{title}",
  75. fallbackContent: $"您发起的审批流程已{statusText}。",
  76. ctx: new() { ["title"] = title, ["statusText"] = statusText }));
  77. }
  78. public Task NotifyTransferred(long targetUserId, long instanceId, string title, string? fromName, string bizType = "")
  79. => NotifyUsers(new List<long> { targetUserId }, Build(FlowNotificationTypeEnum.Transferred, instanceId, bizType,
  80. fallbackTitle: $"【转办】{title}",
  81. fallbackContent: $"{fromName} 将一条审批任务转交给您,请及时处理。",
  82. ctx: new() { ["title"] = title, ["fromName"] = fromName }));
  83. public Task NotifyReturned(List<long> userIds, long instanceId, string title, string? returnedBy, string bizType = "")
  84. => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Returned, instanceId, bizType,
  85. fallbackTitle: $"【退回】{title}",
  86. fallbackContent: $"{returnedBy} 已退回该审批,请重新审核。",
  87. ctx: new() { ["title"] = title, ["fromName"] = returnedBy }));
  88. public Task NotifyAddSign(long targetUserId, long instanceId, string title, string? fromName, string bizType = "")
  89. => NotifyUsers(new List<long> { targetUserId }, Build(FlowNotificationTypeEnum.AddSign, instanceId, bizType,
  90. fallbackTitle: $"【加签】{title}",
  91. fallbackContent: $"{fromName} 邀请您参与审批,请及时处理。",
  92. ctx: new() { ["title"] = title, ["fromName"] = fromName }));
  93. public Task NotifyEscalated(List<long> targetUserIds, long instanceId, string title, string? fromName, string? nodeName, string bizType = "")
  94. => NotifyUsers(targetUserIds, Build(FlowNotificationTypeEnum.Escalated, instanceId, bizType,
  95. fallbackTitle: $"【升级】{title}",
  96. fallbackContent: $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。",
  97. ctx: new() { ["title"] = title, ["fromName"] = fromName, ["nodeName"] = nodeName }));
  98. public Task NotifyTimeout(List<long> userIds, long instanceId, string title, string bizType = "")
  99. => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Timeout, instanceId, bizType,
  100. fallbackTitle: $"【超时提醒】{title}",
  101. fallbackContent: "您有一条审批任务已超时,请尽快处理。",
  102. ctx: new() { ["title"] = title }));
  103. public Task NotifyWithdrawn(List<long> userIds, long instanceId, string title, string? initiatorName, string bizType = "")
  104. => NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Withdrawn, instanceId, bizType,
  105. fallbackTitle: $"【已撤回】{title}",
  106. fallbackContent: $"{initiatorName} 已撤回该审批流程。",
  107. ctx: new() { ["title"] = title, ["initiatorName"] = initiatorName }));
  108. private static FlowNotification Build(FlowNotificationTypeEnum type, long instanceId, string bizType,
  109. string fallbackTitle, string fallbackContent, Dictionary<string, string?> ctx)
  110. {
  111. return new FlowNotification
  112. {
  113. Type = type,
  114. InstanceId = instanceId,
  115. BizType = bizType ?? "",
  116. Title = fallbackTitle,
  117. Content = fallbackContent,
  118. Context = ctx,
  119. };
  120. }
  121. // ═══════════════════════════════════════════
  122. // 日志写入
  123. // ═══════════════════════════════════════════
  124. private async Task SafeWriteLog(FlowNotification notification, string channel, List<long> userIds,
  125. FlowNotifyPushResult result, int elapsedMs)
  126. {
  127. try
  128. {
  129. var csv = string.Join(",", userIds);
  130. if (csv.Length > 1024) csv = csv.Substring(0, 1020) + "...";
  131. var err = result.ErrorMessage;
  132. if (!string.IsNullOrEmpty(err) && err.Length > 1024) err = err.Substring(0, 1020) + "...";
  133. await _logRep.InsertAsync(new ApprovalFlowNotifyLog
  134. {
  135. InstanceId = notification.InstanceId,
  136. NotifyType = notification.Type.ToString(),
  137. Channel = channel,
  138. Title = notification.Title,
  139. TargetUserIds = csv,
  140. TargetCount = userIds.Count,
  141. Success = result.Success,
  142. ErrorMessage = err,
  143. ElapsedMs = elapsedMs,
  144. });
  145. }
  146. catch
  147. {
  148. // 日志写入失败不能影响主流程
  149. }
  150. }
  151. }
  152. public class FlowNotification
  153. {
  154. public FlowNotificationTypeEnum Type { get; set; }
  155. public long InstanceId { get; set; }
  156. /// <summary>业务类型,用于模板 BizType 级覆盖查找</summary>
  157. public string BizType { get; set; } = "";
  158. public string Title { get; set; } = "";
  159. public string Content { get; set; } = "";
  160. /// <summary>模板渲染上下文(变量表)</summary>
  161. public Dictionary<string, string?>? Context { get; set; }
  162. }
  163. /// <summary>
  164. /// 通知类型
  165. /// </summary>
  166. public enum FlowNotificationTypeEnum
  167. {
  168. NewTask,
  169. Urge,
  170. FlowCompleted,
  171. Transferred,
  172. Returned,
  173. AddSign,
  174. Withdrawn,
  175. Escalated,
  176. Timeout,
  177. }
  178. /// <summary>
  179. /// 通知渠道配置(来自 ApprovalFlow:Notify JSON 或 ApprovalFlowNotifyConfig 表的聚合结果)
  180. /// </summary>
  181. public class NotifyChannelConfig
  182. {
  183. public bool SignalR { get; set; } = true;
  184. public bool DingTalk { get; set; }
  185. public bool WorkWeixin { get; set; }
  186. public bool Email { get; set; }
  187. public bool Sms { get; set; }
  188. /// <summary>P4-16: 钉钉群机器人 Webhook URL(含 access_token)</summary>
  189. public string? DingTalkWebhookUrl { get; set; }
  190. /// <summary>P4-16: 钉钉群机器人加签 Secret(可选,启用加签时必填)</summary>
  191. public string? DingTalkSecret { get; set; }
  192. /// <summary>P4-16: 企业微信群机器人 Webhook URL(含 key)</summary>
  193. public string? WorkWeixinWebhookUrl { get; set; }
  194. /// <summary>P4-16: 短信运营商侧的通知模板 Id(阿里云/腾讯云/自定义)</summary>
  195. public string? SmsTemplateId { get; set; }
  196. }