From a2c1b5ece8c15c67ef9c58b75bde61647275f2c2 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 12:50:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E6=B3=A8=E5=86=8C=E9=80=8F?= =?UTF-8?q?=E6=9E=90=E5=A4=84=E6=96=B9=E8=BF=81=E7=A7=BB=20+=20AI=20Prompt?= =?UTF-8?q?=20=E7=A7=8D=E5=AD=90=E6=95=B0=E6=8D=AE=EF=BC=884=20=E4=B8=AA?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=A8=A1=E6=9D=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注册遗漏的 m20260427_000081_create_dialysis_prescription 迁移 - 新增 000082 种子迁移:插入 4 个 AI Prompt 模板 (化验单解读/趋势分析/体检方案/报告摘要) --- crates/erp-server/migration/src/lib.rs | 4 + ...427_000081_create_dialysis_prescription.rs | 133 ++++++++++++++++++ .../src/m20260427_000082_seed_ai_prompts.rs | 84 +++++++++++ 3 files changed, 221 insertions(+) create mode 100644 crates/erp-server/migration/src/m20260427_000081_create_dialysis_prescription.rs create mode 100644 crates/erp-server/migration/src/m20260427_000082_seed_ai_prompts.rs diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index b5e505e..e6376a6 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -80,6 +80,8 @@ mod m20260426_000077_create_alerts; mod m20260427_000078_normalize_follow_up_types; mod m20260427_000079_add_vital_signs_fields; mod m20260427_000080_create_medication_record; +mod m20260427_000081_create_dialysis_prescription; +mod m20260427_000082_seed_ai_prompts; pub struct Migrator; @@ -167,6 +169,8 @@ impl MigratorTrait for Migrator { Box::new(m20260427_000078_normalize_follow_up_types::Migration), Box::new(m20260427_000079_add_vital_signs_fields::Migration), Box::new(m20260427_000080_create_medication_record::Migration), + Box::new(m20260427_000081_create_dialysis_prescription::Migration), + Box::new(m20260427_000082_seed_ai_prompts::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260427_000081_create_dialysis_prescription.rs b/crates/erp-server/migration/src/m20260427_000081_create_dialysis_prescription.rs new file mode 100644 index 0000000..1abcab1 --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000081_create_dialysis_prescription.rs @@ -0,0 +1,133 @@ +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("dialysis_prescription")) + .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("dialyzer_model")).string_len(100).null()) + // 膜面积 (m²) + .col(ColumnDef::new(Alias::new("membrane_area")).decimal_len(5, 2).null()) + // 透析液钾浓度 (mmol/L) + .col(ColumnDef::new(Alias::new("dialysate_potassium")).decimal_len(5, 2).null()) + // 透析液钙浓度 (mmol/L) + .col(ColumnDef::new(Alias::new("dialysate_calcium")).decimal_len(5, 2).null()) + // 透析液碳酸氢盐浓度 (mmol/L) + .col(ColumnDef::new(Alias::new("dialysate_bicarbonate")).decimal_len(5, 2).null()) + // 抗凝方式: heparin/lmwh/heparin_free + .col(ColumnDef::new(Alias::new("anticoagulation_type")).string_len(20).null()) + // 抗凝剂剂量 + .col(ColumnDef::new(Alias::new("anticoagulation_dose")).string_len(50).null()) + // 目标超滤量 (ml) + .col(ColumnDef::new(Alias::new("target_ultrafiltration_ml")).integer().null()) + // 目标干体重 (kg) + .col(ColumnDef::new(Alias::new("target_dry_weight")).decimal_len(5, 2).null()) + // 血流量 (ml/min) + .col(ColumnDef::new(Alias::new("blood_flow_rate")).integer().null()) + // 透析液流量 (ml/min) + .col(ColumnDef::new(Alias::new("dialysate_flow_rate")).integer().null()) + // 每周透析频次 + .col(ColumnDef::new(Alias::new("frequency_per_week")).integer().null()) + // 每次透析时长 (分钟) + .col(ColumnDef::new(Alias::new("duration_minutes")).integer().null()) + // 血管通路类型: avf/avg/cvc + .col(ColumnDef::new(Alias::new("vascular_access_type")).string_len(20).null()) + // 血管通路位置 + .col(ColumnDef::new(Alias::new("vascular_access_location")).string_len(100).null()) + // 生效日期 + .col(ColumnDef::new(Alias::new("effective_from")).date().null()) + // 失效日期 + .col(ColumnDef::new(Alias::new("effective_to")).date().null()) + // 状态: active/discontinued + .col( + ColumnDef::new(Alias::new("status")) + .string_len(20) + .not_null() + .default("active"), + ) + // 开方医生 ID + .col(ColumnDef::new(Alias::new("prescribed_by")).uuid().null()) + // 备注 + .col(ColumnDef::new(Alias::new("notes")).text().null()) + // 标准审计字段 + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col(ColumnDef::new(Alias::new("created_by")).uuid().null()) + .col(ColumnDef::new(Alias::new("updated_by")).uuid().null()) + .col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone().null()) + .col( + ColumnDef::new(Alias::new("version")) + .integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await?; + + // 租户索引 + manager + .create_index( + Index::create() + .if_not_exists() + .name("idx_dialysis_prescription_tenant_id") + .table(Alias::new("dialysis_prescription")) + .col(Alias::new("tenant_id")) + .to_owned(), + ) + .await?; + + // 患者索引 + manager + .create_index( + Index::create() + .if_not_exists() + .name("idx_dialysis_prescription_patient_id") + .table(Alias::new("dialysis_prescription")) + .col(Alias::new("patient_id")) + .to_owned(), + ) + .await?; + + // 患者+状态复合索引(仅未删除记录)— 查询当前生效处方 + manager + .create_index( + Index::create() + .if_not_exists() + .name("idx_dialysis_prescription_patient_status") + .table(Alias::new("dialysis_prescription")) + .col(Alias::new("patient_id")) + .col(Alias::new("status")) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Alias::new("dialysis_prescription")).to_owned()) + .await + } +} diff --git a/crates/erp-server/migration/src/m20260427_000082_seed_ai_prompts.rs b/crates/erp-server/migration/src/m20260427_000082_seed_ai_prompts.rs new file mode 100644 index 0000000..fdbc63b --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000082_seed_ai_prompts.rs @@ -0,0 +1,84 @@ +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> { + let db = manager.get_connection(); + + let result = db.query_one(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "SELECT id::text FROM tenant LIMIT 1".to_string(), + )) + .await?; + + let tid = match result { + Some(row) => row.try_get_by_index::(0).unwrap_or_default(), + None => return Ok(()), + }; + + let sys = "00000000-0000-0000-0000-000000000000"; + let mc = r#"{"model":"claude-sonnet-4-6","temperature":0.3,"max_tokens":2048}"#; + + insert_prompt(db, &tid, sys, "lab_report_interpretation", "化验单解读", "analysis", mc, + "你是一名专业的医学检验解读助手。请根据提供的化验指标数据,为患者提供清晰、准确的解读。\n\n要求:\n1. 逐项分析每个指标的含义和是否在正常范围\n2. 对异常指标提供可能的原因说明(不作为诊断)\n3. 给出建议的后续行动\n4. 使用通俗易懂的语言\n5. 最后给出总体健康提示", + "以下是患者的化验报告数据:\n\n报告日期:{{report_date}}\n科室:{{department}}\n患者年龄段:{{age_group}},性别:{{sex}}\n\n检查项目:\n{{#each items}}\n- {{name}}:{{value}} {{unit}}(参考范围:{{reference_range}},{{#if is_abnormal}}异常{{else}}正常{{/if}})\n{{/each}}\n\n请对以上化验结果进行详细解读。", + ).await?; + + insert_prompt(db, &tid, sys, "health_trend_analysis", "健康趋势分析", "analysis", mc, + "你是一名健康数据分析专家。请根据提供的生命体征趋势数据,分析患者的健康变化趋势。\n\n要求:\n1. 识别数据中的关键趋势\n2. 对异常趋势提出预警\n3. 结合各指标间的关联性进行综合分析\n4. 给出健康管理建议", + "以下是患者的生命体征趋势数据:\n\n患者年龄段:{{age_group}},性别:{{sex}}\n\n监测指标:\n{{#each metrics}}\n### {{name}}({{unit}})\n数据点:{{#each values}}{{this.[0]}}: {{this.[1]}}{{#unless @last}}, {{/unless}}{{/each}}\n{{/each}}\n\n请分析以上健康趋势数据。", + ).await?; + + insert_prompt(db, &tid, sys, "personalized_checkup_plan", "个性化体检方案", "planning", mc, + "你是一名健康管理顾问。请根据患者的健康状况信息,为其制定个性化的体检方案。\n\n要求:\n1. 基于患者年龄、性别、既往病史推荐检查项目\n2. 按优先级排序\n3. 给出建议的检查频率\n4. 说明每项检查的目的和意义", + "请为以下患者制定个性化体检方案:\n\n年龄段:{{age_group}}\n性别:{{sex}}\n慢性疾病:{{#each chronic_conditions}}{{this}}{{#unless @last}}、{{/unless}}{{/each}}\n当前用药:{{#each medications}}{{this}}{{#unless @last}}、{{/unless}}{{/each}}\n家族病史:{{#each family_history}}{{this}}{{#unless @last}}、{{/unless}}{{/each}}\n\n请给出详细的体检方案推荐。", + ).await?; + + insert_prompt(db, &tid, sys, "report_summary_generation", "报告摘要生成", "summary", mc, + "你是一名医疗报告摘要撰写专家。请根据提供的健康报告内容,生成结构清晰的摘要。\n\n要求:\n1. 提取报告中的关键发现\n2. 标注异常项目及其严重程度\n3. 给出简明的结论\n4. 提供行动建议\n5. 控制在 500 字以内", + "请为以下健康报告生成摘要:\n\n报告日期:{{report_date}}\n科室:{{department}}\n患者年龄段:{{age_group}},性别:{{sex}}\n\n报告内容:\n{{#each sections}}\n## {{title}}\n发现:{{#each findings}}{{this}}{{#unless @last}};{{/unless}}{{/each}}\n{{#if abnormal_items.length}}\n异常项:{{#each abnormal_items}}{{this}}{{#unless @last}}、{{/unless}}{{/each}}\n{{/if}}\n{{/each}}\n\n请生成结构化的报告摘要。", + ).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "DELETE FROM ai_prompts WHERE name IN ('lab_report_interpretation','health_trend_analysis','personalized_checkup_plan','report_summary_generation')".to_string(), + )) + .await?; + Ok(()) + } +} + +fn esc(s: &str) -> String { + s.replace('\'', "''") +} + +async fn insert_prompt( + db: &sea_orm_migration::SchemaManagerConnection<'_>, + tenant_id: &str, + sys: &str, + name: &str, + description: &str, + category: &str, + model_config: &str, + system_prompt: &str, + user_template: &str, +) -> Result<(), DbErr> { + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + format!( + "INSERT INTO ai_prompts (id, tenant_id, name, description, system_prompt, user_prompt_template, model_config, version, is_active, category, tags, created_at, updated_at, created_by, updated_by, version_lock) \ + VALUES (gen_random_uuid(), '{}', '{}', '{}', '{}', '{}', '{}', 1, true, '{}', NULL, NOW(), NOW(), '{}', '{}', 1) ON CONFLICT DO NOTHING", + tenant_id, esc(name), esc(description), esc(system_prompt), esc(user_template), esc(model_config), category, sys, sys + ), + )) + .await?; + Ok(()) +}