当前位置: 首页 > news >正文

企业网站建设市场的另一面写字就能赚钱做网站

企业网站建设市场的另一面,写字就能赚钱做网站,wordpress apache php,免费域名申请2021🚀 ABP VNext Razor 邮件模板:动态、多租户隔离、可版本化的邮件与通知系统 📚 目录🚀 ABP VNext Razor 邮件模板:动态、多租户隔离、可版本化的邮件与通知系统🌟 一、TL;DR📈 二、系统流程图…

🚀 ABP VNext + Razor 邮件模板:动态、多租户隔离、可版本化的邮件与通知系统


📚 目录

  • 🚀 ABP VNext + Razor 邮件模板:动态、多租户隔离、可版本化的邮件与通知系统
    • 🌟 一、TL;DR
    • 📈 二、系统流程图
    • 🛠 三、环境与依赖
    • 🏗 四、项目骨架与模块注册
      • 4.1 目录结构
      • 4.2 模块依赖与注册
    • 🏷️ 五、模板定义提供者
    • 🏢 六、多租户隔离与实体设计
    • ⚙️ 七、应用服务:并发安全与原子回滚
    • 🖥️ 八、渲染服务:双层缓存 & 多级回退
    • 📨 九、邮件发送与附件支持(Outbox & 重试)
    • 🔒 十、在线管理界面与权限控制
    • ✅ 十一、自动测试与异常场景覆盖
    • 🔍 十二、日志、监控与运维


🌟 一、TL;DR

  1. 🎯 零依赖第三方:基于 Volo.Abp.TextTemplating.RazorVolo.Abp.MailKit 和内置 IEmailSender/Outbox。
  2. 🏢 多租户隔离:实体实现 IMultiTenant,自动启用租户过滤。
  3. 🔐 并发 & 原子操作:采用 EF Core [Timestamp] 乐观锁与单条 SQL 原子回滚。
  4. 双层缓存:本地 IMemoryCache + 分布式 IDistributedCache,滑动 & 绝对过期。
  5. 🔄 回退安全:利用 ITemplateDefinitionManager 加明确定义,捕获异常并友好报错。
  6. 🔥 预编译 & 预热:在发布时手动调用一次 RenderAsync,避免首次高并发编译。
  7. 完善测试:覆盖多租户隔离、并发冲突、缓存失效、多级回退与异常场景。

📈 二、系统流程图

若无 DB 模板
💾 模板存储与版本管理
🔥 预编译/预热
🏷 缓存 (本地/分布式)
🖥️ 模板渲染
📨 统一发送接口
🔄 Outbox & 重试
📬 邮件投递
🛠️ 在线管理 UI
📦 内置资源回退

🛠 三、环境与依赖

  • .NET SDK:.NET 8 +

  • ABP 版本:ABP VNext 8.x +

  • NuGet 包

    • Volo.Abp.TextTemplating.Razor
    • Volo.Abp.Emailing
    • Volo.Abp.MailKit
    • Volo.Abp.BackgroundJobs.Quartz(Outbox 调度)
  • 数据库:EF Core(SQL Server、PostgreSQL 等)

  • 前端:Blazor Server / Razor Pages + Monaco/CodeMirror


🏗 四、项目骨架与模块注册

4.1 目录结构

src/
└─ Modules/└─ NotificationModule/├─ Application/│   ├─ Dtos/EmailTemplateDto.cs│   ├─ IEmailTemplateAppService.cs│   └─ EmailTemplateAppService.cs├─ Domain/EmailTemplate.cs├─ EntityFrameworkCore/NotificationDbContext.cs├─ Web/Pages/EmailTemplates/{Index,Edit}.cshtml├─ EmailTemplateDefinitionProvider.cs└─ NotificationModule.cs

4.2 模块依赖与注册

