use models::system::event_outbox; use queue::{Envelope, Labels}; use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, EntityTrait, Set}; use serde::Serialize; use uuid::Uuid; fn active_model( kind: &str, labels: Labels, payload: &T, ordering_key: &str, ) -> Result { let source = kind.split('.').next().unwrap_or_default().to_string(); let envelope = Envelope::new(kind, payload) .with_source(source) .with_labels(labels); let subject = envelope .subject() .map_err(|error| DbErr::Custom(error.to_string()))?; let stored = serde_json::to_value(&envelope) .map_err(|error| DbErr::Custom(format!("encoding outbox event: {error}")))?; Ok(event_outbox::ActiveModel { id: Set(envelope.id), subject: Set(subject), kind: Set(kind.to_string()), ordering_key: Set(ordering_key.to_string()), envelope: Set(stored), occurred_at: Set(envelope.occurred_at), available_at: Set(envelope.occurred_at), ..Default::default() }) } pub async fn enqueue( db: &impl ConnectionTrait, enabled: bool, kind: &str, labels: Labels, payload: T, ordering_key: impl Into, ) -> Result<(), DbErr> { if !enabled { return Ok(()); } active_model(kind, labels, &payload, &ordering_key.into())? .insert(db) .await?; Ok(()) } pub fn request_id() -> Option { crate::middleware::request_id() } fn room_labels(room_id: Uuid) -> Labels { let mut labels = Labels::new().with("room_id", room_id); if let Some(request_id) = request_id() { labels.insert("request_id", request_id); } labels } /// 房间级事件的统一入队口:标签固定 `room_id`,ordering key 为 `chat:room:{room_id}`。 /// /// 供不在 `chat` crate 里的写入点(例如 `space` 的频道删除)复用, /// 避免它们为了一个标签类型去直接依赖 `queue`。 pub async fn enqueue_room( db: &impl ConnectionTrait, enabled: bool, kind: &str, room_id: Uuid, payload: T, ) -> Result<(), DbErr> { if !enabled { return Ok(()); } active_model( kind, room_labels(room_id), &payload, &format!("chat:room:{room_id}"), )? .insert(db) .await?; Ok(()) } /// 房间变化扇出给一批收件人:每条一个 `ordering_key`,一次批量写入 outbox。 /// /// 用批量而不是逐条 `enqueue`,是因为公开频道创建时收件人等于整个空间的 /// 活跃成员数——逐条写入会变成 N 次往返。 pub async fn enqueue_chat_users( db: &impl ConnectionTrait, enabled: bool, kind: &str, uids: &[i64], room_id: Uuid, payload: &T, ) -> Result<(), DbErr> { if !enabled || uids.is_empty() { return Ok(()); } let mut models = Vec::with_capacity(uids.len()); for uid in uids { let labels = room_labels(room_id).with("user_id", *uid); models.push(active_model( kind, labels, payload, &format!("chat:user:{uid}"), )?); } event_outbox::Entity::insert_many(models).exec(db).await?; Ok(()) }