"""领域事件 DTO。 定义跨层传递的领域事件值对象,供 ``EventPublisherPort`` 发布与订阅者消费。 事件载荷为不可变值对象(``dataclass(frozen=True)``),不泄露领域实体引用。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from yuxi.channels.contract.dtos.outbox import OutboxStatus from yuxi.channels.contract.errors import ValidationError @dataclass(frozen=True) class OutboxStateChangedEvent: """发件箱状态变更事件(OBX-004)。 描述一条发件箱条目(``OutboxEntry``)的状态迁移,由应用层在 outbox 状态机推进时(``markSent`` / ``markFailed`` / ``markSuppressed`` / ``markSentUnconfirmed`` 等)产出,经 ``EventPublisherPort`` 分发给 订阅者。状态值取自 ``OutboxStatus`` 枚举的字符串形式,便于跨层序列化 与审计。 字段: entry_id: 发件箱条目 ID(``OutboxEntry.outbox_id``)。 old_state: 变更前状态(``OutboxStatus`` 字符串值)。 new_state: 变更后状态(``OutboxStatus`` 字符串值)。 reason: 变更原因(如投递成功、重试失败、栅栏抑制等)。 occurred_at: 事件发生时间。 """ entry_id: str old_state: OutboxStatus new_state: OutboxStatus reason: str occurred_at: datetime def __post_init__(self) -> None: """校验必填字段非空。 ``entry_id`` / ``old_state`` / ``new_state`` / ``reason`` 必须非空, 在构造时即抛出 ``ValidationError``,避免空值事件传播到订阅者 (INV-8)。 """ if not self.entry_id: raise ValidationError("entry_id", "must not be empty") if not self.old_state: raise ValidationError("old_state", "must not be empty") if not self.new_state: raise ValidationError("new_state", "must not be empty") if not self.reason: raise ValidationError("reason", "must not be empty") @dataclass(frozen=True) class OutboxEntryPurgedEvent: """发件箱条目被物理清理事件(OBX-005)。 描述一批发件箱条目(``OutboxEntry``)被物理删除(purge)的事件,由 调度器定时清理 dead / sent 终态条目时产出,经 ``EventPublisherPort`` 分发给订阅者。``entry_ids`` 为本次被清理条目的业务 ID 列表,供下游 做审计、统计与缓存失效等处理。 字段: entry_ids: 被物理清理的发件箱条目业务 ID 列表(``OutboxEntry.outbox_id``)。 occurred_at: 事件发生时间。 """ entry_ids: tuple[str, ...] occurred_at: datetime def __post_init__(self) -> None: """校验 entry_ids 非空。 ``entry_ids`` 必须为非空元组,空列表的清理事件无意义,在构造时即 抛出 ``ValidationError``,避免空事件传播到订阅者(INV-8)。 """ if not self.entry_ids: raise ValidationError("entry_ids", "must not be empty") __all__ = ["OutboxStateChangedEvent", "OutboxEntryPurgedEvent"]