feat(health): consultation_message + follow_up_record PII 加密
- 迁移 m000065/m000066: 添加 key_version 列 - consultation_message: content 加密写入 + 解密读取 - follow_up_record: result/patient_condition/medical_advice 加密 - Entity: 添加 key_version 字段
This commit is contained in:
@@ -22,6 +22,8 @@ pub struct Model {
|
|||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub deleted_at: Option<DateTimeUtc>,
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_version: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub struct Model {
|
|||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub deleted_at: Option<DateTimeUtc>,
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_version: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::entity::{consultation_message, consultation_session, patient};
|
|||||||
use crate::error::{HealthError, HealthResult};
|
use crate::error::{HealthError, HealthResult};
|
||||||
use crate::service::validation::{validate_sender_role, validate_content_type, validate_consultation_type};
|
use crate::service::validation::{validate_sender_role, validate_content_type, validate_consultation_type};
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
use erp_core::crypto as pii;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 咨询会话
|
// 咨询会话
|
||||||
@@ -249,10 +250,14 @@ pub async fn list_messages(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let total_pages = total.div_ceil(limit.max(1));
|
let total_pages = total.div_ceil(limit.max(1));
|
||||||
let data = models.into_iter().map(|m| MessageResp {
|
let kek = state.crypto.kek();
|
||||||
id: m.id, session_id: m.session_id, sender_id: m.sender_id,
|
let data = models.into_iter().map(|m| {
|
||||||
sender_role: m.sender_role, content_type: m.content_type,
|
let content = pii::decrypt(kek, &m.content).unwrap_or(m.content);
|
||||||
content: m.content, is_read: m.is_read, created_at: m.created_at,
|
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();
|
}).collect();
|
||||||
|
|
||||||
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
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_id: Set(sender_id),
|
||||||
sender_role: Set(req.sender_role),
|
sender_role: Set(req.sender_role),
|
||||||
content_type: Set(content_type),
|
content_type: Set(content_type),
|
||||||
content: Set(req.content),
|
content: Set(pii::encrypt(state.crypto.kek(), &req.content)?),
|
||||||
is_read: Set(false),
|
is_read: Set(false),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
@@ -309,6 +314,7 @@ pub async fn create_message(
|
|||||||
updated_by: Set(operator_id),
|
updated_by: Set(operator_id),
|
||||||
deleted_at: Set(None),
|
deleted_at: Set(None),
|
||||||
version: Set(1),
|
version: Set(1),
|
||||||
|
key_version: Set(Some(1)),
|
||||||
};
|
};
|
||||||
let m = active.insert(&txn).await?;
|
let m = active.insert(&txn).await?;
|
||||||
|
|
||||||
@@ -351,9 +357,10 @@ pub async fn create_message(
|
|||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
|
let decrypted_content = pii::decrypt(state.crypto.kek(), &m.content).unwrap_or(m.content);
|
||||||
Ok(MessageResp {
|
Ok(MessageResp {
|
||||||
id: m.id, session_id: m.session_id, sender_id: m.sender_id,
|
id: m.id, session_id: m.session_id, sender_id: m.sender_id,
|
||||||
sender_role: m.sender_role, content_type: m.content_type,
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::entity::{follow_up_record, follow_up_task, patient};
|
|||||||
use crate::error::{HealthError, HealthResult};
|
use crate::error::{HealthError, HealthResult};
|
||||||
use crate::service::validation::validate_follow_up_type;
|
use crate::service::validation::validate_follow_up_type;
|
||||||
use crate::state::HealthState;
|
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 txn = state.db.begin().await?;
|
||||||
|
|
||||||
|
let kek = state.crypto.kek();
|
||||||
let record_active = follow_up_record::ActiveModel {
|
let record_active = follow_up_record::ActiveModel {
|
||||||
id: Set(Uuid::now_v7()),
|
id: Set(Uuid::now_v7()),
|
||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
task_id: Set(req.task_id),
|
task_id: Set(req.task_id),
|
||||||
executed_by: Set(req.executed_by.or(operator_id)),
|
executed_by: Set(req.executed_by.or(operator_id)),
|
||||||
executed_date: Set(req.executed_date),
|
executed_date: Set(req.executed_date),
|
||||||
result: Set(req.result),
|
result: Set(pii::encrypt(kek, &req.result)?),
|
||||||
patient_condition: Set(req.patient_condition),
|
patient_condition: Set(req.patient_condition.as_ref()
|
||||||
medical_advice: Set(req.medical_advice),
|
.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),
|
next_follow_up_date: Set(req.next_follow_up_date),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
@@ -273,6 +277,7 @@ pub async fn create_record(
|
|||||||
updated_by: Set(operator_id),
|
updated_by: Set(operator_id),
|
||||||
deleted_at: Set(None),
|
deleted_at: Set(None),
|
||||||
version: Set(1),
|
version: Set(1),
|
||||||
|
key_version: Set(Some(1)),
|
||||||
};
|
};
|
||||||
let record = record_active.insert(&txn).await?;
|
let record = record_active.insert(&txn).await?;
|
||||||
|
|
||||||
@@ -371,12 +376,20 @@ pub async fn list_records(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let total_pages = total.div_ceil(limit.max(1));
|
let total_pages = total.div_ceil(limit.max(1));
|
||||||
let data = models.into_iter().map(|m| FollowUpRecordResp {
|
let kek = state.crypto.kek();
|
||||||
id: m.id, task_id: m.task_id, executed_by: m.executed_by,
|
let data = models.into_iter().map(|m| {
|
||||||
executed_date: m.executed_date, result: m.result,
|
let result = pii::decrypt(kek, &m.result).unwrap_or(m.result);
|
||||||
patient_condition: m.patient_condition, medical_advice: m.medical_advice,
|
let patient_condition = m.patient_condition.as_ref()
|
||||||
next_follow_up_date: m.next_follow_up_date,
|
.map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone()));
|
||||||
created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
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();
|
}).collect();
|
||||||
|
|
||||||
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ mod m20260426_000061_create_consent;
|
|||||||
mod m20260427_000062_create_tenant_crypto_keys;
|
mod m20260427_000062_create_tenant_crypto_keys;
|
||||||
mod m20260427_000063_content_management;
|
mod m20260427_000063_content_management;
|
||||||
mod m20260427_000064_add_patient_pii_fields;
|
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;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -135,6 +137,8 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260427_000062_create_tenant_crypto_keys::Migration),
|
Box::new(m20260427_000062_create_tenant_crypto_keys::Migration),
|
||||||
Box::new(m20260427_000063_content_management::Migration),
|
Box::new(m20260427_000063_content_management::Migration),
|
||||||
Box::new(m20260427_000064_add_patient_pii_fields::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),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user