using Admin.NET.Core.Service; using Microsoft.AspNetCore.SignalR; namespace Admin.NET.Plugin.ApprovalFlow.Service; /// /// 审批流通知渠道配置服务(P4-16 延伸) /// - 对外:List / Save(管理页使用) + TestSend(向当前登录用户发一条 SignalR 测试通知) /// - 对内:GetEffectiveAsync() → NotifyChannelConfig,供 FlowNotifyService 调用 /// DB 有记录则以 DB 为准,无记录回退 ApprovalFlow.json(零破坏升级) /// [ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 57)] public class FlowNotifyConfigService : IDynamicApiController, ITransient { private readonly SqlSugarRepository _rep; private readonly SysCacheService _cache; private readonly IHubContext _hubContext; private readonly UserManager _userManager; private const string CacheKey = "ApprovalFlow:NotifyConfig:All"; public FlowNotifyConfigService( SqlSugarRepository rep, SysCacheService cache, IHubContext hubContext, UserManager userManager) { _rep = rep; _cache = cache; _hubContext = hubContext; _userManager = userManager; } private static readonly string[] AllChannels = { "SignalR", "Email", "Sms", "DingTalk", "WorkWeixin" }; /// /// 获取全部渠道配置(DB 中缺失的渠道用 ApprovalFlow.json 兜底值回填到返回结果) /// [HttpGet] [ApiDescriptionSettings(Name = "List")] [DisplayName("获取通知渠道配置")] public async Task> List() { var dbRows = await _rep.AsQueryable().ToListAsync(); var jsonCfg = App.GetConfig("ApprovalFlow:Notify") ?? new NotifyChannelConfig(); var result = new List(); foreach (var ch in AllChannels) { var row = dbRows.FirstOrDefault(r => r.ChannelKey == ch); if (row != null) { result.Add(new NotifyChannelConfigOutput { Id = row.Id, ChannelKey = row.ChannelKey, Enabled = row.Enabled, WebhookUrl = row.WebhookUrl, Secret = row.Secret, TemplateId = row.TemplateId, Remark = row.Remark, Source = "DB", }); } else { result.Add(new NotifyChannelConfigOutput { Id = 0, ChannelKey = ch, Enabled = ch switch { "SignalR" => jsonCfg.SignalR, "Email" => jsonCfg.Email, "Sms" => jsonCfg.Sms, "DingTalk" => jsonCfg.DingTalk, "WorkWeixin" => jsonCfg.WorkWeixin, _ => false, }, WebhookUrl = ch switch { "DingTalk" => jsonCfg.DingTalkWebhookUrl, "WorkWeixin" => jsonCfg.WorkWeixinWebhookUrl, _ => null, }, Secret = ch == "DingTalk" ? jsonCfg.DingTalkSecret : null, TemplateId = ch == "Sms" ? jsonCfg.SmsTemplateId : null, Remark = null, Source = "JSON", }); } } return result; } /// /// 保存某渠道配置(Upsert),保存后缓存失效,立即对 FlowNotifyService 生效 /// [HttpPost] [ApiDescriptionSettings(Name = "Save")] [DisplayName("保存通知渠道配置")] public async Task Save(NotifyChannelConfigSaveInput input) { if (string.IsNullOrWhiteSpace(input.ChannelKey) || !AllChannels.Contains(input.ChannelKey)) throw Oops.Oh($"无效渠道标识:{input.ChannelKey}"); var entity = await _rep.AsQueryable().FirstAsync(r => r.ChannelKey == input.ChannelKey); if (entity == null) { entity = new ApprovalFlowNotifyConfig { ChannelKey = input.ChannelKey, Enabled = input.Enabled, WebhookUrl = input.WebhookUrl, Secret = input.Secret, TemplateId = input.TemplateId, Remark = input.Remark, }; await _rep.InsertAsync(entity); } else { entity.Enabled = input.Enabled; entity.WebhookUrl = input.WebhookUrl; entity.Secret = input.Secret; entity.TemplateId = input.TemplateId; entity.Remark = input.Remark; await _rep.UpdateAsync(entity); } InvalidateCache(); return entity.Id; } /// /// 向当前登录用户发一条 SignalR 站内测试通知,便于管理员快速验证链路 /// (本轮仅覆盖 SignalR;外部渠道待需求明确后再加) /// [HttpPost] [ApiDescriptionSettings(Name = "TestSend")] [DisplayName("发送测试通知")] public async Task TestSend() { var uid = _userManager.UserId; var onlineUsers = _cache.HashGetAll(CacheConst.KeyUserOnline); var connectionIds = onlineUsers .Where(u => u.Value.UserId == uid) .Select(u => u.Value.ConnectionId) .ToList(); if (connectionIds.Count == 0) return false; await _hubContext.Clients.Clients(connectionIds).ReceiveMessage(new { title = "【测试通知】审批流通知渠道连通性", message = "这是一条管理员主动触发的测试通知,当前连接收到即表示 SignalR 站内消息链路通畅。", type = FlowNotificationTypeEnum.Urge.ToString(), instanceId = 0L, }); return true; } // ═══════════════════════════════════════════ // 内部:向 FlowNotifyService 提供有效配置 // ═══════════════════════════════════════════ [NonAction] public async Task GetEffectiveAsync() { var cached = _cache.Get(CacheKey); if (cached != null) return cached; var jsonCfg = App.GetConfig("ApprovalFlow:Notify") ?? new NotifyChannelConfig(); var dbRows = await _rep.AsQueryable().ToListAsync(); var cfg = new NotifyChannelConfig { SignalR = Pick(dbRows, "SignalR", r => r.Enabled, jsonCfg.SignalR), Email = Pick(dbRows, "Email", r => r.Enabled, jsonCfg.Email), Sms = Pick(dbRows, "Sms", r => r.Enabled, jsonCfg.Sms), DingTalk = Pick(dbRows, "DingTalk", r => r.Enabled, jsonCfg.DingTalk), WorkWeixin = Pick(dbRows, "WorkWeixin", r => r.Enabled, jsonCfg.WorkWeixin), DingTalkWebhookUrl = PickStr(dbRows, "DingTalk", r => r.WebhookUrl, jsonCfg.DingTalkWebhookUrl), DingTalkSecret = PickStr(dbRows, "DingTalk", r => r.Secret, jsonCfg.DingTalkSecret), WorkWeixinWebhookUrl = PickStr(dbRows, "WorkWeixin", r => r.WebhookUrl, jsonCfg.WorkWeixinWebhookUrl), SmsTemplateId = PickStr(dbRows, "Sms", r => r.TemplateId, jsonCfg.SmsTemplateId), }; _cache.Set(CacheKey, cfg, TimeSpan.FromMinutes(10)); return cfg; } [NonAction] public void InvalidateCache() => _cache.Remove(CacheKey); private static bool Pick(List rows, string key, Func selector, bool fallback) { var row = rows.FirstOrDefault(r => r.ChannelKey == key); return row != null ? selector(row) : fallback; } private static string? PickStr(List rows, string key, Func selector, string? fallback) { var row = rows.FirstOrDefault(r => r.ChannelKey == key); return row != null ? selector(row) : fallback; } } public class NotifyChannelConfigOutput { public long Id { get; set; } public string ChannelKey { get; set; } = ""; public bool Enabled { get; set; } public string? WebhookUrl { get; set; } public string? Secret { get; set; } public string? TemplateId { get; set; } public string? Remark { get; set; } /// 数据来源:DB(已保存过)/ JSON(未保存过,返回兜底值) public string Source { get; set; } = ""; } public class NotifyChannelConfigSaveInput { [Required, MaxLength(16)] public string ChannelKey { get; set; } = ""; public bool Enabled { get; set; } [MaxLength(512)] public string? WebhookUrl { get; set; } [MaxLength(128)] public string? Secret { get; set; } [MaxLength(64)] public string? TemplateId { get; set; } [MaxLength(256)] public string? Remark { get; set; } }