Skip to content
bitzorcas
中EN

Guide

Tickets 事件、通知与审计

说明 Tickets 六类事务集成事件、生成式载荷、Reporting/Webhook 消费边界、通知失败语义、生产审计适配器与待修复契约。

Last updated

Tickets 的消息链已经从“只有公共元数据”演进为生成式业务载荷:聚合事件同时实现 IIntegrationEvent,源生成注册表提供 topic 与字段映射,事务管道把它们写入 CAP Outbox。文档需要把这条已实现链路与仍未闭合的 Id、Broker 绑定和 Webhook 边界分开说明。

1. 六类生命周期事件

行为领域事件topic业务字段
OpenTicketOpenedticket.openedTicketId、TenantId、RequesterId、Subject、PriorityName、OccurredAt
AssignTicketAssignedticket.assignedTicketId、TenantId、AssigneeId、AssignedBy、OccurredAt
StartTicketStartedticket.startedTicketId、TenantId、StartedBy、OccurredAt
ResolveTicketResolvedticket.resolvedTicketId、TenantId、ResolvedBy、OccurredAt
CloseTicketClosedticket.closedTicketId、TenantId、ClosedBy、OccurredAt
ReopenTicketReopenedticket.reopenedTicketId、TenantId、ReopenedBy、OccurredAt

六个 record 都继承 DomainEvent、实现 IIntegrationEvent,并标注 [IntegrationTopic]。EventId 由 DomainEvent 提供;业务字段由事件的公开属性提供。Start 与 Reopen 已经不再是事件空白。

2. 事务发布链

UnitOfWorkCAP PublisherGenerated Topic RegistryDomainEventCollectorRepositoryTicket HandlerUnitOfWorkCAP PublisherGenerated Topic RegistryDomainEventCollectorRepositoryTicket Handler保存聚合Track(ticket)成功 Command 的待发事件topic + camelCase 业务参数写入 CAP Outbox提交业务数据与 Outbox

DomainEventDispatchPipelineBehavior 位于 Transaction pipeline 的内层 post-phase。它只处理成功的 Command,并执行:

  1. 从 IDomainEventCollector 读取事件;
  2. 要求事件实现 IIntegrationEvent;
  3. 通过 IIntegrationTopicRegistry.TryBuildPublication 取得 topic 和生成式业务参数;
  4. 加入保留字段 eventId、eventType;
  5. 调用 CAP publisher,将参数字典作为消息对象写入 Outbox。

映射缺失、字段冲突或发布异常都会向外抛出。Transaction pipeline 随后回滚工作单元,并保留聚合事件供显式重试;只有提交成功才清除事件。这不是 post-commit best-effort。

ticket.opened 的逻辑载荷形状
{
"eventId": "019c48cb7ba47000953d5af574ddcb81",
"eventType": "TicketOpened",
"ticketId": "851987654321",
"tenantId": "tenant-a",
"requesterId": "user-100",
"subject": "无法登录",
"priorityName": "High",
"occurredAt": "2026-08-14T08:00:00.0000000+00:00"
}

架构测试已经固定 TicketOpened 和 TicketStarted 的生成式字段映射。CAP publisher 实际调用 PublishAsync(topic, (object)parameters),所以 wire body 是参数字典的序列化结果,而不是生产者显式 new 出 TicketOpenedIntegrationEvent。

3. opened Id 回填缺陷

Ticket.Open 先以 Id="0" 构造聚合,随后立即执行:

// 此时 ticket.Id 仍是持久化占位值 "0"。
ticket.Raise(new TicketOpened(
ticket.Id,
ticket.TenantId,
ticket.RequesterId,
ticket.Subject,
ticket.Priority.Name,
// 事件是 immutable record;后续 ID 回填不会改写已捕获字段。
nowUtc));

仓储保存后会为聚合回填最终雪花 Id,但已经构造的 immutable event 不会随之变化。框架提供 RaiseWhenPersistenceIdAssigned,Ticket 当前没有使用它。因此 opened 消息可能携带 ticketId: "0",而随后 assigned/started/… 事件使用最终 Id,Reporting 会把同一工单拆成不同投影键。

修复方案应复用框架既有的延迟 Raise 机制,或在调用 Ticket.Open 前生成最终业务 Id。修复验收必须捕获真实 Outbox 消息并断言其 TicketId 等于 API 返回值。

4. Reporting typed consumer

Reporting 订阅全部六个 topic,并分别接收 Ticket*IntegrationEvent:

  • opened 创建或覆盖基础 Mart 行;
  • assigned 设置 AssigneeId 和 Assigned 状态;
  • started、resolved、closed、reopened 更新状态;
  • reopened 清空 ClosedAt;
  • EventId 与当前 LastEventId 相同则跳过;
  • 非 opened 事件早于 LastUpdatedAt 时按陈旧事件跳过;
  • store Failure、缺失前置 opened 或其他可重试异常会抛出,让 CAP 重试。

现有集成测试直接构造六个 consumer DTO 并验证完整序列、重复 reopened 和陈旧 closed。它证明 consumer 状态机,但没有证明 CAP 把生产者的参数字典按实际 serializer 绑定到这些 DTO。两侧字段名目前相容,这是源码推断,不是字节级契约证据。

LastEventId 只识别“当前最后事件的直接重复”。两个不同 EventId 若 OccurredAt 相等,后到事件仍可覆盖前者;opened 也没有陈旧时间保护。可靠的单调投影仍需要 Ticket Version/sequence 或明确定义的相同时刻排序键。

5. Webhook 尚未桥接

