Implement the complete message center with: - Database migrations for message_templates, messages, message_subscriptions tables - erp-message crate with entities, DTOs, services, handlers - Message CRUD, send, read/unread tracking, soft delete - Template management with variable interpolation - Subscription preferences with DND support - Frontend: messages page, notification panel, unread count badge - Server integration with module registration and routing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
4.2 KiB
Rust
117 lines
4.2 KiB
Rust
use chrono::Utc;
|
||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||
use uuid::Uuid;
|
||
|
||
use crate::dto::{MessageSubscriptionResp, UpdateSubscriptionReq};
|
||
use crate::entity::message_subscription;
|
||
use crate::error::{MessageError, MessageResult};
|
||
|
||
/// 消息订阅偏好服务。
|
||
pub struct SubscriptionService;
|
||
|
||
impl SubscriptionService {
|
||
/// 获取用户订阅偏好。
|
||
pub async fn get(
|
||
tenant_id: Uuid,
|
||
user_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> MessageResult<MessageSubscriptionResp> {
|
||
let model = message_subscription::Entity::find()
|
||
.filter(message_subscription::Column::TenantId.eq(tenant_id))
|
||
.filter(message_subscription::Column::UserId.eq(user_id))
|
||
.filter(message_subscription::Column::DeletedAt.is_null())
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| MessageError::Validation(e.to_string()))?
|
||
.ok_or_else(|| MessageError::NotFound("订阅偏好不存在".to_string()))?;
|
||
|
||
Ok(Self::model_to_resp(&model))
|
||
}
|
||
|
||
/// 创建或更新用户订阅偏好(upsert)。
|
||
pub async fn upsert(
|
||
tenant_id: Uuid,
|
||
user_id: Uuid,
|
||
req: &UpdateSubscriptionReq,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> MessageResult<MessageSubscriptionResp> {
|
||
let existing = message_subscription::Entity::find()
|
||
.filter(message_subscription::Column::TenantId.eq(tenant_id))
|
||
.filter(message_subscription::Column::UserId.eq(user_id))
|
||
.filter(message_subscription::Column::DeletedAt.is_null())
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||
|
||
let now = Utc::now();
|
||
|
||
if let Some(model) = existing {
|
||
let mut active: message_subscription::ActiveModel = model.into();
|
||
if let Some(types) = &req.notification_types {
|
||
active.notification_types = Set(Some(types.clone()));
|
||
}
|
||
if let Some(prefs) = &req.channel_preferences {
|
||
active.channel_preferences = Set(Some(prefs.clone()));
|
||
}
|
||
if let Some(dnd) = req.dnd_enabled {
|
||
active.dnd_enabled = Set(dnd);
|
||
}
|
||
if let Some(ref start) = req.dnd_start {
|
||
active.dnd_start = Set(Some(start.clone()));
|
||
}
|
||
if let Some(ref end) = req.dnd_end {
|
||
active.dnd_end = Set(Some(end.clone()));
|
||
}
|
||
active.updated_at = Set(now);
|
||
active.updated_by = Set(user_id);
|
||
|
||
let updated = active
|
||
.update(db)
|
||
.await
|
||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||
|
||
Ok(Self::model_to_resp(&updated))
|
||
} else {
|
||
let id = Uuid::now_v7();
|
||
|
||
let model = message_subscription::ActiveModel {
|
||
id: Set(id),
|
||
tenant_id: Set(tenant_id),
|
||
user_id: Set(user_id),
|
||
notification_types: Set(req.notification_types.clone()),
|
||
channel_preferences: Set(req.channel_preferences.clone()),
|
||
dnd_enabled: Set(req.dnd_enabled.unwrap_or(false)),
|
||
dnd_start: Set(req.dnd_start.clone()),
|
||
dnd_end: Set(req.dnd_end.clone()),
|
||
created_at: Set(now),
|
||
updated_at: Set(now),
|
||
created_by: Set(user_id),
|
||
updated_by: Set(user_id),
|
||
deleted_at: Set(None),
|
||
};
|
||
|
||
let inserted = model
|
||
.insert(db)
|
||
.await
|
||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||
|
||
Ok(Self::model_to_resp(&inserted))
|
||
}
|
||
}
|
||
|
||
fn model_to_resp(m: &message_subscription::Model) -> MessageSubscriptionResp {
|
||
MessageSubscriptionResp {
|
||
id: m.id,
|
||
tenant_id: m.tenant_id,
|
||
user_id: m.user_id,
|
||
notification_types: m.notification_types.clone(),
|
||
channel_preferences: m.channel_preferences.clone(),
|
||
dnd_enabled: m.dnd_enabled,
|
||
dnd_start: m.dnd_start.clone(),
|
||
dnd_end: m.dnd_end.clone(),
|
||
created_at: m.created_at,
|
||
updated_at: m.updated_at,
|
||
}
|
||
}
|
||
}
|