ABP VNext + Twilio:全渠道通知服務(SMS/Email/WhatsApp)

ABP VNext + Twilio:全渠道通知服務(SMS/Email/WhatsApp) 🚀


📚 目錄

  • ABP VNext + Twilio:全渠道通知服務(SMS/Email/WhatsApp) 🚀
    • 一、引言 ?
    • 二、環境與依賴 🛠?
    • 三、系統架構概覽 🏗?
    • 四、Secrets 管理 🔑
    • 五、本地開發 Secrets 模擬 🏠
    • 六、客戶端封裝與插件化 🔌
    • 七、模板管理 📄
    • 八、事務與 Outbox 模式 🔄
    • 九、Quartz 調度與清理作業 ?
      • 9.1 投遞調度
      • 9.2 清理作業
    • 十、監控與健康檢查 📊???🩹
    • 十一、Twilio Webhook 與安全 🔒
    • 十二、死信補償 🎯
    • 參考文檔 📄


一、引言 ?

  • 📝 TL;DR

    1. 📡 使用 Twilio SDK + SendGrid 在 ABP VNext 中統一封裝 SMS、Email、WhatsApp 通道
    2. ?? 單例 RazorLight 引擎模板渲染,一行代碼推送多渠道
    3. 🔄 Outbox + Quartz + ABP Unit of Work 保證事務一致與高可用異步投遞
    4. 🔑 Azure Key Vault 管理密鑰;🐇 RabbitMQ 死信;📈 Prometheus 指標;???🩹 Health Checks;🗑 Quartz 清理

現代應用需在用戶注冊、訂單狀態、營銷推送等場景并發多渠道通知。原生調用缺少模板化、事務保障、限流重試、安全存儲和監控。


二、環境與依賴 🛠?

  • 平臺:.NET 6 + ABP VNext 6.x

  • NuGet 包

    <PackageReference Include="Twilio" Version="5.*" />
    <PackageReference Include="SendGrid" Version="10.*" />
    <PackageReference Include="RazorLight" Version="2.*" />
    <PackageReference Include="Volo.Abp.BackgroundJobs" Version="6.*" />
    <PackageReference Include="Quartz.Extensions.Hosting" Version="3.*" />
    <PackageReference Include="Polly" Version="7.*" />
    <PackageReference Include="DistributedLock.Core" Version="6.*" />
    <PackageReference Include="Microsoft.Extensions.HealthChecks" Version="7.*" />
    <PackageReference Include="prometheus-net.AspNetCore" Version="7.*" />
    <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.*" />
    <PackageReference Include="Microsoft.Azure.KeyVault" Version="4.*" />
    <PackageReference Include="RabbitMQ.Client" Version="7.*" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.*" />
    
  • appsettings.json 示例

    {"KeyVault": {"Enabled": true,"VaultUri": "https://your-vault.vault.azure.net/"},"Twilio": { /*…*/ },"SendGrid": { /*…*/ },"Notification": {"MaxParallelism": 5,"QuartzCron": "0/30 * * * * ?","DeadLetterQueue": "notification.dlx"}
    }
    

三、系統架構概覽 🏗?

平臺與運維
調度層
核心模塊
應用層
調用
Quartz Timer
限流/重試
失敗后
指標
SMS/Email/WhatsApp
Webhook 回調
CallbackController
Metrics Endpoint
/health
Health Checks
OutboxJobScheduler
OutboxJobProcessor
Twilio/SendGridClient
RabbitMQ DeadLetter
Prometheus
NotificationService
NotificationOutbox
INotificationService
ApplicationService

四、Secrets 管理 🔑

public override void ConfigureServices(ServiceConfigurationContext context)
{var cfg = context.Services.GetConfiguration();if (cfg.GetValue<bool>("KeyVault:Enabled")){context.ConfigurationBuilder.AddAzureKeyVault(new Uri(cfg["KeyVault:VaultUri"]),new DefaultAzureCredential());}context.Services.Configure<TwilioOptions>(cfg.GetSection("Twilio"));context.Services.Configure<SendGridOptions>(cfg.GetSection("SendGrid"));// …
}

Tip:生產環境通過 Key Vault 托管密鑰,安全可靠。


五、本地開發 Secrets 模擬 🏠

