- DTO: SystemHealthResp, UserActivityResp, ModuleStatusResp, PointsActivityItem, ArticleStatsResp - Service: get_article_stats, get_points_recent_activity, get_module_status, get_user_activity, get_system_health - Handler: 5 个新端点 + 权限码 health.dashboard.manage - 路由: /health/admin/system-health, user-activity, modules, points/recent-activity, articles/stats
1131 lines
47 KiB
Rust
1131 lines
47 KiB
Rust
use axum::Router;
|
||
use uuid::Uuid;
|
||
|
||
use erp_core::error::AppResult;
|
||
use erp_core::events::EventBus;
|
||
use erp_core::module::{ErpModule, PermissionDescriptor};
|
||
|
||
use crate::handler::{
|
||
action_inbox_handler,
|
||
alert_handler, alert_rule_handler,
|
||
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_handler, device_reading_handler, diagnosis_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||
health_data_handler, medication_record_handler, medication_reminder_handler, patient_handler, points_handler, stats_handler,
|
||
};
|
||
|
||
pub struct HealthModule;
|
||
|
||
impl HealthModule {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
|
||
/// 启动定时逾期随访检查(每 6 小时运行一次),返回 JoinHandle 用于优雅关闭
|
||
pub fn start_overdue_checker(db: sea_orm::DatabaseConnection) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(6 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::follow_up_service::check_overdue_tasks(&db).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "随访逾期检查完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "随访逾期检查失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("随访逾期检查任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动积分过期清理(每 24 小时运行一次),返回 JoinHandle 用于优雅关闭
|
||
pub fn start_points_expiration_checker(db: sea_orm::DatabaseConnection, event_bus: erp_core::events::EventBus) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::points_service::expire_points(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "积分过期清理完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "积分过期清理失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("积分过期清理任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动预约提醒调度(每 1 小时运行一次),扫描明天有预约的患者发送提醒
|
||
pub fn start_appointment_reminder(db: sea_orm::DatabaseConnection, event_bus: erp_core::events::EventBus) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::appointment_service::send_reminders(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "预约提醒发送完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "预约提醒发送失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("预约提醒调度任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动设备原始数据清理(每 24 小时运行一次),删除超过 90 天的 device_readings
|
||
pub fn start_device_readings_cleanup(db: sea_orm::DatabaseConnection) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::device_reading_service::cleanup_stale_readings(&db).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "设备原始数据清理完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "设备原始数据清理失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("设备原始数据清理任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
pub fn public_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
}
|
||
|
||
pub fn protected_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
// 患者管理
|
||
.route(
|
||
"/health/patients",
|
||
axum::routing::get(patient_handler::list_patients)
|
||
.post(patient_handler::create_patient),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}",
|
||
axum::routing::get(patient_handler::get_patient)
|
||
.put(patient_handler::update_patient)
|
||
.delete(patient_handler::delete_patient),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/tags",
|
||
axum::routing::post(patient_handler::manage_patient_tags),
|
||
)
|
||
.route(
|
||
"/health/patient-tags",
|
||
axum::routing::get(patient_handler::list_tags)
|
||
.post(patient_handler::create_tag),
|
||
)
|
||
.route(
|
||
"/health/patient-tags/{id}",
|
||
axum::routing::put(patient_handler::update_tag)
|
||
.delete(patient_handler::delete_tag),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/health-summary",
|
||
axum::routing::get(patient_handler::get_health_summary),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/family-members",
|
||
axum::routing::get(patient_handler::list_family_members)
|
||
.post(patient_handler::create_family_member),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/family-members/{fid}",
|
||
axum::routing::put(patient_handler::update_family_member)
|
||
.delete(patient_handler::delete_family_member),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/doctors",
|
||
axum::routing::post(patient_handler::assign_doctor),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/doctors/{did}",
|
||
axum::routing::delete(patient_handler::remove_doctor),
|
||
)
|
||
// 健康数据
|
||
.route(
|
||
"/health/patients/{id}/vital-signs",
|
||
axum::routing::get(health_data_handler::list_vital_signs)
|
||
.post(health_data_handler::create_vital_signs),
|
||
)
|
||
// 诊断记录
|
||
.route(
|
||
"/health/patients/{id}/diagnoses",
|
||
axum::routing::get(diagnosis_handler::list_diagnoses)
|
||
.post(diagnosis_handler::create_diagnosis),
|
||
)
|
||
.route(
|
||
"/health/diagnoses/{id}",
|
||
axum::routing::put(diagnosis_handler::update_diagnosis)
|
||
.delete(diagnosis_handler::delete_diagnosis),
|
||
)
|
||
// 用药记录
|
||
.route(
|
||
"/health/patients/{id}/medications",
|
||
axum::routing::get(medication_record_handler::list_medications),
|
||
)
|
||
.route(
|
||
"/health/medications",
|
||
axum::routing::post(medication_record_handler::create_medication),
|
||
)
|
||
.route(
|
||
"/health/medications/{id}",
|
||
axum::routing::get(medication_record_handler::get_medication)
|
||
.put(medication_record_handler::update_medication)
|
||
.delete(medication_record_handler::delete_medication),
|
||
)
|
||
// 药物提醒 CRUD
|
||
.route(
|
||
"/health/patients/{id}/medication-reminders",
|
||
axum::routing::get(medication_reminder_handler::list_reminders),
|
||
)
|
||
.route(
|
||
"/health/medication-reminders",
|
||
axum::routing::post(medication_reminder_handler::create_reminder),
|
||
)
|
||
.route(
|
||
"/health/medication-reminders/{id}",
|
||
axum::routing::put(medication_reminder_handler::update_reminder)
|
||
.delete(medication_reminder_handler::delete_reminder),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/vital-signs/{vid}",
|
||
axum::routing::put(health_data_handler::update_vital_signs)
|
||
.delete(health_data_handler::delete_vital_signs),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/lab-reports",
|
||
axum::routing::get(health_data_handler::list_lab_reports)
|
||
.post(health_data_handler::create_lab_report),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/lab-reports/{rid}",
|
||
axum::routing::put(health_data_handler::update_lab_report)
|
||
.delete(health_data_handler::delete_lab_report),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/health-records",
|
||
axum::routing::get(health_data_handler::list_health_records)
|
||
.post(health_data_handler::create_health_record),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/health-records/{rid}",
|
||
axum::routing::put(health_data_handler::update_health_record)
|
||
.delete(health_data_handler::delete_health_record),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/trends",
|
||
axum::routing::get(health_data_handler::list_trends),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/trends/generate",
|
||
axum::routing::post(health_data_handler::generate_trend),
|
||
)
|
||
.route(
|
||
"/health/patients/{id}/trends/{indicator}",
|
||
axum::routing::get(health_data_handler::get_indicator_timeseries),
|
||
)
|
||
// 小程序趋势查询(通过 JWT user_id 关联 patient,无需传 patient_id)
|
||
.route(
|
||
"/health/vital-signs/trend",
|
||
axum::routing::get(health_data_handler::get_mini_trend),
|
||
)
|
||
// 小程序今日体征摘要
|
||
.route(
|
||
"/health/vital-signs/today",
|
||
axum::routing::get(health_data_handler::get_mini_today),
|
||
)
|
||
// 随访模板
|
||
.route(
|
||
"/health/follow-up-templates",
|
||
axum::routing::get(follow_up_template_handler::list_templates)
|
||
.post(follow_up_template_handler::create_template),
|
||
)
|
||
.route(
|
||
"/health/follow-up-templates/{id}",
|
||
axum::routing::get(follow_up_template_handler::get_template)
|
||
.put(follow_up_template_handler::update_template)
|
||
.delete(follow_up_template_handler::delete_template),
|
||
)
|
||
// 日常监测
|
||
.route(
|
||
"/health/patients/{id}/daily-monitoring",
|
||
axum::routing::get(daily_monitoring_handler::list_daily_monitoring),
|
||
)
|
||
.route(
|
||
"/health/daily-monitoring",
|
||
axum::routing::post(daily_monitoring_handler::create_daily_monitoring),
|
||
)
|
||
.route(
|
||
"/health/daily-monitoring/{id}",
|
||
axum::routing::get(daily_monitoring_handler::get_daily_monitoring)
|
||
.put(daily_monitoring_handler::update_daily_monitoring)
|
||
.delete(daily_monitoring_handler::delete_daily_monitoring),
|
||
)
|
||
// 化验报告审阅
|
||
.route(
|
||
"/health/patients/{id}/lab-reports/{rid}/review",
|
||
axum::routing::put(health_data_handler::review_lab_report),
|
||
)
|
||
// 预约排班
|
||
.route(
|
||
"/health/appointments",
|
||
axum::routing::get(appointment_handler::list_appointments)
|
||
.post(appointment_handler::create_appointment),
|
||
)
|
||
.route(
|
||
"/health/appointments/{id}/status",
|
||
axum::routing::put(appointment_handler::update_appointment_status),
|
||
)
|
||
.route(
|
||
"/health/appointments/{id}",
|
||
axum::routing::get(appointment_handler::get_appointment),
|
||
)
|
||
.route(
|
||
"/health/doctor-schedules",
|
||
axum::routing::get(appointment_handler::list_schedules)
|
||
.post(appointment_handler::create_schedule),
|
||
)
|
||
.route(
|
||
"/health/doctor-schedules/{id}",
|
||
axum::routing::put(appointment_handler::update_schedule),
|
||
)
|
||
.route(
|
||
"/health/doctor-schedules/calendar",
|
||
axum::routing::get(appointment_handler::calendar_view),
|
||
)
|
||
// 随访管理
|
||
.route(
|
||
"/health/follow-up-tasks",
|
||
axum::routing::get(follow_up_handler::list_tasks)
|
||
.post(follow_up_handler::create_task),
|
||
)
|
||
.route(
|
||
"/health/follow-up-tasks/{id}",
|
||
axum::routing::get(follow_up_handler::get_task)
|
||
.put(follow_up_handler::update_task)
|
||
.delete(follow_up_handler::delete_task),
|
||
)
|
||
.route(
|
||
"/health/follow-up-tasks/{id}/records",
|
||
axum::routing::post(follow_up_handler::create_record),
|
||
)
|
||
.route(
|
||
"/health/follow-up-records",
|
||
axum::routing::get(follow_up_handler::list_records),
|
||
)
|
||
// 随访批量操作
|
||
.route(
|
||
"/health/follow-up-tasks/batch-create",
|
||
axum::routing::post(follow_up_handler::batch_create_tasks),
|
||
)
|
||
.route(
|
||
"/health/follow-up-tasks/batch-assign",
|
||
axum::routing::post(follow_up_handler::batch_assign_tasks),
|
||
)
|
||
.route(
|
||
"/health/follow-up-tasks/batch-complete",
|
||
axum::routing::post(follow_up_handler::batch_complete_tasks),
|
||
)
|
||
// 咨询管理
|
||
.route(
|
||
"/health/consultation-sessions",
|
||
axum::routing::get(consultation_handler::list_sessions)
|
||
.post(consultation_handler::create_session),
|
||
)
|
||
.route(
|
||
"/health/consultation-sessions/export",
|
||
axum::routing::get(consultation_handler::export_sessions),
|
||
)
|
||
.route(
|
||
"/health/consultation-sessions/{id}",
|
||
axum::routing::get(consultation_handler::get_session),
|
||
)
|
||
.route(
|
||
"/health/consultation-sessions/{id}/messages",
|
||
axum::routing::get(consultation_handler::list_messages),
|
||
)
|
||
.route(
|
||
"/health/consultation-sessions/{id}/close",
|
||
axum::routing::put(consultation_handler::close_session),
|
||
)
|
||
.route(
|
||
"/health/consultation-sessions/{id}/read",
|
||
axum::routing::put(consultation_handler::mark_session_read),
|
||
)
|
||
.route(
|
||
"/health/consultation-messages",
|
||
axum::routing::post(consultation_handler::create_message),
|
||
)
|
||
// 医生仪表盘
|
||
.route(
|
||
"/health/doctor/dashboard",
|
||
axum::routing::get(consultation_handler::get_doctor_dashboard),
|
||
)
|
||
// 医护管理
|
||
.route(
|
||
"/health/doctors",
|
||
axum::routing::get(doctor_handler::list_doctors)
|
||
.post(doctor_handler::create_doctor),
|
||
)
|
||
.route(
|
||
"/health/doctors/{id}",
|
||
axum::routing::get(doctor_handler::get_doctor)
|
||
.put(doctor_handler::update_doctor)
|
||
.delete(doctor_handler::delete_doctor),
|
||
)
|
||
// 健康资讯
|
||
.route(
|
||
"/health/articles",
|
||
axum::routing::get(article_handler::list_articles)
|
||
.post(article_handler::create_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}",
|
||
axum::routing::get(article_handler::get_article)
|
||
.put(article_handler::update_article)
|
||
.delete(article_handler::delete_article),
|
||
)
|
||
// 资讯审核工作流
|
||
.route(
|
||
"/health/articles/{id}/submit",
|
||
axum::routing::post(article_handler::submit_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}/approve",
|
||
axum::routing::post(article_handler::approve_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}/reject",
|
||
axum::routing::post(article_handler::reject_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}/unpublish",
|
||
axum::routing::post(article_handler::unpublish_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}/view",
|
||
axum::routing::post(article_handler::view_article),
|
||
)
|
||
.route(
|
||
"/health/articles/{id}/revisions",
|
||
axum::routing::get(article_handler::list_revisions),
|
||
)
|
||
// 资讯分类
|
||
.route(
|
||
"/health/article-categories",
|
||
axum::routing::get(article_category_handler::list_categories)
|
||
.post(article_category_handler::create_category),
|
||
)
|
||
.route(
|
||
"/health/article-categories/{id}",
|
||
axum::routing::put(article_category_handler::update_category)
|
||
.delete(article_category_handler::delete_category),
|
||
)
|
||
// 资讯标签
|
||
.route(
|
||
"/health/article-tags",
|
||
axum::routing::get(article_tag_handler::list_tags)
|
||
.post(article_tag_handler::create_tag),
|
||
)
|
||
.route(
|
||
"/health/article-tags/{id}",
|
||
axum::routing::put(article_tag_handler::update_tag)
|
||
.delete(article_tag_handler::delete_tag),
|
||
)
|
||
// 积分商城 — 患者端
|
||
.route(
|
||
"/health/points/account",
|
||
axum::routing::get(points_handler::get_my_account),
|
||
)
|
||
.route(
|
||
"/health/points/checkin",
|
||
axum::routing::post(points_handler::daily_checkin),
|
||
)
|
||
.route(
|
||
"/health/points/checkin/status",
|
||
axum::routing::get(points_handler::get_checkin_status),
|
||
)
|
||
.route(
|
||
"/health/points/transactions",
|
||
axum::routing::get(points_handler::list_my_transactions),
|
||
)
|
||
.route(
|
||
"/health/points/products",
|
||
axum::routing::get(points_handler::list_products),
|
||
)
|
||
.route(
|
||
"/health/points/products/{id}",
|
||
axum::routing::get(points_handler::get_product),
|
||
)
|
||
.route(
|
||
"/health/points/exchange",
|
||
axum::routing::post(points_handler::exchange_product),
|
||
)
|
||
.route(
|
||
"/health/points/orders",
|
||
axum::routing::get(points_handler::list_my_orders),
|
||
)
|
||
// 线下活动 — 患者端
|
||
.route(
|
||
"/health/offline-events",
|
||
axum::routing::get(points_handler::list_offline_events),
|
||
)
|
||
.route(
|
||
"/health/offline-events/{id}/register",
|
||
axum::routing::post(points_handler::register_event),
|
||
)
|
||
// 积分商城 — 管理端
|
||
.route(
|
||
"/health/points/verify",
|
||
axum::routing::post(points_handler::verify_order),
|
||
)
|
||
.route(
|
||
"/health/admin/points/rules",
|
||
axum::routing::get(points_handler::list_rules)
|
||
.post(points_handler::create_rule),
|
||
)
|
||
.route(
|
||
"/health/admin/points/rules/{id}",
|
||
axum::routing::put(points_handler::update_rule)
|
||
.delete(points_handler::delete_rule),
|
||
)
|
||
.route(
|
||
"/health/admin/points/products",
|
||
axum::routing::get(points_handler::admin_list_products)
|
||
.post(points_handler::admin_create_product),
|
||
)
|
||
.route(
|
||
"/health/admin/points/products/{id}",
|
||
axum::routing::put(points_handler::admin_update_product)
|
||
.delete(points_handler::admin_delete_product),
|
||
)
|
||
.route(
|
||
"/health/admin/points/orders",
|
||
axum::routing::get(points_handler::admin_list_orders),
|
||
)
|
||
// 积分账户 — 管理端按患者查询
|
||
.route(
|
||
"/health/admin/points/patients/{patient_id}/account",
|
||
axum::routing::get(points_handler::admin_get_patient_account),
|
||
)
|
||
.route(
|
||
"/health/admin/points/patients/{patient_id}/transactions",
|
||
axum::routing::get(points_handler::admin_list_patient_transactions),
|
||
)
|
||
// 线下活动 — 管理端
|
||
.route(
|
||
"/health/admin/offline-events",
|
||
axum::routing::get(points_handler::admin_list_events)
|
||
.post(points_handler::admin_create_event),
|
||
)
|
||
.route(
|
||
"/health/admin/offline-events/{id}",
|
||
axum::routing::put(points_handler::admin_update_event)
|
||
.delete(points_handler::admin_delete_event),
|
||
)
|
||
.route(
|
||
"/health/admin/offline-events/{id}/checkin",
|
||
axum::routing::post(points_handler::admin_checkin_event),
|
||
)
|
||
// 积分统计 — 管理端
|
||
.route(
|
||
"/health/admin/points/statistics",
|
||
axum::routing::get(points_handler::get_points_statistics),
|
||
)
|
||
// 统计数据 — 管理端
|
||
.route(
|
||
"/health/admin/statistics/patients",
|
||
axum::routing::get(stats_handler::get_patient_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/consultations",
|
||
axum::routing::get(stats_handler::get_consultation_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/follow-ups",
|
||
axum::routing::get(stats_handler::get_follow_up_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/dashboard",
|
||
axum::routing::get(stats_handler::get_dashboard_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/lab-reports",
|
||
axum::routing::get(stats_handler::get_lab_report_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/appointments",
|
||
axum::routing::get(stats_handler::get_appointment_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/vital-signs-report-rate",
|
||
axum::routing::get(stats_handler::get_vital_signs_report_rate),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/health-data",
|
||
axum::routing::get(stats_handler::get_health_data_stats),
|
||
)
|
||
.route(
|
||
"/health/admin/statistics/personal-stats",
|
||
axum::routing::get(stats_handler::get_personal_stats),
|
||
)
|
||
// 工作台管理统计 API
|
||
.route(
|
||
"/health/admin/system-health",
|
||
axum::routing::get(stats_handler::get_system_health),
|
||
)
|
||
.route(
|
||
"/health/admin/user-activity",
|
||
axum::routing::get(stats_handler::get_user_activity),
|
||
)
|
||
.route(
|
||
"/health/admin/modules",
|
||
axum::routing::get(stats_handler::get_module_status),
|
||
)
|
||
.route(
|
||
"/health/points/recent-activity",
|
||
axum::routing::get(stats_handler::get_points_recent_activity),
|
||
)
|
||
.route(
|
||
"/health/articles/stats",
|
||
axum::routing::get(stats_handler::get_article_stats),
|
||
)
|
||
// 危急值阈值配置
|
||
.route(
|
||
"/health/critical-value-thresholds",
|
||
axum::routing::get(critical_value_threshold_handler::list_thresholds)
|
||
.post(critical_value_threshold_handler::create_threshold),
|
||
)
|
||
.route(
|
||
"/health/critical-value-thresholds/{id}",
|
||
axum::routing::put(critical_value_threshold_handler::update_threshold)
|
||
.delete(critical_value_threshold_handler::delete_threshold),
|
||
)
|
||
// 患者端只读阈值(仅需认证)
|
||
.route(
|
||
"/health/critical-value-thresholds/public",
|
||
axum::routing::get(critical_value_threshold_handler::list_public_thresholds),
|
||
)
|
||
// 知情同意记录
|
||
.route(
|
||
"/health/patients/{patient_id}/consents",
|
||
axum::routing::get(consent_handler::list_consents),
|
||
)
|
||
.route(
|
||
"/health/consents",
|
||
axum::routing::post(consent_handler::grant_consent),
|
||
)
|
||
.route(
|
||
"/health/consents/{consent_id}/revoke",
|
||
axum::routing::put(consent_handler::revoke_consent),
|
||
)
|
||
// 设备数据采集
|
||
.route(
|
||
"/health/patients/{patient_id}/device-readings/batch",
|
||
axum::routing::post(device_reading_handler::batch_create),
|
||
)
|
||
.route(
|
||
"/health/patients/{patient_id}/device-readings",
|
||
axum::routing::get(device_reading_handler::list_readings),
|
||
)
|
||
.route(
|
||
"/health/patients/{patient_id}/device-readings/hourly",
|
||
axum::routing::get(device_reading_handler::list_hourly),
|
||
)
|
||
// 告警路由
|
||
.route(
|
||
"/health/alerts",
|
||
axum::routing::get(alert_handler::list_alerts),
|
||
)
|
||
.route(
|
||
"/health/alerts/{id}/acknowledge",
|
||
axum::routing::put(alert_handler::acknowledge),
|
||
)
|
||
.route(
|
||
"/health/alerts/{id}/dismiss",
|
||
axum::routing::put(alert_handler::dismiss),
|
||
)
|
||
.route(
|
||
"/health/alerts/{id}/resolve",
|
||
axum::routing::put(alert_handler::resolve),
|
||
)
|
||
// 危急值告警路由
|
||
.route(
|
||
"/health/critical-alerts",
|
||
axum::routing::get(critical_alert_handler::list_critical_alerts),
|
||
)
|
||
.route(
|
||
"/health/critical-alerts/{id}",
|
||
axum::routing::get(critical_alert_handler::get_critical_alert),
|
||
)
|
||
.route(
|
||
"/health/critical-alerts/{id}/acknowledge",
|
||
axum::routing::post(critical_alert_handler::acknowledge_critical_alert),
|
||
)
|
||
.route(
|
||
"/health/alert-rules",
|
||
axum::routing::get(alert_rule_handler::list_rules)
|
||
.post(alert_rule_handler::create),
|
||
)
|
||
.route(
|
||
"/health/alert-rules/{id}",
|
||
axum::routing::put(alert_rule_handler::update),
|
||
)
|
||
.route(
|
||
"/health/alert-rules/{id}/deactivate",
|
||
axum::routing::put(alert_rule_handler::deactivate),
|
||
)
|
||
// 设备管理
|
||
.route(
|
||
"/health/devices",
|
||
axum::routing::get(device_handler::list_devices),
|
||
)
|
||
.route(
|
||
"/health/devices/{id}",
|
||
axum::routing::delete(device_handler::unbind_device),
|
||
)
|
||
// 行动收件箱
|
||
.route(
|
||
"/health/action-inbox",
|
||
axum::routing::get(action_inbox_handler::list_action_inbox),
|
||
)
|
||
.route(
|
||
"/health/action-inbox/stats",
|
||
axum::routing::get(action_inbox_handler::get_workbench_stats),
|
||
)
|
||
.route(
|
||
"/health/action-inbox/team",
|
||
axum::routing::get(action_inbox_handler::get_team_overview),
|
||
)
|
||
.route(
|
||
"/health/action-inbox/{source_ref}/thread",
|
||
axum::routing::get(action_inbox_handler::get_action_thread),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for HealthModule {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl ErpModule for HealthModule {
|
||
fn name(&self) -> &str {
|
||
"health"
|
||
}
|
||
|
||
fn version(&self) -> &str {
|
||
env!("CARGO_PKG_VERSION")
|
||
}
|
||
|
||
fn dependencies(&self) -> Vec<&str> {
|
||
vec!["auth"]
|
||
}
|
||
|
||
fn register_event_handlers(&self, _bus: &EventBus) {
|
||
// 事件处理器已迁移到 on_startup,此处保留空实现以兼容 trait 签名
|
||
}
|
||
|
||
async fn on_startup(&self, ctx: &erp_core::module::ModuleContext) -> erp_core::error::AppResult<()> {
|
||
let crypto = match erp_core::crypto::PiiCrypto::from_kek_hex(
|
||
&std::env::var("ERP__CRYPTO__KEK").unwrap_or_default(),
|
||
) {
|
||
Ok(c) => c,
|
||
Err(_) => {
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
tracing::warn!("ERP__CRYPTO__KEK 未设置或无效,使用开发默认密钥");
|
||
erp_core::crypto::PiiCrypto::dev_default()
|
||
}
|
||
#[cfg(not(debug_assertions))]
|
||
{
|
||
panic!("ERP__CRYPTO__KEK 必须设置为有效的 64 字符 hex 字符串(生产环境不允许回退到开发密钥)");
|
||
}
|
||
}
|
||
};
|
||
|
||
let state = crate::state::HealthState {
|
||
db: ctx.db.clone(),
|
||
event_bus: ctx.event_bus.clone(),
|
||
crypto,
|
||
};
|
||
|
||
crate::event::register_handlers_with_state(state.clone());
|
||
tracing::info!(module = "health", "Health module event handlers registered via on_startup");
|
||
|
||
// 启动逾期随访检查器(立即执行一次 + 每 6 小时重复)
|
||
{
|
||
let state_clone = state.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::follow_up_service::check_overdue_and_notify(&state_clone).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "启动时逾期随访检查完成"),
|
||
Ok(_) => tracing::info!("启动时逾期随访检查完成(无逾期任务)"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时逾期随访检查失败"),
|
||
}
|
||
});
|
||
}
|
||
let _overdue_handle = Self::start_overdue_checker(state.db.clone());
|
||
tracing::info!(module = "health", "Overdue follow-up checker started");
|
||
|
||
// 启动积分过期清理(启动时执行一次 + 每 24 小时重复)
|
||
{
|
||
let db = ctx.db.clone();
|
||
let event_bus = ctx.event_bus.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::points_service::expire_points(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "启动时积分过期清理完成"),
|
||
Ok(_) => tracing::info!("启动时积分过期清理完成(无过期积分)"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时积分过期清理失败"),
|
||
}
|
||
});
|
||
}
|
||
let _expire_handle = Self::start_points_expiration_checker(ctx.db.clone(), ctx.event_bus.clone());
|
||
tracing::info!(module = "health", "Points expiration checker started");
|
||
|
||
// 启动预约提醒调度(启动时立即执行一次 + 每 1 小时重复)
|
||
{
|
||
let db = ctx.db.clone();
|
||
let event_bus = ctx.event_bus.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::appointment_service::send_reminders(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "启动时预约提醒发送完成"),
|
||
Ok(_) => tracing::info!("启动时预约提醒检查完成"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时预约提醒发送失败"),
|
||
}
|
||
});
|
||
}
|
||
let _reminder_handle = Self::start_appointment_reminder(ctx.db.clone(), ctx.event_bus.clone());
|
||
tracing::info!(module = "health", "Appointment reminder scheduler started");
|
||
|
||
// 启动设备原始数据清理(每 24 小时删除超过 90 天的数据)
|
||
let _cleanup_handle = Self::start_device_readings_cleanup(ctx.db.clone());
|
||
tracing::info!(module = "health", "Device readings cleanup task started");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn on_tenant_created(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
_event_bus: &EventBus,
|
||
) -> AppResult<()> {
|
||
crate::service::seed::seed_tenant_health(db, tenant_id)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
tracing::info!(tenant_id = %tenant_id, "Health module tenant initialized");
|
||
Ok(())
|
||
}
|
||
|
||
async fn on_tenant_deleted(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> AppResult<()> {
|
||
crate::service::seed::soft_delete_tenant_data(db, tenant_id)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
tracing::info!(tenant_id = %tenant_id, "Health module tenant data soft-deleted");
|
||
Ok(())
|
||
}
|
||
|
||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||
vec![
|
||
PermissionDescriptor {
|
||
code: "health.patient.list".into(),
|
||
name: "查看患者列表".into(),
|
||
description: "查看和搜索患者列表、详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.patient.manage".into(),
|
||
name: "管理患者".into(),
|
||
description: "创建、编辑、删除患者".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.health-data.list".into(),
|
||
name: "查看健康数据".into(),
|
||
description: "查看体检记录、监测数据、化验报告".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.health-data.manage".into(),
|
||
name: "管理健康数据".into(),
|
||
description: "录入、编辑、删除健康数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.appointment.list".into(),
|
||
name: "查看预约".into(),
|
||
description: "查看预约列表和排班".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.appointment.manage".into(),
|
||
name: "管理预约".into(),
|
||
description: "创建、确认、取消预约".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up.list".into(),
|
||
name: "查看随访".into(),
|
||
description: "查看随访任务和记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up.manage".into(),
|
||
name: "管理随访".into(),
|
||
description: "创建、分配、完成随访任务".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consultation.list".into(),
|
||
name: "查看咨询".into(),
|
||
description: "查看咨询会话和消息记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consultation.manage".into(),
|
||
name: "管理咨询".into(),
|
||
description: "关闭会话、导出记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.doctor.list".into(),
|
||
name: "查看医护".into(),
|
||
description: "查看医护列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.doctor.manage".into(),
|
||
name: "管理医护".into(),
|
||
description: "创建、编辑医护档案、排班".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.list".into(),
|
||
name: "查看资讯".into(),
|
||
description: "查看健康资讯文章列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.manage".into(),
|
||
name: "管理资讯".into(),
|
||
description: "创建、编辑、删除健康资讯文章".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.review".into(),
|
||
name: "审核资讯".into(),
|
||
description: "审核通过或拒绝资讯文章发布".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.points.list".into(),
|
||
name: "查看积分".into(),
|
||
description: "查看积分规则、订单列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.points.manage".into(),
|
||
name: "管理积分".into(),
|
||
description: "创建积分规则、管理商品、核销订单".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.device-readings.list".into(),
|
||
name: "查看设备数据".into(),
|
||
description: "查看患者的设备采集数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.device-readings.manage".into(),
|
||
name: "管理设备数据".into(),
|
||
description: "提交设备采集数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.devices.list".into(),
|
||
name: "查看设备绑定".into(),
|
||
description: "查看设备绑定记录列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.devices.manage".into(),
|
||
name: "管理设备绑定".into(),
|
||
description: "解绑设备".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alerts.list".into(),
|
||
name: "查看告警".into(),
|
||
description: "查看告警记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alerts.manage".into(),
|
||
name: "管理告警".into(),
|
||
description: "确认/处置告警".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alert-rules.list".into(),
|
||
name: "查看告警规则".into(),
|
||
description: "查看告警规则配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alert-rules.manage".into(),
|
||
name: "管理告警规则".into(),
|
||
description: "创建/编辑/启停告警规则".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-alerts.list".into(),
|
||
name: "查看危急值告警".into(),
|
||
description: "查看危急值告警列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-alerts.manage".into(),
|
||
name: "处理危急值告警".into(),
|
||
description: "确认危急值告警".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-value-thresholds.list".into(),
|
||
name: "查看危急值阈值".into(),
|
||
description: "查看危急值阈值配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-value-thresholds.manage".into(),
|
||
name: "管理危急值阈值".into(),
|
||
description: "创建/编辑/删除危急值阈值配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up-templates.list".into(),
|
||
name: "查看随访模板".into(),
|
||
description: "查看随访模板列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up-templates.manage".into(),
|
||
name: "管理随访模板".into(),
|
||
description: "创建/编辑/删除随访模板".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.daily-monitoring.list".into(),
|
||
name: "查看日常监测".into(),
|
||
description: "查看患者日常监测数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.daily-monitoring.manage".into(),
|
||
name: "管理日常监测".into(),
|
||
description: "录入/编辑/删除日常监测数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consent.list".into(),
|
||
name: "查看知情同意".into(),
|
||
description: "查看患者知情同意记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consent.manage".into(),
|
||
name: "管理知情同意".into(),
|
||
description: "签署/撤销知情同意".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-records.list".into(),
|
||
name: "查看用药记录".into(),
|
||
description: "查看患者用药记录列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-records.manage".into(),
|
||
name: "管理用药记录".into(),
|
||
description: "创建/编辑/删除用药记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-reminders.list".into(),
|
||
name: "查看药物提醒".into(),
|
||
description: "查看患者药物提醒列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-reminders.manage".into(),
|
||
name: "管理药物提醒".into(),
|
||
description: "创建/编辑/删除药物提醒".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 行动收件箱
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.list".into(),
|
||
name: "查看行动收件箱".into(),
|
||
description: "查看统一行动收件箱中的待办事项".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.manage".into(),
|
||
name: "管理行动项".into(),
|
||
description: "审批/拒绝/标记行动收件箱中的事项".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.team".into(),
|
||
name: "查看团队概览".into(),
|
||
description: "查看科室团队工作负载和风险分布(主任专属)".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 工作台管理
|
||
PermissionDescriptor {
|
||
code: "health.dashboard.manage".into(),
|
||
name: "工作台管理".into(),
|
||
description: "查看系统健康、用户活跃度、模块状态等管理统计".into(),
|
||
module: "health".into(),
|
||
},
|
||
]
|
||
}
|
||
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
}
|