feat(health): Phase 4 跨模块集成与架构优化 — 通知/标签/待办/数据录入
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

后端:
- erp-message: 添加 appointment.created/confirmed/cancelled 事件监听,自动发送站内通知
- erp-health: 新增 GET /health/patient-tags 标签列表端点 + list_tags service
- wechat-templates: 添加 isTemplateConfigured 运行时校验

前端:
- 新增 Zustand useHealthStore 共享患者/医生名称缓存
- PatientTagManage: UUID 输入替换为 Checkbox 标签选择器
- VitalSignsTab: 添加体征数据录入 Modal (血压/心率/体重/血糖)
- LabReportsTab: 添加化验报告创建 Modal
- HealthRecordsTab: 添加健康记录创建 Modal
- patients API: 添加 TagItem 类型 + listTags 方法

小程序:
- 首页待办事项接入预约和随访 API,替换硬编码 EmptyState
This commit is contained in:
iven
2026-04-25 20:10:50 +08:00
parent 5b520a168c
commit d2baacae7e
14 changed files with 667 additions and 222 deletions

View File

@@ -130,3 +130,11 @@ pub struct PatientListQuery {
pub tag_id: Option<Uuid>,
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct TagResp {
pub id: Uuid,
pub name: String,
pub color: Option<String>,
pub description: Option<String>,
}

View File

@@ -306,3 +306,16 @@ pub struct FamilyMemberUpdateWithVersion {
pub notes: Option<String>,
pub version: i32,
}
pub async fn list_tags<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<Vec<crate::dto::patient_dto::TagResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.patient.list")?;
let tags = patient_service::list_tags(&state, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(tags)))
}

View File

@@ -69,6 +69,10 @@ impl HealthModule {
"/health/patients/{id}/tags",
axum::routing::post(patient_handler::manage_patient_tags),
)
.route(
"/health/patient-tags",
axum::routing::get(patient_handler::list_tags),
)
.route(
"/health/patients/{id}/health-summary",
axum::routing::get(patient_handler::get_health_summary),

View File

@@ -760,3 +760,27 @@ fn model_to_resp_decrypted(crypto: &crate::crypto::HealthCrypto, m: patient::Mod
}
}
pub async fn list_tags(
state: &crate::state::HealthState,
tenant_id: Uuid,
) -> HealthResult<Vec<crate::dto::patient_dto::TagResp>> {
use crate::entity::patient_tag;
let tags = patient_tag::Entity::find()
.filter(patient_tag::Column::TenantId.eq(tenant_id))
.filter(patient_tag::Column::DeletedAt.is_null())
.order_by_asc(patient_tag::Column::Name)
.all(&state.db)
.await
.map_err(|e| crate::error::HealthError::DbError(e.to_string()))?;
Ok(tags
.into_iter()
.map(|t| crate::dto::patient_dto::TagResp {
id: t.id,
name: t.name,
color: t.color,
description: t.description,
})
.collect())
}

View File

@@ -195,6 +195,91 @@ async fn handle_workflow_event(
.map_err(|e| e.to_string())?;
}
}
// 预约事件通知
"appointment.created" => {
let appointment_id = event
.payload
.get("appointment_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
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 {
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
"预约已创建".to_string(),
format!("您的新预约 {} 已创建,请等待确认。", &appointment_id[..8.min(appointment_id.len())]),
"normal",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
db,
event_bus,
)
.await
.map_err(|e| e.to_string())?;
}
}
"appointment.confirmed" => {
let appointment_id = event
.payload
.get("appointment_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
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 {
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
"预约已确认".to_string(),
format!("您的预约 {} 已确认,请按时就诊。", &appointment_id[..8.min(appointment_id.len())]),
"important",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
db,
event_bus,
)
.await
.map_err(|e| e.to_string())?;
}
}
"appointment.cancelled" => {
let appointment_id = event
.payload
.get("appointment_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
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 {
let _ = crate::service::message_service::MessageService::send_system(
event.tenant_id,
pid,
"预约已取消".to_string(),
format!("您的预约 {} 已被取消。", &appointment_id[..8.min(appointment_id.len())]),
"normal",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
db,
event_bus,
)
.await
.map_err(|e| e.to_string())?;
}
}
_ => {}
}
Ok(())