# 使用 .NET 用戶機密存儲
dotnet user-secrets init
dotnet user-secrets set "Twilio:AccountSid" "local-sid"
dotnet user-secrets set "Twilio:AuthToken"  "local-token"
dotnet user-secrets set "SendGrid:ApiKey"    "local-sendgrid-key"

本地調試時,appsettings.json 不包含敏感信息,使用用戶機密模擬 Key Vault。


六、客戶端封裝與插件化 🔌

  1. 通道抽象

    public interface IChannelSender
    {Channel Channel { get; }Task SendAsync(string payloadJson, CancellationToken token = default);
    }
    
  2. SmsSender

    public class SmsSender : IChannelSender, ITransientDependency
    {public Channel Channel => Channel.SMS;private readonly TwilioOptions _opts;public SmsSender(IOptions<TwilioOptions> opts) => _opts = opts.Value;public Task SendAsync(string payloadJson, CancellationToken token = default){var p = JsonSerializer.Deserialize<SmsPayload>(payloadJson)!;var client = new TwilioRestClient(_opts.AccountSid, _opts.AuthToken);return client.Messages.CreateAsync(to: new PhoneNumber(p.To),from: new PhoneNumber(_opts.FromPhone),body: p.Body, cancellationToken: token);}
    }
    
  3. EmailSender、WhatsAppSender 類似,均注入 CancellationToken 支持。

  4. 核心服務

    public class NotificationService : INotificationService, ITransientDependency
    {public async Task SendAsync(Channel channel, string templateKey, object model, string to, string? subjectKey = null){var body = await _tplMgr.RenderAsync($"{channel}/{templateKey}.cshtml", model);var subject = subjectKey == null ? null : await _tplMgr.RenderAsync($"Email/Subject_{subjectKey}.txt", model);var payload = JsonSerializer.Serialize(new { To = to, Subject = subject, Body = body });_db.NotificationOutboxes.Add(new NotificationOutbox(channel, payload));await _uow.SaveChangesAsync(); }
    }
    

七、模板管理 📄

public class RazorLightTemplateManager : ITemplateManager, ISingletonDependency
{private readonly RazorLightEngine _engine;public RazorLightTemplateManager(IConfiguration config){_engine = new RazorLightEngineBuilder().UseFileSystemProject(Path.Combine(AppContext.BaseDirectory, "Templates")).UseMemoryCachingProvider().Build();}public Task<string> RenderAsync(string key, object model) =>_engine.CompileRenderAsync(key, model);
}
/Templates├─ SMS/Sms_VerifyCode.cshtml├─ Email/Subject_OrderShipped.txt├─ Email/OrderShipped.cshtml└─ WhatsApp/Promotion.cshtml

八、事務與 Outbox 模式 🔄

public class NotificationOutbox : Entity<Guid>
{public Channel Channel { get; set; }public string Payload  { get; set; }public bool IsSent     { get; set; }public DateTime CreatedTime { get; set; }public int RetryCount  { get; set; }  // 死信限次public NotificationOutbox(Channel ch, string payload){Id = Guid.NewGuid();Channel = ch; Payload = payload;IsSent = false; RetryCount = 0;CreatedTime = DateTime.UtcNow;}
}

Tip:新增 RetryCount 字段,避免死信循環。


九、Quartz 調度與清理作業 ?

9.1 投遞調度