using Microsoft.CodeAnalysis;
using Volo.Abp.BackgroundJobs.Quartz;
using Volo.Abp.Emailing;
using Volo.Abp.MailKit;
using Volo.Abp.TextTemplating.Razor;
using Volo.Abp.VirtualFileSystem;[DependsOn(typeof(AbpTextTemplatingRazorModule),typeof(AbpEmailingModule),typeof(AbpMailKitModule),typeof(AbpBackgroundJobsQuartzModule)
)]
public class NotificationModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){// 💾 虚拟文件系统:嵌入默认布局与自定义模板Configure<AbpVirtualFileSystemOptions>(opts =>opts.FileSets.AddEmbedded<NotificationModule>());// ⚙️ Razor 编译引用Configure<AbpRazorTemplateCSharpCompilerOptions>(opts =>opts.References.Add(MetadataReference.CreateFromFile(typeof(NotificationModule).Assembly.Location)));// 📧 MailKit SMTP 配置context.Services.Configure<MailKitSmtpOptions>(context.Services.GetConfiguration().GetSection("MailKitSmtp"));// 🔄 启用 Quartz 驱动的 Outbox 重试Configure<AbpBackgroundJobQuartzOptions>(opts =>opts.IsJobExecutionEnabled = true);}
}

🏷️ 五、模板定义提供者

EmailTemplateDefinitionProvider.cs 中,显式注册内置资源模板的 Subject 和 Body 路径:

