在传统的系统设计中,很多开发者在用户注册或下单成功后,直接在主事务中调用第三方邮件或短信 SDK:
// 典型反模式:主事务中直调慢网络await db.SaveChangesAsync();await emailSdk.SendAsync(...); // 若第三方网络超时 10 秒,用户请求直接挂死!这种做法存在严重隐患:
- 慢网络拖垮核心事务:外部邮件网关一旦网络延迟或故障,导致本地数据库连接被长时间占用;
- 状态不一致:若邮件发送成功但后续本地事务提交失败回滚,客户收到了“扣款成功”邮件但数据库根本没有订单!
BitzOrcas.Modern 采用“通知事实先行持久化 + CAP 事务性发件箱异步扇出”架构:业务层只写入站内通知事实,底层由消息总线异步拉起邮件、短信与企业微信/钉钉连接器并行投递。
多渠道消息投递生命周期全景
第一步:业务层只发布通知事实
业务代码无需关心底层通过邮件还是短信发送,只需创建站内信:
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 连接器无需修改业务代码。