public class OutboxJobProcessor : IJob, ISingletonDependency
{private readonly CancellationTokenSource _cts = new();public async Task Execute(IJobExecutionContext _){var batch = await _db.NotificationOutboxes.Where(x=>!x.IsSent && x.RetryCount < 5).Take(NotificationConsts.MaxParallelism).ToListAsync(_cts.Token);foreach (var e in batch){try{using (_latency.NewTimer())using (await _lock.AcquireLockAsync($"lock-{e.Id}", TimeSpan.FromSeconds(30))){await Policy.WrapAsync(Policy.BulkheadAsync(NotificationConsts.MaxParallelism, int.MaxValue),Policy.RateLimitAsync(10, TimeSpan.FromSeconds(1)),Policy.Handle<Exception>().WaitAndRetryAsync(retryCount:3, sleepDurationProvider: i=>TimeSpan.FromSeconds(Math.Pow(2, i)),onRetry: (ex, _, i, _) => {e.RetryCount++;_logger.LogWarning("Outbox {Id} 第 {Count} 次重試", e.Id, i);})).ExecuteAsync(ct=> ProcessAsync(e, ct), _cts.Token);}_total.Inc();}catch (Exception ex){_logger.LogError(ex, "Outbox({Id}) 失敗,轉死信隊列", e.Id);_rabbit.BasicPublish("", _opts.DeadLetterQueue, null, Encoding.UTF8.GetBytes(e.Payload));}}}public Task StopAsync() => Task.Run(() => _cts.Cancel());private async Task ProcessAsync(NotificationOutbox e, CancellationToken token){var sender = _senders.First(s=>s.Channel==e.Channel);await sender.SendAsync(e.Payload, token);e.IsSent = true;await _db.SaveChangesAsync(token);}
}

9.2 清理作業

public class OutboxCleanupJob : IJob, ISingletonDependency
{public async Task Execute(IJobExecutionContext _){var cutoff = DateTime.UtcNow.AddDays(-30);var toDel = await _db.NotificationOutboxes.Where(x=>x.IsSent && x.CreatedTime<cutoff).Take(1000).ToListAsync();_db.RemoveRange(toDel);await _db.SaveChangesAsync();}
}

十、監控與健康檢查 📊???🩹

// Startup.ConfigureServices
services.AddHealthChecks().AddSqlServer(cfg.GetConnectionString("Default"), name:"sql").AddRabbitMQ(cfg["Notification:DeadLetterQueue"], name:"rabbitmq").AddUrlGroup("https://api.twilio.com", name:"twilio").AddUrlGroup("https://api.sendgrid.com", name:"sendgrid");app.UseHttpMetrics(); // Prometheus /metrics
app.UseEndpoints(e=>{e.MapHealthChecks("/health");e.MapMetrics();
});

十一、Twilio Webhook 與安全 🔒

[HttpPost("twilio-callback"), EnableRateLimiting("Default")]
public async Task<IActionResult> TwilioCallback(CancellationToken token)
{var req = Request; var form = await req.ReadFormAsync(token);var sig = req.Headers["X-Twilio-Signature"].FirstOrDefault()!;var url = $"{req.Scheme}://{req.Host}{req.Path}";var validator = new RequestValidator(_opts.AuthToken);if (!validator.Validate(url, form.ToDictionary(k=>k.Key, v=>v.Value.ToString()), sig))return Unauthorized();var sid = form["MessageSid"].ToString();var status = form["MessageStatus"].ToString();var e = await _db.NotificationOutboxes.Where(x=>x.Payload.Contains(sid)).FirstOrDefaultAsync(token);if (e!=null){e.IsSent = status=="delivered";await _db.SaveChangesAsync(token);}return Ok();
}

十二、死信補償 🎯

public class DeadLetterConsumer : BackgroundService
{protected override Task ExecuteAsync(CancellationToken ct){var channel = _rabbit.CreateModel();channel.BasicConsume("notification.dlx", false, new EventingBasicConsumer(channel){Received = async (_, ea) =>{if (ct.IsCancellationRequested) return;var payload = Encoding.UTF8.GetString(ea.Body.ToArray());await _db.NotificationOutboxes.AddAsync(new NotificationOutbox(Channel.SMS, payload), ct);await _db.SaveChangesAsync(ct);channel.BasicAck(ea.DeliveryTag, false);}});return Task.CompletedTask;}
}

參考文檔 📄

  • Twilio 官方文檔
  • SendGrid C# 快速入門
  • Quartz Scheduler 文檔
  • prometheus-net GitHub
  • Azure Key Vault 文檔
  • RabbitMQ 文檔

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87398.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87398.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87398.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

電腦一體機,收銀機畫面顯示不全——深入解析 BIOS 配置電腦分辨率——東方仙盟

在電腦的復雜體系中&#xff0c;BIOS&#xff08;基本輸入輸出系統&#xff09;雖深藏幕后&#xff0c;但其對電腦分辨率的配置卻有著至關重要的影響。理解 BIOS 配置電腦分辨率的作用與意義&#xff0c;有助于我們更深入地挖掘電腦的性能潛力&#xff0c;優化視覺體驗。 一、…

