using Admin.NET.Core.Service; using System.Text.RegularExpressions; namespace Admin.NET.Plugin.ApprovalFlow.Service; /// /// 审批流通知模板服务(P4-16 延伸) /// - 对外:CRUD(管理页使用) + EnsureSystemTemplates(启动种子) /// - 对内:RenderAsync(type, bizType, ctx) → (title, content),供 FlowNotifyService 调用 /// [ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 58)] public class FlowNotifyTemplateService : IDynamicApiController, ITransient { private readonly SqlSugarRepository _rep; private readonly SysCacheService _cache; private const string CacheKey = "ApprovalFlow:NotifyTemplate:All"; private static readonly Regex VarRegex = new(@"\{(\w+)\}", RegexOptions.Compiled); public FlowNotifyTemplateService(SqlSugarRepository rep, SysCacheService cache) { _rep = rep; _cache = cache; } /// /// 获取全部通知模板(含系统预置与业务覆盖) /// [HttpGet] [ApiDescriptionSettings(Name = "List")] [DisplayName("获取通知模板列表")] public async Task> List([FromQuery] NotifyTemplateQueryInput? input = null) { return await _rep.AsQueryable() .WhereIF(!string.IsNullOrEmpty(input?.NotifyType), t => t.NotifyType == input!.NotifyType) .WhereIF(input?.BizType != null, t => t.BizType == input!.BizType) .OrderBy(t => t.BizType) .OrderBy(t => t.NotifyType) .ToListAsync(); } /// /// 新增或更新通知模板(按 NotifyType + BizType 唯一) /// [HttpPost] [ApiDescriptionSettings(Name = "Save")] [DisplayName("保存通知模板")] public async Task Save(NotifyTemplateSaveInput input) { if (string.IsNullOrWhiteSpace(input.NotifyType)) throw Oops.Oh("NotifyType 必填"); if (string.IsNullOrWhiteSpace(input.Title)) throw Oops.Oh("标题模板必填"); if (string.IsNullOrWhiteSpace(input.Content)) throw Oops.Oh("正文模板必填"); var bizType = input.BizType ?? ""; var entity = await _rep.AsQueryable() .FirstAsync(t => t.NotifyType == input.NotifyType && t.BizType == bizType); if (entity == null) { entity = new ApprovalFlowNotifyTemplate { NotifyType = input.NotifyType, BizType = bizType, Title = input.Title, Content = input.Content, IsEnabled = input.IsEnabled ?? true, IsSystem = false, Remark = input.Remark, }; await _rep.InsertAsync(entity); } else { entity.Title = input.Title; entity.Content = input.Content; entity.IsEnabled = input.IsEnabled ?? entity.IsEnabled; entity.Remark = input.Remark; await _rep.UpdateAsync(entity); } InvalidateCache(); return entity.Id; } /// /// 删除通知模板(系统预置不可删) /// [HttpPost] [ApiDescriptionSettings(Name = "Delete")] [DisplayName("删除通知模板")] public async Task Delete(BaseIdInput input) { var entity = await _rep.GetByIdAsync(input.Id) ?? throw Oops.Oh("模板不存在"); if (entity.IsSystem) throw Oops.Oh("系统预置模板不可删除(可改为停用)"); await _rep.DeleteByIdAsync(input.Id); InvalidateCache(); } /// /// 可用变量说明(管理页展示) /// [HttpGet] [ApiDescriptionSettings(Name = "Variables")] [DisplayName("获取可用变量列表")] public List Variables() { return new() { new() { Key = "title", Description = "流程标题(业务单据主标题)", AppliedTo = "全部" }, new() { Key = "nodeName", Description = "当前/相关节点名", AppliedTo = "NewTask / Urge / Escalated / Timeout" }, new() { Key = "initiatorName", Description = "流程发起人", AppliedTo = "Withdrawn / FlowCompleted" }, new() { Key = "fromName", Description = "操作人(转办 / 退回 / 升级 / 加签发起者)", AppliedTo = "Transferred / Returned / AddSign / Escalated" }, new() { Key = "statusText", Description = "\"已通过\" 或 \"已拒绝\"", AppliedTo = "FlowCompleted" }, }; } // ═══════════════════════════════════════════ // 渲染(供 FlowNotifyService 调用) // ═══════════════════════════════════════════ /// /// 按 (notifyType, bizType) 查找启用模板并渲染;未命中则返回 null,调用方走兜底文案。 /// 优先级:BizType 级覆盖 > 全局默认(BizType="") /// [NonAction] public async Task<(string title, string content)?> RenderAsync( FlowNotificationTypeEnum type, string? bizType, Dictionary ctx) { var typeStr = type.ToString(); var all = await GetAllCachedAsync(); ApprovalFlowNotifyTemplate? tpl = null; if (!string.IsNullOrEmpty(bizType)) tpl = all.FirstOrDefault(t => t.IsEnabled && t.NotifyType == typeStr && t.BizType == bizType); tpl ??= all.FirstOrDefault(t => t.IsEnabled && t.NotifyType == typeStr && t.BizType == ""); if (tpl == null) return null; return (Interpolate(tpl.Title, ctx), Interpolate(tpl.Content, ctx)); } private static string Interpolate(string template, Dictionary ctx) { if (string.IsNullOrEmpty(template)) return template; return VarRegex.Replace(template, m => { var key = m.Groups[1].Value; return ctx.TryGetValue(key, out var v) ? (v ?? "") : ""; }); } private async Task> GetAllCachedAsync() { var cached = _cache.Get>(CacheKey); if (cached != null) return cached; var list = await _rep.AsQueryable().ToListAsync(); _cache.Set(CacheKey, list, TimeSpan.FromMinutes(10)); return list; } [NonAction] public void InvalidateCache() => _cache.Remove(CacheKey); // ═══════════════════════════════════════════ // 种子 // ═══════════════════════════════════════════ [NonAction] public async Task EnsureSystemTemplates() { var exists = await _rep.AsQueryable().AnyAsync(t => t.IsSystem); if (exists) return; var defaults = new (string type, string title, string content, string remark)[] { ("NewTask", "【待审批】{title}", "您有一条新的审批任务({nodeName}),请及时处理。", "新任务创建"), ("Urge", "【催办】{title}", "流程发起人催促您尽快审批,请及时处理。", "催办"), ("FlowCompleted", "【审批{statusText}】{title}", "您发起的审批流程已{statusText}。", "流程完成"), ("Transferred", "【转办】{title}", "{fromName} 将一条审批任务转交给您,请及时处理。", "转办"), ("Returned", "【退回】{title}", "{fromName} 已退回该审批,请重新审核。", "退回"), ("AddSign", "【加签】{title}", "{fromName} 邀请您参与审批,请及时处理。", "加签"), ("Withdrawn", "【已撤回】{title}", "{initiatorName} 已撤回该审批流程。", "撤回"), ("Escalated", "【升级】{title}", "{fromName} 将审批任务({nodeName})升级给您,请及时处理。", "升级"), ("Timeout", "【超时提醒】{title}", "您有一条审批任务已超时,请尽快处理。", "超时"), }; foreach (var (type, title, content, remark) in defaults) { await _rep.InsertAsync(new ApprovalFlowNotifyTemplate { NotifyType = type, BizType = "", Title = title, Content = content, IsEnabled = true, IsSystem = true, Remark = remark, }); } InvalidateCache(); } } public class NotifyTemplateQueryInput { public string? NotifyType { get; set; } public string? BizType { get; set; } } public class NotifyTemplateSaveInput { [Required, MaxLength(32)] public string NotifyType { get; set; } = ""; [MaxLength(32)] public string? BizType { get; set; } [Required, MaxLength(256)] public string Title { get; set; } = ""; [Required, MaxLength(1024)] public string Content { get; set; } = ""; public bool? IsEnabled { get; set; } [MaxLength(256)] public string? Remark { get; set; } } public class NotifyTemplateVariableOutput { public string Key { get; set; } = ""; public string Description { get; set; } = ""; public string AppliedTo { get; set; } = ""; }