feat(core): implement event outbox persistence

Add domain_events migration and SeaORM entity. Modify EventBus::publish
to persist events before broadcasting (best-effort: DB failure logs
warning but still broadcasts in-memory). Update all 19 publish call
sites across 4 crates to pass db reference.

Add outbox relay background task that polls pending events every 5s
and re-broadcasts them, ensuring no events are lost on server restart.
This commit is contained in:
iven
2026-04-12 00:10:49 +08:00
parent 529d90ff46
commit 685df5e458
23 changed files with 235 additions and 31 deletions

View File

@@ -0,0 +1,24 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
/// 领域事件实体 — 映射 domain_events 表。
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "domain_events")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub event_type: String,
pub payload: Option<serde_json::Value>,
pub correlation_id: Option<Uuid>,
pub status: String,
pub attempts: i32,
pub last_error: Option<String>,
pub created_at: DateTimeUtc,
pub published_at: Option<DateTimeUtc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1 +1,2 @@
pub mod audit_log;
pub mod domain_event;

View File

@@ -1,9 +1,12 @@
use chrono::{DateTime, Utc};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, Set};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use tracing::{error, info};
use uuid::Uuid;
use crate::entity::domain_event;
/// 领域事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainEvent {
@@ -11,7 +14,7 @@ pub struct DomainEvent {
pub event_type: String,
pub tenant_id: Uuid,
pub payload: serde_json::Value,
pub timestamp: DateTime<Utc>,
pub timestamp: chrono::DateTime<Utc>,
pub correlation_id: Uuid,
}
@@ -46,11 +49,40 @@ impl EventBus {
Self { sender }
}
/// 发布事件
pub fn publish(&self, event: DomainEvent) {
info!(event_type = %event.event_type, event_id = %event.id, "Event published");
/// 发布事件:先持久化到 domain_events 表,再内存广播。
///
/// 持久化失败时仅记录 warning仍然广播best-effort
pub async fn publish(&self, event: DomainEvent, db: &sea_orm::DatabaseConnection) {
// 持久化到 domain_events 表
let model = domain_event::ActiveModel {
id: Set(event.id),
tenant_id: Set(event.tenant_id),
event_type: Set(event.event_type.clone()),
payload: Set(Some(event.payload.clone())),
correlation_id: Set(Some(event.correlation_id)),
status: Set("published".to_string()),
attempts: Set(0),
last_error: Set(None),
created_at: Set(event.timestamp),
published_at: Set(Some(Utc::now())),
};
match model.insert(db).await {
Ok(_) => {}
Err(e) => {
tracing::warn!(event_id = %event.id, error = %e, "领域事件持久化失败");
}
}
// 内存广播
self.broadcast(event);
}
/// 仅内存广播(不持久化,用于内部测试等场景)。
pub fn broadcast(&self, event: DomainEvent) {
info!(event_type = %event.event_type, event_id = %event.id, "Event broadcast");
if let Err(e) = self.sender.send(event) {
error!("Failed to publish event: {}", e);
error!("Failed to broadcast event: {}", e);
}
}