arm系統移植

目錄 1. 流程2. 概念2.1 設備樹2.2 根文件系統2.3 文件說明 3. 交叉編譯鏈3.1 作用3.2 在linux下配置 4. tftp4.1 作用4.2 安裝過程 5. nfs5.1 作用5.2 安裝過程 6. 配置開發板7. linux下的uboot鏡像燒寫到SD卡中7.1 生成uboot二進制文件&#xff0c;二進制文件就是裸機程序。7…

量子算法入門——5.Qiskit庫介紹與簡單應用(2)

參考資料&#xff1a; 【【零基礎入門量子計算】】 來自b站up&#xff1a;溴銻銻躍遷 建議關注他的更多高質量文章&#xff1a;CSDN&#xff1a;【溴銻銻躍遷】 &#xff08;實際上只帶一點點原創&#xff0c;絕大部分資料來自這位大佬&#xff09; 跟著視頻我手打了一遍notebo…

前端如何優雅地實現一個“請求隊列”,避免服務器被卡死?

有這樣一些場景&#xff1a; 頁面一加載&#xff0c;需要同時發 10 個請求&#xff0c;結果頁面卡住&#xff0c;服務器也快崩了。用戶可以批量操作&#xff0c;一次點擊觸發了幾十個上傳文件的請求&#xff0c;瀏覽器直接轉圈圈。 當后端處理不過來時&#xff0c;前端一股腦…

SSL/TLS協議信息泄露漏洞(CVE-2016-2183)、SSL/TLS RC4 信息泄露漏洞(CVE-2013-2566)修復方法

目錄 一、問題原因二、整改步驟 一、問題原因 通過綠盟漏洞檢測工具掃描發現Windows系統存在SSL/TLS協議信息泄露漏洞(CVE-2016-2183)、SSL/TLS RC4 信息泄露漏洞(CVE-2013-2566)、SSL/TLS 受誡禮(BAR-MITZVAH)攻擊漏洞(CVE-2015-2808)。 二、整改步驟 使用gpedit.msc進入組…

MinHook 如何對 .NET 母體 CoreCLR 進行攔截

一&#xff1a;背景 1. 講故事 這篇文章起源于和一家 .NET公司 開線上會議時&#xff0c;提出的一個場景問題&#xff0c;程序出現了非托管內存暴漲&#xff0c;這些非托管內存關聯的對象都囤積在 終結器隊列 中&#xff0c;很顯然這是代碼中沒用 using 及時釋放引發的&#…

DPI深度檢索原理和架構

大家讀完覺得有幫助記得關注和點贊&#xff01;&#xff01;&#xff01; DPI&#xff08;深度包檢測&#xff09;技術通過**透視網絡載荷內容**實現精細化流量管控與威脅檢測&#xff0c;其核心在于突破傳統防火墻僅檢查IP/端口等表層信息的局限&#xff0c;對**應用層數據**進…

QT Creator的返回到上一步、下一步的快捷鍵是什么?

在 Qt Creator 中&#xff0c;用于導航的 返回上一步 (Back) 和 前進下一步 (Forward) 的快捷鍵如下&#xff1a; 默認快捷鍵&#xff1a; 功能Windows/LinuxmacOS返回上一步Alt ←Command [前進下一步Alt →Command ]

UI前端大數據處理策略優化:基于云計算的數據存儲與計算

hello寶子們...我們是艾斯視覺擅長ui設計、前端開發、數字孿生、大數據、三維建模、三維動畫10年經驗!希望我的分享能幫助到您!如需幫助可以評論關注私信我們一起探討!致敬感謝感恩! 一、引言&#xff1a;大數據時代前端處理的挑戰與云計算破局 在數字化轉型的浪潮中&#xff…

機器學習基礎 多層感知機

機器學習基礎 多層感知機 文章目錄 機器學習基礎 多層感知機1. 多層感知機1.1 線性模型的失效1.2 感知機1.3 感知機的收斂定理1.4 從線性到非線性1.5 多層感知機的定義和實現 參考 1. 多層感知機 1.1 線性模型的失效 ? 在李沐《動手學深度學習》中有這樣的描述&#xff1a; …

