Skip to content
bitzorcas
中EN

Guide

Webhooks 事件接线、运维、测试与 GA

审查 Webhook 事件 registry、治理目录、CAP topic/envelope、跨模块生产者差异、死信运维、可观测性、测试证据与商业交付 GA 门禁。

Last updated

Webhooks 最容易出现的假完成是:生产者类、消费者类、topic 常量和测试各自存在,但没有任何一个测试证明真实消息能从业务事务进入 CAP,再以消费者期待的 wire payload 到达 HTTP 目标。本章以跨模块契约为验收对象。

1. 五份事实源目前不一致

一个可投递事件至少经过:

  1. 业务模块实际 publisher/topic;
  2. Webhooks 治理 EventSubscriptionCatalog;
  3. CAP consumer 的 [CapSubscribe];
  4. IWebhookEventTypeRegistry;
  5. 外部订阅 EventType + required Client Scope。
should equalshould equal

实际 publisher
topic + wire type

WebhookIntegrationEventSubscriptions

CapSubscribe
PlatformWebhookEventEnvelope

event type registry
required scope

external subscription

signed HTTP payload

目前 “should equal” 的两段都存在漂移。

2. 源码审查矩阵

意图治理目录声明运行时消费 topic / payload实际生产者结论
文件定稿...Files.Contracts.FileFinalizedIntegrationEventfiles.finalized / PlatformWebhookEventEnvelopeIIntegrationEventPublisher<FileAssetSummary>;无 IntegrationTopic,topic 为 BitzOrcas.Platform.Files.Contracts.FileAssetSummary,payload 为 summarytopic 与 payload 均不匹配,治理类型名也不存在
文件删除治理目录没有声明files.deleted / envelope同样发布 FileAssetSummary 类型全名不匹配且治理漏项
发票签发...InvoiceIssuedIntegrationEventbilling.invoice-issued / envelope发布同 typed event,默认 topic 确为类型全名治理接近生产者,但 runtime consumer 不匹配
工单打开...Tickets.Contracts.TicketOpenedIntegrationEventtickets.opened / envelope领域事件 [IntegrationTopic("ticket.opened")],当前 dispatch wire 不是 envelope三处 topic/type 不一致
Workflow 开始/完成治理目录没有声明workflow.started/completed / envelope当前 Workflow 主要发 NotificationMessage,没有这两个 envelope publisher没有实际接线
API 版本弃用治理目录没有声明api.version.deprecated / envelope示例端点直接发布正确 topic + envelopeCAP 可到达,但 registry 未注册,DeliveryService 拒绝

WebhookEventTypes 注释声称值由 CAP topic、订阅 EventType 和外部 payload 共享,当前代码没有实现这一承诺。

3. CAP consumer 还会吞掉投递结果

WebhookPlatformEventConsumer.DeliverAsync await 投递服务,但不检查返回的 Result。未注册 EventType、仓储失败等 typed failure 不会抛异常,CAP 会把消费视为完成。DeliveryService 又忽略单订阅失败,所以消息可能在 CAP 与 Webhook 两层都没有可重试错误。

目标 consumer:把可重试失败交还 CAP
// topic 由入口固定,envelope 只承载该 topic 的版本化事实。
private async Task DeliverAsync(
string eventType,
PlatformWebhookEventEnvelope envelope,
CancellationToken cancellationToken)
{
var result = await deliveryService.DeliverAsync(
new WebhookEvent(
envelope.EventId,
envelope.TenantId,
eventType,
envelope.PayloadJson,
envelope.OccurredAt),
cancellationToken);
if (result.IsFailure)
{
// 只有可重试基础设施错误才应抛回 CAP;契约错误应告警并隔离。
// 生产代码应按错误类别区分 terminal configuration 与 transient infrastructure。
throw new WebhookEventConsumptionException(result.Error);
}
}

不能把所有失败都盲目 throw:EventType 未注册是配置/契约错误,无限 CAP retry 没用;数据库暂时不可用则应 retry。需要稳定错误分类和告警。

4. 选择唯一、版本化的 envelope

推荐由事件 owner 定义 typed integration event,并显式 [IntegrationTopic("platform.files.finalized.v1")]。Webhooks consumer 接收同一个 typed contract,或由专门 adapter 转成通用外部 envelope;不要让每个模块手拼 JSON string。