using Volo.Abp.TextTemplating;
using Volo.Abp.TextTemplating.Razor;
using Volo.Abp.Emailing.Templates;public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider
{public override void Define(ITemplateDefinitionContext context){// 欢迎邮件context.Add(new TemplateDefinition(name: "Email.Welcome.Subject",virtualFilePath: "/Volo/Abp/Emailing/Templates/Welcome.Subject.cshtml"));context.Add(new TemplateDefinition(name: "Email.Welcome.Body",virtualFilePath: "/Volo/Abp/Emailing/Templates/Welcome.cshtml"));// 可继续为其他邮件模板定义 Subject/Body...}
}

🏢 六、多租户隔离与实体设计

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;public class EmailTemplate : FullAuditedAggregateRoot<Guid>, IMultiTenant
{public Guid? TenantId { get; set; }                 // 🏷️ 多租户隔离[Timestamp]public byte[] RowVersion { get; set; }              // 🔐 乐观并发public string Name { get; set; }public string Language { get; set; }public int Version { get; set; }public string Subject { get; set; }public string Body { get; set; }public bool IsActive { get; set; } = true;
}

⚙️ 七、应用服务:并发安全与原子回滚

using System.Data;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow;public class EmailTemplateAppService : ApplicationService, IEmailTemplateAppService
{private readonly IRepository<EmailTemplate, Guid> _repo;private readonly IMemoryCache _memCache;private readonly IDistributedCache<EmailTemplateCacheItem> _distCache;private readonly ITemplateRenderer _templateRenderer;private readonly IDbContextProvider<NotificationDbContext> _dbContextProvider;public EmailTemplateAppService(IRepository<EmailTemplate, Guid> repo,IMemoryCache memCache,IDistributedCache<EmailTemplateCacheItem> distCache,ITemplateRenderer templateRenderer,IDbContextProvider<NotificationDbContext> dbContextProvider){_repo = repo;_memCache = memCache;_distCache = distCache;_templateRenderer = templateRenderer;_dbContextProvider = dbContextProvider;}[UnitOfWork][Authorize(NotificationPermissions.EmailTemplate.Manage)]public async Task<EmailTemplateDto> CreateOrUpdateAsync(CreateOrUpdateDto input){var existing = await _repo.FindAsync(t => t.Name == input.Name &&t.Language == input.Language &&t.IsActive);if (existing != null){// 乐观并发检查if (!existing.RowVersion.SequenceEqual(input.RowVersion))throw new AbpConcurrencyException("模板已被其他人修改,请刷新后重试。");existing.Subject = input.Subject;existing.Body    = input.Body;existing.Version++;await _repo.UpdateAsync(existing);}else{existing = new EmailTemplate(GuidGenerator.Create(),input.Name,input.Language,1,input.Subject,input.Body){ TenantId = CurrentTenant.Id };await _repo.InsertAsync(existing);}// 🔥 预编译/预热:调用一次 RenderAsyncawait _templateRenderer.RenderAsync(existing.Subject, new { });await _templateRenderer.RenderAsync(existing.Body,    new { });// 🏷️ 清理缓存var key = CacheKey(input.Name, input.Language);_memCache.Remove(key);await _distCache.RemoveAsync(key);return ObjectMapper.Map<EmailTemplate, EmailTemplateDto>(existing);}[UnitOfWork][Authorize(NotificationPermissions.EmailTemplate.Manage)]public async Task RollbackAsync(RollbackDto input){var dbContext = await _dbContextProvider.GetDbContextAsync();// 原子批量回滚await dbContext.Database.ExecuteSqlRawAsync(@"UPDATE EmailTemplatesSET IsActive = CASE WHEN Version = {0} THEN 1 ELSE 0 ENDWHERE Name = {1} AND Language = {2} AND TenantId = {3}",input.Version, input.Name, input.Language, CurrentTenant.Id);// 🏷️ 清理缓存var key = CacheKey(input.Name, input.Language);_memCache.Remove(key);await _distCache.RemoveAsync(key);}private string CacheKey(string name, string lang) =>$"Tpl:{CurrentTenant.Id}:{name}:{lang}:active";
}

🖥️ 八、渲染服务:双层缓存 & 多级回退

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Volo.Abp.TextTemplating;
using Volo.Abp.Domain.Repositories;public class EmailTemplateRenderer : IEmailTemplateRenderer, ITransientDependency
{private const string DefaultLang = "en";private readonly IRepository<EmailTemplate, Guid> _repo;private readonly IMemoryCache _memCache;private readonly IDistributedCache<EmailTemplateCacheItem> _distCache;private readonly ITemplateRenderer _templateRenderer;private readonly ITemplateDefinitionManager _defManager;public EmailTemplateRenderer(IRepository<EmailTemplate, Guid> repo,IMemoryCache memCache,IDistributedCache<EmailTemplateCacheItem> distCache,ITemplateRenderer templateRenderer,ITemplateDefinitionManager defManager){_repo = repo;_memCache = memCache;_distCache = distCache;_templateRenderer = templateRenderer;_defManager = defManager;}public Task<string> RenderSubjectAsync(string name, string lang, object model)=> RenderAsync(name, lang, model, true);public Task<string> RenderBodyAsync(string name, string lang, object model)=> RenderAsync(name, lang, model, false);private async Task<string> RenderAsync(string name, string lang, object model, bool isSubject){var suffix = isSubject ? "Subject" : "Body";var key    = $"Tpl:{CurrentTenant.Id}:{name}:{lang}:{suffix}";// 1⃣ 本地缓存if (_memCache.TryGetValue(key, out EmailTemplateCacheItem cacheItem))return isSubject ? cacheItem.Subject : cacheItem.Body;// 2⃣ 分布式缓存cacheItem = await _distCache.GetAsync(key, async () =>{// 3⃣ DB 指定语言 & 默认语言查找var tpl = await _repo.FindAsync(t =>t.TenantId == CurrentTenant.Id &&t.Name     == name &&t.Language == lang &&t.IsActive) ?? await _repo.FindAsync(t =>t.TenantId == CurrentTenant.Id &&t.Name     == name &&t.Language == DefaultLang &&t.IsActive);if (tpl != null)return new EmailTemplateCacheItem(tpl.Subject, tpl.Body);// 4⃣ 内置资源回退var defName = $"Email.{name}.{suffix}";var def     = _defManager.GetOrNull(defName);if (def == null)throw new EntityNotFoundException(typeof(EmailTemplate), name);var text = await _templateRenderer.RenderAsync(def.VirtualFilePath, model);return isSubject? new EmailTemplateCacheItem(text, string.Empty): new EmailTemplateCacheItem(string.Empty, text);});// 5⃣ 本地缓存设置_memCache.Set(key, cacheItem, new MemoryCacheEntryOptions{SlidingExpiration = TimeSpan.FromMinutes(30),AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)});return isSubject ? cacheItem.Subject : cacheItem.Body;}
}[Serializable]
public class EmailTemplateCacheItem
{public string Subject { get; }public string Body    { get; }public EmailTemplateCacheItem(string subject, string body) => (Subject, Body) = (subject, body);
}

📨 九、邮件发送与附件支持(Outbox & 重试)

public class NotificationManager : DomainService
{private readonly IEmailTemplateRenderer _renderer;private readonly IEmailSender _emailSender;private readonly ILogger<NotificationManager> _logger;public NotificationManager(IEmailTemplateRenderer renderer,IEmailSender emailSender,ILogger<NotificationManager> logger){_renderer    = renderer;_emailSender = emailSender;_logger      = logger;}public async Task SendWelcomeAsync(string to, object model){try{var subj = await _renderer.RenderSubjectAsync("Welcome", "zh-CN", model);var body = await _renderer.RenderBodyAsync("Welcome", "zh-CN", model);await _emailSender.SendAsync(new[] { to },subj,body,isBodyHtml: true,plainText: $"Hello, {(model as dynamic).UserName}!");}catch (Exception ex){_logger.LogError(ex, "发送 Welcome 邮件失败,收件人:{To}", to);throw;}}public async Task SendReportWithAttachmentAsync(string to, object model, byte[] attachment, string fileName){var subj = await _renderer.RenderSubjectAsync("MonthlyReport", "en", model);var body = await _renderer.RenderBodyAsync("MonthlyReport", "en", model);await _emailSender.SendWithAttachmentAsync(new[] { to },subj,body,true,attachments: new[] { new Attachment(fileName, attachment) });}
}

🔒 十、在线管理界面与权限控制

  • 多租户筛选:仅展示当前租户模板

  • 列表/版本NameLanguageVersionIsActive

  • 编辑:Monaco Editor,继承 RazorTemplatePageBase<TModel>,支持语法校验

  • 预览:输入 JSON 调用 Preview API 实时渲染

  • 回滚:一键触发原子回滚

  • 权限:所有管理接口与页面标注

    [Authorize(NotificationPermissions.EmailTemplate.Manage)]
    

✅ 十一、自动测试与异常场景覆盖

  • 多租户隔离:不同租户同名模板互不干扰
  • 并发冲突:重复提交抛 AbpConcurrencyException
  • 缓存失效:更新/回滚后渲染内容正确
  • 多级回退:DB 无模板使用内置资源,否则友好抛错

🔍 十二、日志、监控与运维

  • 日志:记录发送失败上下文(收件人、模板、租户)
  • 审计:ABP 审计日志记录增删改、回滚操作
  • 性能指标:Prometheus 埋点——渲染耗时、发送耗时、失败率
  • 报警:Quartz Dashboard / Grafana 对重复失败 Outbox 任务告警

http://www.dtcms.com/a/552764.html

相关文章:

  • 【已解决】解决CondaVerificationError:PyTorch安装包损坏问题
  • UI引擎里AceAbility::OnStart函数1
  • 卸载工具uninstall tool下载安装教程(附安装包)绿色版
  • Bug: 升级内核后有线网络无法使用
  • 帕金森症手绘图像分类数据集
  • 本地生活曝光缺失?GEO语义锚点来救场
  • Rust开发之Result枚举与?运算符简化错误传播
  • Rust专项——其他集合类型详解:BTreeMap、VecDeque、BinaryHeap
  • 软件开发模式架构选择
  • 网站开发设计注册注册小程序
  • Git命令(三)
  • Spring Security 新手学习教程
  • 72.是否可以把所有Bean都通过Spring容器来管
  • DevExpress WPF中文教程:Data Grid - 如何使用虚拟源?(四)
  • 车载软件需求开发与管理 --- 需求收集与整理
  • [linux仓库]线程控制[线程·叁]
  • 从工行“余额归零”事件看CAP定理:当金融系统在一致性与可用性之间做出选择
  • Java的stream使用方案
  • 给网站做视频怎么赚钱电影网站系统源码
  • React Server Components 进阶:数据预取与缓存
  • MR30分布式I/O助力物流分拣系统智能化升级
  • 当UAF漏洞敲响提权警钟:技术剖析与应对之道
  • Flink(用Scala版本写Word Count 出现假报错情况解决方案)假报错,一直显示红色报错
  • Smartbi 10 月版本亮点:AIChat对话能力提升,国产化部署更安全
  • 网站备案单位商业网站源码免费下载
  • 外贸网站经典营销案例搭建服务器做网站
  • MQTT 协议详解与工业物联网架构设计指南
  • JMeter WebSocket异步接口测试简明指南
  • [论文]Colmap-PCD: An Open-source Tool for Fine Image-to-point cloud Registration
  • 网站开发合作协议自主建站系统