Skip to content
bitzorcas
中EN

Guide

Notifications 测试、运营与 GA 门禁

汇总 Notifications 当前测试资产、缺失契约、可观测性、容量、数据对账、事故处置和商业发布阻断项。

Last updated

通知系统的成功不是 Create API 返回 200,而是目标用户在允许的渠道收到正确、唯一、安全、可追踪的内容;失败时可重试、可解释、可对账。当前源码已有聚合/持久化基础,但投递与模板闭环不足以支撑这一 SLO。

1. 当前测试资产

仓库已有:

  • NotificationAggregate 状态/恢复测试;
  • NotificationPreference 与 ReadMarker 单元测试;
  • 通知/模板 read-query handler 测试;
  • TemplateSecurityChecker、VariableAnalyzer、NamingConverter、StorageProjector 测试;
  • NotificationArchivePagination 测试;
  • Notification/Template repository 与 read model 双 ORM parity;
  • Notifications Infrastructure 架构测试和生产 adapter readiness evidence;
  • Workflow notification adapter/dispatcher 测试;
  • 模板迁移 tenant backfill parity。

没有直接覆盖 NotificationService.CreateAsync、NotificationCreatedConsumer、MultiChannelDeliveryAdapter、NotificationOrchestrator、TemplateAppService、TemplateRendererService、ScribanTemplateCompiler、19 条 HTTP 契约和供应商故障恢复的测试。

2. 测试分层

E2E
intent → inbox → CAP → provider fake → receipt

契约/集成
HTTP / auth / tenant / outbox / ORM / connector

单元
state / preference / version / render / sanitizer

单元测试固定纯规则,契约测试固定边界和失败语义,E2E 使用可控 provider fake 验证一次完整交付。真实供应商用 sandbox smoke,不应替代确定性契约。

3. 创建与投递红测试

Inbox 关闭时外部投递仍需可恢复
[Fact]
public async Task Create_Should_Persist_Delivery_Fact_When_Inbox_Is_Disabled()
{
// 用户不展示站内信,但保留邮件;逻辑通知仍须存在供 consumer 回读。
preferences.Set("tenant-a", "user-1", "security.login", new(
InboxEnabled: false,
ExternalDeliveryEnabled: true));
var created = await service.CreateAsync(
"tenant-a", "user-1", "security.login",
"新设备登录", "检测到新的登录设备",
severity: NotificationSeverity.Warning,
metadataJson: "{\"email\":\"user@example.com\"}");
created.IsSuccess.ShouldBeTrue();
// 当前实现未保存,下面断言会失败;它是修复可靠事实模型的 red test。
(await repository.FindByIdAsync(created.Value!.NotificationId, default))
.IsSuccess.ShouldBeTrue();
}

还要证明 Save 与 outbox 原子、同 BusinessId 不重复、Consumer 在目标租户回读、每渠道唯一 attempt、端口异常进入 retry/dead 而非被吞掉。

4. 模板红测试

渲染必须使用当前租户并保留验证失败
[Fact]
public async Task Render_Should_Resolve_Tenant_Template_And_Reject_Unsafe_Content()
{
// 租户自有模板用于暴露当前 RenderAsync 固定空 TenantId 的缺陷。
await templates.SeedAsync(
tenantId: "tenant-a",
key: "Tickets.Assigned.Email.zh-CN",
body: "<script>alert(1)</script>{{ user_name }}");
var validation = await renderer.ValidateTemplateAsync(
"title", "<script>alert(1)</script>{{ user_name }}",
TemplateEngine.Scriban,
new Dictionary<string, object> { ["user_name"] = "张三" });
validation.Value!.IsValid.ShouldBeFalse();
// 目标入口必须带 tenant;当前 RenderAsync 固定空 TenantId,查不到该模板。
var rendered = await facade.RenderAsync(
"tenant-a", "Tickets.Assigned.Email.zh-CN", ModuleCaller.Notification,
new Dictionary<string, object> { ["user_name"] = "张三" });
rendered.IsFailure.ShouldBeTrue();
}

版本测试必须包含 1.0.9/1.0.10/1.0.11、并发更新、Description、语言唯一性、导入导出 round-trip、Create/Update 安全门禁和 Preview 验证报告。

5. HTTP 与授权契约

为每条生成路由验证:401、错误权限 403、正确权限到 handler、RouteParam、请求/响应 JSON、Problem Details、timeout、rate limit。重点漂移:

  • POST /api/notifications 需要 Create action,但 catalog 没有 notification.create;
  • Activate 使用 template.update 而不是 template.activate;
  • Render/Preview/Validate 共用 template.view,template.preview 未用;
  • 当前用户专属 API不能指定 recipient;
  • Read/Unread/Archive 对他人 Id 返回同一 NotFound。

权限测试不能只检查 attribute 存在,必须用真实 catalog seed/authorization middleware 发请求。

