use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; use validator::Validate; // ============ 消息 DTO ============ /// 消息响应 #[derive(Debug, Serialize, ToSchema)] pub struct MessageResp { pub id: Uuid, pub tenant_id: Uuid, pub template_id: Option, pub sender_id: Option, pub sender_type: String, pub recipient_id: Uuid, pub recipient_type: String, pub title: String, pub body: String, pub priority: String, pub business_type: Option, pub business_id: Option, pub is_read: bool, #[serde(skip_serializing_if = "Option::is_none")] pub read_at: Option>, pub is_archived: bool, pub status: String, #[serde(skip_serializing_if = "Option::is_none")] pub sent_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, pub version: i32, } /// 发送消息请求 #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct SendMessageReq { #[validate(length(min = 1, max = 200, message = "标题不能为空且不超过200字符"))] pub title: String, #[validate(length(min = 1, message = "内容不能为空"))] pub body: String, pub recipient_id: Uuid, #[serde(default = "default_recipient_type")] #[validate(custom(function = "validate_recipient_type"))] pub recipient_type: String, #[serde(default = "default_priority")] #[validate(custom(function = "validate_priority"))] pub priority: String, pub template_id: Option, pub business_type: Option, pub business_id: Option, } fn validate_recipient_type(value: &str) -> Result<(), validator::ValidationError> { match value { "user" | "role" | "department" | "all" => Ok(()), _ => Err(validator::ValidationError::new("invalid_recipient_type")), } } fn validate_priority(value: &str) -> Result<(), validator::ValidationError> { match value { "normal" | "important" | "urgent" => Ok(()), _ => Err(validator::ValidationError::new("invalid_priority")), } } fn default_recipient_type() -> String { "user".to_string() } fn default_priority() -> String { "normal".to_string() } /// 消息列表查询参数 #[derive(Debug, Deserialize, ToSchema)] pub struct MessageQuery { pub page: Option, pub page_size: Option, pub is_read: Option, pub priority: Option, pub business_type: Option, pub status: Option, } impl MessageQuery { /// 获取安全的分页大小(上限 100)。 pub fn safe_page_size(&self) -> u64 { self.page_size.unwrap_or(20).min(100) } } /// 未读消息计数响应 #[derive(Debug, Serialize, ToSchema)] pub struct UnreadCountResp { pub count: i64, } // ============ 消息模板 DTO ============ /// 消息模板响应 #[derive(Debug, Serialize, ToSchema)] pub struct MessageTemplateResp { pub id: Uuid, pub tenant_id: Uuid, pub name: String, pub code: String, pub channel: String, pub title_template: String, pub body_template: String, pub language: String, pub created_at: DateTime, pub updated_at: DateTime, } /// 创建消息模板请求 #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateTemplateReq { #[validate(length(min = 1, max = 100, message = "名称不能为空且不超过100字符"))] pub name: String, #[validate(length(min = 1, max = 50, message = "编码不能为空且不超过50字符"))] pub code: String, #[serde(default = "default_channel")] #[validate(custom(function = "validate_channel"))] pub channel: String, #[validate(length(min = 1, max = 200, message = "标题模板不能为空"))] pub title_template: String, #[validate(length(min = 1, message = "内容模板不能为空"))] pub body_template: String, #[serde(default = "default_language")] pub language: String, } fn default_channel() -> String { "in_app".to_string() } fn validate_channel(value: &str) -> Result<(), validator::ValidationError> { match value { "in_app" | "email" | "sms" | "wechat" => Ok(()), _ => Err(validator::ValidationError::new("invalid_channel")), } } fn default_language() -> String { "zh-CN".to_string() } // ============ 消息订阅偏好 DTO ============ /// 消息订阅偏好响应 #[derive(Debug, Serialize, ToSchema)] pub struct MessageSubscriptionResp { pub id: Uuid, pub tenant_id: Uuid, pub user_id: Uuid, pub notification_types: Option, pub channel_preferences: Option, pub dnd_enabled: bool, pub dnd_start: Option, pub dnd_end: Option, pub created_at: DateTime, pub updated_at: DateTime, pub version: i32, } /// 更新消息订阅偏好请求 #[derive(Debug, Deserialize, ToSchema)] pub struct UpdateSubscriptionReq { pub notification_types: Option, pub channel_preferences: Option, pub dnd_enabled: Option, pub dnd_start: Option, pub dnd_end: Option, pub version: i32, }