FlowNotifyConfigService.cs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. using Admin.NET.Core.Service;
  2. using Microsoft.AspNetCore.SignalR;
  3. namespace Admin.NET.Plugin.ApprovalFlow.Service;
  4. /// <summary>
  5. /// 审批流通知渠道配置服务(P4-16 延伸)
  6. /// - 对外:List / Save(管理页使用) + TestSend(向当前登录用户发一条 SignalR 测试通知)
  7. /// - 对内:GetEffectiveAsync() → NotifyChannelConfig,供 FlowNotifyService 调用
  8. /// DB 有记录则以 DB 为准,无记录回退 ApprovalFlow.json(零破坏升级)
  9. /// </summary>
  10. [ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 57)]
  11. public class FlowNotifyConfigService : IDynamicApiController, ITransient
  12. {
  13. private readonly SqlSugarRepository<ApprovalFlowNotifyConfig> _rep;
  14. private readonly SysCacheService _cache;
  15. private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _hubContext;
  16. private readonly UserManager _userManager;
  17. private const string CacheKey = "ApprovalFlow:NotifyConfig:All";
  18. public FlowNotifyConfigService(
  19. SqlSugarRepository<ApprovalFlowNotifyConfig> rep,
  20. SysCacheService cache,
  21. IHubContext<OnlineUserHub, IOnlineUserHub> hubContext,
  22. UserManager userManager)
  23. {
  24. _rep = rep;
  25. _cache = cache;
  26. _hubContext = hubContext;
  27. _userManager = userManager;
  28. }
  29. private static readonly string[] AllChannels = { "SignalR", "Email", "Sms", "DingTalk", "WorkWeixin" };
  30. /// <summary>
  31. /// 获取全部渠道配置(DB 中缺失的渠道用 ApprovalFlow.json 兜底值回填到返回结果)
  32. /// </summary>
  33. [HttpGet]
  34. [ApiDescriptionSettings(Name = "List")]
  35. [DisplayName("获取通知渠道配置")]
  36. public async Task<List<NotifyChannelConfigOutput>> List()
  37. {
  38. var dbRows = await _rep.AsQueryable().ToListAsync();
  39. var jsonCfg = App.GetConfig<NotifyChannelConfig>("ApprovalFlow:Notify") ?? new NotifyChannelConfig();
  40. var result = new List<NotifyChannelConfigOutput>();
  41. foreach (var ch in AllChannels)
  42. {
  43. var row = dbRows.FirstOrDefault(r => r.ChannelKey == ch);
  44. if (row != null)
  45. {
  46. result.Add(new NotifyChannelConfigOutput
  47. {
  48. Id = row.Id,
  49. ChannelKey = row.ChannelKey,
  50. Enabled = row.Enabled,
  51. WebhookUrl = row.WebhookUrl,
  52. Secret = row.Secret,
  53. TemplateId = row.TemplateId,
  54. Remark = row.Remark,
  55. Source = "DB",
  56. });
  57. }
  58. else
  59. {
  60. result.Add(new NotifyChannelConfigOutput
  61. {
  62. Id = 0,
  63. ChannelKey = ch,
  64. Enabled = ch switch
  65. {
  66. "SignalR" => jsonCfg.SignalR,
  67. "Email" => jsonCfg.Email,
  68. "Sms" => jsonCfg.Sms,
  69. "DingTalk" => jsonCfg.DingTalk,
  70. "WorkWeixin" => jsonCfg.WorkWeixin,
  71. _ => false,
  72. },
  73. WebhookUrl = ch switch
  74. {
  75. "DingTalk" => jsonCfg.DingTalkWebhookUrl,
  76. "WorkWeixin" => jsonCfg.WorkWeixinWebhookUrl,
  77. _ => null,
  78. },
  79. Secret = ch == "DingTalk" ? jsonCfg.DingTalkSecret : null,
  80. TemplateId = ch == "Sms" ? jsonCfg.SmsTemplateId : null,
  81. Remark = null,
  82. Source = "JSON",
  83. });
  84. }
  85. }
  86. return result;
  87. }
  88. /// <summary>
  89. /// 保存某渠道配置(Upsert),保存后缓存失效,立即对 FlowNotifyService 生效
  90. /// </summary>
  91. [HttpPost]
  92. [ApiDescriptionSettings(Name = "Save")]
  93. [DisplayName("保存通知渠道配置")]
  94. public async Task<long> Save(NotifyChannelConfigSaveInput input)
  95. {
  96. if (string.IsNullOrWhiteSpace(input.ChannelKey) || !AllChannels.Contains(input.ChannelKey))
  97. throw Oops.Oh($"无效渠道标识:{input.ChannelKey}");
  98. var entity = await _rep.AsQueryable().FirstAsync(r => r.ChannelKey == input.ChannelKey);
  99. if (entity == null)
  100. {
  101. entity = new ApprovalFlowNotifyConfig
  102. {
  103. ChannelKey = input.ChannelKey,
  104. Enabled = input.Enabled,
  105. WebhookUrl = input.WebhookUrl,
  106. Secret = input.Secret,
  107. TemplateId = input.TemplateId,
  108. Remark = input.Remark,
  109. };
  110. await _rep.InsertAsync(entity);
  111. }
  112. else
  113. {
  114. entity.Enabled = input.Enabled;
  115. entity.WebhookUrl = input.WebhookUrl;
  116. entity.Secret = input.Secret;
  117. entity.TemplateId = input.TemplateId;
  118. entity.Remark = input.Remark;
  119. await _rep.UpdateAsync(entity);
  120. }
  121. InvalidateCache();
  122. return entity.Id;
  123. }
  124. /// <summary>
  125. /// 向当前登录用户发一条 SignalR 站内测试通知,便于管理员快速验证链路
  126. /// (本轮仅覆盖 SignalR;外部渠道待需求明确后再加)
  127. /// </summary>
  128. [HttpPost]
  129. [ApiDescriptionSettings(Name = "TestSend")]
  130. [DisplayName("发送测试通知")]
  131. public async Task<bool> TestSend()
  132. {
  133. var uid = _userManager.UserId;
  134. var onlineUsers = _cache.HashGetAll<SysOnlineUser>(CacheConst.KeyUserOnline);
  135. var connectionIds = onlineUsers
  136. .Where(u => u.Value.UserId == uid)
  137. .Select(u => u.Value.ConnectionId)
  138. .ToList();
  139. if (connectionIds.Count == 0) return false;
  140. await _hubContext.Clients.Clients(connectionIds).ReceiveMessage(new
  141. {
  142. title = "【测试通知】审批流通知渠道连通性",
  143. message = "这是一条管理员主动触发的测试通知,当前连接收到即表示 SignalR 站内消息链路通畅。",
  144. type = FlowNotificationTypeEnum.Urge.ToString(),
  145. instanceId = 0L,
  146. });
  147. return true;
  148. }
  149. // ═══════════════════════════════════════════
  150. // 内部:向 FlowNotifyService 提供有效配置
  151. // ═══════════════════════════════════════════
  152. [NonAction]
  153. public async Task<NotifyChannelConfig> GetEffectiveAsync()
  154. {
  155. var cached = _cache.Get<NotifyChannelConfig>(CacheKey);
  156. if (cached != null) return cached;
  157. var jsonCfg = App.GetConfig<NotifyChannelConfig>("ApprovalFlow:Notify") ?? new NotifyChannelConfig();
  158. var dbRows = await _rep.AsQueryable().ToListAsync();
  159. var cfg = new NotifyChannelConfig
  160. {
  161. SignalR = Pick(dbRows, "SignalR", r => r.Enabled, jsonCfg.SignalR),
  162. Email = Pick(dbRows, "Email", r => r.Enabled, jsonCfg.Email),
  163. Sms = Pick(dbRows, "Sms", r => r.Enabled, jsonCfg.Sms),
  164. DingTalk = Pick(dbRows, "DingTalk", r => r.Enabled, jsonCfg.DingTalk),
  165. WorkWeixin = Pick(dbRows, "WorkWeixin", r => r.Enabled, jsonCfg.WorkWeixin),
  166. DingTalkWebhookUrl = PickStr(dbRows, "DingTalk", r => r.WebhookUrl, jsonCfg.DingTalkWebhookUrl),
  167. DingTalkSecret = PickStr(dbRows, "DingTalk", r => r.Secret, jsonCfg.DingTalkSecret),
  168. WorkWeixinWebhookUrl = PickStr(dbRows, "WorkWeixin", r => r.WebhookUrl, jsonCfg.WorkWeixinWebhookUrl),
  169. SmsTemplateId = PickStr(dbRows, "Sms", r => r.TemplateId, jsonCfg.SmsTemplateId),
  170. };
  171. _cache.Set(CacheKey, cfg, TimeSpan.FromMinutes(10));
  172. return cfg;
  173. }
  174. [NonAction]
  175. public void InvalidateCache() => _cache.Remove(CacheKey);
  176. private static bool Pick(List<ApprovalFlowNotifyConfig> rows, string key,
  177. Func<ApprovalFlowNotifyConfig, bool> selector, bool fallback)
  178. {
  179. var row = rows.FirstOrDefault(r => r.ChannelKey == key);
  180. return row != null ? selector(row) : fallback;
  181. }
  182. private static string? PickStr(List<ApprovalFlowNotifyConfig> rows, string key,
  183. Func<ApprovalFlowNotifyConfig, string?> selector, string? fallback)
  184. {
  185. var row = rows.FirstOrDefault(r => r.ChannelKey == key);
  186. return row != null ? selector(row) : fallback;
  187. }
  188. }
  189. public class NotifyChannelConfigOutput
  190. {
  191. public long Id { get; set; }
  192. public string ChannelKey { get; set; } = "";
  193. public bool Enabled { get; set; }
  194. public string? WebhookUrl { get; set; }
  195. public string? Secret { get; set; }
  196. public string? TemplateId { get; set; }
  197. public string? Remark { get; set; }
  198. /// <summary>数据来源:DB(已保存过)/ JSON(未保存过,返回兜底值)</summary>
  199. public string Source { get; set; } = "";
  200. }
  201. public class NotifyChannelConfigSaveInput
  202. {
  203. [Required, MaxLength(16)]
  204. public string ChannelKey { get; set; } = "";
  205. public bool Enabled { get; set; }
  206. [MaxLength(512)]
  207. public string? WebhookUrl { get; set; }
  208. [MaxLength(128)]
  209. public string? Secret { get; set; }
  210. [MaxLength(64)]
  211. public string? TemplateId { get; set; }
  212. [MaxLength(256)]
  213. public string? Remark { get; set; }
  214. }