Files
hms/crates/erp-health/src/module.rs
iven 0006e427e2 feat(health): 5 个工作台管理统计 API — 系统健康/用户活跃/模块状态/积分动态/文章统计
- 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
2026-05-02 11:49:34 +08:00

1131 lines
47 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}