feat(message): add message center module (Phase 5)

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>
This commit is contained in:
iven
2026-04-11 12:25:05 +08:00
parent 91ecaa3ed7
commit 5ceed71e62
35 changed files with 2252 additions and 15 deletions

View File

@@ -0,0 +1,144 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use utoipa::ToSchema;
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>,
}
/// 发送消息请求
#[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")]
pub recipient_type: String,
#[serde(default = "default_priority")]
pub priority: String,
pub template_id: Option<Uuid>,
pub business_type: Option<String>,
pub business_id: Option<Uuid>,
}
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>,
}
/// 未读消息计数响应
#[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")]
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 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>,
}
/// 更新消息订阅偏好请求
#[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>,
}