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; }
}