FlowNotifyService.cs 8.4 KB

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