版本化的文件定稿事件示例
// 显式 topic 避免默认类型全名随 namespace 重构而漂移。
[IntegrationTopic("platform.files.finalized.v1")]
public sealed record FileFinalizedV1(
string EventId,
string TenantId,
string FileId,
string FileName,
string ContentType,
long Size,
string ContentHash,
// OccurredAt 是业务事实时间,不是 CAP 消费或 HTTP 发送时间。
DateTimeOffset OccurredAt) : IIntegrationEvent;

外部 payload 仍可包一层公共 envelope:

建议的外部 envelope
{
"specVersion": "1.0",
"eventId": "019c48cb7ba47000953d5af574ddc531",
"eventType": "platform.files.finalized.v1",
"occurredAt": "2026-07-15T08:00:00Z",
"tenantId": "tenant-a",
"data": {
"fileId": "file-42",
"fileName": "contract.pdf",
"contentType": "application/pdf",
"size": 482193,
"contentHash": "sha256:..."
}
}

字段分级要由 owner 审批。不要把内部聚合完整序列化到外部;TenantId 是否暴露、文件名是否敏感、Ticket 描述是否含个人信息,都需单独策略。

5. Consumer contract test 应验证什么

独立 contract test 不引用生产者的常量来构造 expected topic,否则两边一起改错仍会通过。测试 artifact 至少冻结:

  • topic 字符串;
  • content type / serializer options;
  • envelope 必填字段与类型;
  • EventId/TenantId/OccurredAt 语义;
  • data schema 与向后兼容规则;
  • Webhooks registry 和 required scope;
  • HMAC test vector;
  • 同一事件从 producer 到 fake HTTP receiver 的端到端结果。
独立消费者契约断言示意
[Fact]
public async Task File_finalized_v1_should_reach_external_receiver()
{
// 字面量由消费者契约拥有,不能从生产者常量导入。
// 字面量属于消费者契约,不从生产者常量读取。
const string expectedTopic = "platform.files.finalized.v1";
await files.FinalizeAsync(fileId, cancellationToken);
var capMessage = await capOutbox.WaitForAsync(expectedTopic, cancellationToken);
capMessage.Json.ShouldContain("\"eventId\"");
capMessage.Json.ShouldContain("\"tenantId\"");
var request = await receiver.WaitForEventAsync(cancellationToken);
// HTTP 层同时验证 topic 传播和一次性 secret 的真实签名。
request.Headers["X-BitzOrcas-Event-Type"].ShouldBe(expectedTopic);
receiver.VerifySignature(request, oneTimeSecret).ShouldBeTrue();
}

6. Schema 演进

兼容规则:新增可选字段可留在 v1;删除/重命名/改变含义/单位/枚举封闭性通常升 v2。消费者必须忽略未知字段;生产者在 sunset 前双发或提供迁移窗口。EventId 在双发 v1/v2 时是否相同要明确:相同表示同一事实的两种表示,接收端去重键必须包含 EventType/version。

事件 registry 当前是单例内存 Dictionary,Register 会覆盖同名 scope,没有冲突检测、版本元数据或 owner。生产应用应由 compile-time governance catalog 生成不可变 registry,并在启动时验证 topic 唯一和 producer/subscriber closure。

7. OpsExtension 死信操作

OpsExtension 提供:

  • 按当前 TenantId 分页查询 DeadLettered delivery;
  • 调用 WebhookDeliveryService 做真实手动重投;
  • 软删除死信行;
  • 并列查看/重投 CAP failed message;
  • RetryDeadLetter 后写 IActivityAuditSink。

Webhooks 自己还有 POST /api/webhooks/deliveries/{id}/retry,没有显式 activity audit。两条运维入口权限不同、审计不同,容易绕过审批。商业交付应选定单一 operator path;普通集成管理员与平台值班员分权。

dead-letter alert

按 tenant / event / error 分类

修复 endpoint / secret / network / contract

审批 replay scope + reason

rate-limited batch replay

receiver ack + metrics

incident + audit

8. 当前测试证据

