From 26a9781d4f432710ed755c90f81551263e8c4413 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 30 Apr 2026 07:18:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20=E8=8D=AF=E7=89=A9=E6=8F=90?= =?UTF-8?q?=E9=86=92=E5=90=8E=E7=AB=AF=20API=20+=20=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=BB=9F=E4=B8=80=20+=20dead=20code=20?= =?UTF-8?q?=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-3: medication_reminder 全栈实现 - migration 000096: 创建 medication_reminder 表(含患者关联/提醒时间/频率) - entity + dto + service + handler: 完整 CRUD(乐观锁/软删除/审计日志) - 路由注册: GET /patients/{id}/medication-reminders, POST/PUT/DELETE - HealthError 新增 MedicationReminderNotFound P2-4: 后台任务启动统一 - appointment_reminder 迁移到 HealthModule::on_startup()(启动时立即执行 + 周期循环) - 删除 main.rs 中重复的 overdue_checker/points_expiration/appointment_reminder 调用 - 所有 Health 后台任务现由模块 on_startup 统一管理 P2-5: Web dead code 清理 - 删除 healthData.ts 中 getMiniTrend/getMiniToday(小程序专用端点,Web 无调用) - 删除 patients.ts 中 getHealthSummary(标记 TODO 未使用) --- apps/web/src/api/health/healthData.ts | 17 -- apps/web/src/api/health/patients.ts | 9 - .../src/dto/medication_reminder_dto.rs | 68 ++++++ crates/erp-health/src/dto/mod.rs | 1 + .../src/entity/medication_reminder.rs | 51 +++++ crates/erp-health/src/entity/mod.rs | 1 + crates/erp-health/src/error.rs | 6 +- .../handler/medication_reminder_handler.rs | 99 ++++++++ crates/erp-health/src/handler/mod.rs | 1 + crates/erp-health/src/module.rs | 31 ++- .../service/medication_reminder_service.rs | 216 ++++++++++++++++++ crates/erp-health/src/service/mod.rs | 1 + crates/erp-server/migration/src/lib.rs | 2 + ...60430_000096_create_medication_reminder.rs | 53 +++++ crates/erp-server/src/main.rs | 12 +- 15 files changed, 529 insertions(+), 39 deletions(-) create mode 100644 crates/erp-health/src/dto/medication_reminder_dto.rs create mode 100644 crates/erp-health/src/entity/medication_reminder.rs create mode 100644 crates/erp-health/src/handler/medication_reminder_handler.rs create mode 100644 crates/erp-health/src/service/medication_reminder_service.rs create mode 100644 crates/erp-server/migration/src/m20260430_000096_create_medication_reminder.rs diff --git a/apps/web/src/api/health/healthData.ts b/apps/web/src/api/health/healthData.ts index 4e820fc..4826cf0 100644 --- a/apps/web/src/api/health/healthData.ts +++ b/apps/web/src/api/health/healthData.ts @@ -220,21 +220,4 @@ export const healthDataApi = { }>(`/health/patients/${patientId}/trends/${encodeURIComponent(indicator)}`); return data.data; }, - - // Mini program endpoints - getMiniTrend: async (params: { indicator?: string; days?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: { date: string; value: number }[]; - }>('/health/vital-signs/trend', { params }); - return data.data; - }, - - getMiniToday: async () => { - const { data } = await client.get<{ - success: boolean; - data: VitalSigns | null; - }>('/health/vital-signs/today'); - return data.data; - }, }; diff --git a/apps/web/src/api/health/patients.ts b/apps/web/src/api/health/patients.ts index 0c29900..2f0e094 100644 --- a/apps/web/src/api/health/patients.ts +++ b/apps/web/src/api/health/patients.ts @@ -131,15 +131,6 @@ export const patientApi = { await client.post(`/health/patients/${id}/tags`, { tag_ids: tagIds }); }, - // TODO: 未使用,待未来健康摘要功能接入时启用 - getHealthSummary: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: Record; - }>(`/health/patients/${id}/health-summary`); - return data.data; - }, - listFamilyMembers: async (id: string) => { const { data } = await client.get<{ success: boolean; diff --git a/crates/erp-health/src/dto/medication_reminder_dto.rs b/crates/erp-health/src/dto/medication_reminder_dto.rs new file mode 100644 index 0000000..7e961a3 --- /dev/null +++ b/crates/erp-health/src/dto/medication_reminder_dto.rs @@ -0,0 +1,68 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Deserialize, ToSchema)] +pub struct CreateMedicationReminderReq { + pub patient_id: uuid::Uuid, + pub medication_name: String, + pub dosage: Option, + pub frequency: Option, + /// JSON 数组,如 ["08:00", "20:00"] + pub reminder_times: serde_json::Value, + pub start_date: Option, + pub end_date: Option, + #[serde(default = "default_true")] + pub is_active: Option, + pub notes: Option, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct UpdateMedicationReminderReq { + pub medication_name: Option, + pub dosage: Option, + pub frequency: Option, + pub reminder_times: Option, + pub start_date: Option, + pub end_date: Option, + pub is_active: Option, + pub notes: Option, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct MedicationReminderResp { + pub id: uuid::Uuid, + pub patient_id: uuid::Uuid, + pub medication_name: String, + pub dosage: Option, + pub frequency: Option, + pub reminder_times: serde_json::Value, + pub start_date: Option, + pub end_date: Option, + pub is_active: bool, + pub notes: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub version: i32, +} + +impl CreateMedicationReminderReq { + pub fn sanitize(&mut self) { + self.medication_name = self.medication_name.trim().to_string(); + if let Some(ref mut d) = self.dosage { *d = d.trim().to_string(); } + if let Some(ref mut f) = self.frequency { *f = f.trim().to_string(); } + if let Some(ref mut n) = self.notes { *n = n.trim().to_string(); } + } +} + +impl UpdateMedicationReminderReq { + pub fn sanitize(&mut self) { + if let Some(ref mut n) = self.medication_name { *n = n.trim().to_string(); } + if let Some(ref mut d) = self.dosage { *d = d.trim().to_string(); } + if let Some(ref mut f) = self.frequency { *f = f.trim().to_string(); } + if let Some(ref mut n) = self.notes { *n = n.trim().to_string(); } + } +} + +fn default_true() -> Option { + Some(true) +} diff --git a/crates/erp-health/src/dto/mod.rs b/crates/erp-health/src/dto/mod.rs index c7f25fe..4f5db43 100644 --- a/crates/erp-health/src/dto/mod.rs +++ b/crates/erp-health/src/dto/mod.rs @@ -6,6 +6,7 @@ pub mod consultation_dto; pub mod daily_monitoring_dto; pub mod diagnosis_dto; pub mod medication_record_dto; +pub mod medication_reminder_dto; pub mod doctor_dto; pub mod follow_up_dto; pub mod follow_up_template_dto; diff --git a/crates/erp-health/src/entity/medication_reminder.rs b/crates/erp-health/src/entity/medication_reminder.rs new file mode 100644 index 0000000..8025cf1 --- /dev/null +++ b/crates/erp-health/src/entity/medication_reminder.rs @@ -0,0 +1,51 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "medication_reminder")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub patient_id: Uuid, + pub medication_name: String, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub dosage: Option, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub frequency: Option, + pub reminder_times: serde_json::Value, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub end_date: Option, + pub is_active: bool, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub notes: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub created_by: Option, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub updated_by: Option, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::patient::Entity", + from = "Column::PatientId", + to = "super::patient::Column::Id" + )] + Patient, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Patient.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-health/src/entity/mod.rs b/crates/erp-health/src/entity/mod.rs index c9466f5..62acc9e 100644 --- a/crates/erp-health/src/entity/mod.rs +++ b/crates/erp-health/src/entity/mod.rs @@ -40,5 +40,6 @@ pub mod points_transaction; pub mod offline_event; pub mod offline_event_registration; pub mod medication_record; +pub mod medication_reminder; pub mod vital_signs; pub mod vital_signs_hourly; diff --git a/crates/erp-health/src/error.rs b/crates/erp-health/src/error.rs index aa9f7a8..162e5b2 100644 --- a/crates/erp-health/src/error.rs +++ b/crates/erp-health/src/error.rs @@ -80,6 +80,9 @@ pub enum HealthError { #[error("危急值告警不存在")] CriticalAlertNotFound, + #[error("药物提醒不存在")] + MedicationReminderNotFound, + #[error("状态转换无效: {0}")] InvalidStatusTransition(String), @@ -117,7 +120,8 @@ impl From for AppError { | HealthError::DeviceNotFound | HealthError::AlertNotFound | HealthError::FollowUpTemplateNotFound - | HealthError::CriticalAlertNotFound => AppError::NotFound(err.to_string()), + | HealthError::CriticalAlertNotFound + | HealthError::MedicationReminderNotFound => AppError::NotFound(err.to_string()), HealthError::ScheduleFull => AppError::Validation(err.to_string()), HealthError::InvalidStatusTransition(s) => AppError::Validation(s), HealthError::VersionMismatch => AppError::VersionMismatch, diff --git a/crates/erp-health/src/handler/medication_reminder_handler.rs b/crates/erp-health/src/handler/medication_reminder_handler.rs new file mode 100644 index 0000000..49a85f0 --- /dev/null +++ b/crates/erp-health/src/handler/medication_reminder_handler.rs @@ -0,0 +1,99 @@ +use axum::Extension; +use axum::extract::{FromRef, Json, Path, Query, State}; +use erp_core::error::AppError; +use erp_core::rbac::require_permission; +use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; + +use crate::dto::medication_reminder_dto::{CreateMedicationReminderReq, MedicationReminderResp, UpdateMedicationReminderReq}; +use crate::service::medication_reminder_service; +use crate::state::HealthState; + +#[derive(Debug, serde::Deserialize)] +pub struct PaginationParams { + pub page: Option, + pub page_size: Option, +} + +pub async fn list_reminders( + State(state): State, + Extension(ctx): Extension, + Path(patient_id): Path, + Query(params): Query, +) -> Result>>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.health-data.list")?; + let page = params.page.unwrap_or(1); + let page_size = params.page_size.unwrap_or(20); + let result = medication_reminder_service::list_reminders( + &state, ctx.tenant_id, patient_id, page, page_size, + ).await?; + Ok(Json(ApiResponse::ok(result))) +} + +pub async fn create_reminder( + State(state): State, + Extension(ctx): Extension, + mut req: Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.health-data.manage")?; + req.sanitize(); + let result = medication_reminder_service::create_reminder( + &state, ctx.tenant_id, Some(ctx.user_id), req.0, + ).await?; + Ok(Json(ApiResponse::ok(result))) +} + +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +pub struct UpdateReminderWithVersion { + #[serde(flatten)] + pub data: UpdateMedicationReminderReq, + pub version: i32, +} + +pub async fn update_reminder( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.health-data.manage")?; + let mut data = req.data; + data.sanitize(); + let result = medication_reminder_service::update_reminder( + &state, ctx.tenant_id, id, Some(ctx.user_id), req.version, data, + ).await?; + Ok(Json(ApiResponse::ok(result))) +} + +#[derive(Debug, serde::Deserialize)] +pub struct DeleteWithVersion { + pub version: i32, +} + +pub async fn delete_reminder( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.health-data.manage")?; + medication_reminder_service::delete_reminder( + &state, ctx.tenant_id, id, Some(ctx.user_id), req.version, + ).await?; + Ok(Json(ApiResponse::ok(()))) +} diff --git a/crates/erp-health/src/handler/mod.rs b/crates/erp-health/src/handler/mod.rs index 086e7b7..47222ec 100644 --- a/crates/erp-health/src/handler/mod.rs +++ b/crates/erp-health/src/handler/mod.rs @@ -13,6 +13,7 @@ pub mod device_handler; pub mod device_reading_handler; pub mod diagnosis_handler; pub mod medication_record_handler; +pub mod medication_reminder_handler; pub mod doctor_handler; pub mod follow_up_handler; pub mod follow_up_template_handler; diff --git a/crates/erp-health/src/module.rs b/crates/erp-health/src/module.rs index eb12a09..afd79e6 100644 --- a/crates/erp-health/src/module.rs +++ b/crates/erp-health/src/module.rs @@ -8,7 +8,7 @@ use erp_core::module::{ErpModule, PermissionDescriptor}; use crate::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, patient_handler, points_handler, stats_handler, + health_data_handler, medication_record_handler, medication_reminder_handler, patient_handler, points_handler, stats_handler, }; pub struct HealthModule; @@ -200,6 +200,20 @@ impl HealthModule { .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) @@ -741,6 +755,21 @@ impl ErpModule for HealthModule { 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"); diff --git a/crates/erp-health/src/service/medication_reminder_service.rs b/crates/erp-health/src/service/medication_reminder_service.rs new file mode 100644 index 0000000..eab820c --- /dev/null +++ b/crates/erp-health/src/service/medication_reminder_service.rs @@ -0,0 +1,216 @@ +use chrono::Utc; +use sea_orm::entity::prelude::*; +use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; +use uuid::Uuid; + +use erp_core::audit::AuditLog; +use erp_core::audit_service; +use erp_core::error::check_version; +use erp_core::types::PaginatedResponse; + +use crate::dto::medication_reminder_dto::*; +use crate::entity::medication_reminder; +use crate::entity::patient; +use crate::error::{HealthError, HealthResult}; +use crate::state::HealthState; + +pub async fn list_reminders( + state: &HealthState, + tenant_id: Uuid, + patient_id: Uuid, + page: u64, + page_size: u64, +) -> HealthResult> { + let limit = page_size.min(100); + let offset = page.saturating_sub(1) * limit; + + // 校验患者存在且属于当前租户 + patient::Entity::find_by_id(patient_id) + .filter(patient::Column::TenantId.eq(tenant_id)) + .filter(patient::Column::DeletedAt.is_null()) + .one(&state.db) + .await? + .ok_or(HealthError::PatientNotFound)?; + + let mut query = medication_reminder::Entity::find() + .filter(medication_reminder::Column::TenantId.eq(tenant_id)) + .filter(medication_reminder::Column::PatientId.eq(patient_id)) + .filter(medication_reminder::Column::DeletedAt.is_null()); + + let total = query.clone().count(&state.db).await?; + let models = query + .order_by_desc(medication_reminder::Column::CreatedAt) + .offset(offset) + .limit(limit) + .all(&state.db) + .await?; + + let data = models.into_iter().map(|m| MedicationReminderResp { + id: m.id, + patient_id: m.patient_id, + medication_name: m.medication_name, + dosage: m.dosage, + frequency: m.frequency, + reminder_times: m.reminder_times, + start_date: m.start_date, + end_date: m.end_date, + is_active: m.is_active, + notes: m.notes, + created_at: m.created_at, + updated_at: m.updated_at, + version: m.version, + }).collect(); + + let total_pages = total.div_ceil(limit.max(1)); + Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) +} + +pub async fn create_reminder( + state: &HealthState, + tenant_id: Uuid, + operator_id: Option, + req: CreateMedicationReminderReq, +) -> HealthResult { + // 校验患者存在 + patient::Entity::find_by_id(req.patient_id) + .filter(patient::Column::TenantId.eq(tenant_id)) + .filter(patient::Column::DeletedAt.is_null()) + .one(&state.db) + .await? + .ok_or(HealthError::PatientNotFound)?; + + let now = Utc::now(); + let id = Uuid::now_v7(); + let active = medication_reminder::ActiveModel { + id: Set(id), + tenant_id: Set(tenant_id), + patient_id: Set(req.patient_id), + medication_name: Set(req.medication_name), + dosage: Set(req.dosage), + frequency: Set(req.frequency), + reminder_times: Set(req.reminder_times), + start_date: Set(req.start_date), + end_date: Set(req.end_date), + is_active: Set(req.is_active.unwrap_or(true)), + notes: Set(req.notes), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(operator_id), + updated_by: Set(operator_id), + deleted_at: Set(None), + version: Set(1), + }; + + let model = active.insert(&state.db).await?; + + audit_service::record( + AuditLog::new(tenant_id, operator_id, "medication_reminder.created", "medication_reminder") + .with_resource_id(model.id), + &state.db, + ).await; + + Ok(MedicationReminderResp { + id: model.id, + patient_id: model.patient_id, + medication_name: model.medication_name, + dosage: model.dosage, + frequency: model.frequency, + reminder_times: model.reminder_times, + start_date: model.start_date, + end_date: model.end_date, + is_active: model.is_active, + notes: model.notes, + created_at: model.created_at, + updated_at: model.updated_at, + version: model.version, + }) +} + +pub async fn update_reminder( + state: &HealthState, + tenant_id: Uuid, + reminder_id: Uuid, + operator_id: Option, + expected_version: i32, + req: UpdateMedicationReminderReq, +) -> HealthResult { + let model = medication_reminder::Entity::find_by_id(reminder_id) + .filter(medication_reminder::Column::TenantId.eq(tenant_id)) + .filter(medication_reminder::Column::DeletedAt.is_null()) + .one(&state.db) + .await? + .ok_or(HealthError::MedicationReminderNotFound)?; + + let next_ver = check_version(expected_version, model.version) + .map_err(|_| HealthError::VersionMismatch)?; + + let mut active: medication_reminder::ActiveModel = model.into(); + if let Some(v) = req.medication_name { active.medication_name = Set(v); } + if let Some(v) = req.dosage { active.dosage = Set(Some(v)); } + if let Some(v) = req.frequency { active.frequency = Set(Some(v)); } + if let Some(v) = req.reminder_times { active.reminder_times = Set(v); } + if let Some(v) = req.start_date { active.start_date = Set(Some(v)); } + if let Some(v) = req.end_date { active.end_date = Set(Some(v)); } + if let Some(v) = req.is_active { active.is_active = Set(v); } + if let Some(v) = req.notes { active.notes = Set(Some(v)); } + active.updated_at = Set(Utc::now()); + active.updated_by = Set(operator_id); + active.version = Set(next_ver); + + let model = active.update(&state.db).await?; + + audit_service::record( + AuditLog::new(tenant_id, operator_id, "medication_reminder.updated", "medication_reminder") + .with_resource_id(model.id), + &state.db, + ).await; + + Ok(MedicationReminderResp { + id: model.id, + patient_id: model.patient_id, + medication_name: model.medication_name, + dosage: model.dosage, + frequency: model.frequency, + reminder_times: model.reminder_times, + start_date: model.start_date, + end_date: model.end_date, + is_active: model.is_active, + notes: model.notes, + created_at: model.created_at, + updated_at: model.updated_at, + version: model.version, + }) +} + +pub async fn delete_reminder( + state: &HealthState, + tenant_id: Uuid, + reminder_id: Uuid, + operator_id: Option, + expected_version: i32, +) -> HealthResult<()> { + let model = medication_reminder::Entity::find_by_id(reminder_id) + .filter(medication_reminder::Column::TenantId.eq(tenant_id)) + .filter(medication_reminder::Column::DeletedAt.is_null()) + .one(&state.db) + .await? + .ok_or(HealthError::MedicationReminderNotFound)?; + + let next_ver = check_version(expected_version, model.version) + .map_err(|_| HealthError::VersionMismatch)?; + + let mut active: medication_reminder::ActiveModel = model.into(); + active.deleted_at = Set(Some(Utc::now())); + active.updated_at = Set(Utc::now()); + active.updated_by = Set(operator_id); + active.version = Set(next_ver); + active.update(&state.db).await?; + + audit_service::record( + AuditLog::new(tenant_id, operator_id, "medication_reminder.deleted", "medication_reminder") + .with_resource_id(reminder_id), + &state.db, + ).await; + + Ok(()) +} diff --git a/crates/erp-health/src/service/mod.rs b/crates/erp-health/src/service/mod.rs index 81ffd7a..3dbb065 100644 --- a/crates/erp-health/src/service/mod.rs +++ b/crates/erp-health/src/service/mod.rs @@ -14,6 +14,7 @@ pub mod device_reading_service; pub mod device_service; pub mod diagnosis_service; pub mod medication_record_service; +pub mod medication_reminder_service; pub mod doctor_service; pub mod follow_up_service; pub mod follow_up_template_service; diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 3bf66d6..6f7c61c 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -95,6 +95,7 @@ mod m20260429_000092_device_readings_metric; mod m20260429_000093_trend_analysis_prompt_v2; mod m20260429_000094_device_readings_unique_constraint; mod m20260429_000095_seed_alert_device_menus; +mod m20260430_000096_create_medication_reminder; pub struct Migrator; @@ -197,6 +198,7 @@ impl MigratorTrait for Migrator { Box::new(m20260429_000093_trend_analysis_prompt_v2::Migration), Box::new(m20260429_000094_device_readings_unique_constraint::Migration), Box::new(m20260429_000095_seed_alert_device_menus::Migration), + Box::new(m20260430_000096_create_medication_reminder::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260430_000096_create_medication_reminder.rs b/crates/erp-server/migration/src/m20260430_000096_create_medication_reminder.rs new file mode 100644 index 0000000..fffca24 --- /dev/null +++ b/crates/erp-server/migration/src/m20260430_000096_create_medication_reminder.rs @@ -0,0 +1,53 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Alias::new("medication_reminder")) + .if_not_exists() + .col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key()) + .col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("patient_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("medication_name")).string().not_null()) + .col(ColumnDef::new(Alias::new("dosage")).string()) + .col(ColumnDef::new(Alias::new("frequency")).string()) + .col(ColumnDef::new(Alias::new("reminder_times")).json().not_null()) + .col(ColumnDef::new(Alias::new("start_date")).date()) + .col(ColumnDef::new(Alias::new("end_date")).date()) + .col(ColumnDef::new(Alias::new("is_active")).boolean().not_null().default(true)) + .col(ColumnDef::new(Alias::new("notes")).string()) + .col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().not_null()) + .col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().not_null()) + .col(ColumnDef::new(Alias::new("created_by")).uuid()) + .col(ColumnDef::new(Alias::new("updated_by")).uuid()) + .col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone()) + .col(ColumnDef::new(Alias::new("version")).integer().not_null().default(1)) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_medication_reminder_patient") + .table(Alias::new("medication_reminder")) + .col(Alias::new("tenant_id")) + .col(Alias::new("patient_id")) + .col(Alias::new("deleted_at")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Alias::new("medication_reminder")).to_owned()) + .await + } +} diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index b690ba1..81e288b 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -435,17 +435,7 @@ async fn main() -> anyhow::Result<()> { erp_workflow::WorkflowModule::start_timeout_checker(db.clone(), event_bus.clone()); tracing::info!("Timeout checker started"); - // Start follow-up overdue checker (every 6h) - erp_health::HealthModule::start_overdue_checker(db.clone()); - tracing::info!("Follow-up overdue checker started"); - - // Start points expiration checker (every 24h) - erp_health::HealthModule::start_points_expiration_checker(db.clone(), event_bus.clone()); - tracing::info!("Points expiration checker started"); - - // Start appointment reminder scheduler (every 1h) - erp_health::HealthModule::start_appointment_reminder(db.clone(), event_bus.clone()); - tracing::info!("Appointment reminder scheduler started"); + // Health 模块后台任务已统一在 HealthModule::on_startup() 中启动 let host = config.server.host.clone(); let port = config.server.port;