| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- using Admin.NET.Core.Service;
- using Microsoft.AspNetCore.SignalR;
- namespace Admin.NET.Plugin.ApprovalFlow.Service;
- /// <summary>
- /// 审批流通知渠道配置服务(P4-16 延伸)
- /// - 对外:List / Save(管理页使用) + TestSend(向当前登录用户发一条 SignalR 测试通知)
- /// - 对内:GetEffectiveAsync() → NotifyChannelConfig,供 FlowNotifyService 调用
- /// DB 有记录则以 DB 为准,无记录回退 ApprovalFlow.json(零破坏升级)
- /// </summary>
- [ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 57)]
- public class FlowNotifyConfigService : IDynamicApiController, ITransient
- {
- private readonly SqlSugarRepository<ApprovalFlowNotifyConfig> _rep;
- private readonly SysCacheService _cache;
- private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _hubContext;
- private readonly UserManager _userManager;
- private const string CacheKey = "ApprovalFlow:NotifyConfig:All";
- public FlowNotifyConfigService(
- SqlSugarRepository<ApprovalFlowNotifyConfig> rep,
- SysCacheService cache,
- IHubContext<OnlineUserHub, IOnlineUserHub> hubContext,
- UserManager userManager)
- {
- _rep = rep;
- _cache = cache;
- _hubContext = hubContext;
- _userManager = userManager;
- }
- private static readonly string[] AllChannels = { "SignalR", "Email", "Sms", "DingTalk", "WorkWeixin" };
- /// <summary>
- /// 获取全部渠道配置(DB 中缺失的渠道用 ApprovalFlow.json 兜底值回填到返回结果)
- /// </summary>
- [HttpGet]
- [ApiDescriptionSettings(Name = "List")]
- [DisplayName("获取通知渠道配置")]
- public async Task<List<NotifyChannelConfigOutput>> List()
- {
- var dbRows = await _rep.AsQueryable().ToListAsync();
- var jsonCfg = App.GetConfig<NotifyChannelConfig>("ApprovalFlow:Notify") ?? new NotifyChannelConfig();
- var result = new List<NotifyChannelConfigOutput>();
- 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;
- }
- /// <summary>
- /// 保存某渠道配置(Upsert),保存后缓存失效,立即对 FlowNotifyService 生效
- /// </summary>
- [HttpPost]
- [ApiDescriptionSettings(Name = "Save")]
- [DisplayName("保存通知渠道配置")]
- public async Task<long> 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;
- }
- /// <summary>
- /// 向当前登录用户发一条 SignalR 站内测试通知,便于管理员快速验证链路
- /// (本轮仅覆盖 SignalR;外部渠道待需求明确后再加)
- /// </summary>
- [HttpPost]
- [ApiDescriptionSettings(Name = "TestSend")]
- [DisplayName("发送测试通知")]
- public async Task<bool> TestSend()
- {
- var uid = _userManager.UserId;
- var onlineUsers = _cache.HashGetAll<SysOnlineUser>(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<NotifyChannelConfig> GetEffectiveAsync()
- {
- var cached = _cache.Get<NotifyChannelConfig>(CacheKey);
- if (cached != null) return cached;
- var jsonCfg = App.GetConfig<NotifyChannelConfig>("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<ApprovalFlowNotifyConfig> rows, string key,
- Func<ApprovalFlowNotifyConfig, bool> selector, bool fallback)
- {
- var row = rows.FirstOrDefault(r => r.ChannelKey == key);
- return row != null ? selector(row) : fallback;
- }
- private static string? PickStr(List<ApprovalFlowNotifyConfig> rows, string key,
- Func<ApprovalFlowNotifyConfig, string?> 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; }
- /// <summary>数据来源:DB(已保存过)/ JSON(未保存过,返回兜底值)</summary>
- 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; }
- }
|