6. 可观测性

业务指标

  • notification_intent_total / deduplicated_total;
  • inbox_projection_total / hidden_by_preference;
  • delivery_attempt_total{channel,result,provider};
  • delivery_latency、queue_age、retry_count、dead_count;
  • template_resolve{scope,locale,channel,result};
  • render_latency、compile_cache_hit、validation_reject、degraded_total;
  • unread_count 与 inbox query latency;
  • hot/archive rows、archive lag、reconciliation_delta。

日志最小字段

EventId、NotificationId、tenant_hash、code、channel、attempt、provider、error_code、traceId。禁止 Body、模板变量、MetadataJson、Email、Phone、UserId 原值。对错误消息也要清洗,供应商异常常带地址或内容。

7. SLO 与告警

示例 SLO 需按产品等级确认:

SLI示例目标
intent→inbox P99< 2s
intent→provider accepted P95< 60s
24h 最终投递成功率≥99.9%,排除永久地址错误
duplicate delivered< 1 ppm
template resolve/render success≥99.99%
queue oldest age< 5 min

告警必须基于 attempt 状态/队列,而不是仅搜 Error 日志。当前没有 attempt ledger,无法可靠计算这些指标,是 GA 阻断项。

8. 事故手册

信号首轮判断处置
CAP 有事件但找不到通知Inbox=false 或 tenant context 缺失暂停消费,核对事件 tenant/DB,修复后 replay
某渠道全量跳过port 未注册/联系信息缺失检查配置与 secret,不记录联系值
供应商 429/5xx瞬时限流/故障降并发,按 Retry-After,保留 attempt
重复短信/邮件无幂等或未知结果重试停 replay,按 provider message id 对账
模板大量 NotFound空 TenantId/fallback 错误回滚调用路径,不复制模板规避
模板预览绿、线上 XSSPreview 丢验证/输出未清洗禁用受影响模板,切安全 fallback,排查历史
unread badge 不一致归档含 unread/计数只看热表核对热冷状态,修复统计口径

没有持久化 attempt 前无法安全“重放失败通知”;不要从日志批量重新调用 provider,以免重复发送。

9. 容量与恢复

容量矩阵:每秒 intent、单租户热点、单用户 10 万未读、MarkAllRead、深页热冷合并、模板 10/100/10000、变量 1/100/1000、正文大小、SMS 分段、provider timeout、CAP backlog。

备份恢复必须同时覆盖 SysNotification、Archive、Preference、三张模板表、CAP outbox/inbox,未来还包括 DeliveryAttempt。恢复后用 Id/业务键/版本/哈希对账,验证不会把已 delivered attempt 再次发送。

SQL Server 通知归档是 provider-specific 物理能力;EF Core runtime 与非 SQL Server 的行为不能从 read-model parity 推导,需要明确支持矩阵。

10. GA 阻断项

  • Create 权限与 catalog 一致;Activate/Preview 使用专用权限或删除虚假权限。
  • 所有 HTTP/后台路径统一 EffectiveTenant,RequireUserId 语义明确。
  • NotificationIntent 有业务幂等键,Save+outbox 原子。
  • Inbox=false 不会破坏外部投递;mandatory 策略存在。
  • Consumer 建立 tenant context,不吞异常,支持 retry/dead/replay。
  • 每渠道 DeliveryAttempt、唯一键、状态、ProviderMessageId 与对账落地。
  • EnabledChannels 真实生效,联系信息不长期明文留在 Inbox metadata。
  • 模板按 tenant/PLATFORM/locale/channel 正确解析。
  • 只接受真实支持的 TemplateEngine,CallerModule 服务端授权。
  • Create/Update 必经 compile、schema、安全审查与发布审批。
  • Preview 保留验证结果,渲染失败不回显模板源码。
  • HTML 使用上下文编码和成熟 allowlist sanitizer。
  • 版本号数值化、唯一约束、并发更新与 Description 修复。
  • 双语 seed、导入导出 round-trip、100+ 模板分页修复。
  • HTTP、双 ORM、CAP、provider、容量、归档、备份恢复测试通过。

11. 验证命令

Terminal window
# 当前模块相关测试资产。
dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \
--filter 'FullyQualifiedName~Notification|FullyQualifiedName~Template'
dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \
--filter 'FullyQualifiedName~Notifications'
dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj \
--filter 'FullyQualifiedName~NotificationsInfrastructureArchitectureTests'
# 关键缺口全局扫描。
rg -n 'skipExternalDelivery|EnabledChannels|GetByKeyAsync\(\s*string.Empty' \
src/Platform/Notifications -g '*.cs'
rg -n 'DeliveryAttempt|ProviderMessageId|DeduplicationKey|ModuleAuthorization' \
src/Platform/Notifications -g '*.cs'

返回 Notifications 总览

100%

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