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

@@ -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);
}
}