feat: 审计修复 Phase 6-7 — SSE 推送/工作流补全/消息群发/前端收尾
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

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:
iven
2026-04-26 19:44:04 +08:00
parent 83fe89cbcd
commit b05b7c27a0
28 changed files with 996 additions and 67 deletions

View File

@@ -1,3 +1,4 @@
pub mod message_handler;
pub mod sse_handler;
pub mod subscription_handler;
pub mod template_handler;

View File

@@ -0,0 +1,53 @@
use std::convert::Infallible;
use axum::extract::Extension;
use axum::response::sse::{Event, KeepAlive, Sse};
use futures::stream::Stream;
use erp_core::error::AppError;
use erp_core::types::TenantContext;
use crate::message_state::MessageState;
/// SSE 消息推送端点。
///
/// 客户端连接后监听 `message.sent` 事件,仅推送当前用户的消息。
/// 使用 EventBus 的 filtered subscriber 按前缀过滤事件。
pub async fn message_stream(
axum::extract::State(state): axum::extract::State<MessageState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, AppError> {
let user_id = ctx.user_id;
let tenant_id = ctx.tenant_id;
let (mut rx, _handle) = state.event_bus.subscribe_filtered("message.sent".to_string());
let sse_stream = async_stream::stream! {
loop {
match rx.recv().await {
Some(event) => {
if event.tenant_id != tenant_id {
continue;
}
let is_recipient = event.payload.get("recipient_id")
.and_then(|v: &serde_json::Value| v.as_str())
.map(|s| s == user_id.to_string())
.unwrap_or(false);
if !is_recipient {
continue;
}
let data = serde_json::to_string(&event.payload)
.unwrap_or_default();
yield Ok(Event::default()
.event("message")
.data(data));
}
None => {
break;
}
}
}
};
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}

View File

@@ -10,7 +10,7 @@ use erp_core::events::EventBus;
use erp_core::module::ErpModule;
use crate::entity::message_subscription;
use crate::handler::{message_handler, subscription_handler, template_handler};
use crate::handler::{message_handler, sse_handler, subscription_handler, template_handler};
/// 消息中心模块,实现 ErpModule trait。
pub struct MessageModule;
@@ -36,6 +36,8 @@ impl MessageModule {
.route("/messages/{id}/read", put(message_handler::mark_read))
.route("/messages/read-all", put(message_handler::mark_all_read))
.route("/messages/{id}", delete(message_handler::delete_message))
// SSE 实时推送
.route("/messages/stream", get(sse_handler::message_stream))
// 模板路由
.route(
"/message-templates",

View File

@@ -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())
}
/// 系统发送消息(由事件处理器调用)。