- Run cargo fmt on all Rust crates for consistent formatting - Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions - Update wiki: add WASM plugin architecture, rewrite dev environment docs - Minor frontend cleanup (unused imports)
179 lines
5.1 KiB
Rust
179 lines
5.1 KiB
Rust
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)]
|
||
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>,
|
||
}
|
||
|
||
/// 创建消息模板请求
|
||
#[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<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, ToSchema)]
|
||
pub struct UpdateSubscriptionReq {
|
||
pub notification_types: Option<serde_json::Value>,
|
||
pub channel_preferences: Option<serde_json::Value>,
|
||
pub dnd_enabled: Option<bool>,
|
||
pub dnd_start: Option<String>,
|
||
pub dnd_end: Option<String>,
|
||
pub version: i32,
|
||
}
|