using System.Diagnostics;
namespace Admin.NET.Plugin.ApprovalFlow.Service;
///
/// 流程通知服务(P4-16 重构 + 模板化/DB 配置)
/// - 聚合所有 INotifyPusher,按渠道启用状态串行调度
/// - 标题/正文优先走 FlowNotifyTemplateService 模板渲染(支持 {变量} + BizType 覆盖),未命中回退兜底文案
/// - 渠道开关/凭据优先从 DB(ApprovalFlowNotifyConfig)读取,无记录回退 ApprovalFlow.json
/// - 每次分发写 ApprovalFlowNotifyLog 便于运维追溯与降级审计
///
public class FlowNotifyService : ITransient
{
private readonly IEnumerable _pushers;
private readonly SqlSugarRepository _logRep;
private readonly FlowNotifyTemplateService _tplService;
private readonly FlowNotifyConfigService _cfgService;
public FlowNotifyService(
IEnumerable pushers,
SqlSugarRepository logRep,
FlowNotifyTemplateService tplService,
FlowNotifyConfigService cfgService)
{
_pushers = pushers;
_logRep = logRep;
_tplService = tplService;
_cfgService = cfgService;
}
public async Task NotifyUsers(List userIds, FlowNotification notification)
{
if (userIds.Count == 0) return;
// 模板渲染(未命中则用传入的 Title/Content 兜底)
var ctx = notification.Context ?? new Dictionary();
var rendered = await _tplService.RenderAsync(notification.Type, notification.BizType, ctx);
if (rendered != null)
{
notification.Title = rendered.Value.title;
notification.Content = rendered.Value.content;
}
var cfg = await _cfgService.GetEffectiveAsync();
foreach (var pusher in _pushers)
{
if (!pusher.IsEnabled(cfg)) continue;
var sw = Stopwatch.StartNew();
FlowNotifyPushResult result;
try
{
result = await pusher.PushAsync(userIds, notification);
}
catch (Exception ex)
{
result = FlowNotifyPushResult.Fail(ex.Message);
}
sw.Stop();
await SafeWriteLog(notification, pusher.Channel, userIds, result, (int)sw.ElapsedMilliseconds);
}
}
// ═══════════════════════════════════════════
// 事件专用便捷方法(FlowEngineService 调用)
// 每个方法收 bizType 作为最后一个参数;构造 FlowNotification 时带上变量上下文
// ═══════════════════════════════════════════
public Task NotifyUrge(List userIds, long instanceId, string title, string bizType = "")
=> NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Urge, instanceId, bizType,
fallbackTitle: $"【催办】{title}",
fallbackContent: "流程发起人催促您尽快审批,请及时处理。",
ctx: new() { ["title"] = title }));
public Task NotifyNewTask(List userIds, long instanceId, string title, string? nodeName, string bizType = "")
=> NotifyUsers(userIds, Build(FlowNotificationTypeEnum.NewTask, instanceId, bizType,
fallbackTitle: $"【待审批】{title}",
fallbackContent: $"您有一条新的审批任务({nodeName}),请及时处理。",
ctx: new() { ["title"] = title, ["nodeName"] = nodeName }));
public Task NotifyFlowCompleted(long initiatorId, long instanceId, string title, FlowInstanceStatusEnum finalStatus, string bizType = "")
{
var statusText = finalStatus == FlowInstanceStatusEnum.Approved ? "已通过" : "已拒绝";
return NotifyUsers(new List { initiatorId }, Build(FlowNotificationTypeEnum.FlowCompleted, instanceId, bizType,
fallbackTitle: $"【审批{statusText}】{title}",
fallbackContent: $"您发起的审批流程已{statusText}。",
ctx: new() { ["title"] = title, ["statusText"] = statusText }));
}
public Task NotifyTransferred(long targetUserId, long instanceId, string title, string? fromName, string bizType = "")
=> NotifyUsers(new List { targetUserId }, Build(FlowNotificationTypeEnum.Transferred, instanceId, bizType,
fallbackTitle: $"【转办】{title}",
fallbackContent: $"{fromName} 将一条审批任务转交给您,请及时处理。",
ctx: new() { ["title"] = title, ["fromName"] = fromName }));
public Task NotifyReturned(List userIds, long instanceId, string title, string? returnedBy, string bizType = "")
=> NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Returned, instanceId, bizType,
fallbackTitle: $"【退回】{title}",
fallbackContent: $"{returnedBy} 已退回该审批,请重新审核。",
ctx: new() { ["title"] = title, ["fromName"] = returnedBy }));
public Task NotifyAddSign(long targetUserId, long instanceId, string title, string? fromName, string bizType = "")
=> NotifyUsers(new List { targetUserId }, Build(FlowNotificationTypeEnum.AddSign, instanceId, bizType,
fallbackTitle: $"【加签】{title}",
fallbackContent: $"{fromName} 邀请您参与审批,请及时处理。",
ctx: new() { ["title"] = title, ["fromName"] = fromName }));
public Task NotifyEscalated(List targetUserIds, long instanceId, string title, string? fromName, string? nodeName, string bizType = "")
=> NotifyUsers(targetUserIds, Build(FlowNotificationTypeEnum.Escalated, instanceId, bizType,
fallbackTitle: $"【升级】{title}",
fallbackContent: $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。",
ctx: new() { ["title"] = title, ["fromName"] = fromName, ["nodeName"] = nodeName }));
public Task NotifyTimeout(List userIds, long instanceId, string title, string bizType = "")
=> NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Timeout, instanceId, bizType,
fallbackTitle: $"【超时提醒】{title}",
fallbackContent: "您有一条审批任务已超时,请尽快处理。",
ctx: new() { ["title"] = title }));
public Task NotifyWithdrawn(List userIds, long instanceId, string title, string? initiatorName, string bizType = "")
=> NotifyUsers(userIds, Build(FlowNotificationTypeEnum.Withdrawn, instanceId, bizType,
fallbackTitle: $"【已撤回】{title}",
fallbackContent: $"{initiatorName} 已撤回该审批流程。",
ctx: new() { ["title"] = title, ["initiatorName"] = initiatorName }));
private static FlowNotification Build(FlowNotificationTypeEnum type, long instanceId, string bizType,
string fallbackTitle, string fallbackContent, Dictionary ctx)
{
return new FlowNotification
{
Type = type,
InstanceId = instanceId,
BizType = bizType ?? "",
Title = fallbackTitle,
Content = fallbackContent,
Context = ctx,
};
}
// ═══════════════════════════════════════════
// 日志写入
// ═══════════════════════════════════════════
private async Task SafeWriteLog(FlowNotification notification, string channel, List userIds,
FlowNotifyPushResult result, int elapsedMs)
{
try
{
var csv = string.Join(",", userIds);
if (csv.Length > 1024) csv = csv.Substring(0, 1020) + "...";
var err = result.ErrorMessage;
if (!string.IsNullOrEmpty(err) && err.Length > 1024) err = err.Substring(0, 1020) + "...";
await _logRep.InsertAsync(new ApprovalFlowNotifyLog
{
InstanceId = notification.InstanceId,
NotifyType = notification.Type.ToString(),
Channel = channel,
Title = notification.Title,
TargetUserIds = csv,
TargetCount = userIds.Count,
Success = result.Success,
ErrorMessage = err,
ElapsedMs = elapsedMs,
});
}
catch
{
// 日志写入失败不能影响主流程
}
}
}
public class FlowNotification
{
public FlowNotificationTypeEnum Type { get; set; }
public long InstanceId { get; set; }
/// 业务类型,用于模板 BizType 级覆盖查找
public string BizType { get; set; } = "";
public string Title { get; set; } = "";
public string Content { get; set; } = "";
/// 模板渲染上下文(变量表)
public Dictionary? Context { get; set; }
}
///
/// 通知类型
///
public enum FlowNotificationTypeEnum
{
NewTask,
Urge,
FlowCompleted,
Transferred,
Returned,
AddSign,
Withdrawn,
Escalated,
Timeout,
}
///
/// 通知渠道配置(来自 ApprovalFlow:Notify JSON 或 ApprovalFlowNotifyConfig 表的聚合结果)
///
public class NotifyChannelConfig
{
public bool SignalR { get; set; } = true;
public bool DingTalk { get; set; }
public bool WorkWeixin { get; set; }
public bool Email { get; set; }
public bool Sms { get; set; }
/// P4-16: 钉钉群机器人 Webhook URL(含 access_token)
public string? DingTalkWebhookUrl { get; set; }
/// P4-16: 钉钉群机器人加签 Secret(可选,启用加签时必填)
public string? DingTalkSecret { get; set; }
/// P4-16: 企业微信群机器人 Webhook URL(含 key)
public string? WorkWeixinWebhookUrl { get; set; }
/// P4-16: 短信运营商侧的通知模板 Id(阿里云/腾讯云/自定义)
public string? SmsTemplateId { get; set; }
}