Tickets 发布 ticket.opened。WebhookPlatformEventConsumer 订阅的值是 tickets.opened,并要求 PlatformWebhookEventEnvelope(EventId, TenantId, PayloadJson, OccurredAt)。当前可见源码中没有 topic 转换器或 envelope 映射器。

修复时应明确三个层次:内部 Tickets topic、平台 Webhook envelope、对外 event name。不要只把字符串改成相似值;还要转换 payload、保留 TenantId/EventId/OccurredAt,并用真实序列化器做契约测试。

6. 通知失败语义

Assign、状态变更和 AddComment 在业务变更后同步调用 NotificationService.CreateAsync。当前辅助方法会检查每次 Result:

  • 分派主体可以是直接用户或用户组;用户组会展开为当前租户内的有效成员;
  • 状态和评论通知合并 requester 与 assignee 成员,并排除当前 actor;
  • HashSet 去除同一用户的重复通知;
  • 任一通知创建返回 Failure,Ticket Command 返回相同错误;
  • 外层 Transaction pipeline 对 Result.Failure 执行回滚,因此本次 Ticket 保存、通知事实和尚未提交的 Outbox 不会提交;
  • 通知 title/body 仍是 Handler 中的中文硬编码文本,没有经过模板与本地化目录。

这条语义保证的是“通知意图与业务命令共同成功或回滚”,不是邮件、短信或实时渠道已经送达。渠道投递仍由 Notifications 自己的 outbox/consumer/attempt 语义负责。

批量分派当前直接保存和发布搜索投影,没有复用单工单分派的参与人通知、领域事件收集或 Ticket 审计流程。批量行为与单项行为并不完全等价,必须作为独立产品契约验收。

7. 生产审计适配器

Application 默认有 NullTicketAuditSink,便于不加载审计基础设施的测试和开发组合。API 的生产持久化注册会执行:

services.RemoveAll<ITicketAuditSink>();
services.AddSingleton<ITicketAuditSink, PersistentTicketAuditSink>();

PersistentTicketAuditSink 将 TicketAuditRecord 转为 EntityChangeRecord,写入 IEntityChangeAuditSink。生产就绪守卫把 NullTicketAuditSink 列为禁止适配器,因此生产环境解析到 Null 会导致 readiness 失败。

当前记录包含 TicketId、TenantId、Actor、Action、Before、After 和 OccurredAt。适配器把 CorrelationId 固定为 null;部分命令的 Before/After 只记录状态或 Id,详情更新也可能没有字段级差异。因此“已持久化”不等于“具备完整取证上下文”。

审计端口返回 Task 而不是 Result;异常会向外传播并触发命令事务回滚。生产策略实际是 fail-closed,不是静默降级。

8. 搜索投影是另一条集成链

Open、详情更新、分派和状态变化等写路径会发布 SearchIndexChangedIntegrationEvent,由 Search owner 建立工单索引。它与六个 Reporting topic 是不同契约:搜索事件表达索引文档的 upsert/remove,Reporting 事件表达生命周期事实。

排障时应分别观察 Search projection lag 与 Reporting Mart lag。一个正常不代表另一个正常,也不能用搜索重建替代审计或 Reporting 重放。

9. 事务语义总表

失败点当前结果业务数据
Repository 返回 FailureCommand Failure回滚
通知创建返回 FailureCommand Failure回滚
审计抛异常异常回滚
generated topic 映射缺失配置异常回滚
CAP Outbox 发布抛异常异常回滚
UnitOfWork commit 失败异常/未知提交语义按事务框架处理,事件不清除
下游 Reporting 消费失败CAP 重试Ticket 事务已经提交

10. 必须保留的契约测试

  1. 六个 domain event 的 topic 与公开字段映射;
  2. Open 的最终 TicketId,不得为 "0";
  3. 实际 CAP serializer 产物可绑定六个 Reporting DTO;
  4. consumer store Failure/异常进入 retry,重复 EventId 不重复写;
  5. 相同时刻和倒序事件的确定性规则;
  6. Webhook topic 与 envelope 的显式映射;
  7. 通知 Failure 与审计异常均回滚 Ticket;
  8. 生产 Profile 不能解析到 NullTicketAuditSink;
  9. 批量与单项命令在事件、通知、审计上的预期差异。

11. 可观测性

至少记录 outbox enqueue/publish、consumer attempt、Reporting/Search projection lag、notification intent/attempt 和 audit append。标签使用 module/topic/result/error_code;TenantId/UserId 做受控 hash。Subject、Description、评论正文、通知正文与 StorageKey 不应进入日志。

告警项:oldest outbox age、typed binding/deserialization failure、Reporting/Search lag、生产解析到 Null audit、notification intent failure ratio,以及 opened ticketId="0"。

12. 审查命令

Terminal window
# 六个事件、生成式业务载荷和事务派发。
rg -n "IntegrationTopic|TryBuildPublication|BuildParameters|RaiseWhenPersistenceIdAssigned" \
src/Platform/Tickets src/Framework tests -g '*.cs'
# Reporting 与 Webhook 的订阅 topic/envelope。
rg -n "CapSubscribe|Ticket.*IntegrationEvent|tickets\.opened|PlatformWebhookEventEnvelope" \
src/Platform/Reporting src/Platform/Webhooks -g '*.cs'
# 开发空落点、生产替换与 readiness 禁止清单。
rg -n "ITicketAuditSink|NullTicketAuditSink|PersistentTicketAuditSink" src/Platform/Tickets src/Hosts tests -g '*.cs'

返回 Tickets 总览 · Reporting 模块 · Notifications 模块 · 测试与 GA 门禁

100%

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