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:
24
crates/erp-core/src/entity/domain_event.rs
Normal file
24
crates/erp-core/src/entity/domain_event.rs
Normal 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 {}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod audit_log;
|
||||
pub mod domain_event;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user