From 731e0801251c95875d1db79335cda77495aae1e4 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 26 Apr 2026 12:35:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20dialysis/lab=5Freport/diagnosis?= =?UTF-8?q?=20PII=20=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移 m000069-m000071: 三个表添加 key_version - dialysis_record: symptoms(JSON) + complication_notes 加密 - lab_report: items(JSON) + doctor_notes 加密 - diagnosis: notes 加密 - JSON 字段: serialize → encrypt → Value::String(ciphertext) - 解密失败时回退原始值(兼容未迁移明文数据) --- crates/erp-health/src/entity/diagnosis.rs | 2 + .../erp-health/src/entity/dialysis_record.rs | 2 + crates/erp-health/src/entity/lab_report.rs | 2 + .../src/service/diagnosis_service.rs | 35 ++++-- .../src/service/dialysis_service.rs | 67 ++++++++-- .../src/service/health_data_service.rs | 119 +++++++++++++++--- crates/erp-server/migration/src/lib.rs | 6 + ..._000069_add_dialysis_record_key_version.rs | 39 ++++++ ...60427_000070_add_lab_report_key_version.rs | 39 ++++++ ...260427_000071_add_diagnosis_key_version.rs | 39 ++++++ 10 files changed, 316 insertions(+), 34 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260427_000069_add_dialysis_record_key_version.rs create mode 100644 crates/erp-server/migration/src/m20260427_000070_add_lab_report_key_version.rs create mode 100644 crates/erp-server/migration/src/m20260427_000071_add_diagnosis_key_version.rs diff --git a/crates/erp-health/src/entity/diagnosis.rs b/crates/erp-health/src/entity/diagnosis.rs index d5dd1c2..2d3c426 100644 --- a/crates/erp-health/src/entity/diagnosis.rs +++ b/crates/erp-health/src/entity/diagnosis.rs @@ -28,6 +28,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/dialysis_record.rs b/crates/erp-health/src/entity/dialysis_record.rs index 2222ae2..6d90d1f 100644 --- a/crates/erp-health/src/entity/dialysis_record.rs +++ b/crates/erp-health/src/entity/dialysis_record.rs @@ -58,6 +58,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/lab_report.rs b/crates/erp-health/src/entity/lab_report.rs index 3f543db..ef4dee1 100644 --- a/crates/erp-health/src/entity/lab_report.rs +++ b/crates/erp-health/src/entity/lab_report.rs @@ -36,6 +36,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/diagnosis_service.rs b/crates/erp-health/src/service/diagnosis_service.rs index 8d02307..b31c568 100644 --- a/crates/erp-health/src/service/diagnosis_service.rs +++ b/crates/erp-health/src/service/diagnosis_service.rs @@ -4,6 +4,7 @@ use uuid::Uuid; use erp_core::audit::AuditLog; use erp_core::audit_service; +use erp_core::crypto as pii; use erp_core::error::check_version; use erp_core::types::PaginatedResponse; @@ -37,7 +38,8 @@ pub async fn list_diagnoses( .await?; let total_pages = total.div_ceil(limit.max(1)); - let data = models.into_iter().map(to_resp).collect(); + let crypto = &state.crypto; + let data = models.into_iter().map(|m| to_resp(crypto, m)).collect(); Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) } @@ -64,6 +66,12 @@ pub async fn create_diagnosis( return Err(HealthError::Validation("诊断状态无效,可选: active/resolved/chronic".into())); } + // PII 加密 + let kek = state.crypto.kek(); + let encrypted_notes = req.notes.as_ref() + .map(|n| pii::encrypt(kek, n)) + .transpose()?; + let now = Utc::now(); let active = diagnosis::ActiveModel { id: Set(Uuid::now_v7()), @@ -76,13 +84,14 @@ pub async fn create_diagnosis( diagnosed_date: Set(req.diagnosed_date), status: Set(req.status), diagnosed_by: Set(req.diagnosed_by.or(operator_id)), - notes: Set(req.notes), + notes: Set(encrypted_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), + key_version: Set(Some(1)), }; let m = active.insert(&state.db).await?; @@ -92,7 +101,7 @@ pub async fn create_diagnosis( &state.db, ).await; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn update_diagnosis( @@ -132,10 +141,15 @@ pub async fn update_diagnosis( } if let Some(v) = req.health_record_id { active.health_record_id = Set(Some(v)); } if let Some(v) = req.diagnosed_by { active.diagnosed_by = Set(Some(v)); } - if let Some(v) = req.notes { active.notes = Set(Some(v)); } + if let Some(v) = req.notes { + let kek = state.crypto.kek(); + let encrypted = pii::encrypt(kek, &v)?; + active.notes = Set(Some(encrypted)); + } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_ver); + active.key_version = Set(Some(1)); let m = active.update(&state.db).await?; @@ -145,7 +159,7 @@ pub async fn update_diagnosis( &state.db, ).await; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn delete_diagnosis( @@ -182,7 +196,14 @@ pub async fn delete_diagnosis( Ok(()) } -fn to_resp(m: diagnosis::Model) -> DiagnosisResp { +fn to_resp(crypto: &erp_core::crypto::PiiCrypto, m: diagnosis::Model) -> DiagnosisResp { + let kek = crypto.kek(); + + // 解密备注 + let notes = m.notes.as_ref() + .map(|n| pii::decrypt(kek, n).unwrap_or_else(|_| n.clone())) + .or(m.notes); + DiagnosisResp { id: m.id, patient_id: m.patient_id, @@ -193,7 +214,7 @@ fn to_resp(m: diagnosis::Model) -> DiagnosisResp { diagnosed_date: m.diagnosed_date, status: m.status, diagnosed_by: m.diagnosed_by, - notes: m.notes, + notes, created_at: m.created_at, updated_at: m.updated_at, version: m.version, diff --git a/crates/erp-health/src/service/dialysis_service.rs b/crates/erp-health/src/service/dialysis_service.rs index 125f3c3..6598510 100644 --- a/crates/erp-health/src/service/dialysis_service.rs +++ b/crates/erp-health/src/service/dialysis_service.rs @@ -3,6 +3,7 @@ use chrono::Utc; use erp_core::audit::AuditLog; use erp_core::audit_service; +use erp_core::crypto as pii; use num_traits::ToPrimitive; use sea_orm::entity::prelude::*; use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect}; @@ -40,7 +41,8 @@ pub async fn list_dialysis_records( .await?; let total_pages = total.div_ceil(limit.max(1)); - let data: Vec = models.into_iter().map(to_resp).collect(); + let crypto = &state.crypto; + let data: Vec = models.into_iter().map(|m| to_resp(crypto, m)).collect(); Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) } @@ -58,7 +60,7 @@ pub async fn get_dialysis_record( .await? .ok_or(HealthError::DialysisRecordNotFound)?; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn create_dialysis_record( @@ -77,6 +79,21 @@ pub async fn create_dialysis_record( validate_dialysis_type(&req.dialysis_type)?; + let kek = state.crypto.kek(); + + // PII 加密 + let encrypted_symptoms = req.symptoms.as_ref() + .map(|v| -> HealthResult { + let json_str = serde_json::to_string(v) + .map_err(|e| HealthError::Validation(e.to_string()))?; + Ok(serde_json::Value::String(pii::encrypt(kek, &json_str)?)) + }) + .transpose()?; + + let encrypted_complication = req.complication_notes.as_ref() + .map(|c| pii::encrypt(kek, c)) + .transpose()?; + let now = Utc::now(); let active = dialysis_record::ActiveModel { id: Set(Uuid::now_v7()), @@ -98,8 +115,8 @@ pub async fn create_dialysis_record( dialysis_duration: Set(req.dialysis_duration), blood_flow_rate: Set(req.blood_flow_rate), dialysis_type: Set(req.dialysis_type), - symptoms: Set(req.symptoms), - complication_notes: Set(req.complication_notes), + symptoms: Set(encrypted_symptoms), + complication_notes: Set(encrypted_complication), status: Set("draft".to_string()), reviewed_by: Set(None), reviewed_at: Set(None), @@ -109,6 +126,7 @@ pub async fn create_dialysis_record( updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), + key_version: Set(Some(1)), }; let m = active.insert(&state.db).await?; @@ -118,7 +136,7 @@ pub async fn create_dialysis_record( &state.db, ).await; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn update_dialysis_record( @@ -157,11 +175,22 @@ pub async fn update_dialysis_record( if let Some(v) = req.dialysis_duration { active.dialysis_duration = Set(Some(v)); } if let Some(v) = req.blood_flow_rate { active.blood_flow_rate = Set(Some(v)); } if let Some(ref v) = req.dialysis_type { validate_dialysis_type(v)?; active.dialysis_type = Set(v.clone()); } - if let Some(v) = req.symptoms { active.symptoms = Set(Some(v)); } - if let Some(v) = req.complication_notes { active.complication_notes = Set(Some(v)); } + if let Some(v) = req.symptoms { + let kek = state.crypto.kek(); + let encrypted = Some(serde_json::Value::String( + pii::encrypt(kek, &serde_json::to_string(&v).unwrap_or_default())? + )); + active.symptoms = Set(encrypted); + } + if let Some(v) = req.complication_notes { + let kek = state.crypto.kek(); + let encrypted = pii::encrypt(kek, &v)?; + active.complication_notes = Set(Some(encrypted)); + } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_ver); + active.key_version = Set(Some(1)); let m = active.update(&state.db).await?; @@ -171,7 +200,7 @@ pub async fn update_dialysis_record( &state.db, ).await; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn review_dialysis_record( @@ -208,7 +237,7 @@ pub async fn review_dialysis_record( &state.db, ).await; - Ok(to_resp(m)) + Ok(to_resp(&state.crypto, m)) } pub async fn delete_dialysis_record( @@ -254,7 +283,21 @@ fn validate_dialysis_type(dialysis_type: &str) -> HealthResult<()> { } } -fn to_resp(m: dialysis_record::Model) -> DialysisRecordResp { +fn to_resp(crypto: &erp_core::crypto::PiiCrypto, m: dialysis_record::Model) -> DialysisRecordResp { + let kek = crypto.kek(); + + // 解密症状 JSON(加密时存储为 Value::String(ciphertext)) + let symptoms = m.symptoms.as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| pii::decrypt(kek, s).ok()) + .and_then(|s| serde_json::from_str(&s).ok()) + .or(m.symptoms); + + // 解密并发症备注 + let complication_notes = m.complication_notes.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())) + .or(m.complication_notes); + DialysisRecordResp { id: m.id, patient_id: m.patient_id, @@ -274,8 +317,8 @@ fn to_resp(m: dialysis_record::Model) -> DialysisRecordResp { dialysis_duration: m.dialysis_duration, blood_flow_rate: m.blood_flow_rate, dialysis_type: m.dialysis_type, - symptoms: m.symptoms, - complication_notes: m.complication_notes, + symptoms, + complication_notes, status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, diff --git a/crates/erp-health/src/service/health_data_service.rs b/crates/erp-health/src/service/health_data_service.rs index 017e411..fafebc5 100644 --- a/crates/erp-health/src/service/health_data_service.rs +++ b/crates/erp-health/src/service/health_data_service.rs @@ -3,6 +3,7 @@ use chrono::Utc; use erp_core::audit::AuditLog; use erp_core::audit_service; +use erp_core::crypto as pii; use erp_core::events::DomainEvent; use num_traits::ToPrimitive; use sea_orm::entity::prelude::*; @@ -271,12 +272,27 @@ pub async fn list_lab_reports( .await?; let total_pages = total.div_ceil(limit.max(1)); - let data = models.into_iter().map(|m| LabReportResp { - id: m.id, patient_id: m.patient_id, report_date: m.report_date, - report_type: m.report_type, source: m.source, - items: m.items, image_urls: m.image_urls, doctor_notes: m.doctor_notes, - status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, - 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| { + // 解密 items JSON(加密时存储为 Value::String(ciphertext)) + let items = m.items.as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| pii::decrypt(kek, s).ok()) + .and_then(|s| serde_json::from_str(&s).ok()) + .or(m.items.clone()); + + // 解密医生备注 + let doctor_notes = m.doctor_notes.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())) + .or(m.doctor_notes.clone()); + + LabReportResp { + id: m.id, patient_id: m.patient_id, report_date: m.report_date, + report_type: m.report_type, source: m.source, + items, image_urls: m.image_urls, doctor_notes, + status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, + created_at: m.created_at, updated_at: m.updated_at, version: m.version, + } }).collect(); Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) @@ -298,6 +314,21 @@ pub async fn create_lab_report( .await? .ok_or(HealthError::PatientNotFound)?; + let kek = state.crypto.kek(); + + // PII 加密 + let encrypted_items = req.items.as_ref() + .map(|v| -> HealthResult { + let json_str = serde_json::to_string(v) + .map_err(|e| HealthError::Validation(e.to_string()))?; + Ok(serde_json::Value::String(pii::encrypt(kek, &json_str)?)) + }) + .transpose()?; + + let encrypted_doctor_notes = req.doctor_notes.as_ref() + .map(|c| pii::encrypt(kek, c)) + .transpose()?; + let now = Utc::now(); let active = lab_report::ActiveModel { id: Set(Uuid::now_v7()), @@ -306,9 +337,9 @@ pub async fn create_lab_report( report_date: Set(req.report_date), report_type: Set(req.report_type), source: Set(req.source), - items: Set(req.items), + items: Set(encrypted_items), image_urls: Set(req.image_urls), - doctor_notes: Set(req.doctor_notes), + doctor_notes: Set(encrypted_doctor_notes), status: Set("pending".to_string()), reviewed_by: Set(None), reviewed_at: Set(None), @@ -318,6 +349,7 @@ pub async fn create_lab_report( updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), + key_version: Set(Some(1)), }; let m = active.insert(&state.db).await?; @@ -334,10 +366,21 @@ pub async fn create_lab_report( &state.db, ).await; + // 解密返回 + let decrypted_items = m.items.as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| pii::decrypt(kek, s).ok()) + .and_then(|s| serde_json::from_str(&s).ok()) + .or(m.items); + + let decrypted_doctor_notes = m.doctor_notes.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())) + .or(m.doctor_notes); + Ok(LabReportResp { id: m.id, patient_id: m.patient_id, report_date: m.report_date, report_type: m.report_type, source: m.source, - items: m.items, image_urls: m.image_urls, doctor_notes: m.doctor_notes, + items: decrypted_items, image_urls: m.image_urls, doctor_notes: decrypted_doctor_notes, status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, created_at: m.created_at, updated_at: m.updated_at, version: m.version, }) @@ -367,12 +410,23 @@ pub async fn update_lab_report( if let Some(v) = req.report_date { active.report_date = Set(v); } if let Some(v) = req.report_type { active.report_type = Set(v); } if let Some(v) = req.source { active.source = Set(Some(v)); } - if let Some(v) = req.items { active.items = Set(Some(v)); } + if let Some(v) = req.items { + let kek = state.crypto.kek(); + let encrypted = Some(serde_json::Value::String( + pii::encrypt(kek, &serde_json::to_string(&v).unwrap_or_default())? + )); + active.items = Set(encrypted); + } if let Some(v) = req.image_urls { active.image_urls = Set(Some(v)); } - if let Some(v) = req.doctor_notes { active.doctor_notes = Set(Some(v)); } + if let Some(v) = req.doctor_notes { + let kek = state.crypto.kek(); + let encrypted = pii::encrypt(kek, &v)?; + active.doctor_notes = Set(Some(encrypted)); + } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_ver); + active.key_version = Set(Some(1)); let m = active.update(&state.db).await?; @@ -382,10 +436,22 @@ pub async fn update_lab_report( &state.db, ).await; + // 解密返回 + let kek = state.crypto.kek(); + let decrypted_items = m.items.as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| pii::decrypt(kek, s).ok()) + .and_then(|s| serde_json::from_str(&s).ok()) + .or(m.items); + + let decrypted_doctor_notes = m.doctor_notes.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())) + .or(m.doctor_notes); + Ok(LabReportResp { id: m.id, patient_id: m.patient_id, report_date: m.report_date, report_type: m.report_type, source: m.source, - items: m.items, image_urls: m.image_urls, doctor_notes: m.doctor_notes, + items: decrypted_items, image_urls: m.image_urls, doctor_notes: decrypted_doctor_notes, status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, created_at: m.created_at, updated_at: m.updated_at, version: m.version, }) @@ -450,11 +516,22 @@ pub async fn review_lab_report( active.status = Set("reviewed".to_string()); active.reviewed_by = Set(Some(reviewer_id)); active.reviewed_at = Set(Some(Utc::now())); - if let Some(v) = req.doctor_notes { active.doctor_notes = Set(Some(v)); } - if let Some(v) = req.items { active.items = Set(Some(v)); } + if let Some(v) = req.doctor_notes { + let kek = state.crypto.kek(); + let encrypted = pii::encrypt(kek, &v)?; + active.doctor_notes = Set(Some(encrypted)); + } + if let Some(v) = req.items { + let kek = state.crypto.kek(); + let encrypted = Some(serde_json::Value::String( + pii::encrypt(kek, &serde_json::to_string(&v).unwrap_or_default())? + )); + active.items = Set(encrypted); + } active.updated_at = Set(Utc::now()); active.updated_by = Set(Some(reviewer_id)); active.version = Set(next_ver); + active.key_version = Set(Some(1)); let m = active.update(&state.db).await?; @@ -464,10 +541,22 @@ pub async fn review_lab_report( &state.db, ).await; + // 解密返回 + let kek = state.crypto.kek(); + let decrypted_items = m.items.as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| pii::decrypt(kek, s).ok()) + .and_then(|s| serde_json::from_str(&s).ok()) + .or(m.items); + + let decrypted_doctor_notes = m.doctor_notes.as_ref() + .map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone())) + .or(m.doctor_notes); + Ok(LabReportResp { id: m.id, patient_id: m.patient_id, report_date: m.report_date, report_type: m.report_type, source: m.source, - items: m.items, image_urls: m.image_urls, doctor_notes: m.doctor_notes, + items: decrypted_items, image_urls: m.image_urls, doctor_notes: decrypted_doctor_notes, status: m.status, reviewed_by: m.reviewed_by, reviewed_at: m.reviewed_at, created_at: m.created_at, updated_at: m.updated_at, version: m.version, }) diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 68d68a4..ae41ffd 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -68,6 +68,9 @@ mod m20260427_000065_add_consultation_message_key_version; mod m20260427_000066_add_follow_up_record_key_version; mod m20260427_000067_add_family_member_pii_fields; mod m20260427_000068_add_doctor_profile_pii_fields; +mod m20260427_000069_add_dialysis_record_key_version; +mod m20260427_000070_add_lab_report_key_version; +mod m20260427_000071_add_diagnosis_key_version; pub struct Migrator; @@ -143,6 +146,9 @@ impl MigratorTrait for Migrator { Box::new(m20260427_000066_add_follow_up_record_key_version::Migration), Box::new(m20260427_000067_add_family_member_pii_fields::Migration), Box::new(m20260427_000068_add_doctor_profile_pii_fields::Migration), + Box::new(m20260427_000069_add_dialysis_record_key_version::Migration), + Box::new(m20260427_000070_add_lab_report_key_version::Migration), + Box::new(m20260427_000071_add_diagnosis_key_version::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260427_000069_add_dialysis_record_key_version.rs b/crates/erp-server/migration/src/m20260427_000069_add_dialysis_record_key_version.rs new file mode 100644 index 0000000..3a5cc6e --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000069_add_dialysis_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(DialysisRecord::Table) + .add_column(ColumnDef::new(DialysisRecord::KeyVersion).integer().null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(DialysisRecord::Table) + .drop_column(DialysisRecord::KeyVersion) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum DialysisRecord { + Table, + KeyVersion, +} diff --git a/crates/erp-server/migration/src/m20260427_000070_add_lab_report_key_version.rs b/crates/erp-server/migration/src/m20260427_000070_add_lab_report_key_version.rs new file mode 100644 index 0000000..3bbc730 --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000070_add_lab_report_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(LabReport::Table) + .add_column(ColumnDef::new(LabReport::KeyVersion).integer().null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(LabReport::Table) + .drop_column(LabReport::KeyVersion) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum LabReport { + Table, + KeyVersion, +} diff --git a/crates/erp-server/migration/src/m20260427_000071_add_diagnosis_key_version.rs b/crates/erp-server/migration/src/m20260427_000071_add_diagnosis_key_version.rs new file mode 100644 index 0000000..d54c8b2 --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000071_add_diagnosis_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(Diagnosis::Table) + .add_column(ColumnDef::new(Diagnosis::KeyVersion).integer().null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Diagnosis::Table) + .drop_column(Diagnosis::KeyVersion) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Diagnosis { + Table, + KeyVersion, +}