已有:

  • WebhookSignatureTests:确定性、密钥/载荷篡改、Verify;
  • WebhookPlatformTests:基础重试分类、摘要不泄密、顺序幂等、Scope 拒绝、原 payload 重放、Succeeded 禁重试、严格 JSON、租户拒绝;
  • WebhookProductionPolicyTests:fail-closed、CIDR 基础、Redis 不可用、dead-letter port;
  • WebhookProductionAdapterContractTests:真实 Redis 跨实例限流与 readiness;
  • WebhookProductionOperationsDrillTests:缺配置、CIDR 越界和 Redis 故障日志;
  • 双 ORM parity:订阅、投递、payload、租户隔离;
  • Architecture tests:Infrastructure 不依赖具体 ORM、统一模型、read-store 使用。

这些测试没有证明任何 Files/Billing/Tickets/Workflow 事件被 WebhookPlatformEventConsumer 实际消费,也没有真实 HTTP receiver、九条 endpoint contract、自动 retry、redirect/DNS rebinding、重复 Save 后签名、轮换重叠或并发幂等。

9. 可观测性与 SLO

建议分三段 SLI:

  1. producer transaction → CAP outbox committed;
  2. CAP published → delivery row scheduled;
  3. delivery scheduled → receiver 2xx。

否则一个“成功率”无法区分 topic 根本没接上、CAP 堆积、Guard 拒绝与客户 endpoint 失败。SLO 可按付费等级设置,例如 99.9% 可投递事件在 5 分钟内成功或进入可操作死信;不要把客户永久 400 算平台可用性故障,但要计入交付结果。

Trace 应贯穿 producer trace、CAP message、EventId、DeliveryId、AttemptId 和 HTTP client span。当前 delivery log 没有 correlation/trace 或 attempt history,需要扩展持久化契约。

10. GA 门禁

契约

  • 每个公开事件有 owner、version、topic、schema、scope、PII classification 和 sunset policy;
  • producer、治理目录、consumer、registry 的 compile-time closure test 全绿;
  • 独立 consumer contract test 不共享错误常量;
  • 九条 HTTP OpenAPI/Problem Details 契约固定。

可靠性

  • 持久化 retry scheduler、lease、jitter、Retry-After 和 attempt history;
  • 并发幂等、崩溃窗口、Complete 失败与停机恢复通过;
  • dead-letter 批量回放限速、审批、审计和停止条件完成;
  • payload retention/encryption/erasure 策略落地。

安全

  • DNS pinning/egress proxy、redirect、global deny ranges、端口、timeout 和 payload size 关闭红线;
  • DataProtection key ring 备份恢复与双重加密回归通过;
  • versioned KeyId 轮换协议和紧急泄露 Runbook 通过;
  • webhooks.delivery entitlement 真正运行时生效。

运维

  • readiness + synthetic delivery + adapter report 联合门禁;
  • producer/CAP/delivery 三段 dashboard 与告警;
  • Redis、DNS、证书、客户 429/5xx、密钥环故障演练;
  • 值班可在不查数据库、不看 secret/payload 的情况下定位并恢复。

11. 全局核查命令

Terminal window
# 同时查看 producer、治理、consumer 三层,不能只查 Webhooks 目录。
# 暴露 topic 三套事实源;期望修复后每个业务事件只有一个版本化值。
rg -n "CapSubscribe|IntegrationTopic|EventDefinition|files\.finalized|ticket[s]?\.opened|invoice-issued" \
src/Platform/Files src/Platform/PlatformBilling src/Platform/Tickets \
src/Platform/Workflow src/Platform/Webhooks -g '*.cs'
# consumer 不应忽略 Result,DeliveryService 不应吞单订阅失败。
rg -n "DeliverAsync\(|IsFailure|GetValueOrThrow|logs.Add" \
src/Platform/Webhooks/BitzOrcas.Platform.Webhooks.{Application,Infrastructure} -g '*.cs'
# 当前只应找到 API 弃用示例构造平台 envelope;修复后各 owner 都有契约测试。
rg -n "new PlatformWebhookEventEnvelope|PlatformWebhookEventEnvelope" src tests -g '*.cs'
# 测试名用于暴露仍缺失的真实端点、并发和网络安全场景。
# 测试名应出现 endpoint、consumer contract、receiver、concurrent、rotation、rebind/redirect。
rg -n "Webhook.*(Endpoint|Consumer|Receiver|Concurrent|Rotation|Redirect|Rebind)" tests -g '*.cs'

返回 Webhooks 总览 · 投递与重试 · OpsExtension · Files · Tickets

100%

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