Files
hms/crates/erp-health/src/event/points.rs
iven ec7f76127d feat(health): 积分触发扩展 + 随访模板关联 — Phase 3
- 新增 follow_up.completed 事件积分消费者,随访完成触发 30 积分
- follow_up_task 新增 template_id FK 关联随访模板
- follow_up_record 新增 form_data JSONB 存储结构化表单数据
- 新增 POST /health/follow-up-tasks/from-template 基于模板创建随访任务端点
- 数据库迁移 160:follow_up_task.template_id + follow_up_record.form_data + 积分规则种子
2026-05-21 00:50:29 +08:00

337 lines
14 KiB
Rust

/// points.earned/exchanged/expired → 积分变动通知
/// daily_monitoring.created → 健康数据上报积分
/// lab_report.uploaded → 化验报告上传积分
/// follow_up.completed → 随访完成积分
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
let mut handles = Vec::new();
let (mut points_rx, points_handle) = state.event_bus.subscribe_filtered("points.".to_string());
handles.push(points_handle);
let points_db = state.db.clone();
let points_bus = state.event_bus.clone();
tokio::spawn(async move {
loop {
match points_rx.recv().await {
Some(event) if event.event_type == super::POINTS_EARNED => {
if erp_core::events::is_event_processed(
&points_db,
event.id,
"points_earned_notifier",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event.payload.get("patient_id").and_then(|v| v.as_str());
let amount = event.payload.get("amount").and_then(|v| v.as_u64());
if let (Some(pid), Some(amt)) = (patient_id, amount) {
let notify = erp_core::events::DomainEvent::new(
"message.send",
event.tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"channel": "in_app",
"recipient_type": "patient",
"recipient_id": pid,
"template_key": "POINTS_EARNED",
"params": { "amount": amt }
})),
);
points_bus.publish(notify, &points_db).await;
tracing::info!(patient_id = pid, amount = amt, "积分获得通知已发送");
}
let _ = erp_core::events::mark_event_processed(
&points_db,
event.id,
"points_earned_notifier",
)
.await;
}
Some(event) if event.event_type == super::POINTS_EXCHANGED => {
if erp_core::events::is_event_processed(
&points_db,
event.id,
"points_exchanged_notifier",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event.payload.get("patient_id").and_then(|v| v.as_str());
let amount = event.payload.get("amount").and_then(|v| v.as_u64());
if let (Some(pid), Some(amt)) = (patient_id, amount) {
let notify = erp_core::events::DomainEvent::new(
"message.send",
event.tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"channel": "in_app",
"recipient_type": "patient",
"recipient_id": pid,
"template_key": "POINTS_EXCHANGED",
"params": { "amount": amt }
})),
);
points_bus.publish(notify, &points_db).await;
tracing::info!(patient_id = pid, amount = amt, "积分兑换通知已发送");
}
let _ = erp_core::events::mark_event_processed(
&points_db,
event.id,
"points_exchanged_notifier",
)
.await;
}
Some(event) if event.event_type == super::POINTS_EXPIRED => {
if erp_core::events::is_event_processed(
&points_db,
event.id,
"points_expired_notifier",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event.payload.get("patient_id").and_then(|v| v.as_str());
let amount = event.payload.get("amount").and_then(|v| v.as_u64());
if let (Some(pid), Some(amt)) = (patient_id, amount) {
let notify = erp_core::events::DomainEvent::new(
"message.send",
event.tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"channel": "in_app",
"recipient_type": "patient",
"recipient_id": pid,
"template_key": "POINTS_EXPIRED",
"params": { "amount": amt }
})),
);
points_bus.publish(notify, &points_db).await;
tracing::info!(patient_id = pid, amount = amt, "积分过期通知已发送");
}
let _ = erp_core::events::mark_event_processed(
&points_db,
event.id,
"points_expired_notifier",
)
.await;
}
Some(_) => {}
None => break,
}
}
});
// daily_monitoring.created → 健康数据上报积分
let (mut dm_rx, dm_handle) = state
.event_bus
.subscribe_filtered("daily_monitoring.".to_string());
handles.push(dm_handle);
let dm_state = state.clone();
tokio::spawn(async move {
loop {
match dm_rx.recv().await {
Some(event) if event.event_type == super::DAILY_MONITORING_CREATED => {
if erp_core::events::is_event_processed(
&dm_state.db,
event.id,
"daily_monitoring_points",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event
.payload
.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
match crate::service::points_service::earn_points(
&dm_state,
event.tenant_id,
pid,
"health_data_report",
None,
)
.await
{
Ok(tx) => {
tracing::info!(
patient_id = %pid,
points = tx.amount,
"健康数据上报积分已发放"
);
}
Err(e) => {
// 无匹配规则时不告警(租户可能未配置该规则)
let err_str = e.to_string();
if !err_str.contains("无匹配的积分规则") {
tracing::warn!(
patient_id = %pid,
error = %e,
"健康数据上报积分发放失败"
);
}
}
}
}
let _ = erp_core::events::mark_event_processed(
&dm_state.db,
event.id,
"daily_monitoring_points",
)
.await;
}
Some(_) => {}
None => break,
}
}
});
// lab_report.uploaded → 化验报告上传积分
let (mut lr_rx, lr_handle) = state
.event_bus
.subscribe_filtered("lab_report.".to_string());
handles.push(lr_handle);
let lr_state = state.clone();
tokio::spawn(async move {
loop {
match lr_rx.recv().await {
Some(event) if event.event_type == super::LAB_REPORT_UPLOADED => {
if erp_core::events::is_event_processed(
&lr_state.db,
event.id,
"lab_report_points",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event
.payload
.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
match crate::service::points_service::earn_points(
&lr_state,
event.tenant_id,
pid,
"lab_report_upload",
None,
)
.await
{
Ok(tx) => {
tracing::info!(
patient_id = %pid,
points = tx.amount,
"化验报告上传积分已发放"
);
}
Err(e) => {
let err_str = e.to_string();
if !err_str.contains("无匹配的积分规则") {
tracing::warn!(
patient_id = %pid,
error = %e,
"化验报告上传积分发放失败"
);
}
}
}
}
let _ = erp_core::events::mark_event_processed(
&lr_state.db,
event.id,
"lab_report_points",
)
.await;
}
Some(_) => {}
None => break,
}
}
});
// follow_up.completed → 随访完成积分
let (mut fu_rx, fu_handle) = state.event_bus.subscribe_filtered("follow_up.".to_string());
handles.push(fu_handle);
let fu_state = state.clone();
tokio::spawn(async move {
loop {
match fu_rx.recv().await {
Some(event) if event.event_type == super::FOLLOW_UP_COMPLETED => {
if erp_core::events::is_event_processed(
&fu_state.db,
event.id,
"follow_up_points",
)
.await
.unwrap_or(false)
{
continue;
}
let patient_id = event
.payload
.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
if let Some(pid) = patient_id {
match crate::service::points_service::earn_points(
&fu_state,
event.tenant_id,
pid,
"follow_up_completion",
None,
)
.await
{
Ok(tx) => {
tracing::info!(
patient_id = %pid,
points = tx.amount,
"随访完成积分已发放"
);
}
Err(e) => {
let err_str = e.to_string();
if !err_str.contains("无匹配的积分规则") {
tracing::warn!(
patient_id = %pid,
error = %e,
"随访完成积分发放失败"
);
}
}
}
}
let _ = erp_core::events::mark_event_processed(
&fu_state.db,
event.id,
"follow_up_points",
)
.await;
}
Some(_) => {}
None => break,
}
}
});
handles
}