feat: 通知分发器 DND 检查 + 咨询/报告事件 + 线下活动页面
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

Iteration 2 剩余工作:

通知分发器改进(erp-message module.rs):
- 添加 should_skip_for_dnd() 免打扰检查(urgent 级别不受限)
- DND 支持跨午夜窗口(如 22:00-08:00)
- 新增 consultation.new_message 事件(患者发消息通知医生)
- 新增 lab_report.reviewed 事件(报告审核完成通知患者)
- 改进已有事件:预约确认含日期、随访逾期含患者名

积分前端补充:
- points.ts 新增 OfflineEvent/EventRegistration 接口 + API
- 新增线下活动列表页面(报名/人数/积分奖励)
- 注册 events 页面路由
This commit is contained in:
iven
2026-04-26 13:43:54 +08:00
parent 9f546a519b
commit 0cf69815d9
5 changed files with 432 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
use axum::Router;
use axum::routing::{delete, get, put};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use std::sync::Arc;
use tokio::sync::Semaphore;
use uuid::Uuid;
@@ -8,6 +9,7 @@ use erp_core::error::AppResult;
use erp_core::events::EventBus;
use erp_core::module::ErpModule;
use crate::entity::message_subscription;
use crate::handler::{message_handler, subscription_handler, template_handler};
/// 消息中心模块,实现 ErpModule trait。
@@ -131,6 +133,45 @@ impl ErpModule for MessageModule {
}
}
/// 检查用户是否启用了 DND免打扰且当前时间在 DND 窗口内。
/// 返回 true 表示应该跳过发送。
async fn should_skip_for_dnd(
tenant_id: Uuid,
recipient_id: Uuid,
priority: &str,
db: &sea_orm::DatabaseConnection,
) -> bool {
// 紧急消息永远不跳过
if priority == "urgent" {
return false;
}
let sub = match message_subscription::Entity::find()
.filter(message_subscription::Column::TenantId.eq(tenant_id))
.filter(message_subscription::Column::UserId.eq(recipient_id))
.filter(message_subscription::Column::DeletedAt.is_null())
.one(db)
.await
{
Ok(Some(s)) => s,
_ => return false,
};
if !sub.dnd_enabled {
return false;
}
let (start, end) = match (sub.dnd_start, sub.dnd_end) {
(Some(s), Some(e)) => (s, e),
_ => return false,
};
let now = chrono::Local::now();
let now_time = now.format("%H:%M").to_string();
// DND 窗口比较(支持跨午夜,如 22:00-08:00
if start <= end {
now_time >= start && now_time < end
} else {
now_time >= start || now_time < end
}
}
/// 处理工作流事件,生成对应的消息通知。
async fn handle_workflow_event(
event: &erp_core::events::DomainEvent,
@@ -151,6 +192,9 @@ async fn handle_workflow_event(
Ok(id) => id,
Err(_) => return Ok(()),
};
if should_skip_for_dnd(event.tenant_id, recipient, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
recipient,
@@ -167,7 +211,6 @@ async fn handle_workflow_event(
}
}
"task.completed" => {
// 任务完成时通知流程发起人
let task_id = event
.payload
.get("task_id")
@@ -180,6 +223,9 @@ async fn handle_workflow_event(
Ok(id) => id,
Err(_) => return Ok(()),
};
if should_skip_for_dnd(event.tenant_id, recipient, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
recipient,
@@ -209,6 +255,9 @@ async fn handle_workflow_event(
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
@@ -230,6 +279,11 @@ async fn handle_workflow_event(
.get("appointment_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let appointment_date = event
.payload
.get("appointment_date")
.and_then(|v| v.as_str())
.unwrap_or("");
let patient_id = event
.payload
.get("patient_id")
@@ -237,11 +291,19 @@ async fn handle_workflow_event(
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
if should_skip_for_dnd(event.tenant_id, pid, "important", db).await {
return Ok(());
}
let date_info = if appointment_date.is_empty() {
String::new()
} else {
format!("{}", appointment_date)
};
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
"预约已确认".to_string(),
format!("您的预约 {} 已确认,请按时就诊。", &appointment_id[..8.min(appointment_id.len())]),
format!("您的预约{}已确认,请按时就诊。", date_info),
"important",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
@@ -265,6 +327,9 @@ async fn handle_workflow_event(
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
@@ -305,7 +370,7 @@ async fn handle_workflow_event(
_ => "偏高",
};
// 通知责任医生(优先)
// 通知责任医生(优先)— urgent 不跳过 DND
if let Some(doctor_uid) = event
.payload
.get("doctor_user_id")
@@ -337,7 +402,6 @@ async fn handle_workflow_event(
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok())
{
// 避免医生和操作人是同一人时重复通知
let is_doctor = event
.payload
.get("doctor_user_id")
@@ -346,6 +410,9 @@ async fn handle_workflow_event(
.unwrap_or(false);
if !is_doctor {
if should_skip_for_dnd(event.tenant_id, operator_uid, "important", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
operator_uid,
@@ -381,15 +448,28 @@ async fn handle_workflow_event(
.get("planned_date")
.and_then(|v| v.as_str())
.unwrap_or("未知日期");
let patient_name = event
.payload
.get("patient_name")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(assignee) = assigned_to {
if should_skip_for_dnd(event.tenant_id, assignee, "important", db).await {
return Ok(());
}
let patient_info = if patient_name.is_empty() {
String::new()
} else {
format!("(患者:{}", patient_name)
};
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
assignee,
"随访任务逾期提醒".to_string(),
format!(
"您的随访任务(计划日期:{})已逾期,请尽快处理。",
planned_date
"您的随访任务{}(计划日期:{})已逾期,请尽快处理。",
patient_info, planned_date
),
"important",
Some("follow_up".to_string()),
@@ -401,6 +481,80 @@ async fn handle_workflow_event(
.map_err(|e| e.to_string())?;
}
}
// 咨询新消息通知医生
"consultation.new_message" => {
let doctor_id = event
.payload
.get("doctor_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let patient_name = event
.payload
.get("patient_name")
.and_then(|v| v.as_str())
.unwrap_or("患者");
let session_id = event
.payload
.get("session_id")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(did) = doctor_id {
if should_skip_for_dnd(event.tenant_id, did, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
did,
format!("新咨询消息 — {}", patient_name),
format!("患者 {} 发来了一条咨询消息,请及时回复。", patient_name),
"normal",
Some("consultation".to_string()),
uuid::Uuid::parse_str(session_id).ok(),
db,
event_bus,
)
.await
.map_err(|e| e.to_string())?;
}
}
// 化验报告审核完成通知患者
"lab_report.reviewed" => {
let patient_id = event
.payload
.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let report_type = event
.payload
.get("report_type")
.and_then(|v| v.as_str())
.unwrap_or("化验报告");
let report_id = event
.payload
.get("report_id")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(pid) = patient_id {
if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await {
return Ok(());
}
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
format!("{}已审核", report_type),
format!("您的{}已由医生审核完成,请查看医生注释。", report_type),
"normal",
Some("lab_report".to_string()),
uuid::Uuid::parse_str(report_id).ok(),
db,
event_bus,
)
.await
.map_err(|e| e.to_string())?;
}
}
_ => {}
}
Ok(())