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, utoipa::IntoParams)] 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, pub version: i32, } /// 创建消息模板请求 #[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() } /// 更新消息模板请求 #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateTemplateReq { #[validate(length(min = 1, max = 100, message = "名称不能为空且不超过100字符"))] pub name: Option, #[validate(length(min = 1, max = 200, message = "标题模板不能为空"))] pub title_template: Option, #[validate(length(min = 1, message = "内容模板不能为空"))] pub body_template: Option, #[validate(length(min = 1, max = 10, message = "语言代码无效"))] pub language: Option, #[validate(custom(function = "validate_channel"))] pub channel: Option, pub version: i32, } // ============ 消息订阅偏好 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, Validate, ToSchema)] pub struct UpdateSubscriptionReq { pub notification_types: Option, pub channel_preferences: Option, pub dnd_enabled: Option, #[validate(length(max = 8, message = "免打扰开始时间格式无效"))] pub dnd_start: Option, #[validate(length(max = 8, message = "免打扰结束时间格式无效"))] pub dnd_end: Option, pub version: i32, } #[cfg(test)] mod tests { use super::*; use uuid::Uuid; use validator::Validate; // ============ SendMessageReq 测试 ============ fn valid_send_message_req() -> SendMessageReq { SendMessageReq { title: "系统通知".to_string(), body: "您有一条新消息".to_string(), recipient_id: Uuid::now_v7(), recipient_type: "user".to_string(), priority: "normal".to_string(), template_id: None, business_type: None, business_id: None, } } #[test] fn send_message_req_valid() { let req = valid_send_message_req(); assert!(req.validate().is_ok()); } #[test] fn send_message_req_empty_title_fails() { let mut req = valid_send_message_req(); req.title = "".to_string(); assert!(req.validate().is_err()); } #[test] fn send_message_req_title_too_long_fails() { let mut req = valid_send_message_req(); req.title = "x".repeat(201); assert!(req.validate().is_err()); } #[test] fn send_message_req_title_max_length_ok() { let mut req = valid_send_message_req(); req.title = "x".repeat(200); assert!(req.validate().is_ok()); } #[test] fn send_message_req_empty_body_fails() { let mut req = valid_send_message_req(); req.body = "".to_string(); assert!(req.validate().is_err()); } #[test] fn send_message_req_valid_recipient_types() { for rt in &["user", "role", "department", "all"] { let mut req = valid_send_message_req(); req.recipient_type = rt.to_string(); assert!( req.validate().is_ok(), "recipient_type '{}' should be valid", rt ); } } #[test] fn send_message_req_invalid_recipient_type_fails() { let mut req = valid_send_message_req(); req.recipient_type = "invalid".to_string(); assert!(req.validate().is_err()); } #[test] fn send_message_req_valid_priorities() { for p in &["normal", "important", "urgent"] { let mut req = valid_send_message_req(); req.priority = p.to_string(); assert!(req.validate().is_ok(), "priority '{}' should be valid", p); } } #[test] fn send_message_req_invalid_priority_fails() { let mut req = valid_send_message_req(); req.priority = "critical".to_string(); assert!(req.validate().is_err()); } #[test] fn send_message_req_default_recipient_type_is_user() { assert_eq!(default_recipient_type(), "user"); } #[test] fn send_message_req_default_priority_is_normal() { assert_eq!(default_priority(), "normal"); } // ============ MessageQuery 测试 ============ #[test] fn message_query_safe_page_size_default() { let query = MessageQuery { page: None, page_size: None, is_read: None, priority: None, business_type: None, status: None, }; assert_eq!(query.safe_page_size(), 20); } #[test] fn message_query_safe_page_size_custom() { let query = MessageQuery { page: None, page_size: Some(50), is_read: None, priority: None, business_type: None, status: None, }; assert_eq!(query.safe_page_size(), 50); } #[test] fn message_query_safe_page_size_capped_at_100() { let query = MessageQuery { page: None, page_size: Some(200), is_read: None, priority: None, business_type: None, status: None, }; assert_eq!(query.safe_page_size(), 100); } #[test] fn message_query_safe_page_size_exactly_100() { let query = MessageQuery { page: None, page_size: Some(100), is_read: None, priority: None, business_type: None, status: None, }; assert_eq!(query.safe_page_size(), 100); } // ============ CreateTemplateReq 测试 ============ fn valid_create_template_req() -> CreateTemplateReq { CreateTemplateReq { name: "欢迎模板".to_string(), code: "WELCOME".to_string(), channel: "in_app".to_string(), title_template: "欢迎加入".to_string(), body_template: "您好,{{name}},欢迎加入平台".to_string(), language: "zh-CN".to_string(), } } #[test] fn create_template_req_valid() { let req = valid_create_template_req(); assert!(req.validate().is_ok()); } #[test] fn create_template_req_empty_name_fails() { let mut req = valid_create_template_req(); req.name = "".to_string(); assert!(req.validate().is_err()); } #[test] fn create_template_req_name_too_long_fails() { let mut req = valid_create_template_req(); req.name = "x".repeat(101); assert!(req.validate().is_err()); } #[test] fn create_template_req_name_max_length_ok() { let mut req = valid_create_template_req(); req.name = "x".repeat(100); assert!(req.validate().is_ok()); } #[test] fn create_template_req_empty_code_fails() { let mut req = valid_create_template_req(); req.code = "".to_string(); assert!(req.validate().is_err()); } #[test] fn create_template_req_code_too_long_fails() { let mut req = valid_create_template_req(); req.code = "X".repeat(51); assert!(req.validate().is_err()); } #[test] fn create_template_req_code_max_length_ok() { let mut req = valid_create_template_req(); req.code = "X".repeat(50); assert!(req.validate().is_ok()); } #[test] fn create_template_req_valid_channels() { for ch in &["in_app", "email", "sms", "wechat"] { let mut req = valid_create_template_req(); req.channel = ch.to_string(); assert!(req.validate().is_ok(), "channel '{}' should be valid", ch); } } #[test] fn create_template_req_invalid_channel_fails() { let mut req = valid_create_template_req(); req.channel = "telegram".to_string(); assert!(req.validate().is_err()); } #[test] fn create_template_req_empty_title_template_fails() { let mut req = valid_create_template_req(); req.title_template = "".to_string(); assert!(req.validate().is_err()); } #[test] fn create_template_req_title_template_too_long_fails() { let mut req = valid_create_template_req(); req.title_template = "x".repeat(201); assert!(req.validate().is_err()); } #[test] fn create_template_req_empty_body_template_fails() { let mut req = valid_create_template_req(); req.body_template = "".to_string(); assert!(req.validate().is_err()); } #[test] fn create_template_req_default_channel_is_in_app() { assert_eq!(default_channel(), "in_app"); } #[test] fn create_template_req_default_language_is_zh_cn() { assert_eq!(default_language(), "zh-CN"); } // ============ 自定义验证函数测试 ============ #[test] fn validate_recipient_type_valid() { for rt in &["user", "role", "department", "all"] { assert!( validate_recipient_type(rt).is_ok(), "'{}' should be a valid recipient type", rt ); } } #[test] fn validate_recipient_type_invalid() { assert!(validate_recipient_type("invalid").is_err()); assert!(validate_recipient_type("").is_err()); assert!(validate_recipient_type("USER").is_err()); } #[test] fn validate_priority_valid() { for p in &["normal", "important", "urgent"] { assert!( validate_priority(p).is_ok(), "'{}' should be a valid priority", p ); } } #[test] fn validate_priority_invalid() { assert!(validate_priority("critical").is_err()); assert!(validate_priority("").is_err()); assert!(validate_priority("NORMAL").is_err()); } #[test] fn validate_channel_valid() { for ch in &["in_app", "email", "sms", "wechat"] { assert!( validate_channel(ch).is_ok(), "'{}' should be a valid channel", ch ); } } #[test] fn validate_channel_invalid() { assert!(validate_channel("slack").is_err()); assert!(validate_channel("").is_err()); assert!(validate_channel("EMAIL").is_err()); } }