關于安裝Ollama大語言模型本地部署工具

一、Ollama 安裝方法概述 Ollama 是一個開源的大型語言模型(LLM)本地部署工具&#xff0c;支持在 Windows、macOS 和 Linux 系統上運行。它簡化了在本地計算機上運行和管理大語言模型的流程&#xff0c;讓開發者能夠輕松部署各種開源模型。 Windows 系統安裝步驟 訪問 Ollam…

html配置rem實現頁面自適應

1.在js文件使用&#xff0c;建議放到全局js中 // 全局js文件 $(function () {// 設置根目錄字體大小var baseSize 16; // 設計稿的基準字體大小&#xff0c;通常是16pxvar baseWidth 750; // 設計稿的基準寬度&#xff0c;通常是750pxfunction adjustFontSize() {const widt…

POI實現文檔的圖片的提取和替換

1. 簡介 在日常辦公自動化開發中&#xff0c;常常需要對 Word 文檔中的圖片進行批量提取、保存&#xff0c;甚至將圖片替換為自定義的文本或鏈接。Apache POI 是一款強大的 Java 開源庫&#xff0c;支持對 Microsoft Office 文檔&#xff08;包括 Word、Excel、PowerPoint 等&…

毫米波雷達 – 深度學習

目錄 數據表示 公開數據庫 未來發展方向 稀疏點云 + 深度學習 直接處理點云 (1/2) 候選生成+特征提取+候選分類(DL* ) 候選生成+特征提取+候選分類(DL) 直接處理點云 (2/2) 候選生成+特征提取(DL)+候選分類(DL) 網格數據+端對端檢測(DL) 稠密數據塊 + 深度學習 直接…

Redis——常用指令匯總指南(一)

目錄 1.set & get ①set指令 ②get指令 2.keys 3.del 4.expire & setex & psetex 5.ttl 6.exists 7.setnx 8.flushall 9.object encoding 10. type 1.set & get set & get指令中key和value都是字符串&#xff0c;但是不需要加單引號或雙引號。 …

PDF處理控件Aspose.PDF教程:在 Java 中刪除 PDF 頁面

您是否需要使用 Java 從PDF文檔中刪除特定頁面&#xff1f;無論您是要清理空白頁、刪除機密部分&#xff0c;還是僅僅在分發前調整內容&#xff0c;以編程方式操作 PDF 頁面的能力都將大有裨益。本指南將向您展示如何借助Aspose.PDF僅用幾行代碼刪除不需要的頁面。讓我們深入了…

RediSearch 字段類型與配置選項

1. 數值字段&#xff08;NUMERIC&#xff09; 用途&#xff1a;存儲整數或浮點數&#xff0c;可進行范圍查詢與排序。 選項&#xff1a; SORTABLE&#xff1a;允許用 SORTBY 排序NOINDEX&#xff1a;不參與索引&#xff0c;僅供返回 定義語法 FT.CREATE idx ON HASH PREFIX…

PHP Yii2 安裝SQL Server擴展-MAC M4 Pro芯片

MAC M4 Pro芯片版本&#xff0c;千錘百煉編譯十幾次終于成功 # 設置基礎鏡像并強制使用 x86_64 架構&#xff08;適配 M4 芯片&#xff09; FROM --platformlinux/amd64 php:8.1-fpm-alpine3.18WORKDIR /var/www/html# 可選&#xff1a;設置時區 ARG TZAsia/Shanghai ENV TZ${…

HTML初學者第二天

<1>HTML的語法規范 1.1標簽 -雙標簽&#xff1a;如 <html></html> 前面的叫開始標簽&#xff0c;后面的叫結束標簽。 -單標簽&#xff1a;如 <br /> 1.2基本語法概述 -HTML標簽是由尖括號包圍的關鍵詞&#xff0c;例如<html>。 -HTML標…

【加解密與C】HASH系列(二) SHA

SHA&#xff08;安全散列算法&#xff09;簡介 SHA&#xff08;Secure Hash Algorithm&#xff09;是由美國國家安全局&#xff08;NSA&#xff09;設計的一系列密碼散列函數&#xff0c;用于將任意長度的數據轉換為固定長度的散列值。SHA家族包括SHA-1、SHA-2&#xff08;含S…