Files
nj/crates/erp-message/src/dto.rs
iven c539e6fd83 feat: initialize Nuanji (Warm Notes) project
- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin)
- Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs)
- Integrated erp-diary into workspace and erp-server
- Added DiaryModule registration in main.rs
- Added DiaryState FromRef in state.rs
- Diary routes mounted (empty routes, ready for implementation)
- Product design spec v1.2 preserved in docs/
- Implementation plan preserved in plans/

Cargo check: OK
Cargo test: OK (78+ base tests passing)
2026-05-31 20:52:19 +08:00

518 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Uuid>,
pub sender_id: Option<Uuid>,
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<String>,
pub business_id: Option<Uuid>,
pub is_read: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub read_at: Option<DateTime<Utc>>,
pub is_archived: bool,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sent_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
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<Uuid>,
pub business_type: Option<String>,
pub business_id: Option<Uuid>,
}
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<u64>,
pub page_size: Option<u64>,
pub is_read: Option<bool>,
pub priority: Option<String>,
pub business_type: Option<String>,
pub status: Option<String>,
}
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<Utc>,
pub updated_at: DateTime<Utc>,
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<String>,
#[validate(length(min = 1, max = 200, message = "标题模板不能为空"))]
pub title_template: Option<String>,
#[validate(length(min = 1, message = "内容模板不能为空"))]
pub body_template: Option<String>,
#[validate(length(min = 1, max = 10, message = "语言代码无效"))]
pub language: Option<String>,
#[validate(custom(function = "validate_channel"))]
pub channel: Option<String>,
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<serde_json::Value>,
pub channel_preferences: Option<serde_json::Value>,
pub dnd_enabled: bool,
pub dnd_start: Option<String>,
pub dnd_end: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub version: i32,
}
/// 更新消息订阅偏好请求
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct UpdateSubscriptionReq {
pub notification_types: Option<serde_json::Value>,
pub channel_preferences: Option<serde_json::Value>,
pub dnd_enabled: Option<bool>,
#[validate(length(max = 8, message = "免打扰开始时间格式无效"))]
pub dnd_start: Option<String>,
#[validate(length(max = 8, message = "免打扰结束时间格式无效"))]
pub dnd_end: Option<String>,
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());
}
}