diff --git a/crates/erp-health/src/entity/consultation_message.rs b/crates/erp-health/src/entity/consultation_message.rs index cc0c02b..12ffc69 100644 --- a/crates/erp-health/src/entity/consultation_message.rs +++ b/crates/erp-health/src/entity/consultation_message.rs @@ -22,6 +22,8 @@ pub struct Model { #[sea_orm(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, pub version: i32, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub key_version: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-health/src/entity/follow_up_record.rs b/crates/erp-health/src/entity/follow_up_record.rs index 0f80401..ad5cd80 100644 --- a/crates/erp-health/src/entity/follow_up_record.rs +++ b/crates/erp-health/src/entity/follow_up_record.rs @@ -27,6 +27,8 @@ pub struct Model { #[sea_orm(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, pub version: i32, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub key_version: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-health/src/service/consultation_service.rs b/crates/erp-health/src/service/consultation_service.rs index 0917102..f88b981 100644 --- a/crates/erp-health/src/service/consultation_service.rs +++ b/crates/erp-health/src/service/consultation_service.rs @@ -16,6 +16,7 @@ use crate::entity::{consultation_message, consultation_session, patient}; use crate::error::{HealthError, HealthResult}; use crate::service::validation::{validate_sender_role, validate_content_type, validate_consultation_type}; use crate::state::HealthState; +use erp_core::crypto as pii; // --------------------------------------------------------------------------- // 咨询会话 @@ -249,10 +250,14 @@ pub async fn list_messages( .await?; let total_pages = total.div_ceil(limit.max(1)); - let data = models.into_iter().map(|m| MessageResp { - id: m.id, session_id: m.session_id, sender_id: m.sender_id, - sender_role: m.sender_role, content_type: m.content_type, - content: m.content, is_read: m.is_read, created_at: m.created_at, + let kek = state.crypto.kek(); + let data = models.into_iter().map(|m| { + let content = pii::decrypt(kek, &m.content).unwrap_or(m.content); + MessageResp { + id: m.id, session_id: m.session_id, sender_id: m.sender_id, + sender_role: m.sender_role, content_type: m.content_type, + content, is_read: m.is_read, created_at: m.created_at, + } }).collect(); Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) @@ -301,7 +306,7 @@ pub async fn create_message( sender_id: Set(sender_id), sender_role: Set(req.sender_role), content_type: Set(content_type), - content: Set(req.content), + content: Set(pii::encrypt(state.crypto.kek(), &req.content)?), is_read: Set(false), created_at: Set(now), updated_at: Set(now), @@ -309,6 +314,7 @@ pub async fn create_message( updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), + key_version: Set(Some(1)), }; let m = active.insert(&txn).await?; @@ -351,9 +357,10 @@ pub async fn create_message( &state.db, ).await; + let decrypted_content = pii::decrypt(state.crypto.kek(), &m.content).unwrap_or(m.content); Ok(MessageResp { id: m.id, session_id: m.session_id, sender_id: m.sender_id, sender_role: m.sender_role, content_type: m.content_type, - content: m.content, is_read: m.is_read, created_at: m.created_at, + content: decrypted_content, is_read: m.is_read, created_at: m.created_at, }) } diff --git a/crates/erp-health/src/service/follow_up_service.rs b/crates/erp-health/src/service/follow_up_service.rs index 3f6369b..6f36a04 100644 --- a/crates/erp-health/src/service/follow_up_service.rs +++ b/crates/erp-health/src/service/follow_up_service.rs @@ -16,6 +16,7 @@ use crate::entity::{follow_up_record, follow_up_task, patient}; use crate::error::{HealthError, HealthResult}; use crate::service::validation::validate_follow_up_type; use crate::state::HealthState; +use erp_core::crypto as pii; // --------------------------------------------------------------------------- // 随访任务 @@ -257,15 +258,18 @@ pub async fn create_record( // 事务包裹:插入记录 + 更新任务状态 + 创建后续任务 let txn = state.db.begin().await?; + let kek = state.crypto.kek(); let record_active = follow_up_record::ActiveModel { id: Set(Uuid::now_v7()), tenant_id: Set(tenant_id), task_id: Set(req.task_id), executed_by: Set(req.executed_by.or(operator_id)), executed_date: Set(req.executed_date), - result: Set(req.result), - patient_condition: Set(req.patient_condition), - medical_advice: Set(req.medical_advice), + result: Set(pii::encrypt(kek, &req.result)?), + patient_condition: Set(req.patient_condition.as_ref() + .map(|p| pii::encrypt(kek, p)).transpose()?), + medical_advice: Set(req.medical_advice.as_ref() + .map(|m| pii::encrypt(kek, m)).transpose()?), next_follow_up_date: Set(req.next_follow_up_date), created_at: Set(now), updated_at: Set(now), @@ -273,6 +277,7 @@ pub async fn create_record( updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), + key_version: Set(Some(1)), }; let record = record_active.insert(&txn).await?; @@ -371,12 +376,20 @@ pub async fn list_records( .await?; let total_pages = total.div_ceil(limit.max(1)); - let data = models.into_iter().map(|m| FollowUpRecordResp { - id: m.id, task_id: m.task_id, executed_by: m.executed_by, - executed_date: m.executed_date, result: m.result, - patient_condition: m.patient_condition, medical_advice: m.medical_advice, - next_follow_up_date: m.next_follow_up_date, - created_at: m.created_at, updated_at: m.updated_at, version: m.version, + let kek = state.crypto.kek(); + let data = models.into_iter().map(|m| { + let result = pii::decrypt(kek, &m.result).unwrap_or(m.result); + let patient_condition = m.patient_condition.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())); + let medical_advice = m.medical_advice.as_ref() + .map(|a| pii::decrypt(kek, a).unwrap_or_else(|_| a.clone())); + FollowUpRecordResp { + id: m.id, task_id: m.task_id, executed_by: m.executed_by, + executed_date: m.executed_date, result, + patient_condition, medical_advice, + next_follow_up_date: m.next_follow_up_date, + created_at: m.created_at, updated_at: m.updated_at, version: m.version, + } }).collect(); Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index cf89217..81d1e20 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -64,6 +64,8 @@ mod m20260426_000061_create_consent; mod m20260427_000062_create_tenant_crypto_keys; mod m20260427_000063_content_management; mod m20260427_000064_add_patient_pii_fields; +mod m20260427_000065_add_consultation_message_key_version; +mod m20260427_000066_add_follow_up_record_key_version; pub struct Migrator; @@ -135,6 +137,8 @@ impl MigratorTrait for Migrator { Box::new(m20260427_000062_create_tenant_crypto_keys::Migration), Box::new(m20260427_000063_content_management::Migration), Box::new(m20260427_000064_add_patient_pii_fields::Migration), + Box::new(m20260427_000065_add_consultation_message_key_version::Migration), + Box::new(m20260427_000066_add_follow_up_record_key_version::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260427_000065_add_consultation_message_key_version.rs b/crates/erp-server/migration/src/m20260427_000065_add_consultation_message_key_version.rs new file mode 100644 index 0000000..bb24fbe --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000065_add_consultation_message_key_version.rs @@ -0,0 +1,39 @@ +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 + .alter_table( + Table::alter() + .table(ConsultationMessage::Table) + .add_column(ColumnDef::new(ConsultationMessage::KeyVersion).integer().null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(ConsultationMessage::Table) + .drop_column(ConsultationMessage::KeyVersion) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum ConsultationMessage { + Table, + KeyVersion, +} diff --git a/crates/erp-server/migration/src/m20260427_000066_add_follow_up_record_key_version.rs b/crates/erp-server/migration/src/m20260427_000066_add_follow_up_record_key_version.rs new file mode 100644 index 0000000..d5887ba --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000066_add_follow_up_record_key_version.rs @@ -0,0 +1,39 @@ +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 + .alter_table( + Table::alter() + .table(FollowUpRecord::Table) + .add_column(ColumnDef::new(FollowUpRecord::KeyVersion).integer().null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(FollowUpRecord::Table) + .drop_column(FollowUpRecord::KeyVersion) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum FollowUpRecord { + Table, + KeyVersion, +}