Skip to content
bitzorcas
中EN

Reference

多渠道消息投递构建块:从通知事实到外部扇出

掌握 BitzOrcas.Modern 企业级消息投递架构,深入通知事实落盘与异步扇出解耦、邮件/短信/企微连接器与发送状态防丢失。

Last updated

在传统的系统设计中,很多开发者在用户注册或下单成功后,直接在主事务中调用第三方邮件或短信 SDK:

// 典型反模式:主事务中直调慢网络
await db.SaveChangesAsync();
await emailSdk.SendAsync(...); // 若第三方网络超时 10 秒,用户请求直接挂死!

这种做法存在严重隐患:

  1. 慢网络拖垮核心事务:外部邮件网关一旦网络延迟或故障,导致本地数据库连接被长时间占用;
  2. 状态不一致:若邮件发送成功但后续本地事务提交失败回滚,客户收到了“扣款成功”邮件但数据库根本没有订单!

BitzOrcas.Modern 采用“通知事实先行持久化 + CAP 事务性发件箱异步扇出”架构:业务层只写入站内通知事实,底层由消息总线异步拉起邮件、短信与企业微信/钉钉连接器并行投递。

多渠道消息投递生命周期全景

1. 业务用例 (如用户注册)

2. NotificationService (站内信落库)

3. CAP 事务性发件箱 (notification.created)

4. NotificationCreatedConsumer (异步消费者)

5. INotificationOrchestrator (根据用户偏好解析渠道)

6. IEmailDeliveryPort (邮件连接器)

7. ISmsDeliveryPort (短信连接器)

8. IInstantMessageDeliveryPort (企微/钉钉/飞书)


第一步:业务层只发布通知事实

业务代码无需关心底层通过邮件还是短信发送,只需创建站内信:

发送业务通知:NotificationService.ComposeAsync
using BitzOrcas.Domain.Results;
// 平台统一出口是 NotificationService.ComposeAsync:一次调用写入收件箱事实,
// 并携带元数据进入渠道编排;不存在 INotificationAppService/CreateNotificationRequest。
public sealed class LegalContractSignedHandler(NotificationService notifications)
{
public async ValueTask<Result> Handle(LegalContractSigned command, CancellationToken ct)
{
var created = await notifications.ComposeAsync(
tenantId: command.TenantId,
userId: command.SignerUserId,
code: "contract.contract.signed",
title: "电子合同已签署",
body: $"合同 {command.ContractNo} 已完成签署,请下载存档。",
type: NotificationType.Business,
category: command.BusinessType,
severity: NotificationSeverity.Normal,
linkUrl: null,
linkText: null,
metadataJson: metadataJson,
cancellationToken: ct);
return created.IsSuccess ? Result.Success() : Result.Failure(created.Error);
}
}

第二步:异步消费者与渠道路由

后台消费者接收到事件后,由 INotificationOrchestrator 根据接收人的多渠道偏好配置进行并行扇出:

渠道路由与出站端口
// 渠道解析接收完整上下文(租户/用户/code/severity/category/metadata);
// 偏好语义是"只能做减法":用户关闭某渠道后编排层不再命中它。
var channels = await orchestrator.ResolveChannelsAsync(
new NotificationContext(tenantId, userId, code, severity, category, metadataJson),
ct);
// 2. 异步并行分发至对应投递端口(单渠道失败绝不影响其他渠道)
if (channels.Contains("Email"))
{
// EmailDeliveryRequest 必填六参(NotificationId/TenantId/RecipientUserId/Subject/TextBody/Channel),HtmlBody 可空。
await emailPort.SendAsync(new EmailDeliveryRequest(
To: @event.EmailAddress,
Subject: @event.Title,
HtmlBody: @event.Content), ct);
}
}
}

总结

多渠道消息投递构建块彻底解耦了业务与网络 I/O:

  • 核心事务零阻塞:站内信同事务毫秒级提交;
  • 通道故障物理隔离:邮件服务商宕机不会导致业务报错;
  • 渠道插件化:新增飞书或 Telegram 连接器无需修改业务代码。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%