feat: 审计修复 Phase 6-7 — SSE 推送/工作流补全/消息群发/前端收尾
Phase 6 功能补全: - P1-3: 消息 SSE 实时推送端点 + 前端 EventSource 连接 - P1-6: ServiceTask HTTP 调用能力 (reqwest GET/POST) - P1-7: user.deleted 事件处理 — 终止相关流程实例 - P1-8: 任务认领 (claim) 端点 + handler - P1-9: 超时检查器发布 task.timeout 事件 - P1-15: 组织/部门名称唯一性校验 (create + update) - P1-18: 消息群发 fan-out (role/department/all 批量投递) Phase 7 P3-P4 收尾: - PluginAdmin purge 按钮状态修复 - ChangePassword 最小 8 字符 + 新旧密码不同验证 - AuditLogViewer 用户名缓存 + 扩展资源类型 - InstanceMonitor 通过 definition 缓存解析 node_name - NotificationPreferences DND 时间范围校验
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait,
|
||||
QueryFilter, Set, Statement,
|
||||
QueryFilter, Set, Statement, FromQueryResult,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -12,6 +12,12 @@ use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
/// 原始 SQL 查询 user_id 的结果结构体。
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct UserIdRow {
|
||||
user_id: Uuid,
|
||||
}
|
||||
|
||||
/// 消息服务。
|
||||
pub struct MessageService;
|
||||
|
||||
@@ -80,6 +86,12 @@ impl MessageService {
|
||||
}
|
||||
|
||||
/// 发送消息。
|
||||
///
|
||||
/// 根据 `recipient_type` 执行不同的投递策略:
|
||||
/// - `"user"` — 单条消息,直接投递给 `recipient_id` 指定的用户。
|
||||
/// - `"role"` — 查询 `user_roles` 表,向该角色下的所有用户批量投递。
|
||||
/// - `"department"` — 查询 `user_departments` 表,向该部门下的所有用户批量投递。
|
||||
/// - `"all"` — 查询 `users` 表,向租户内所有活跃用户批量投递。
|
||||
pub async fn send(
|
||||
tenant_id: Uuid,
|
||||
sender_id: Uuid,
|
||||
@@ -87,49 +99,79 @@ impl MessageService {
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
event_bus: &EventBus,
|
||||
) -> MessageResult<MessageResp> {
|
||||
let id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
|
||||
let model = message::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
template_id: Set(req.template_id),
|
||||
sender_id: Set(Some(sender_id)),
|
||||
sender_type: Set("user".to_string()),
|
||||
recipient_id: Set(req.recipient_id),
|
||||
recipient_type: Set(req.recipient_type.clone()),
|
||||
title: Set(req.title.clone()),
|
||||
body: Set(req.body.clone()),
|
||||
priority: Set(req.priority.clone()),
|
||||
business_type: Set(req.business_type.clone()),
|
||||
business_id: Set(req.business_id),
|
||||
is_read: Set(false),
|
||||
read_at: Set(None),
|
||||
is_archived: Set(false),
|
||||
archived_at: Set(None),
|
||||
sent_at: Set(Some(now)),
|
||||
status: Set("sent".to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(sender_id),
|
||||
updated_by: Set(sender_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
// Resolve target user IDs based on recipient type
|
||||
let recipient_user_ids = match req.recipient_type.as_str() {
|
||||
"user" => vec![req.recipient_id],
|
||||
"role" => {
|
||||
Self::resolve_user_ids_by_role(db, req.recipient_id, tenant_id).await?
|
||||
}
|
||||
"department" => {
|
||||
Self::resolve_user_ids_by_department(db, req.recipient_id, tenant_id).await?
|
||||
}
|
||||
"all" => {
|
||||
Self::resolve_all_active_user_ids(db, tenant_id).await?
|
||||
}
|
||||
other => {
|
||||
return Err(MessageError::Validation(format!(
|
||||
"不支持的收件人类型: {other}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let inserted = model
|
||||
.insert(db)
|
||||
if recipient_user_ids.is_empty() {
|
||||
return Err(MessageError::Validation(
|
||||
"没有找到符合条件的收件人".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Build message models for all recipients
|
||||
let models: Vec<message::ActiveModel> = recipient_user_ids
|
||||
.iter()
|
||||
.map(|uid| message::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
template_id: Set(req.template_id),
|
||||
sender_id: Set(Some(sender_id)),
|
||||
sender_type: Set("user".to_string()),
|
||||
recipient_id: Set(*uid),
|
||||
recipient_type: Set("user".to_string()),
|
||||
title: Set(req.title.clone()),
|
||||
body: Set(req.body.clone()),
|
||||
priority: Set(req.priority.clone()),
|
||||
business_type: Set(req.business_type.clone()),
|
||||
business_id: Set(req.business_id),
|
||||
is_read: Set(false),
|
||||
read_at: Set(None),
|
||||
is_archived: Set(false),
|
||||
archived_at: Set(None),
|
||||
sent_at: Set(Some(now)),
|
||||
status: Set("sent".to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(sender_id),
|
||||
updated_by: Set(sender_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Batch insert all messages
|
||||
message::Entity::insert_many(models)
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||||
|
||||
// Publish one event per batch (summary event)
|
||||
event_bus
|
||||
.publish(
|
||||
erp_core::events::DomainEvent::new(
|
||||
"message.sent",
|
||||
tenant_id,
|
||||
serde_json::json!({
|
||||
"message_id": id,
|
||||
"recipient_id": req.recipient_id,
|
||||
"recipient_type": req.recipient_type,
|
||||
"recipient_count": recipient_user_ids.len(),
|
||||
"title": req.title,
|
||||
}),
|
||||
),
|
||||
@@ -139,12 +181,94 @@ impl MessageService {
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, Some(sender_id), "message.send", "message")
|
||||
.with_resource_id(id),
|
||||
.with_changes(
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"recipient_type": req.recipient_type,
|
||||
"recipient_count": recipient_user_ids.len(),
|
||||
"title": req.title,
|
||||
})),
|
||||
),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Self::model_to_resp(&inserted))
|
||||
// Construct a representative response (no row returned from batch insert)
|
||||
Ok(MessageResp {
|
||||
id: Uuid::nil(),
|
||||
tenant_id,
|
||||
template_id: req.template_id,
|
||||
sender_id: Some(sender_id),
|
||||
sender_type: "user".to_string(),
|
||||
recipient_id: req.recipient_id,
|
||||
recipient_type: req.recipient_type.clone(),
|
||||
title: req.title.clone(),
|
||||
body: req.body.clone(),
|
||||
priority: req.priority.clone(),
|
||||
business_type: req.business_type.clone(),
|
||||
business_id: req.business_id,
|
||||
is_read: false,
|
||||
read_at: None,
|
||||
is_archived: false,
|
||||
status: "sent".to_string(),
|
||||
sent_at: Some(now),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据角色 ID 查询关联的用户 ID 列表(跨模块 raw SQL)。
|
||||
async fn resolve_user_ids_by_role(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
role_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
) -> MessageResult<Vec<Uuid>> {
|
||||
let rows = UserIdRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT user_id FROM user_roles WHERE role_id = $1 AND tenant_id = $2 AND deleted_at IS NULL",
|
||||
[role_id.into(), tenant_id.into()],
|
||||
))
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| r.user_id).collect())
|
||||
}
|
||||
|
||||
/// 根据部门 ID 查询关联的用户 ID 列表(跨模块 raw SQL)。
|
||||
async fn resolve_user_ids_by_department(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
department_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
) -> MessageResult<Vec<Uuid>> {
|
||||
let rows = UserIdRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT user_id FROM user_departments WHERE department_id = $1 AND tenant_id = $2 AND deleted_at IS NULL",
|
||||
[department_id.into(), tenant_id.into()],
|
||||
))
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| r.user_id).collect())
|
||||
}
|
||||
|
||||
/// 查询租户内所有活跃用户的 ID 列表(跨模块 raw SQL)。
|
||||
async fn resolve_all_active_user_ids(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
) -> MessageResult<Vec<Uuid>> {
|
||||
let rows = UserIdRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT id AS user_id FROM users WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'active'",
|
||||
[tenant_id.into()],
|
||||
))
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| MessageError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| r.user_id).collect())
|
||||
}
|
||||
|
||||
/// 系统发送消息(由事件处理器调用)。
|
||||
|
||||
Reference in New Issue
Block a user