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()))
}