fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
This commit is contained in:
iven
2026-05-07 23:43:14 +08:00
parent 786f57c151
commit 6d5a711d2c
323 changed files with 15662 additions and 6603 deletions

View File

@@ -1,3 +1,5 @@
#![allow(clippy::too_many_arguments)]
pub use sea_orm_migration::prelude::*;
mod m20260410_000001_create_tenant;
@@ -49,18 +51,23 @@ mod m20260424_000046_health_constraints_fix;
mod m20260424_000047_health_index_fix;
mod m20260425_000048_add_patient_id_number_hash;
mod m20260425_000049_widen_patient_id_number;
mod m20260425_00050_add_doctor_name_column;
mod m20260425_000051_dialysis_and_lab_enhance;
mod m20260425_000052_create_ai_tables;
mod m20260425_000053_create_points_tables;
mod m20260425_000054_create_daily_monitoring;
mod m20260425_000055_points_checkin_standard_fields;
mod m20260425_00050_add_doctor_name_column;
mod m20260426_000056_create_diagnosis;
mod m20260426_000057_rename_points_transaction_type_column;
mod m20260426_000058_merge_daily_monitoring_into_vital_signs;
mod m20260426_000059_seed_menus;
mod m20260426_000060_create_critical_value_thresholds;
mod m20260426_000061_create_consent;
mod m20260426_000073_create_device_readings;
mod m20260426_000074_create_vital_signs_hourly;
mod m20260426_000075_create_patient_devices;
mod m20260426_000076_create_alert_rules;
mod m20260426_000077_create_alerts;
mod m20260427_000062_create_tenant_crypto_keys;
mod m20260427_000063_content_management;
mod m20260427_000064_add_patient_pii_fields;
@@ -72,11 +79,6 @@ mod m20260427_000069_add_dialysis_record_key_version;
mod m20260427_000070_add_lab_report_key_version;
mod m20260427_000071_add_diagnosis_key_version;
mod m20260427_000072_widen_encrypted_phone_columns;
mod m20260426_000073_create_device_readings;
mod m20260426_000074_create_vital_signs_hourly;
mod m20260426_000075_create_patient_devices;
mod m20260426_000076_create_alert_rules;
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;
@@ -128,6 +130,7 @@ mod m20260506_000125_restructure_menus_and_roles;
mod m20260506_000126_fix_role_permissions_cleanup;
mod m20260507_000127_fix_doctor_extra_permissions;
mod m20260507_000128_fix_alert_status_and_menu_perms;
mod m20260507_000129_fix_nurse_operator_points_permissions;
pub struct Migrator;
@@ -241,7 +244,9 @@ impl MigratorTrait for Migrator {
Box::new(m20260504_000104_create_vital_signs_daily::Migration),
Box::new(m20260504_000105_alter_patient_devices_add_status::Migration),
Box::new(m20260504_000106_create_api_clients::Migration),
Box::new(m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete::Migration),
Box::new(
m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete::Migration,
),
Box::new(m20260504_000108_alter_vital_signs_hourly_add_soft_delete::Migration),
Box::new(m20260504_000109_add_missing_fk_constraints::Migration),
Box::new(m20260504_000110_alter_critical_alerts_version_i32::Migration),
@@ -263,6 +268,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260506_000126_fix_role_permissions_cleanup::Migration),
Box::new(m20260507_000127_fix_doctor_extra_permissions::Migration),
Box::new(m20260507_000128_fix_alert_status_and_menu_perms::Migration),
Box::new(m20260507_000129_fix_nurse_operator_points_permissions::Migration),
]
}
}

View File

@@ -19,8 +19,16 @@ impl MigrationTrait for Migration {
.primary_key(),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("name")).string_len(200).not_null())
.col(ColumnDef::new(Alias::new("plugin_version")).string_len(50).not_null())
.col(
ColumnDef::new(Alias::new("name"))
.string_len(200)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("plugin_version"))
.string_len(50)
.not_null(),
)
.col(ColumnDef::new(Alias::new("description")).text().null())
.col(ColumnDef::new(Alias::new("author")).string_len(200).null())
.col(
@@ -29,9 +37,21 @@ impl MigrationTrait for Migration {
.not_null()
.default("uploaded"),
)
.col(ColumnDef::new(Alias::new("manifest_json")).json().not_null())
.col(ColumnDef::new(Alias::new("wasm_binary")).binary().not_null())
.col(ColumnDef::new(Alias::new("wasm_hash")).string_len(64).not_null())
.col(
ColumnDef::new(Alias::new("manifest_json"))
.json()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("wasm_binary"))
.binary()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("wasm_hash"))
.string_len(64)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("config_json"))
.json()
@@ -39,8 +59,16 @@ impl MigrationTrait for Migration {
.default(Expr::val("{}")),
)
.col(ColumnDef::new(Alias::new("error_message")).text().null())
.col(ColumnDef::new(Alias::new("installed_at")).timestamp_with_time_zone().null())
.col(ColumnDef::new(Alias::new("enabled_at")).timestamp_with_time_zone().null())
.col(
ColumnDef::new(Alias::new("installed_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("enabled_at"))
.timestamp_with_time_zone()
.null(),
)
// 标准字段
.col(
ColumnDef::new(Alias::new("created_at"))
@@ -56,7 +84,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -102,8 +134,16 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("plugin_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("entity_name")).string_len(100).not_null())
.col(ColumnDef::new(Alias::new("table_name")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("entity_name"))
.string_len(100)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("table_name"))
.string_len(200)
.not_null(),
)
.col(ColumnDef::new(Alias::new("schema_json")).json().not_null())
// 标准字段
.col(
@@ -120,7 +160,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -154,7 +198,11 @@ impl MigrationTrait for Migration {
.primary_key(),
)
.col(ColumnDef::new(Alias::new("plugin_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("event_pattern")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("event_pattern"))
.string_len(200)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
@@ -180,10 +228,18 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("plugin_event_subscriptions")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("plugin_event_subscriptions"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("plugin_entities")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("plugin_entities"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("plugins")).to_owned())

View File

@@ -65,14 +65,19 @@ impl MigrationTrait for Migration {
WHERE permission_id IN (
SELECT id FROM permissions WHERE code IN ('plugin.admin', 'plugin.list')
)
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 删除 plugin 权限
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"DELETE FROM permissions WHERE code IN ('plugin.admin', 'plugin.list')".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}

View File

@@ -26,9 +26,17 @@ impl MigrationTrait for Migration {
.primary_key(),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("plugin_entity_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("plugin_entity_id"))
.uuid()
.not_null(),
)
.col(ColumnDef::new(Alias::new("field_name")).string().not_null())
.col(ColumnDef::new(Alias::new("column_name")).string().not_null())
.col(
ColumnDef::new(Alias::new("column_name"))
.string()
.not_null(),
)
.col(ColumnDef::new(Alias::new("sql_type")).string().not_null())
.col(
ColumnDef::new(Alias::new("is_generated"))

View File

@@ -11,21 +11,13 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("user_departments"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("user_id"))
.uuid()
.not_null(),
)
.col(ColumnDef::new(Alias::new("user_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("department_id"))
.uuid()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("tenant_id"))
.uuid()
.not_null(),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("is_primary"))
.boolean()
@@ -92,7 +84,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("user_departments")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("user_departments"))
.to_owned(),
)
.await
}
}

View File

@@ -26,8 +26,11 @@ impl MigrationTrait for Migration {
action = 'customer_tag.manage',
updated_at = NOW()
WHERE code = 'erp-crm.tag.manage' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 2. 重命名权限码erp-crm.relationship.list → erp-crm.customer_relationship.list
db.execute(sea_orm::Statement::from_string(
@@ -40,8 +43,11 @@ impl MigrationTrait for Migration {
action = 'customer_relationship.list',
updated_at = NOW()
WHERE code = 'erp-crm.relationship.list' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 3. 重命名权限码erp-crm.relationship.manage → erp-crm.customer_relationship.manage
db.execute(sea_orm::Statement::from_string(
@@ -54,8 +60,11 @@ impl MigrationTrait for Migration {
action = 'customer_relationship.manage',
updated_at = NOW()
WHERE code = 'erp-crm.relationship.manage' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 4. 补充缺失的 customer_tag.list 权限(原 manifest 只有 manage 没有 list
db.execute(sea_orm::Statement::from_string(
@@ -101,14 +110,19 @@ impl MigrationTrait for Migration {
WHERE permission_id IN (
SELECT id FROM permissions WHERE code = 'erp-crm.customer_tag.list'
)
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 删除新增的 customer_tag.list 权限
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"DELETE FROM permissions WHERE code = 'erp-crm.customer_tag.list'".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 回滚权限码erp-crm.customer_tag.manage → erp-crm.tag.manage
db.execute(sea_orm::Statement::from_string(
@@ -120,8 +134,11 @@ impl MigrationTrait for Migration {
action = 'tag.manage',
updated_at = NOW()
WHERE code = 'erp-crm.customer_tag.manage' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 回滚erp-crm.customer_relationship.list → erp-crm.relationship.list
db.execute(sea_orm::Statement::from_string(
@@ -133,8 +150,11 @@ impl MigrationTrait for Migration {
action = 'relationship.list',
updated_at = NOW()
WHERE code = 'erp-crm.customer_relationship.list' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 回滚erp-crm.customer_relationship.manage → erp-crm.relationship.manage
db.execute(sea_orm::Statement::from_string(
@@ -146,8 +166,11 @@ impl MigrationTrait for Migration {
action = 'relationship.manage',
updated_at = NOW()
WHERE code = 'erp-crm.customer_relationship.manage' AND deleted_at IS NULL
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
"#
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}

View File

@@ -27,26 +27,55 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Alias::new("tags")).json()) // 标签列表
.col(ColumnDef::new(Alias::new("icon_url")).string())
.col(ColumnDef::new(Alias::new("screenshots")).json()) // 截图 URL 列表
.col(ColumnDef::new(Alias::new("wasm_binary")).binary().not_null())
.col(ColumnDef::new(Alias::new("manifest_toml")).text().not_null())
.col(
ColumnDef::new(Alias::new("wasm_binary"))
.binary()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("manifest_toml"))
.text()
.not_null(),
)
.col(ColumnDef::new(Alias::new("wasm_hash")).string().not_null())
.col(ColumnDef::new(Alias::new("min_platform_version")).string())
.col(ColumnDef::new(Alias::new("status"))
.string()
.not_null()
.default("published")) // published | suspended
.col(ColumnDef::new(Alias::new("download_count")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("rating_avg")).decimal().not_null().default(0.0))
.col(ColumnDef::new(Alias::new("rating_count")).integer().not_null().default(0))
.col(
ColumnDef::new(Alias::new("status"))
.string()
.not_null()
.default("published"),
) // published | suspended
.col(
ColumnDef::new(Alias::new("download_count"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("rating_avg"))
.decimal()
.not_null()
.default(0.0),
)
.col(
ColumnDef::new(Alias::new("rating_count"))
.integer()
.not_null()
.default(0),
)
.col(ColumnDef::new(Alias::new("changelog")).text()) // 版本更新日志
.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_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()),
)
.to_owned(),
)
.await?;
@@ -65,13 +94,19 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("user_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("market_entry_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("market_entry_id"))
.uuid()
.not_null(),
)
.col(ColumnDef::new(Alias::new("rating")).integer().not_null()) // 1-5
.col(ColumnDef::new(Alias::new("review_text")).text())
.col(ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()))
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await?;
@@ -94,10 +129,18 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("plugin_market_reviews")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("plugin_market_reviews"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("plugin_market_entries")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("plugin_market_entries"))
.to_owned(),
)
.await
}
}

View File

@@ -21,18 +21,31 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("user_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("plugin_id")).string().not_null())
.col(ColumnDef::new(Alias::new("entity_name")).string().not_null())
.col(
ColumnDef::new(Alias::new("entity_name"))
.string()
.not_null(),
)
.col(ColumnDef::new(Alias::new("view_name")).string().not_null())
.col(ColumnDef::new(Alias::new("view_config")).json().not_null())
.col(ColumnDef::new(Alias::new("is_default")).boolean().not_null().default(false))
.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("is_default"))
.boolean()
.not_null()
.default(false),
)
.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()),
)
.to_owned(),
)
.await
@@ -40,7 +53,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("plugin_user_views")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("plugin_user_views"))
.to_owned(),
)
.await
}
}

View File

@@ -22,8 +22,16 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Patient::IdNumber).string_len(20).null())
.col(ColumnDef::new(Patient::AllergyHistory).text().null())
.col(ColumnDef::new(Patient::MedicalHistorySummary).text().null())
.col(ColumnDef::new(Patient::EmergencyContactName).string_len(100).null())
.col(ColumnDef::new(Patient::EmergencyContactPhone).string_len(20).null())
.col(
ColumnDef::new(Patient::EmergencyContactName)
.string_len(100)
.null(),
)
.col(
ColumnDef::new(Patient::EmergencyContactPhone)
.string_len(20)
.null(),
)
.col(
ColumnDef::new(Patient::Status)
.string_len(20)
@@ -52,7 +60,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Patient::CreatedBy).uuid().null())
.col(ColumnDef::new(Patient::UpdatedBy).uuid().null())
.col(ColumnDef::new(Patient::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(Patient::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Patient::Version)
.integer()
@@ -110,11 +122,31 @@ impl MigrationTrait for Migration {
.not_null()
.primary_key(),
)
.col(ColumnDef::new(PatientFamilyMember::TenantId).uuid().not_null())
.col(ColumnDef::new(PatientFamilyMember::PatientId).uuid().not_null())
.col(ColumnDef::new(PatientFamilyMember::Name).string_len(100).not_null())
.col(ColumnDef::new(PatientFamilyMember::Relationship).string_len(50).not_null())
.col(ColumnDef::new(PatientFamilyMember::Phone).string_len(20).null())
.col(
ColumnDef::new(PatientFamilyMember::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientFamilyMember::PatientId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientFamilyMember::Name)
.string_len(100)
.not_null(),
)
.col(
ColumnDef::new(PatientFamilyMember::Relationship)
.string_len(50)
.not_null(),
)
.col(
ColumnDef::new(PatientFamilyMember::Phone)
.string_len(20)
.null(),
)
.col(ColumnDef::new(PatientFamilyMember::BirthDate).date().null())
.col(ColumnDef::new(PatientFamilyMember::Notes).text().null())
.col(
@@ -158,7 +190,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(PatientTag::Table)
.if_not_exists()
.col(ColumnDef::new(PatientTag::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(PatientTag::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(PatientTag::TenantId).uuid().not_null())
.col(ColumnDef::new(PatientTag::Name).string_len(50).not_null())
.col(ColumnDef::new(PatientTag::Color).string_len(20).null())
@@ -183,7 +220,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(PatientTag::CreatedBy).uuid().null())
.col(ColumnDef::new(PatientTag::UpdatedBy).uuid().null())
.col(ColumnDef::new(PatientTag::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(PatientTag::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(PatientTag::Version)
.integer()
@@ -206,8 +247,16 @@ impl MigrationTrait for Migration {
.not_null()
.primary_key(),
)
.col(ColumnDef::new(PatientTagRelation::TenantId).uuid().not_null())
.col(ColumnDef::new(PatientTagRelation::PatientId).uuid().not_null())
.col(
ColumnDef::new(PatientTagRelation::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientTagRelation::PatientId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(PatientTagRelation::TagId).uuid().not_null())
.col(
ColumnDef::new(PatientTagRelation::CreatedAt)
@@ -287,14 +336,35 @@ impl MigrationTrait for Migration {
Table::create()
.table(DoctorProfile::Table)
.if_not_exists()
.col(ColumnDef::new(DoctorProfile::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(DoctorProfile::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(DoctorProfile::TenantId).uuid().not_null())
.col(ColumnDef::new(DoctorProfile::UserId).uuid().null())
.col(ColumnDef::new(DoctorProfile::Name).string_len(100).not_null())
.col(ColumnDef::new(DoctorProfile::Department).string_len(100).null())
.col(
ColumnDef::new(DoctorProfile::Name)
.string_len(100)
.not_null(),
)
.col(
ColumnDef::new(DoctorProfile::Department)
.string_len(100)
.null(),
)
.col(ColumnDef::new(DoctorProfile::Title).string_len(50).null())
.col(ColumnDef::new(DoctorProfile::Specialty).string_len(200).null())
.col(ColumnDef::new(DoctorProfile::LicenseNumber).string_len(50).null())
.col(
ColumnDef::new(DoctorProfile::Specialty)
.string_len(200)
.null(),
)
.col(
ColumnDef::new(DoctorProfile::LicenseNumber)
.string_len(50)
.null(),
)
.col(ColumnDef::new(DoctorProfile::Bio).text().null())
.col(
ColumnDef::new(DoctorProfile::OnlineStatus)
@@ -316,7 +386,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(DoctorProfile::CreatedBy).uuid().null())
.col(ColumnDef::new(DoctorProfile::UpdatedBy).uuid().null())
.col(ColumnDef::new(DoctorProfile::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(DoctorProfile::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(DoctorProfile::Version)
.integer()
@@ -352,9 +426,21 @@ impl MigrationTrait for Migration {
.not_null()
.primary_key(),
)
.col(ColumnDef::new(PatientDoctorRelation::TenantId).uuid().not_null())
.col(ColumnDef::new(PatientDoctorRelation::PatientId).uuid().not_null())
.col(ColumnDef::new(PatientDoctorRelation::DoctorId).uuid().not_null())
.col(
ColumnDef::new(PatientDoctorRelation::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::PatientId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::DoctorId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::RelationshipType)
.string_len(20)
@@ -373,8 +459,16 @@ impl MigrationTrait for Migration {
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(PatientDoctorRelation::CreatedBy).uuid().null())
.col(ColumnDef::new(PatientDoctorRelation::UpdatedBy).uuid().null())
.col(
ColumnDef::new(PatientDoctorRelation::CreatedBy)
.uuid()
.null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::UpdatedBy)
.uuid()
.null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::DeletedAt)
.timestamp_with_time_zone()
@@ -382,13 +476,19 @@ impl MigrationTrait for Migration {
)
.foreign_key(
ForeignKey::create()
.from(PatientDoctorRelation::Table, PatientDoctorRelation::PatientId)
.from(
PatientDoctorRelation::Table,
PatientDoctorRelation::PatientId,
)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(PatientDoctorRelation::Table, PatientDoctorRelation::DoctorId)
.from(
PatientDoctorRelation::Table,
PatientDoctorRelation::DoctorId,
)
.to(DoctorProfile::Table, DoctorProfile::Id)
.on_delete(ForeignKeyAction::Cascade),
)
@@ -425,7 +525,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(HealthRecord::Table)
.if_not_exists()
.col(ColumnDef::new(HealthRecord::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(HealthRecord::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(HealthRecord::TenantId).uuid().not_null())
.col(ColumnDef::new(HealthRecord::PatientId).uuid().not_null())
.col(
@@ -436,8 +541,16 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(HealthRecord::RecordDate).date().not_null())
.col(ColumnDef::new(HealthRecord::Source).string_len(200).null())
.col(ColumnDef::new(HealthRecord::OverallAssessment).text().null())
.col(ColumnDef::new(HealthRecord::ReportFileUrl).string_len(500).null())
.col(
ColumnDef::new(HealthRecord::OverallAssessment)
.text()
.null(),
)
.col(
ColumnDef::new(HealthRecord::ReportFileUrl)
.string_len(500)
.null(),
)
.col(ColumnDef::new(HealthRecord::Notes).text().null())
.col(
ColumnDef::new(HealthRecord::CreatedAt)
@@ -453,7 +566,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(HealthRecord::CreatedBy).uuid().null())
.col(ColumnDef::new(HealthRecord::UpdatedBy).uuid().null())
.col(ColumnDef::new(HealthRecord::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(HealthRecord::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(HealthRecord::Version)
.integer()
@@ -489,17 +606,42 @@ impl MigrationTrait for Migration {
Table::create()
.table(VitalSigns::Table)
.if_not_exists()
.col(ColumnDef::new(VitalSigns::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(VitalSigns::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(VitalSigns::TenantId).uuid().not_null())
.col(ColumnDef::new(VitalSigns::PatientId).uuid().not_null())
.col(ColumnDef::new(VitalSigns::RecordDate).date().not_null())
.col(ColumnDef::new(VitalSigns::SystolicBpMorning).integer().null())
.col(ColumnDef::new(VitalSigns::DiastolicBpMorning).integer().null())
.col(ColumnDef::new(VitalSigns::SystolicBpEvening).integer().null())
.col(ColumnDef::new(VitalSigns::DiastolicBpEvening).integer().null())
.col(
ColumnDef::new(VitalSigns::SystolicBpMorning)
.integer()
.null(),
)
.col(
ColumnDef::new(VitalSigns::DiastolicBpMorning)
.integer()
.null(),
)
.col(
ColumnDef::new(VitalSigns::SystolicBpEvening)
.integer()
.null(),
)
.col(
ColumnDef::new(VitalSigns::DiastolicBpEvening)
.integer()
.null(),
)
.col(ColumnDef::new(VitalSigns::HeartRate).integer().null())
.col(ColumnDef::new(VitalSigns::Weight).decimal_len(5, 1).null())
.col(ColumnDef::new(VitalSigns::BloodSugar).decimal_len(5, 1).null())
.col(
ColumnDef::new(VitalSigns::BloodSugar)
.decimal_len(5, 1)
.null(),
)
.col(ColumnDef::new(VitalSigns::WaterIntakeMl).integer().null())
.col(ColumnDef::new(VitalSigns::UrineOutputMl).integer().null())
.col(ColumnDef::new(VitalSigns::Notes).text().null())
@@ -517,7 +659,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(VitalSigns::CreatedBy).uuid().null())
.col(ColumnDef::new(VitalSigns::UpdatedBy).uuid().null())
.col(ColumnDef::new(VitalSigns::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(VitalSigns::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(VitalSigns::Version)
.integer()
@@ -553,14 +699,27 @@ impl MigrationTrait for Migration {
Table::create()
.table(LabReport::Table)
.if_not_exists()
.col(ColumnDef::new(LabReport::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(LabReport::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(LabReport::TenantId).uuid().not_null())
.col(ColumnDef::new(LabReport::PatientId).uuid().not_null())
.col(ColumnDef::new(LabReport::ReportDate).date().not_null())
.col(ColumnDef::new(LabReport::ReportType).string_len(50).not_null())
.col(
ColumnDef::new(LabReport::ReportType)
.string_len(50)
.not_null(),
)
.col(ColumnDef::new(LabReport::Indicators).json_binary().null())
.col(ColumnDef::new(LabReport::ImageUrls).json_binary().null())
.col(ColumnDef::new(LabReport::DoctorInterpretation).text().null())
.col(
ColumnDef::new(LabReport::DoctorInterpretation)
.text()
.null(),
)
.col(
ColumnDef::new(LabReport::CreatedAt)
.timestamp_with_time_zone()
@@ -575,7 +734,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(LabReport::CreatedBy).uuid().null())
.col(ColumnDef::new(LabReport::UpdatedBy).uuid().null())
.col(ColumnDef::new(LabReport::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(LabReport::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(LabReport::Version)
.integer()
@@ -611,20 +774,37 @@ impl MigrationTrait for Migration {
Table::create()
.table(HealthTrend::Table)
.if_not_exists()
.col(ColumnDef::new(HealthTrend::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(HealthTrend::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(HealthTrend::TenantId).uuid().not_null())
.col(ColumnDef::new(HealthTrend::PatientId).uuid().not_null())
.col(ColumnDef::new(HealthTrend::PeriodStart).date().not_null())
.col(ColumnDef::new(HealthTrend::PeriodEnd).date().not_null())
.col(ColumnDef::new(HealthTrend::IndicatorSummary).json_binary().null())
.col(ColumnDef::new(HealthTrend::AbnormalItems).json_binary().null())
.col(
ColumnDef::new(HealthTrend::IndicatorSummary)
.json_binary()
.null(),
)
.col(
ColumnDef::new(HealthTrend::AbnormalItems)
.json_binary()
.null(),
)
.col(
ColumnDef::new(HealthTrend::GenerationType)
.string_len(20)
.not_null()
.default("auto"),
)
.col(ColumnDef::new(HealthTrend::ReportFileUrl).string_len(500).null())
.col(
ColumnDef::new(HealthTrend::ReportFileUrl)
.string_len(500)
.null(),
)
.col(
ColumnDef::new(HealthTrend::CreatedAt)
.timestamp_with_time_zone()
@@ -639,7 +819,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(HealthTrend::CreatedBy).uuid().null())
.col(ColumnDef::new(HealthTrend::UpdatedBy).uuid().null())
.col(ColumnDef::new(HealthTrend::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(HealthTrend::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(HealthTrend::Version)
.integer()
@@ -662,7 +846,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(Appointment::Table)
.if_not_exists()
.col(ColumnDef::new(Appointment::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(Appointment::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Appointment::TenantId).uuid().not_null())
.col(ColumnDef::new(Appointment::PatientId).uuid().not_null())
.col(ColumnDef::new(Appointment::DoctorId).uuid().null())
@@ -672,7 +861,11 @@ impl MigrationTrait for Migration {
.not_null()
.default("outpatient"),
)
.col(ColumnDef::new(Appointment::AppointmentDate).date().not_null())
.col(
ColumnDef::new(Appointment::AppointmentDate)
.date()
.not_null(),
)
.col(ColumnDef::new(Appointment::StartTime).time().not_null())
.col(ColumnDef::new(Appointment::EndTime).time().not_null())
.col(
@@ -697,7 +890,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Appointment::CreatedBy).uuid().null())
.col(ColumnDef::new(Appointment::UpdatedBy).uuid().null())
.col(ColumnDef::new(Appointment::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(Appointment::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Appointment::Version)
.integer()
@@ -751,10 +948,19 @@ impl MigrationTrait for Migration {
Table::create()
.table(DoctorSchedule::Table)
.if_not_exists()
.col(ColumnDef::new(DoctorSchedule::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(DoctorSchedule::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(DoctorSchedule::TenantId).uuid().not_null())
.col(ColumnDef::new(DoctorSchedule::DoctorId).uuid().not_null())
.col(ColumnDef::new(DoctorSchedule::ScheduleDate).date().not_null())
.col(
ColumnDef::new(DoctorSchedule::ScheduleDate)
.date()
.not_null(),
)
.col(
ColumnDef::new(DoctorSchedule::PeriodType)
.string_len(20)
@@ -763,7 +969,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(DoctorSchedule::StartTime).time().not_null())
.col(ColumnDef::new(DoctorSchedule::EndTime).time().not_null())
.col(ColumnDef::new(DoctorSchedule::MaxAppointments).integer().not_null())
.col(
ColumnDef::new(DoctorSchedule::MaxAppointments)
.integer()
.not_null(),
)
.col(
ColumnDef::new(DoctorSchedule::CurrentAppointments)
.integer()
@@ -846,7 +1056,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(FollowUpTask::Table)
.if_not_exists()
.col(ColumnDef::new(FollowUpTask::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(FollowUpTask::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(FollowUpTask::TenantId).uuid().not_null())
.col(ColumnDef::new(FollowUpTask::PatientId).uuid().not_null())
.col(ColumnDef::new(FollowUpTask::AssignedTo).uuid().null())
@@ -864,7 +1079,11 @@ impl MigrationTrait for Migration {
.default("pending"),
)
.col(ColumnDef::new(FollowUpTask::ContentTemplate).text().null())
.col(ColumnDef::new(FollowUpTask::RelatedAppointmentId).uuid().null())
.col(
ColumnDef::new(FollowUpTask::RelatedAppointmentId)
.uuid()
.null(),
)
.col(
ColumnDef::new(FollowUpTask::CreatedAt)
.timestamp_with_time_zone()
@@ -879,7 +1098,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(FollowUpTask::CreatedBy).uuid().null())
.col(ColumnDef::new(FollowUpTask::UpdatedBy).uuid().null())
.col(ColumnDef::new(FollowUpTask::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(FollowUpTask::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(FollowUpTask::Version)
.integer()
@@ -927,20 +1150,37 @@ impl MigrationTrait for Migration {
Table::create()
.table(FollowUpRecord::Table)
.if_not_exists()
.col(ColumnDef::new(FollowUpRecord::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(FollowUpRecord::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(FollowUpRecord::TenantId).uuid().not_null())
.col(ColumnDef::new(FollowUpRecord::TaskId).uuid().not_null())
.col(ColumnDef::new(FollowUpRecord::ExecutedBy).uuid().null())
.col(ColumnDef::new(FollowUpRecord::ExecutedDate).date().not_null())
.col(
ColumnDef::new(FollowUpRecord::ExecutedDate)
.date()
.not_null(),
)
.col(
ColumnDef::new(FollowUpRecord::Result)
.string_len(20)
.not_null()
.default("followed_up"),
)
.col(ColumnDef::new(FollowUpRecord::PatientCondition).text().null())
.col(
ColumnDef::new(FollowUpRecord::PatientCondition)
.text()
.null(),
)
.col(ColumnDef::new(FollowUpRecord::MedicalAdvice).text().null())
.col(ColumnDef::new(FollowUpRecord::NextFollowUpDate).date().null())
.col(
ColumnDef::new(FollowUpRecord::NextFollowUpDate)
.date()
.null(),
)
.col(
ColumnDef::new(FollowUpRecord::CreatedAt)
.timestamp_with_time_zone()
@@ -1000,8 +1240,16 @@ impl MigrationTrait for Migration {
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ConsultationSession::TenantId).uuid().not_null())
.col(ColumnDef::new(ConsultationSession::PatientId).uuid().not_null())
.col(
ColumnDef::new(ConsultationSession::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ConsultationSession::PatientId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(ConsultationSession::DoctorId).uuid().null())
.col(
ColumnDef::new(ConsultationSession::ConsultationType)
@@ -1015,7 +1263,11 @@ impl MigrationTrait for Migration {
.not_null()
.default("waiting"),
)
.col(ColumnDef::new(ConsultationSession::LastMessageAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(ConsultationSession::LastMessageAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ConsultationSession::UnreadCountPatient)
.integer()
@@ -1106,9 +1358,21 @@ impl MigrationTrait for Migration {
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ConsultationMessage::TenantId).uuid().not_null())
.col(ColumnDef::new(ConsultationMessage::SessionId).uuid().not_null())
.col(ColumnDef::new(ConsultationMessage::SenderId).uuid().not_null())
.col(
ColumnDef::new(ConsultationMessage::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::SessionId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::SenderId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::SenderRole)
.string_len(20)
@@ -1120,7 +1384,11 @@ impl MigrationTrait for Migration {
.not_null()
.default("text"),
)
.col(ColumnDef::new(ConsultationMessage::Content).text().not_null())
.col(
ColumnDef::new(ConsultationMessage::Content)
.text()
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::IsRead)
.boolean()
@@ -1192,22 +1460,54 @@ impl MigrationTrait for Migration {
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(ConsultationMessage::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(ConsultationSession::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(FollowUpRecord::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(FollowUpTask::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(Appointment::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(DoctorSchedule::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(HealthTrend::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(LabReport::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(VitalSigns::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(HealthRecord::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(PatientDoctorRelation::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(PatientTagRelation::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(PatientTag::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(PatientFamilyMember::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(DoctorProfile::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(Patient::Table).to_owned()).await?;
manager
.drop_table(Table::drop().table(ConsultationMessage::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(ConsultationSession::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(FollowUpRecord::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(FollowUpTask::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Appointment::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(DoctorSchedule::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(HealthTrend::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(LabReport::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(VitalSigns::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(HealthRecord::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(PatientDoctorRelation::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(PatientTagRelation::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(PatientTag::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(PatientFamilyMember::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(DoctorProfile::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Patient::Table).to_owned())
.await?;
Ok(())
}
}
@@ -1410,6 +1710,7 @@ enum HealthTrend {
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Appointment {
Table,
Id,

View File

@@ -11,7 +11,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(WechatUsers::Table)
.if_not_exists()
.col(ColumnDef::new(WechatUsers::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(WechatUsers::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(WechatUsers::TenantId).uuid().not_null())
.col(ColumnDef::new(WechatUsers::Openid).string().not_null())
.col(ColumnDef::new(WechatUsers::UnionId).string())
@@ -31,11 +36,13 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(WechatUsers::CreatedBy).uuid())
.col(ColumnDef::new(WechatUsers::UpdatedBy).uuid())
.col(ColumnDef::new(WechatUsers::DeletedAt).timestamp_with_time_zone())
.col(
ColumnDef::new(WechatUsers::DeletedAt)
.timestamp_with_time_zone(),
ColumnDef::new(WechatUsers::Version)
.integer()
.not_null()
.default(1),
)
.col(ColumnDef::new(WechatUsers::Version).integer().not_null().default(1))
.to_owned(),
)
.await?;

View File

@@ -19,7 +19,11 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Article::CoverImage).string_len(500).null())
.col(ColumnDef::new(Article::Category).string_len(50).null())
.col(ColumnDef::new(Article::Author).string_len(100).null())
.col(ColumnDef::new(Article::PublishedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(Article::PublishedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Article::CreatedAt)
.timestamp_with_time_zone()
@@ -34,7 +38,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Article::CreatedBy).uuid().null())
.col(ColumnDef::new(Article::UpdatedBy).uuid().null())
.col(ColumnDef::new(Article::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(Article::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Article::Version)
.integer()
@@ -72,7 +80,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_article_tenant_published").to_owned())
.drop_index(
Index::drop()
.name("idx_article_tenant_published")
.to_owned(),
)
.await?;
manager
.drop_index(Index::drop().name("idx_article_tenant_category").to_owned())

View File

@@ -59,10 +59,18 @@ impl MigrationTrait for Migration {
)
.await?;
manager
.drop_index(Index::drop().name("idx_health_trend_patient_period").to_owned())
.drop_index(
Index::drop()
.name("idx_health_trend_patient_period")
.to_owned(),
)
.await?;
manager
.drop_index(Index::drop().name("idx_follow_up_record_task_date").to_owned())
.drop_index(
Index::drop()
.name("idx_follow_up_record_task_date")
.to_owned(),
)
.await?;
Ok(())
}

View File

@@ -10,19 +10,22 @@ impl MigrationTrait for Migration {
let conn = manager.get_connection();
// C-4: patient.id_number 唯一索引 — 重建为 partial index WHERE deleted_at IS NULL
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number) WHERE deleted_at IS NULL AND id_number IS NOT NULL"
).await?;
// C-5: patient_tag.name 唯一索引 — 重建为 partial index WHERE deleted_at IS NULL
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tag_tenant_name_unique ON patient_tag (tenant_id, name) WHERE deleted_at IS NULL"
).await?;
// C-6: doctor_schedule 唯一索引 — 重建为 partial index修正列选择为 (tenant_id, doctor_id, schedule_date, period_type)
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_doctor_schedule_unique_slot ON doctor_schedule (tenant_id, doctor_id, schedule_date, period_type) WHERE deleted_at IS NULL"
).await?;
@@ -39,12 +42,14 @@ impl MigrationTrait for Migration {
// H-8: follow_up_task.related_appointment_id 添加 FK 约束
conn.execute_unprepared(
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment"
).await?;
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment",
)
.await?;
conn.execute_unprepared(
"ALTER TABLE follow_up_task ADD CONSTRAINT fk_follow_up_task_appointment \
FOREIGN KEY (related_appointment_id) REFERENCES appointment(id) ON DELETE SET NULL"
).await?;
FOREIGN KEY (related_appointment_id) REFERENCES appointment(id) ON DELETE SET NULL",
)
.await?;
// M-6: lab_report 添加 (tenant_id, report_type) 索引
conn.execute_unprepared(
@@ -58,36 +63,40 @@ impl MigrationTrait for Migration {
let conn = manager.get_connection();
// 恢复原始索引(非 partial
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number)"
).await?;
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number)",
)
.await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tag_tenant_name_unique ON patient_tag (tenant_id, name)"
).await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot").await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_doctor_schedule_unique_slot ON doctor_schedule (tenant_id, doctor_id, schedule_date, start_time)"
).await?;
conn.execute_unprepared(
"ALTER TABLE patient_tag_relation DROP COLUMN IF EXISTS version"
).await?;
conn.execute_unprepared("ALTER TABLE patient_tag_relation DROP COLUMN IF EXISTS version")
.await?;
conn.execute_unprepared(
"ALTER TABLE patient_doctor_relation DROP COLUMN IF EXISTS version"
).await?;
"ALTER TABLE patient_doctor_relation DROP COLUMN IF EXISTS version",
)
.await?;
conn.execute_unprepared(
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment"
).await?;
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment",
)
.await?;
conn.execute_unprepared(
"DROP INDEX IF EXISTS idx_lab_report_tenant_type"
).await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_lab_report_tenant_type")
.await?;
Ok(())
}

View File

@@ -9,21 +9,22 @@ impl MigrationTrait for Migration {
let db = manager.get_connection();
// 删除旧索引(缺少 tenant_id 前导列)
db.execute_unprepared(
"DROP INDEX IF EXISTS idx_health_trend_patient_period"
).await?;
db.execute_unprepared("DROP INDEX IF EXISTS idx_health_trend_patient_period")
.await?;
// 重建为包含 tenant_id 的正确索引
db.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_health_trend_tenant_patient_period \
ON health_trend (tenant_id, patient_id, period_start DESC)"
).await?;
ON health_trend (tenant_id, patient_id, period_start DESC)",
)
.await?;
// 添加 follow_up_record 缺失的 (tenant_id, executed_date) 索引
db.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_follow_up_record_tenant_executed_date \
ON follow_up_record (tenant_id, executed_date DESC)"
).await?;
ON follow_up_record (tenant_id, executed_date DESC)",
)
.await?;
Ok(())
}
@@ -31,18 +32,17 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DROP INDEX IF EXISTS idx_health_trend_tenant_patient_period"
).await?;
db.execute_unprepared("DROP INDEX IF EXISTS idx_health_trend_tenant_patient_period")
.await?;
db.execute_unprepared(
"DROP INDEX IF EXISTS idx_follow_up_record_tenant_executed_date"
).await?;
db.execute_unprepared("DROP INDEX IF EXISTS idx_follow_up_record_tenant_executed_date")
.await?;
db.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_health_trend_patient_period \
ON health_trend (patient_id, period_start)"
).await?;
ON health_trend (patient_id, period_start)",
)
.await?;
Ok(())
}

View File

@@ -15,11 +15,7 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Alias::new("patient"))
.add_column(
ColumnDef::new(Alias::new("id_number_hash"))
.string()
.null(),
)
.add_column(ColumnDef::new(Alias::new("id_number_hash")).string().null())
.to_owned(),
)
.await

View File

@@ -18,10 +18,8 @@ impl MigrationTrait for Migration {
.await?;
// 加宽 id_number 列varchar(20) → varchar(255),容纳 AES-256-GCM 加密值(~88 字符)
conn.execute_unprepared(
"ALTER TABLE patient ALTER COLUMN id_number TYPE varchar(255)",
)
.await?;
conn.execute_unprepared("ALTER TABLE patient ALTER COLUMN id_number TYPE varchar(255)")
.await?;
// 重建唯一索引partial排除软删除和空值
conn.execute_unprepared(
@@ -37,10 +35,8 @@ impl MigrationTrait for Migration {
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number")
.await?;
conn.execute_unprepared(
"ALTER TABLE patient ALTER COLUMN id_number TYPE varchar(20)",
)
.await?;
conn.execute_unprepared("ALTER TABLE patient ALTER COLUMN id_number TYPE varchar(20)")
.await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number) WHERE deleted_at IS NULL AND id_number IS NOT NULL",

View File

@@ -13,16 +13,37 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("dialysis_record"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("dialysis_date")).date().not_null())
.col(
ColumnDef::new(Alias::new("dialysis_date"))
.date()
.not_null(),
)
.col(ColumnDef::new(Alias::new("start_time")).time())
.col(ColumnDef::new(Alias::new("end_time")).time())
// 体重 (Decimal 5,1)
.col(ColumnDef::new(Alias::new("dry_weight")).decimal().extra("CHECK(dry_weight IS NULL OR dry_weight >= 0)"))
.col(ColumnDef::new(Alias::new("pre_weight")).decimal().extra("CHECK(pre_weight IS NULL OR pre_weight >= 0)"))
.col(ColumnDef::new(Alias::new("post_weight")).decimal().extra("CHECK(post_weight IS NULL OR post_weight >= 0)"))
.col(
ColumnDef::new(Alias::new("dry_weight"))
.decimal()
.extra("CHECK(dry_weight IS NULL OR dry_weight >= 0)"),
)
.col(
ColumnDef::new(Alias::new("pre_weight"))
.decimal()
.extra("CHECK(pre_weight IS NULL OR pre_weight >= 0)"),
)
.col(
ColumnDef::new(Alias::new("post_weight"))
.decimal()
.extra("CHECK(post_weight IS NULL OR post_weight >= 0)"),
)
// 血压
.col(ColumnDef::new(Alias::new("pre_bp_systolic")).integer())
.col(ColumnDef::new(Alias::new("pre_bp_diastolic")).integer())
@@ -36,20 +57,45 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Alias::new("dialysis_duration")).integer())
.col(ColumnDef::new(Alias::new("blood_flow_rate")).integer())
// HD / HDF / HF
.col(ColumnDef::new(Alias::new("dialysis_type")).string().not_null().default("HD"))
.col(
ColumnDef::new(Alias::new("dialysis_type"))
.string()
.not_null()
.default("HD"),
)
.col(ColumnDef::new(Alias::new("symptoms")).json())
.col(ColumnDef::new(Alias::new("complication_notes")).text())
// draft / completed / reviewed
.col(ColumnDef::new(Alias::new("status")).string().not_null().default("draft"))
.col(
ColumnDef::new(Alias::new("status"))
.string()
.not_null()
.default("draft"),
)
.col(ColumnDef::new(Alias::new("reviewed_by")).uuid())
.col(ColumnDef::new(Alias::new("reviewed_at")).timestamp_with_time_zone())
// 标准字段
.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_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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -84,7 +130,11 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Alias::new("lab_report"))
.add_column(ColumnDef::new(Alias::new("source")).string().default("manual_input"))
.add_column(
ColumnDef::new(Alias::new("source"))
.string()
.default("manual_input"),
)
.to_owned(),
)
.await?;
@@ -94,7 +144,12 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Alias::new("lab_report"))
.add_column(ColumnDef::new(Alias::new("status")).string().not_null().default("pending"))
.add_column(
ColumnDef::new(Alias::new("status"))
.string()
.not_null()
.default("pending"),
)
.to_owned(),
)
.await?;
@@ -112,37 +167,53 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Alias::new("lab_report"))
.add_column(ColumnDef::new(Alias::new("reviewed_at")).timestamp_with_time_zone())
.add_column(
ColumnDef::new(Alias::new("reviewed_at")).timestamp_with_time_zone(),
)
.to_owned(),
)
.await?;
// 重命名 indicators → items (V2 JSON 结构含 name/value/unit/reference_low/reference_high/is_abnormal)
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN indicators TO items".to_string(),
)).await?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN indicators TO items".to_string(),
))
.await?;
// 重命名 doctor_interpretation → doctor_notes
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN doctor_interpretation TO doctor_notes".to_string(),
)).await?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN doctor_interpretation TO doctor_notes"
.to_string(),
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 恢复 lab_report 列名
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN doctor_notes TO doctor_interpretation".to_string(),
)).await?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN doctor_notes TO doctor_interpretation"
.to_string(),
))
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN items TO indicators".to_string(),
)).await?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"ALTER TABLE lab_report RENAME COLUMN items TO indicators".to_string(),
))
.await?;
// 删除新增列
manager
@@ -159,7 +230,12 @@ impl MigrationTrait for Migration {
// 删除 dialysis_record 表
manager
.drop_table(Table::drop().table(Alias::new("dialysis_record")).if_exists().to_owned())
.drop_table(
Table::drop()
.table(Alias::new("dialysis_record"))
.if_exists()
.to_owned(),
)
.await?;
Ok(())

View File

@@ -62,7 +62,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(AiPrompt::CreatedBy).uuid().null())
.col(ColumnDef::new(AiPrompt::UpdatedBy).uuid().null())
.col(ColumnDef::new(AiPrompt::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(AiPrompt::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(AiPrompt::VersionLock)
.integer()
@@ -92,7 +96,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiAnalysis::Table)
.if_not_exists()
.col(ColumnDef::new(AiAnalysis::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(AiAnalysis::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AiAnalysis::TenantId).uuid().not_null())
.col(ColumnDef::new(AiAnalysis::PatientId).uuid().not_null())
.col(
@@ -106,7 +115,11 @@ impl MigrationTrait for Migration {
.not_null(),
)
.col(ColumnDef::new(AiAnalysis::PromptId).uuid().not_null())
.col(ColumnDef::new(AiAnalysis::PromptVersion).integer().not_null())
.col(
ColumnDef::new(AiAnalysis::PromptVersion)
.integer()
.not_null(),
)
.col(
ColumnDef::new(AiAnalysis::ModelUsed)
.string_len(100)

View File

@@ -13,16 +13,56 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_account"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("balance")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("total_earned")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("total_spent")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("total_expired")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("version")).integer().not_null().default(1))
.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("balance"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("total_earned"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("total_spent"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("total_expired"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.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())
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
@@ -30,7 +70,15 @@ impl MigrationTrait for Migration {
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_account_patient").table(Alias::new("points_account")).col(Alias::new("patient_id")).unique().to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_account_patient")
.table(Alias::new("points_account"))
.col(Alias::new("patient_id"))
.unique()
.to_owned(),
)
.await?;
// 2. points_rule — 积分获取规则
@@ -39,28 +87,93 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_rule"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("event_type")).string_len(64).not_null())
.col(ColumnDef::new(Alias::new("name")).string_len(128).not_null())
.col(
ColumnDef::new(Alias::new("event_type"))
.string_len(64)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("name"))
.string_len(128)
.not_null(),
)
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("points_value")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("daily_cap")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("streak_7d_bonus")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("streak_14d_bonus")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("streak_30d_bonus")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("is_active")).boolean().not_null().default(true))
.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("points_value"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("daily_cap"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("streak_7d_bonus"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("streak_14d_bonus"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("streak_30d_bonus"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("is_active"))
.boolean()
.not_null()
.default(true),
)
.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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_rule_event_type").table(Alias::new("points_rule")).col(Alias::new("event_type")).to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_rule_event_type")
.table(Alias::new("points_rule"))
.col(Alias::new("event_type"))
.to_owned(),
)
.await?;
// 3. points_transaction — 积分流水FIFO 桶模型)
@@ -69,31 +182,79 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_transaction"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("account_id")).uuid().not_null())
// earn / spend / expired / refund
.col(ColumnDef::new(Alias::new("r#type")).string_len(16).not_null())
.col(
ColumnDef::new(Alias::new("r#type"))
.string_len(16)
.not_null(),
)
.col(ColumnDef::new(Alias::new("amount")).integer().not_null())
.col(ColumnDef::new(Alias::new("remaining_amount")).integer().not_null().default(0))
.col(
ColumnDef::new(Alias::new("remaining_amount"))
.integer()
.not_null()
.default(0),
)
// active / expired / consumed
.col(ColumnDef::new(Alias::new("status")).string_len(16).not_null().default("active"))
.col(
ColumnDef::new(Alias::new("status"))
.string_len(16)
.not_null()
.default("active"),
)
.col(ColumnDef::new(Alias::new("expires_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("balance_after")).integer().not_null().default(0))
.col(
ColumnDef::new(Alias::new("balance_after"))
.integer()
.not_null()
.default(0),
)
.col(ColumnDef::new(Alias::new("rule_id")).uuid())
.col(ColumnDef::new(Alias::new("order_id")).uuid())
.col(ColumnDef::new(Alias::new("description")).string_len(256))
.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_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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_txn_account").table(Alias::new("points_transaction")).col(Alias::new("account_id")).col(Alias::new("status")).col(Alias::new("expires_at")).to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_txn_account")
.table(Alias::new("points_transaction"))
.col(Alias::new("account_id"))
.col(Alias::new("status"))
.col(Alias::new("expires_at"))
.to_owned(),
)
.await?;
// 4. points_product — 兑换商品
@@ -102,24 +263,72 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_product"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("name")).string_len(128).not_null())
.col(
ColumnDef::new(Alias::new("name"))
.string_len(128)
.not_null(),
)
// physical / service / privilege
.col(ColumnDef::new(Alias::new("product_type")).string_len(16).not_null().default("physical"))
.col(ColumnDef::new(Alias::new("points_cost")).integer().not_null())
.col(ColumnDef::new(Alias::new("stock")).integer().not_null().default(-1))
.col(
ColumnDef::new(Alias::new("product_type"))
.string_len(16)
.not_null()
.default("physical"),
)
.col(
ColumnDef::new(Alias::new("points_cost"))
.integer()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("stock"))
.integer()
.not_null()
.default(-1),
)
.col(ColumnDef::new(Alias::new("image_url")).string_len(512))
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("service_config")).json())
.col(ColumnDef::new(Alias::new("is_active")).boolean().not_null().default(true))
.col(ColumnDef::new(Alias::new("sort_order")).integer().not_null().default(0))
.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("is_active"))
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(Alias::new("sort_order"))
.integer()
.not_null()
.default(0),
)
.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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -130,32 +339,76 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_order"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("product_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("points_cost")).integer().not_null())
.col(
ColumnDef::new(Alias::new("points_cost"))
.integer()
.not_null(),
)
// pending / verified / cancelled / expired
.col(ColumnDef::new(Alias::new("status")).string_len(16).not_null().default("pending"))
.col(
ColumnDef::new(Alias::new("status"))
.string_len(16)
.not_null()
.default("pending"),
)
.col(ColumnDef::new(Alias::new("qr_code")).uuid())
.col(ColumnDef::new(Alias::new("verified_by")).uuid())
.col(ColumnDef::new(Alias::new("verified_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("expires_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("notes")).string_len(256))
.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_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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_order_patient").table(Alias::new("points_order")).col(Alias::new("patient_id")).col(Alias::new("status")).to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_order_patient")
.table(Alias::new("points_order"))
.col(Alias::new("patient_id"))
.col(Alias::new("status"))
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_order_qr").table(Alias::new("points_order")).col(Alias::new("qr_code")).to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_order_qr")
.table(Alias::new("points_order"))
.col(Alias::new("qr_code"))
.to_owned(),
)
.await?;
// 6. points_checkin — 每日打卡
@@ -164,17 +417,41 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("points_checkin"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("checkin_date")).date().not_null())
.col(ColumnDef::new(Alias::new("consecutive_days")).integer().not_null().default(1))
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(Alias::new("consecutive_days"))
.integer()
.not_null()
.default(1),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_points_checkin_unique").table(Alias::new("points_checkin")).col(Alias::new("patient_id")).col(Alias::new("checkin_date")).unique().to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_points_checkin_unique")
.table(Alias::new("points_checkin"))
.col(Alias::new("patient_id"))
.col(Alias::new("checkin_date"))
.unique()
.to_owned(),
)
.await?;
// 7. offline_event — 线下活动
@@ -183,26 +460,70 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("offline_event"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("title")).string_len(256).not_null())
.col(
ColumnDef::new(Alias::new("title"))
.string_len(256)
.not_null(),
)
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("event_date")).date().not_null())
.col(ColumnDef::new(Alias::new("start_time")).time())
.col(ColumnDef::new(Alias::new("end_time")).time())
.col(ColumnDef::new(Alias::new("location")).string_len(256))
.col(ColumnDef::new(Alias::new("points_reward")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("max_participants")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("current_participants")).integer().not_null().default(0))
.col(
ColumnDef::new(Alias::new("points_reward"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("max_participants"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("current_participants"))
.integer()
.not_null()
.default(0),
)
// draft / published / ongoing / completed / cancelled
.col(ColumnDef::new(Alias::new("status")).string_len(16).not_null().default("draft"))
.col(
ColumnDef::new(Alias::new("status"))
.string_len(16)
.not_null()
.default("draft"),
)
.col(ColumnDef::new(Alias::new("image_url")).string_len(512))
.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_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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -213,26 +534,65 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("offline_event_registration"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("event_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("patient_id")).uuid().not_null())
// registered / checked_in / cancelled
.col(ColumnDef::new(Alias::new("status")).string_len(16).not_null().default("registered"))
.col(
ColumnDef::new(Alias::new("status"))
.string_len(16)
.not_null()
.default("registered"),
)
.col(ColumnDef::new(Alias::new("checked_in_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("checked_in_by")).uuid())
.col(ColumnDef::new(Alias::new("points_granted")).boolean().not_null().default(false))
.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("points_granted"))
.boolean()
.not_null()
.default(false),
)
.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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(Index::create().if_not_exists().name("idx_event_reg_unique").table(Alias::new("offline_event_registration")).col(Alias::new("event_id")).col(Alias::new("patient_id")).unique().to_owned())
.create_index(
Index::create()
.if_not_exists()
.name("idx_event_reg_unique")
.table(Alias::new("offline_event_registration"))
.col(Alias::new("event_id"))
.col(Alias::new("patient_id"))
.unique()
.to_owned(),
)
.await?;
Ok(())

View File

@@ -12,7 +12,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("daily_monitoring"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("record_date")).date().not_null())
@@ -23,21 +28,44 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Alias::new("evening_bp_systolic")).integer())
.col(ColumnDef::new(Alias::new("evening_bp_diastolic")).integer())
// 体重 (Decimal 5,1)
.col(ColumnDef::new(Alias::new("weight")).decimal().extra("CHECK(weight IS NULL OR weight >= 0)"))
.col(
ColumnDef::new(Alias::new("weight"))
.decimal()
.extra("CHECK(weight IS NULL OR weight >= 0)"),
)
// 血糖 (Decimal 4,1)
.col(ColumnDef::new(Alias::new("blood_sugar")).decimal().extra("CHECK(blood_sugar IS NULL OR blood_sugar >= 0)"))
.col(
ColumnDef::new(Alias::new("blood_sugar"))
.decimal()
.extra("CHECK(blood_sugar IS NULL OR blood_sugar >= 0)"),
)
// 出入量
.col(ColumnDef::new(Alias::new("fluid_intake")).integer())
.col(ColumnDef::new(Alias::new("urine_output")).integer())
// 备注
.col(ColumnDef::new(Alias::new("notes")).text())
// 标准字段
.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_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())
.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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -74,7 +102,12 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("daily_monitoring")).if_exists().to_owned())
.drop_table(
Table::drop()
.table(Alias::new("daily_monitoring"))
.if_exists()
.to_owned(),
)
.await?;
Ok(())

View File

@@ -22,8 +22,16 @@ impl MigrationTrait for Migration {
.col(string("status").not_null().default("active"))
.col(uuid_null("diagnosed_by"))
.col(string_null("notes"))
.col(timestamp_with_time_zone("created_at").not_null().default(Expr::current_timestamp()))
.col(timestamp_with_time_zone("updated_at").not_null().default(Expr::current_timestamp()))
.col(
timestamp_with_time_zone("created_at")
.not_null()
.default(Expr::current_timestamp()),
)
.col(
timestamp_with_time_zone("updated_at")
.not_null()
.default(Expr::current_timestamp()),
)
.col(uuid_null("created_by"))
.col(uuid_null("updated_by"))
.col(timestamp_with_time_zone_null("deleted_at"))

View File

@@ -16,11 +16,12 @@ impl MigrationTrait for Migration {
.await?;
// 获取默认租户 ID
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 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::<String>(0).unwrap_or_default(),
@@ -31,45 +32,330 @@ impl MigrationTrait for Migration {
let nil = "NULL";
// === Directory 节点 ===
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000001", "基础模块", 1, sys).await?;
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000002", "业务模块", 2, sys).await?;
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000003", "健康管理", 3, sys).await?;
insert_dir(db, &tid, "a0000000-0000-0000-0000-000000000004", "系统", 4, sys).await?;
insert_dir(
db,
&tid,
"a0000000-0000-0000-0000-000000000001",
"基础模块",
1,
sys,
)
.await?;
insert_dir(
db,
&tid,
"a0000000-0000-0000-0000-000000000002",
"业务模块",
2,
sys,
)
.await?;
insert_dir(
db,
&tid,
"a0000000-0000-0000-0000-000000000003",
"健康管理",
3,
sys,
)
.await?;
insert_dir(
db,
&tid,
"a0000000-0000-0000-0000-000000000004",
"系统",
4,
sys,
)
.await?;
// === 基础模块菜单 ===
let d1 = "a0000000-0000-0000-0000-000000000001";
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000001", "工作台", "/", "HomeOutlined", 0, sys).await?;
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000002", "用户管理", "/users", "UserOutlined", 1, sys).await?;
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000003", "权限管理", "/roles", "SafetyOutlined", 2, sys).await?;
insert_menu(db, &tid, d1, "b0000001-0000-0000-0000-000000000004", "组织架构", "/organizations", "ApartmentOutlined", 3, sys).await?;
insert_menu(
db,
&tid,
d1,
"b0000001-0000-0000-0000-000000000001",
"工作台",
"/",
"HomeOutlined",
0,
sys,
)
.await?;
insert_menu(
db,
&tid,
d1,
"b0000001-0000-0000-0000-000000000002",
"用户管理",
"/users",
"UserOutlined",
1,
sys,
)
.await?;
insert_menu(
db,
&tid,
d1,
"b0000001-0000-0000-0000-000000000003",
"权限管理",
"/roles",
"SafetyOutlined",
2,
sys,
)
.await?;
insert_menu(
db,
&tid,
d1,
"b0000001-0000-0000-0000-000000000004",
"组织架构",
"/organizations",
"ApartmentOutlined",
3,
sys,
)
.await?;
// === 业务模块菜单 ===
let d2 = "a0000000-0000-0000-0000-000000000002";
insert_menu(db, &tid, d2, "b0000002-0000-0000-0000-000000000001", "工作流", "/workflow", "PartitionOutlined", 0, sys).await?;
insert_menu(db, &tid, d2, "b0000002-0000-0000-0000-000000000002", "消息中心", "/messages", "MessageOutlined", 1, sys).await?;
insert_menu(
db,
&tid,
d2,
"b0000002-0000-0000-0000-000000000001",
"工作流",
"/workflow",
"PartitionOutlined",
0,
sys,
)
.await?;
insert_menu(
db,
&tid,
d2,
"b0000002-0000-0000-0000-000000000002",
"消息中心",
"/messages",
"MessageOutlined",
1,
sys,
)
.await?;
// === 健康管理菜单 ===
let d3 = "a0000000-0000-0000-0000-000000000003";
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000001", "统计报表", "/health/statistics", "DashboardOutlined", 0, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000002", "患者管理", "/health/patients", "TeamOutlined", 1, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000003", "医护管理", "/health/doctors", "MedicineBoxOutlined", 2, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000004", "预约排班", "/health/appointments", "CalendarOutlined", 3, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000005", "排班管理", "/health/schedules", "HeartOutlined", 4, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000006", "随访管理", "/health/follow-up-tasks", "PhoneOutlined", 5, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000007", "咨询管理", "/health/consultations", "CommentOutlined", 6, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000008", "标签管理", "/health/tags", "TagsOutlined", 7, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000009", "积分规则", "/health/points-rules", "TrophyOutlined", 8, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000010", "商品管理", "/health/points-products", "ShopOutlined", 9, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000011", "订单管理", "/health/points-orders", "FileTextOutlined", 10, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000012", "线下活动", "/health/offline-events", "CalendarOutlined", 11, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000013", "AI Prompt 管理", "/health/ai-prompts", "RobotOutlined", 12, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000014", "AI 分析历史", "/health/ai-analysis", "HistoryOutlined", 13, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000015", "AI 用量统计", "/health/ai-usage", "BarChartOutlined", 14, sys).await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000001",
"统计报表",
"/health/statistics",
"DashboardOutlined",
0,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000002",
"患者管理",
"/health/patients",
"TeamOutlined",
1,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000003",
"医护管理",
"/health/doctors",
"MedicineBoxOutlined",
2,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000004",
"预约排班",
"/health/appointments",
"CalendarOutlined",
3,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000005",
"排班管理",
"/health/schedules",
"HeartOutlined",
4,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000006",
"随访管理",
"/health/follow-up-tasks",
"PhoneOutlined",
5,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000007",
"咨询管理",
"/health/consultations",
"CommentOutlined",
6,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000008",
"标签管理",
"/health/tags",
"TagsOutlined",
7,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000009",
"积分规则",
"/health/points-rules",
"TrophyOutlined",
8,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000010",
"商品管理",
"/health/points-products",
"ShopOutlined",
9,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000011",
"订单管理",
"/health/points-orders",
"FileTextOutlined",
10,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000012",
"线下活动",
"/health/offline-events",
"CalendarOutlined",
11,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000013",
"AI Prompt 管理",
"/health/ai-prompts",
"RobotOutlined",
12,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000014",
"AI 分析历史",
"/health/ai-analysis",
"HistoryOutlined",
13,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000015",
"AI 用量统计",
"/health/ai-usage",
"BarChartOutlined",
14,
sys,
)
.await?;
// === 系统菜单 ===
let d4 = "a0000000-0000-0000-0000-000000000004";
insert_menu(db, &tid, d4, "b0000004-0000-0000-0000-000000000001", "系统设置", "/settings", "SettingOutlined", 0, sys).await?;
insert_menu(db, &tid, d4, "b0000004-0000-0000-0000-000000000002", "插件管理", "/plugins/admin", "AppstoreOutlined", 1, sys).await?;
insert_menu(
db,
&tid,
d4,
"b0000004-0000-0000-0000-000000000001",
"系统设置",
"/settings",
"SettingOutlined",
0,
sys,
)
.await?;
insert_menu(
db,
&tid,
d4,
"b0000004-0000-0000-0000-000000000002",
"插件管理",
"/plugins/admin",
"AppstoreOutlined",
1,
sys,
)
.await?;
let _ = nil;
Ok(())

View File

@@ -42,10 +42,7 @@ impl MigrationTrait for Migration {
.not_null()
.default("critical"),
)
.col(
ColumnDef::new(CriticalValueThreshold::Department)
.string_len(100),
)
.col(ColumnDef::new(CriticalValueThreshold::Department).string_len(100))
.col(ColumnDef::new(CriticalValueThreshold::AgeMin).integer())
.col(ColumnDef::new(CriticalValueThreshold::AgeMax).integer())
.col(
@@ -68,7 +65,10 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(CriticalValueThreshold::CreatedBy).uuid())
.col(ColumnDef::new(CriticalValueThreshold::UpdatedBy).uuid())
.col(ColumnDef::new(CriticalValueThreshold::DeletedAt).timestamp_with_time_zone())
.col(
ColumnDef::new(CriticalValueThreshold::DeletedAt)
.timestamp_with_time_zone(),
)
.col(
ColumnDef::new(CriticalValueThreshold::Version)
.integer()

View File

@@ -10,16 +10,19 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(Consent::Table)
.col(
ColumnDef::new(Consent::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Consent::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Consent::TenantId).uuid().not_null())
.col(ColumnDef::new(Consent::PatientId).uuid().not_null())
.col(ColumnDef::new(Consent::ConsentType).string_len(50).not_null())
.col(ColumnDef::new(Consent::ConsentScope).string_len(100).not_null())
.col(
ColumnDef::new(Consent::ConsentType)
.string_len(50)
.not_null(),
)
.col(
ColumnDef::new(Consent::ConsentScope)
.string_len(100)
.not_null(),
)
.col(
ColumnDef::new(Consent::Status)
.string_len(20)
@@ -79,6 +82,7 @@ impl MigrationTrait for Migration {
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Consent {
Table,
Id,

View File

@@ -24,9 +24,10 @@ impl MigrationTrait for Migration {
manager.get_connection().execute_unprepared(sql).await?;
// 分区表主键必须包含分区键
manager.get_connection().execute_unprepared(
"ALTER TABLE device_readings ADD PRIMARY KEY (id, measured_at);"
).await?;
manager
.get_connection()
.execute_unprepared("ALTER TABLE device_readings ADD PRIMARY KEY (id, measured_at);")
.await?;
// 核心查询索引
manager.get_connection().execute_unprepared(
@@ -47,7 +48,10 @@ impl MigrationTrait for Migration {
let partition_sql = format!(
"CREATE TABLE IF NOT EXISTS device_readings_{suffix} PARTITION OF device_readings FOR VALUES FROM ('{start}') TO ('{end}');"
);
manager.get_connection().execute_unprepared(&partition_sql).await?;
manager
.get_connection()
.execute_unprepared(&partition_sql)
.await?;
}
Ok(())
@@ -55,13 +59,16 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
for suffix in ["2026_05", "2026_06", "2026_07", "2026_08"] {
manager.get_connection().execute_unprepared(
&format!("DROP TABLE IF EXISTS device_readings_{suffix};")
).await.ok();
manager
.get_connection()
.execute_unprepared(&format!("DROP TABLE IF EXISTS device_readings_{suffix};"))
.await
.ok();
}
manager.get_connection().execute_unprepared(
"DROP TABLE IF EXISTS device_readings;"
).await?;
manager
.get_connection()
.execute_unprepared("DROP TABLE IF EXISTS device_readings;")
.await?;
Ok(())
}
}

View File

@@ -6,53 +6,97 @@ 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("vital_signs_hourly"))
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key().default(Expr::cust("gen_random_uuid()")))
.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("device_type")).string().not_null())
.col(ColumnDef::new(Alias::new("hour_start")).timestamp_with_time_zone().not_null())
.col(ColumnDef::new(Alias::new("min_val")).double())
.col(ColumnDef::new(Alias::new("max_val")).double())
.col(ColumnDef::new(Alias::new("avg_val")).double().not_null())
.col(ColumnDef::new(Alias::new("sample_count")).integer().not_null().default(1))
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("version")).integer().not_null().default(1))
.to_owned(),
).await?;
manager
.create_table(
Table::create()
.table(Alias::new("vital_signs_hourly"))
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
.default(Expr::cust("gen_random_uuid()")),
)
.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("device_type"))
.string()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("hour_start"))
.timestamp_with_time_zone()
.not_null(),
)
.col(ColumnDef::new(Alias::new("min_val")).double())
.col(ColumnDef::new(Alias::new("max_val")).double())
.col(ColumnDef::new(Alias::new("avg_val")).double().not_null())
.col(
ColumnDef::new(Alias::new("sample_count"))
.integer()
.not_null()
.default(1),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
// UNIQUE 约束 — 每个患者每个指标每小时一条
manager.create_index(
Index::create()
.name("idx_vsh_unique")
.table(Alias::new("vital_signs_hourly"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_type"))
.col(Alias::new("hour_start"))
.unique()
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_vsh_unique")
.table(Alias::new("vital_signs_hourly"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_type"))
.col(Alias::new("hour_start"))
.unique()
.to_owned(),
)
.await?;
// 查询索引
manager.create_index(
Index::create()
.name("idx_vsh_tenant_patient")
.table(Alias::new("vital_signs_hourly"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_type"))
.col(Alias::new("hour_start"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_vsh_tenant_patient")
.table(Alias::new("vital_signs_hourly"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_type"))
.col(Alias::new("hour_start"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("vital_signs_hourly")).to_owned()).await
manager
.drop_table(
Table::drop()
.table(Alias::new("vital_signs_hourly"))
.to_owned(),
)
.await
}
}

View File

@@ -6,52 +6,87 @@ 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("patient_devices"))
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key().default(Expr::cust("gen_random_uuid()")))
.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("device_id")).string().not_null())
.col(ColumnDef::new(Alias::new("device_model")).string())
.col(ColumnDef::new(Alias::new("device_type")).string())
.col(ColumnDef::new(Alias::new("bound_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("last_sync_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.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_table(
Table::create()
.table(Alias::new("patient_devices"))
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
.default(Expr::cust("gen_random_uuid()")),
)
.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("device_id")).string().not_null())
.col(ColumnDef::new(Alias::new("device_model")).string())
.col(ColumnDef::new(Alias::new("device_type")).string())
.col(
ColumnDef::new(Alias::new("bound_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(ColumnDef::new(Alias::new("last_sync_at")).timestamp_with_time_zone())
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.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_pd_unique")
.table(Alias::new("patient_devices"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_id"))
.unique()
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_pd_unique")
.table(Alias::new("patient_devices"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("device_id"))
.unique()
.to_owned(),
)
.await?;
// 查询索引
manager.create_index(
Index::create()
.name("idx_pd_tenant_patient")
.table(Alias::new("patient_devices"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_pd_tenant_patient")
.table(Alias::new("patient_devices"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("patient_devices")).to_owned()).await
manager
.drop_table(
Table::drop()
.table(Alias::new("patient_devices"))
.to_owned(),
)
.await
}
}

View File

@@ -6,45 +6,102 @@ 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("alert_rules"))
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key().default(Expr::cust("gen_random_uuid()")))
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("name")).string().not_null())
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("device_type")).string().not_null())
.col(ColumnDef::new(Alias::new("condition_type")).string().not_null())
.col(ColumnDef::new(Alias::new("condition_params")).json_binary().not_null().default(Expr::cust("'{}'::jsonb")))
.col(ColumnDef::new(Alias::new("severity")).string().not_null().default("'warning'"))
.col(ColumnDef::new(Alias::new("is_active")).boolean().not_null().default(Expr::cust("true")))
.col(ColumnDef::new(Alias::new("apply_tags")).json_binary())
.col(ColumnDef::new(Alias::new("notify_roles")).json_binary().default(Expr::cust("'[]'::jsonb")))
.col(ColumnDef::new(Alias::new("cooldown_minutes")).integer().not_null().default(60))
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.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_table(
Table::create()
.table(Alias::new("alert_rules"))
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
.default(Expr::cust("gen_random_uuid()")),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("name")).string().not_null())
.col(ColumnDef::new(Alias::new("description")).text())
.col(
ColumnDef::new(Alias::new("device_type"))
.string()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("condition_type"))
.string()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("condition_params"))
.json_binary()
.not_null()
.default(Expr::cust("'{}'::jsonb")),
)
.col(
ColumnDef::new(Alias::new("severity"))
.string()
.not_null()
.default("'warning'"),
)
.col(
ColumnDef::new(Alias::new("is_active"))
.boolean()
.not_null()
.default(Expr::cust("true")),
)
.col(ColumnDef::new(Alias::new("apply_tags")).json_binary())
.col(
ColumnDef::new(Alias::new("notify_roles"))
.json_binary()
.default(Expr::cust("'[]'::jsonb")),
)
.col(
ColumnDef::new(Alias::new("cooldown_minutes"))
.integer()
.not_null()
.default(60),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.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_ar_tenant_active")
.table(Alias::new("alert_rules"))
.col(Alias::new("tenant_id"))
.col(Alias::new("is_active"))
.col(Alias::new("device_type"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_ar_tenant_active")
.table(Alias::new("alert_rules"))
.col(Alias::new("tenant_id"))
.col(Alias::new("is_active"))
.col(Alias::new("device_type"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("alert_rules")).to_owned()).await
manager
.drop_table(Table::drop().table(Alias::new("alert_rules")).to_owned())
.await
}
}

View File

@@ -6,71 +6,109 @@ 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("alerts"))
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key().default(Expr::cust("gen_random_uuid()")))
.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("rule_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("severity")).string().not_null())
.col(ColumnDef::new(Alias::new("title")).string().not_null())
.col(ColumnDef::new(Alias::new("detail")).json_binary().default(Expr::cust("'{}'::jsonb")))
.col(ColumnDef::new(Alias::new("status")).string().not_null().default("'pending'"))
.col(ColumnDef::new(Alias::new("acknowledged_by")).uuid())
.col(ColumnDef::new(Alias::new("acknowledged_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("resolved_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().default(Expr::cust("NOW()")))
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("version")).integer().not_null().default(1))
.foreign_key(
ForeignKey::create()
.from(Alias::new("alerts"), Alias::new("rule_id"))
.to(Alias::new("alert_rules"), Alias::new("id"))
.on_delete(ForeignKeyAction::Restrict)
)
.to_owned(),
).await?;
manager
.create_table(
Table::create()
.table(Alias::new("alerts"))
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key()
.default(Expr::cust("gen_random_uuid()")),
)
.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("rule_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("severity")).string().not_null())
.col(ColumnDef::new(Alias::new("title")).string().not_null())
.col(
ColumnDef::new(Alias::new("detail"))
.json_binary()
.default(Expr::cust("'{}'::jsonb")),
)
.col(
ColumnDef::new(Alias::new("status"))
.string()
.not_null()
.default("'pending'"),
)
.col(ColumnDef::new(Alias::new("acknowledged_by")).uuid())
.col(ColumnDef::new(Alias::new("acknowledged_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("resolved_at")).timestamp_with_time_zone())
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.default(Expr::cust("NOW()")),
)
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(Alias::new("alerts"), Alias::new("rule_id"))
.to(Alias::new("alert_rules"), Alias::new("id"))
.on_delete(ForeignKeyAction::Restrict),
)
.to_owned(),
)
.await?;
// 按患者查询告警
manager.create_index(
Index::create()
.name("idx_alerts_tenant_patient")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("created_at"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_alerts_tenant_patient")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("created_at"))
.to_owned(),
)
.await?;
// 按状态筛选
manager.create_index(
Index::create()
.name("idx_alerts_status")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("status"))
.col(Alias::new("created_at"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_alerts_status")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("status"))
.col(Alias::new("created_at"))
.to_owned(),
)
.await?;
// 冷却期查询 — 同规则同患者
manager.create_index(
Index::create()
.name("idx_alerts_cooldown")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("rule_id"))
.col(Alias::new("created_at"))
.to_owned(),
).await?;
manager
.create_index(
Index::create()
.name("idx_alerts_cooldown")
.table(Alias::new("alerts"))
.col(Alias::new("tenant_id"))
.col(Alias::new("patient_id"))
.col(Alias::new("rule_id"))
.col(Alias::new("created_at"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("alerts")).to_owned()).await
manager
.drop_table(Table::drop().table(Alias::new("alerts")).to_owned())
.await
}
}

View File

@@ -17,7 +17,11 @@ impl MigrationTrait for Migration {
.primary_key(),
)
.col(ColumnDef::new(TenantCryptoKey::TenantId).uuid().not_null())
.col(ColumnDef::new(TenantCryptoKey::EncryptedDek).string_len(128).not_null())
.col(
ColumnDef::new(TenantCryptoKey::EncryptedDek)
.string_len(128)
.not_null(),
)
.col(
ColumnDef::new(TenantCryptoKey::KeyVersion)
.integer()

View File

@@ -11,14 +11,38 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Article::Table)
.add_column(ColumnDef::new(Article::Status).string_len(20).not_null().default("draft"))
.add_column(
ColumnDef::new(Article::Status)
.string_len(20)
.not_null()
.default("draft"),
)
.add_column(ColumnDef::new(Article::Slug).string_len(200).null())
.add_column(ColumnDef::new(Article::ContentType).string_len(20).not_null().default("rich_text"))
.add_column(
ColumnDef::new(Article::ContentType)
.string_len(20)
.not_null()
.default("rich_text"),
)
.add_column(ColumnDef::new(Article::ReviewedBy).uuid().null())
.add_column(ColumnDef::new(Article::ReviewedAt).timestamp_with_time_zone().null())
.add_column(
ColumnDef::new(Article::ReviewedAt)
.timestamp_with_time_zone()
.null(),
)
.add_column(ColumnDef::new(Article::ReviewNote).text().null())
.add_column(ColumnDef::new(Article::ViewCount).integer().not_null().default(0))
.add_column(ColumnDef::new(Article::SortOrder).integer().not_null().default(0))
.add_column(
ColumnDef::new(Article::ViewCount)
.integer()
.not_null()
.default(0),
)
.add_column(
ColumnDef::new(Article::SortOrder)
.integer()
.not_null()
.default(0),
)
.add_column(ColumnDef::new(Article::CategoryId).uuid().null())
.to_owned(),
)
@@ -37,19 +61,52 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(ArticleCategory::Table)
.col(ColumnDef::new(ArticleCategory::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(ArticleCategory::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ArticleCategory::TenantId).uuid().not_null())
.col(ColumnDef::new(ArticleCategory::Name).string_len(100).not_null())
.col(
ColumnDef::new(ArticleCategory::Name)
.string_len(100)
.not_null(),
)
.col(ColumnDef::new(ArticleCategory::Slug).string_len(100).null())
.col(ColumnDef::new(ArticleCategory::ParentId).uuid().null())
.col(ColumnDef::new(ArticleCategory::Description).text().null())
.col(ColumnDef::new(ArticleCategory::SortOrder).integer().not_null().default(0))
.col(ColumnDef::new(ArticleCategory::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(ArticleCategory::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(ArticleCategory::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(ArticleCategory::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ArticleCategory::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(ArticleCategory::CreatedBy).uuid().null())
.col(ColumnDef::new(ArticleCategory::UpdatedBy).uuid().null())
.col(ColumnDef::new(ArticleCategory::DeletedAt).timestamp_with_time_zone().null())
.col(ColumnDef::new(ArticleCategory::Version).integer().not_null().default(1))
.col(
ColumnDef::new(ArticleCategory::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ArticleCategory::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -69,15 +126,39 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(ArticleTag::Table)
.col(ColumnDef::new(ArticleTag::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(ArticleTag::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ArticleTag::TenantId).uuid().not_null())
.col(ColumnDef::new(ArticleTag::Name).string_len(50).not_null())
.col(ColumnDef::new(ArticleTag::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(ArticleTag::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(ArticleTag::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ArticleTag::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(ArticleTag::CreatedBy).uuid().null())
.col(ColumnDef::new(ArticleTag::UpdatedBy).uuid().null())
.col(ColumnDef::new(ArticleTag::DeletedAt).timestamp_with_time_zone().null())
.col(ColumnDef::new(ArticleTag::Version).integer().not_null().default(1))
.col(
ColumnDef::new(ArticleTag::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ArticleTag::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -97,7 +178,11 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(ArticleArticleTag::Table)
.col(ColumnDef::new(ArticleArticleTag::ArticleId).uuid().not_null())
.col(
ColumnDef::new(ArticleArticleTag::ArticleId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(ArticleArticleTag::TagId).uuid().not_null())
.primary_key(
Index::create()
@@ -113,15 +198,33 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(ArticleRevision::Table)
.col(ColumnDef::new(ArticleRevision::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(ArticleRevision::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ArticleRevision::TenantId).uuid().not_null())
.col(ColumnDef::new(ArticleRevision::ArticleId).uuid().not_null())
.col(ColumnDef::new(ArticleRevision::RevisionNumber).integer().not_null())
.col(ColumnDef::new(ArticleRevision::Title).string_len(255).not_null())
.col(
ColumnDef::new(ArticleRevision::RevisionNumber)
.integer()
.not_null(),
)
.col(
ColumnDef::new(ArticleRevision::Title)
.string_len(255)
.not_null(),
)
.col(ColumnDef::new(ArticleRevision::Content).text().not_null())
.col(ColumnDef::new(ArticleRevision::Summary).text().null())
.col(ColumnDef::new(ArticleRevision::CreatedBy).uuid().null())
.col(ColumnDef::new(ArticleRevision::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(ArticleRevision::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await?;
@@ -140,10 +243,18 @@ impl MigrationTrait for Migration {
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(ArticleRevision::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(ArticleArticleTag::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(ArticleTag::Table).to_owned()).await?;
manager.drop_table(Table::drop().table(ArticleCategory::Table).to_owned()).await?;
manager
.drop_table(Table::drop().table(ArticleRevision::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(ArticleArticleTag::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(ArticleTag::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(ArticleCategory::Table).to_owned())
.await?;
manager
.alter_table(

View File

@@ -10,7 +10,11 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(Patient::Table)
.add_column(ColumnDef::new(Patient::EmergencyContactPhoneHash).string_len(64).null())
.add_column(
ColumnDef::new(Patient::EmergencyContactPhoneHash)
.string_len(64)
.null(),
)
.add_column(ColumnDef::new(Patient::KeyVersion).integer().null())
.to_owned(),
)
@@ -31,7 +35,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_patient_emergency_phone_hash").to_owned())
.drop_index(
Index::drop()
.name("idx_patient_emergency_phone_hash")
.to_owned(),
)
.await?;
manager

View File

@@ -10,7 +10,11 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(ConsultationMessage::Table)
.add_column(ColumnDef::new(ConsultationMessage::KeyVersion).integer().null())
.add_column(
ColumnDef::new(ConsultationMessage::KeyVersion)
.integer()
.null(),
)
.to_owned(),
)
.await?;

View File

@@ -10,8 +10,16 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(PatientFamilyMember::Table)
.add_column(ColumnDef::new(PatientFamilyMember::PhoneHash).string_len(64).null())
.add_column(ColumnDef::new(PatientFamilyMember::KeyVersion).integer().null())
.add_column(
ColumnDef::new(PatientFamilyMember::PhoneHash)
.string_len(64)
.null(),
)
.add_column(
ColumnDef::new(PatientFamilyMember::KeyVersion)
.integer()
.null(),
)
.to_owned(),
)
.await?;
@@ -31,7 +39,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_family_member_phone_hash").to_owned())
.drop_index(
Index::drop()
.name("idx_family_member_phone_hash")
.to_owned(),
)
.await?;
manager

View File

@@ -10,7 +10,11 @@ impl MigrationTrait for Migration {
.alter_table(
Table::alter()
.table(DoctorProfile::Table)
.add_column(ColumnDef::new(DoctorProfile::LicenseNumberHash).string_len(64).null())
.add_column(
ColumnDef::new(DoctorProfile::LicenseNumberHash)
.string_len(64)
.null(),
)
.add_column(ColumnDef::new(DoctorProfile::KeyVersion).integer().null())
.to_owned(),
)
@@ -31,7 +35,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_doctor_profile_license_hash").to_owned())
.drop_index(
Index::drop()
.name("idx_doctor_profile_license_hash")
.to_owned(),
)
.await?;
manager

View File

@@ -18,10 +18,8 @@ impl MigrationTrait for Migration {
)
.await?;
conn.execute_unprepared(
"ALTER TABLE vital_signs ADD COLUMN IF NOT EXISTS spo2 INTEGER",
)
.await?;
conn.execute_unprepared("ALTER TABLE vital_signs ADD COLUMN IF NOT EXISTS spo2 INTEGER")
.await?;
conn.execute_unprepared(
"ALTER TABLE vital_signs ADD COLUMN IF NOT EXISTS blood_sugar_type VARCHAR(20) DEFAULT 'fasting'",
@@ -34,20 +32,14 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
conn.execute_unprepared(
"ALTER TABLE vital_signs DROP COLUMN IF EXISTS blood_sugar_type",
)
.await?;
conn.execute_unprepared("ALTER TABLE vital_signs DROP COLUMN IF EXISTS blood_sugar_type")
.await?;
conn.execute_unprepared(
"ALTER TABLE vital_signs DROP COLUMN IF EXISTS spo2",
)
.await?;
conn.execute_unprepared("ALTER TABLE vital_signs DROP COLUMN IF EXISTS spo2")
.await?;
conn.execute_unprepared(
"ALTER TABLE vital_signs DROP COLUMN IF EXISTS body_temperature",
)
.await?;
conn.execute_unprepared("ALTER TABLE vital_signs DROP COLUMN IF EXISTS body_temperature")
.await?;
Ok(())
}

View File

@@ -11,18 +11,40 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("medication_record"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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_len(200).not_null())
.col(ColumnDef::new(Alias::new("generic_name")).string_len(200).null())
.col(
ColumnDef::new(Alias::new("medication_name"))
.string_len(200)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("generic_name"))
.string_len(200)
.null(),
)
.col(ColumnDef::new(Alias::new("dosage")).string_len(50).null())
.col(ColumnDef::new(Alias::new("unit")).string_len(20).null())
.col(ColumnDef::new(Alias::new("frequency")).string_len(20).null())
.col(
ColumnDef::new(Alias::new("frequency"))
.string_len(20)
.null(),
)
.col(ColumnDef::new(Alias::new("route")).string_len(20).null())
.col(ColumnDef::new(Alias::new("start_date")).date().null())
.col(ColumnDef::new(Alias::new("end_date")).date().null())
.col(ColumnDef::new(Alias::new("is_current")).boolean().not_null().default(true))
.col(
ColumnDef::new(Alias::new("is_current"))
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(Alias::new("prescribed_by")).uuid().null())
.col(ColumnDef::new(Alias::new("notes")).text().null())
.col(
@@ -39,7 +61,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -80,7 +106,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("medication_record")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("medication_record"))
.to_owned(),
)
.await
}
}

View File

@@ -11,39 +11,104 @@ impl MigrationTrait for Migration {
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("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())
.col(
ColumnDef::new(Alias::new("dialyzer_model"))
.string_len(100)
.null(),
)
// 膜面积 (m²)
.col(ColumnDef::new(Alias::new("membrane_area")).decimal_len(5, 2).null())
.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())
.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())
.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())
.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_type"))
.string_len(20)
.null(),
)
// 抗凝剂剂量
.col(ColumnDef::new(Alias::new("anticoagulation_dose")).string_len(50).null())
.col(
ColumnDef::new(Alias::new("anticoagulation_dose"))
.string_len(50)
.null(),
)
// 目标超滤量 (ml)
.col(ColumnDef::new(Alias::new("target_ultrafiltration_ml")).integer().null())
.col(
ColumnDef::new(Alias::new("target_ultrafiltration_ml"))
.integer()
.null(),
)
// 目标干体重 (kg)
.col(ColumnDef::new(Alias::new("target_dry_weight")).decimal_len(5, 2).null())
.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())
.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("dialysate_flow_rate"))
.integer()
.null(),
)
// 每周透析频次
.col(ColumnDef::new(Alias::new("frequency_per_week")).integer().null())
.col(
ColumnDef::new(Alias::new("frequency_per_week"))
.integer()
.null(),
)
// 每次透析时长 (分钟)
.col(ColumnDef::new(Alias::new("duration_minutes")).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_type"))
.string_len(20)
.null(),
)
// 血管通路位置
.col(ColumnDef::new(Alias::new("vascular_access_location")).string_len(100).null())
.col(
ColumnDef::new(Alias::new("vascular_access_location"))
.string_len(100)
.null(),
)
// 生效日期
.col(ColumnDef::new(Alias::new("effective_from")).date().null())
// 失效日期
@@ -74,7 +139,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -127,7 +196,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("dialysis_prescription")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("dialysis_prescription"))
.to_owned(),
)
.await
}
}

View File

@@ -8,11 +8,12 @@ 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 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::<String>(0).unwrap_or_default(),

View File

@@ -12,14 +12,27 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("follow_up_template"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("name")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("name"))
.string_len(200)
.not_null(),
)
// 模板描述
.col(ColumnDef::new(Alias::new("description")).text().null())
// 随访类型: phone/outpatient/home_visit/online/wechat
.col(ColumnDef::new(Alias::new("follow_up_type")).string_len(20).not_null())
.col(
ColumnDef::new(Alias::new("follow_up_type"))
.string_len(20)
.not_null(),
)
// 适用疾病/科室JSON 数组)
.col(ColumnDef::new(Alias::new("applicable_scope")).text().null())
// 状态: active/disabled
@@ -44,7 +57,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -72,15 +89,32 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("follow_up_template_field"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("template_id")).uuid().not_null())
// 字段标签
.col(ColumnDef::new(Alias::new("label")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("label"))
.string_len(200)
.not_null(),
)
// 字段键名(用于程序引用)
.col(ColumnDef::new(Alias::new("field_key")).string_len(100).not_null())
.col(
ColumnDef::new(Alias::new("field_key"))
.string_len(100)
.not_null(),
)
// 字段类型: text/number/date/select/checkbox/textarea/scale
.col(ColumnDef::new(Alias::new("field_type")).string_len(20).not_null())
.col(
ColumnDef::new(Alias::new("field_type"))
.string_len(20)
.not_null(),
)
// 是否必填
.col(
ColumnDef::new(Alias::new("required"))
@@ -91,7 +125,11 @@ impl MigrationTrait for Migration {
// 选项JSON 数组select/checkbox 时使用)
.col(ColumnDef::new(Alias::new("options")).text().null())
// 占位提示
.col(ColumnDef::new(Alias::new("placeholder")).string_len(200).null())
.col(
ColumnDef::new(Alias::new("placeholder"))
.string_len(200)
.null(),
)
// 校验规则JSON
.col(ColumnDef::new(Alias::new("validation")).text().null())
// 排序序号
@@ -116,7 +154,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()
@@ -154,10 +196,18 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("follow_up_template_field")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("follow_up_template_field"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("follow_up_template")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("follow_up_template"))
.to_owned(),
)
.await
}
}

View File

@@ -12,17 +12,48 @@ impl MigrationTrait for Migration {
Table::create()
.table(Alias::new("domain_events_archive"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key())
.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("event_type")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("event_type"))
.string_len(200)
.not_null(),
)
.col(ColumnDef::new(Alias::new("payload")).json().null())
.col(ColumnDef::new(Alias::new("correlation_id")).uuid().null())
.col(ColumnDef::new(Alias::new("status")).string_len(20).not_null())
.col(ColumnDef::new(Alias::new("attempts")).integer().not_null().default(0))
.col(
ColumnDef::new(Alias::new("status"))
.string_len(20)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("attempts"))
.integer()
.not_null()
.default(0),
)
.col(ColumnDef::new(Alias::new("last_error")).text().null())
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().not_null())
.col(ColumnDef::new(Alias::new("published_at")).timestamp_with_time_zone().null())
.col(ColumnDef::new(Alias::new("archived_at")).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("published_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("archived_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await?;
@@ -85,7 +116,11 @@ impl MigrationTrait for Migration {
.await?;
manager
.drop_table(Table::drop().table(Alias::new("domain_events_archive")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("domain_events_archive"))
.to_owned(),
)
.await?;
Ok(())

View File

@@ -12,9 +12,22 @@ impl MigrationTrait for Migration {
.table(Alias::new("processed_events"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("event_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("consumer_id")).string_len(200).not_null())
.col(ColumnDef::new(Alias::new("processed_at")).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.primary_key(Index::create().col(Alias::new("event_id")).col(Alias::new("consumer_id")))
.col(
ColumnDef::new(Alias::new("consumer_id"))
.string_len(200)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("processed_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.primary_key(
Index::create()
.col(Alias::new("event_id"))
.col(Alias::new("consumer_id")),
)
.to_owned(),
)
.await?;
@@ -56,7 +69,11 @@ impl MigrationTrait for Migration {
.await?;
manager
.drop_table(Table::drop().table(Alias::new("processed_events")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("processed_events"))
.to_owned(),
)
.await?;
Ok(())

View File

@@ -37,7 +37,8 @@ impl MigrationTrait for Migration {
END;
$$;
"#,
).await?;
)
.await?;
Ok(())
}
@@ -66,7 +67,8 @@ impl MigrationTrait for Migration {
END;
$$;
"#,
).await?;
)
.await?;
Ok(())
}

View File

@@ -8,15 +8,11 @@ impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
conn.execute_unprepared(
"ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS prev_hash TEXT",
)
.await?;
conn.execute_unprepared("ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS prev_hash TEXT")
.await?;
conn.execute_unprepared(
"ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS record_hash TEXT",
)
.await?;
conn.execute_unprepared("ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS record_hash TEXT")
.await?;
// 为 record_hash 创建索引(用于快速查找最新哈希)
conn.execute_unprepared(
@@ -38,25 +34,17 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
conn.execute_unprepared(
"DROP INDEX IF EXISTS idx_audit_logs_tenant_created",
)
.await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_audit_logs_tenant_created")
.await?;
conn.execute_unprepared(
"DROP INDEX IF EXISTS idx_audit_logs_record_hash",
)
.await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_audit_logs_record_hash")
.await?;
conn.execute_unprepared(
"ALTER TABLE audit_logs DROP COLUMN IF EXISTS record_hash",
)
.await?;
conn.execute_unprepared("ALTER TABLE audit_logs DROP COLUMN IF EXISTS record_hash")
.await?;
conn.execute_unprepared(
"ALTER TABLE audit_logs DROP COLUMN IF EXISTS prev_hash",
)
.await?;
conn.execute_unprepared("ALTER TABLE audit_logs DROP COLUMN IF EXISTS prev_hash")
.await?;
Ok(())
}

View File

@@ -37,7 +37,8 @@ impl MigrationTrait for Migration {
END;
$$;
"#,
).await?;
)
.await?;
Ok(())
}
@@ -73,7 +74,8 @@ impl MigrationTrait for Migration {
END;
$$;
"#,
).await?;
)
.await?;
Ok(())
}

View File

@@ -88,10 +88,7 @@ impl MigrationTrait for Migration {
.default("pending"),
)
.col(ColumnDef::new(CriticalAlert::AcknowledgedBy).uuid())
.col(
ColumnDef::new(CriticalAlert::AcknowledgedAt)
.timestamp_with_time_zone(),
)
.col(ColumnDef::new(CriticalAlert::AcknowledgedAt).timestamp_with_time_zone())
.col(
ColumnDef::new(CriticalAlert::EscalationLevel)
.small_integer()
@@ -182,8 +179,7 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(CriticalAlertResponse::CreatedBy).uuid())
.col(ColumnDef::new(CriticalAlertResponse::UpdatedBy).uuid())
.col(
ColumnDef::new(CriticalAlertResponse::DeletedAt)
.timestamp_with_time_zone(),
ColumnDef::new(CriticalAlertResponse::DeletedAt).timestamp_with_time_zone(),
)
.col(
ColumnDef::new(CriticalAlertResponse::Version)
@@ -193,10 +189,7 @@ impl MigrationTrait for Migration {
)
.foreign_key(
ForeignKey::create()
.from(
CriticalAlertResponse::Table,
CriticalAlertResponse::AlertId,
)
.from(CriticalAlertResponse::Table, CriticalAlertResponse::AlertId)
.to(CriticalAlert::Table, CriticalAlert::Id),
)
.to_owned(),
@@ -206,11 +199,7 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(CriticalAlertResponse::Table)
.to_owned(),
)
.drop_table(Table::drop().table(CriticalAlertResponse::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(CriticalAlert::Table).to_owned())

View File

@@ -16,8 +16,9 @@ impl MigrationTrait for Migration {
// 同一患者、同一设备、同一指标、同一测量时间只允许一条记录
db.execute_unprepared(
"CREATE UNIQUE INDEX IF NOT EXISTS uq_device_readings_dedup
ON device_readings (tenant_id, patient_id, device_id, metric, measured_at);"
).await?;
ON device_readings (tenant_id, patient_id, device_id, metric, measured_at);",
)
.await?;
Ok(())
}
@@ -25,9 +26,8 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DROP INDEX IF EXISTS uq_device_readings_dedup;"
).await?;
db.execute_unprepared("DROP INDEX IF EXISTS uq_device_readings_dedup;")
.await?;
Ok(())
}

View File

@@ -11,11 +11,12 @@ impl MigrationTrait for Migration {
let db = manager.get_connection();
// 获取默认租户 ID
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 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::<String>(0).unwrap_or_default(),
@@ -26,12 +27,56 @@ impl MigrationTrait for Migration {
let d3 = "a0000000-0000-0000-0000-000000000003"; // 健康管理目录
// 告警相关菜单(排在 AI 用量统计之后)
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000016", "告警仪表盘", "/health/alert-dashboard", "AlertOutlined", 15, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000017", "告警列表", "/health/alerts", "BellOutlined", 16, sys).await?;
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000018", "告警规则", "/health/alert-rules", "ControlOutlined", 17, sys).await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000016",
"告警仪表盘",
"/health/alert-dashboard",
"AlertOutlined",
15,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000017",
"告警列表",
"/health/alerts",
"BellOutlined",
16,
sys,
)
.await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000018",
"告警规则",
"/health/alert-rules",
"ControlOutlined",
17,
sys,
)
.await?;
// 设备管理菜单
insert_menu(db, &tid, d3, "b0000003-0000-0000-0000-000000000019", "设备管理", "/health/devices", "ApiOutlined", 18, sys).await?;
insert_menu(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000019",
"设备管理",
"/health/devices",
"ApiOutlined",
18,
sys,
)
.await?;
Ok(())
}

View File

@@ -11,23 +11,54 @@ impl MigrationTrait for Migration {
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("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("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("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("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_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))
.col(
ColumnDef::new(Alias::new("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
@@ -47,7 +78,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("medication_reminder")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("medication_reminder"))
.to_owned(),
)
.await
}
}

View File

@@ -13,35 +13,116 @@ impl MigrationTrait for Migration {
// === 更新已有菜单的 permission 字段 ===
// 健康管理菜单
update_perm(db, "b0000003-0000-0000-0000-000000000002", "health.patient.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000003", "health.doctor.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000004", "health.appointment.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000005", "health.appointment.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000006", "health.follow-up.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000007", "health.consultation.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000008", "health.patient.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000009", "health.points.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000010", "health.points.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000011", "health.points.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000012", "health.points.list").await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000002",
"health.patient.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000003",
"health.doctor.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000004",
"health.appointment.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000005",
"health.appointment.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000006",
"health.follow-up.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000007",
"health.consultation.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000008",
"health.patient.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000009",
"health.points.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000010",
"health.points.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000011",
"health.points.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000012",
"health.points.list",
)
.await?;
// AI 模块菜单
update_perm(db, "b0000003-0000-0000-0000-000000000013", "ai.prompt.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000014", "ai.analysis.list").await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000014",
"ai.analysis.list",
)
.await?;
update_perm(db, "b0000003-0000-0000-0000-000000000015", "ai.usage.list").await?;
// 告警菜单
update_perm(db, "b0000003-0000-0000-0000-000000000016", "health.alerts.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000017", "health.alerts.list").await?;
update_perm(db, "b0000003-0000-0000-0000-000000000018", "health.alert-rules.list").await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000016",
"health.alerts.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000017",
"health.alerts.list",
)
.await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000018",
"health.alert-rules.list",
)
.await?;
// 设备菜单
update_perm(db, "b0000003-0000-0000-0000-000000000019", "health.devices.list").await?;
update_perm(
db,
"b0000003-0000-0000-0000-000000000019",
"health.devices.list",
)
.await?;
// === 补全缺失菜单 ===
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 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::<String>(0).unwrap_or_default(),
@@ -52,9 +133,33 @@ impl MigrationTrait for Migration {
let d3 = "a0000000-0000-0000-0000-000000000003"; // 健康管理目录
// 透析管理sort 19
insert_menu_with_perm(db, &tid, d3, "b0000003-0000-0000-0000-000000000020", "透析管理", "/health/dialysis", "ExperimentOutlined", 19, "health.dialysis.list", sys).await?;
insert_menu_with_perm(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000020",
"透析管理",
"/health/dialysis",
"ExperimentOutlined",
19,
"health.dialysis.list",
sys,
)
.await?;
// 资讯管理sort 20
insert_menu_with_perm(db, &tid, d3, "b0000003-0000-0000-0000-000000000021", "资讯管理", "/health/articles", "ReadOutlined", 20, "health.articles.list", sys).await?;
insert_menu_with_perm(
db,
&tid,
d3,
"b0000003-0000-0000-0000-000000000021",
"资讯管理",
"/health/articles",
"ReadOutlined",
20,
"health.articles.list",
sys,
)
.await?;
Ok(())
}

View File

@@ -29,7 +29,11 @@ impl MigrationTrait for Migration {
.string_len(10)
.not_null(),
)
.col(ColumnDef::new(Alias::new("params")).json_binary().not_null())
.col(
ColumnDef::new(Alias::new("params"))
.json_binary()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("status"))
.string_len(20)
@@ -52,9 +56,7 @@ impl MigrationTrait for Migration {
)
.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("deleted_at")).timestamp_with_time_zone())
.col(
ColumnDef::new(Alias::new("version_lock"))
.integer()

View File

@@ -38,9 +38,7 @@ impl MigrationTrait for Migration {
)
.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("deleted_at")).timestamp_with_time_zone())
.col(
ColumnDef::new(Alias::new("version_lock"))
.integer()
@@ -68,7 +66,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("ai_risk_threshold")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("ai_risk_threshold"))
.to_owned(),
)
.await
}
}

View File

@@ -54,10 +54,8 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DELETE FROM menus WHERE path = '/health/action-inbox'",
)
.await?;
db.execute_unprepared("DELETE FROM menus WHERE path = '/health/action-inbox'")
.await?;
Ok(())
}
}

View File

@@ -26,7 +26,16 @@ impl MigrationTrait for Migration {
// ── 1. 科室 health_department ──
let dict_dept = "d1000001-0000-0000-0000-000000000001";
insert_dict(db, &tid, dict_dept, "科室", "health_department", "医护科室分类", sys).await?;
insert_dict(
db,
&tid,
dict_dept,
"科室",
"health_department",
"医护科室分类",
sys,
)
.await?;
let dept_items = [
("全科", "全科", 1),
("内科", "内科", 2),
@@ -41,12 +50,32 @@ impl MigrationTrait for Migration {
("体检中心", "体检中心", 11),
];
for (i, (label, value, sort)) in dept_items.iter().enumerate() {
insert_item(db, &tid, dict_dept, label, value, *sort, None, &(i + 1), sys).await?;
insert_item(
db,
&tid,
dict_dept,
label,
value,
*sort,
None,
&(i + 1),
sys,
)
.await?;
}
// ── 2. 职称 health_title ──
let dict_title = "d1000001-0000-0000-0000-000000000002";
insert_dict(db, &tid, dict_title, "职称", "health_title", "医护职称分类", sys).await?;
insert_dict(
db,
&tid,
dict_title,
"职称",
"health_title",
"医护职称分类",
sys,
)
.await?;
let title_items = [
("住院医师", "住院医师", 1),
("主治医师", "主治医师", 2),
@@ -59,12 +88,32 @@ impl MigrationTrait for Migration {
("主任护师", "主任护师", 9),
];
for (i, (label, value, sort)) in title_items.iter().enumerate() {
insert_item(db, &tid, dict_title, label, value, *sort, None, &(i + 1), sys).await?;
insert_item(
db,
&tid,
dict_title,
label,
value,
*sort,
None,
&(i + 1),
sys,
)
.await?;
}
// ── 3. 设备类型 health_device_type ──
let dict_dev = "d1000001-0000-0000-0000-000000000003";
insert_dict(db, &tid, dict_dev, "设备类型", "health_device_type", "健康监测设备类型", sys).await?;
insert_dict(
db,
&tid,
dict_dev,
"设备类型",
"health_device_type",
"健康监测设备类型",
sys,
)
.await?;
let dev_items = [
("血压计", "blood_pressure", 1),
("血糖仪", "blood_glucose", 2),
@@ -81,7 +130,16 @@ impl MigrationTrait for Migration {
// ── 4. 随访类型 health_follow_up_type ──
let dict_fu = "d1000001-0000-0000-0000-000000000004";
insert_dict(db, &tid, dict_fu, "随访类型", "health_follow_up_type", "随访方式分类", sys).await?;
insert_dict(
db,
&tid,
dict_fu,
"随访类型",
"health_follow_up_type",
"随访方式分类",
sys,
)
.await?;
let fu_items = [
("电话", "phone", 1),
("门诊", "outpatient", 2),
@@ -96,7 +154,13 @@ impl MigrationTrait for Migration {
// ── 5. 咨询类型 health_consultation_type ──
let dict_consult = "d1000001-0000-0000-0000-000000000005";
insert_dict(
db, &tid, dict_consult, "咨询类型", "health_consultation_type", "咨询会话类型", sys,
db,
&tid,
dict_consult,
"咨询类型",
"health_consultation_type",
"咨询会话类型",
sys,
)
.await?;
let consult_items = [
@@ -105,12 +169,32 @@ impl MigrationTrait for Migration {
("健康咨询", "health_consultation", 3),
];
for (i, (label, value, sort)) in consult_items.iter().enumerate() {
insert_item(db, &tid, dict_consult, label, value, *sort, None, &(i + 1), sys).await?;
insert_item(
db,
&tid,
dict_consult,
label,
value,
*sort,
None,
&(i + 1),
sys,
)
.await?;
}
// ── 6. 关系 health_relationship ──
let dict_rel = "d1000001-0000-0000-0000-000000000006";
insert_dict(db, &tid, dict_rel, "关系", "health_relationship", "家属与患者关系", sys).await?;
insert_dict(
db,
&tid,
dict_rel,
"关系",
"health_relationship",
"家属与患者关系",
sys,
)
.await?;
let rel_items = [
("父母", "parent", 1),
("配偶", "spouse", 2),

View File

@@ -56,10 +56,8 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DELETE FROM menus WHERE path = '/health/follow-up-templates'",
)
.await?;
db.execute_unprepared("DELETE FROM menus WHERE path = '/health/follow-up-templates'")
.await?;
Ok(())
}
}

View File

@@ -18,12 +18,20 @@ impl MigrationTrait for Migration {
)
.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("device_type")).string().not_null())
.col(
ColumnDef::new(Alias::new("device_type"))
.string()
.not_null(),
)
.col(ColumnDef::new(Alias::new("date_bucket")).date().not_null())
.col(ColumnDef::new(Alias::new("min_val")).double())
.col(ColumnDef::new(Alias::new("max_val")).double())
.col(ColumnDef::new(Alias::new("avg_val")).double().not_null())
.col(ColumnDef::new(Alias::new("sample_count")).integer().not_null())
.col(
ColumnDef::new(Alias::new("sample_count"))
.integer()
.not_null(),
)
.col(ColumnDef::new(Alias::new("percentile_95")).double())
.col(
ColumnDef::new(Alias::new("created_at"))
@@ -67,7 +75,11 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("vital_signs_daily")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("vital_signs_daily"))
.to_owned(),
)
.await
}
}

View File

@@ -33,7 +33,11 @@ impl MigrationTrait for Migration {
.not_null(),
)
.col(ColumnDef::new(Alias::new("scopes")).json().not_null())
.col(ColumnDef::new(Alias::new("allowed_patient_ids")).json().null())
.col(
ColumnDef::new(Alias::new("allowed_patient_ids"))
.json()
.null(),
)
.col(
ColumnDef::new(Alias::new("rate_limit_per_minute"))
.integer()
@@ -66,7 +70,11 @@ impl MigrationTrait for Migration {
)
.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("deleted_at"))
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Alias::new("version"))
.integer()

View File

@@ -10,14 +10,70 @@ impl MigrationTrait for Migration {
// 安全创建外键:先检查是否已存在,不存在才创建
let fks: &[(&str, &str, &str, &str, &str, &str)] = &[
("fk_follow_up_task_appointment", "follow_up_task", "related_appointment_id", "appointment", "id", "SET NULL"),
("fk_points_transaction_account", "points_transaction", "account_id", "points_account", "id", "CASCADE"),
("fk_points_transaction_rule", "points_transaction", "rule_id", "points_rule", "id", "SET NULL"),
("fk_points_transaction_order", "points_transaction", "order_id", "points_order", "id", "SET NULL"),
("fk_points_order_product", "points_order", "product_id", "points_product", "id", "RESTRICT"),
("fk_points_order_patient", "points_order", "patient_id", "patient", "id", "CASCADE"),
("fk_offline_event_registration_event", "offline_event_registration", "event_id", "offline_event", "id", "CASCADE"),
("fk_offline_event_registration_patient", "offline_event_registration", "patient_id", "patient", "id", "CASCADE"),
(
"fk_follow_up_task_appointment",
"follow_up_task",
"related_appointment_id",
"appointment",
"id",
"SET NULL",
),
(
"fk_points_transaction_account",
"points_transaction",
"account_id",
"points_account",
"id",
"CASCADE",
),
(
"fk_points_transaction_rule",
"points_transaction",
"rule_id",
"points_rule",
"id",
"SET NULL",
),
(
"fk_points_transaction_order",
"points_transaction",
"order_id",
"points_order",
"id",
"SET NULL",
),
(
"fk_points_order_product",
"points_order",
"product_id",
"points_product",
"id",
"RESTRICT",
),
(
"fk_points_order_patient",
"points_order",
"patient_id",
"patient",
"id",
"CASCADE",
),
(
"fk_offline_event_registration_event",
"offline_event_registration",
"event_id",
"offline_event",
"id",
"CASCADE",
),
(
"fk_offline_event_registration_patient",
"offline_event_registration",
"patient_id",
"patient",
"id",
"CASCADE",
),
];
for &(name, from_table, from_col, to_table, to_col, on_delete) in fks {
@@ -54,11 +110,9 @@ impl MigrationTrait for Migration {
"fk_follow_up_task_appointment",
];
for fk in &fks {
db.execute_unprepared(&format!(
"ALTER TABLE dummy DROP CONSTRAINT IF EXISTS {fk}"
))
.await
.ok();
db.execute_unprepared(&format!("ALTER TABLE dummy DROP CONSTRAINT IF EXISTS {fk}"))
.await
.ok();
}
Ok(())
}

View File

@@ -31,7 +31,11 @@ impl MigrationTrait for Migration {
.not_null()
.default("draft"),
)
.col(ColumnDef::new(Alias::new("title")).string_len(200).not_null())
.col(
ColumnDef::new(Alias::new("title"))
.string_len(200)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("goals"))
.json_binary()
@@ -122,7 +126,11 @@ impl MigrationTrait for Migration {
.default("pending"),
)
.col(ColumnDef::new(Alias::new("schedule")).string_len(100))
.col(ColumnDef::new(Alias::new("sort_order")).integer().default(0))
.col(
ColumnDef::new(Alias::new("sort_order"))
.integer()
.default(0),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
@@ -198,10 +206,7 @@ impl MigrationTrait for Migration {
.not_null(),
)
.col(ColumnDef::new(Alias::new("current_value")).string_len(50))
.col(
ColumnDef::new(Alias::new("measured_at"))
.timestamp_with_time_zone(),
)
.col(ColumnDef::new(Alias::new("measured_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("notes")).text())
.col(
ColumnDef::new(Alias::new("created_at"))
@@ -252,10 +257,18 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("care_plan_outcomes")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("care_plan_outcomes"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("care_plan_items")).to_owned())
.drop_table(
Table::drop()
.table(Alias::new("care_plan_items"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("care_plans")).to_owned())

View File

@@ -74,17 +74,30 @@ impl MigrationTrait for Migration {
// 外键(幂等)
let fks = [
("fk_patient_assignments_shift", "ALTER TABLE patient_assignment ADD CONSTRAINT fk_patient_assignments_shift FOREIGN KEY (shift_id) REFERENCES shift(id) ON DELETE CASCADE"),
("fk_handoff_log_from_shift", "ALTER TABLE handoff_log ADD CONSTRAINT fk_handoff_log_from_shift FOREIGN KEY (from_shift_id) REFERENCES shift(id) ON DELETE CASCADE"),
("fk_handoff_log_to_shift", "ALTER TABLE handoff_log ADD CONSTRAINT fk_handoff_log_to_shift FOREIGN KEY (to_shift_id) REFERENCES shift(id) ON DELETE CASCADE"),
(
"fk_patient_assignments_shift",
"ALTER TABLE patient_assignment ADD CONSTRAINT fk_patient_assignments_shift FOREIGN KEY (shift_id) REFERENCES shift(id) ON DELETE CASCADE",
),
(
"fk_handoff_log_from_shift",
"ALTER TABLE handoff_log ADD CONSTRAINT fk_handoff_log_from_shift FOREIGN KEY (from_shift_id) REFERENCES shift(id) ON DELETE CASCADE",
),
(
"fk_handoff_log_to_shift",
"ALTER TABLE handoff_log ADD CONSTRAINT fk_handoff_log_to_shift FOREIGN KEY (to_shift_id) REFERENCES shift(id) ON DELETE CASCADE",
),
];
for (name, sql) in &fks {
let check = format!(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_name = '{name}'"
);
if let Some(row) = db.query_one(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres, check,
)).await? {
if let Some(row) = db
.query_one(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
check,
))
.await?
{
let count: i64 = row.try_get_by_index::<i64>(0).unwrap_or(0);
if count == 0 {
db.execute_unprepared(sql).await?;
@@ -97,8 +110,10 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared("DROP TABLE IF EXISTS handoff_log").await?;
db.execute_unprepared("DROP TABLE IF EXISTS patient_assignment").await?;
db.execute_unprepared("DROP TABLE IF EXISTS handoff_log")
.await?;
db.execute_unprepared("DROP TABLE IF EXISTS patient_assignment")
.await?;
db.execute_unprepared("DROP TABLE IF EXISTS shift").await?;
Ok(())
}

View File

@@ -51,10 +51,22 @@ impl MigrationTrait for Migration {
// 索引(幂等)
let indexes = [
("idx_ble_gateways_tenant_id", "CREATE INDEX IF NOT EXISTS idx_ble_gateways_tenant_id ON ble_gateways (tenant_id)"),
("idx_ble_gateways_api_key_prefix", "CREATE INDEX IF NOT EXISTS idx_ble_gateways_api_key_prefix ON ble_gateways (api_key_prefix)"),
("idx_gateway_patient_bindings_gateway", "CREATE INDEX IF NOT EXISTS idx_gateway_patient_bindings_gateway ON gateway_patient_bindings (gateway_id_fk)"),
("idx_gateway_patient_bindings_patient", "CREATE INDEX IF NOT EXISTS idx_gateway_patient_bindings_patient ON gateway_patient_bindings (patient_id)"),
(
"idx_ble_gateways_tenant_id",
"CREATE INDEX IF NOT EXISTS idx_ble_gateways_tenant_id ON ble_gateways (tenant_id)",
),
(
"idx_ble_gateways_api_key_prefix",
"CREATE INDEX IF NOT EXISTS idx_ble_gateways_api_key_prefix ON ble_gateways (api_key_prefix)",
),
(
"idx_gateway_patient_bindings_gateway",
"CREATE INDEX IF NOT EXISTS idx_gateway_patient_bindings_gateway ON gateway_patient_bindings (gateway_id_fk)",
),
(
"idx_gateway_patient_bindings_patient",
"CREATE INDEX IF NOT EXISTS idx_gateway_patient_bindings_patient ON gateway_patient_bindings (patient_id)",
),
];
for (_, sql) in &indexes {
db.execute_unprepared(sql).await.ok();
@@ -62,17 +74,25 @@ impl MigrationTrait for Migration {
// 外键约束(幂等)
let fks = [
("fk_gpb_gateway", "ALTER TABLE gateway_patient_bindings ADD CONSTRAINT fk_gpb_gateway FOREIGN KEY (gateway_id_fk) REFERENCES ble_gateways(id) ON DELETE CASCADE"),
("fk_gpb_patient", "ALTER TABLE gateway_patient_bindings ADD CONSTRAINT fk_gpb_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE"),
(
"fk_gpb_gateway",
"ALTER TABLE gateway_patient_bindings ADD CONSTRAINT fk_gpb_gateway FOREIGN KEY (gateway_id_fk) REFERENCES ble_gateways(id) ON DELETE CASCADE",
),
(
"fk_gpb_patient",
"ALTER TABLE gateway_patient_bindings ADD CONSTRAINT fk_gpb_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE",
),
];
for (name, sql) in &fks {
let check = format!(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_name = '{name}'"
);
let result = db.query_one(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
check,
)).await?;
let result = db
.query_one(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
check,
))
.await?;
let count: i64 = result.unwrap().try_get_by_index::<i64>(0).unwrap_or(0);
if count == 0 {
db.execute_unprepared(sql).await?;

View File

@@ -10,17 +10,83 @@ impl MigrationTrait for Migration {
// 批量插入缺失的健康管理菜单(多租户安全)
let menus: &[(&str, &str, &str, &str, i32)] = &[
("b0000003-0000-7000-8000-000000000022", "护理计划", "/health/care-plans", "SolutionOutlined", 19),
("b0000003-0000-7000-8000-000000000023", "班次管理", "/health/shifts", "ClockCircleOutlined", 20),
("b0000003-0000-7000-8000-000000000024", "用药记录", "/health/medications", "MedicineBoxOutlined", 21),
("b0000003-0000-7000-8000-000000000025", "BLE 网关", "/health/ble-gateways", "WifiOutlined", 22),
("b0000003-0000-7000-8000-000000000026", "危急值阈值", "/health/critical-value-thresholds","SafetyCertificateOutlined", 23),
("b0000003-0000-7000-8000-000000000027", "诊断记录", "/health/diagnoses", "FileSearchOutlined", 24),
("b0000003-0000-7000-8000-000000000028", "家庭健康代理", "/health/family-proxy", "TeamOutlined", 25),
("b0000003-0000-7000-8000-000000000029", "知情同意", "/health/consents", "AuditOutlined", 26),
("b0000003-0000-7000-8000-000000000030", "实时监控", "/health/realtime-monitor", "MonitorOutlined", 27),
("b0000003-0000-7000-8000-000000000031", "OAuth 合作方", "/health/oauth-clients", "ApiOutlined", 28),
("b0000003-0000-7000-8000-000000000032", "随访模板管理", "/health/follow-up-templates", "FormOutlined", 29),
(
"b0000003-0000-7000-8000-000000000022",
"护理计划",
"/health/care-plans",
"SolutionOutlined",
19,
),
(
"b0000003-0000-7000-8000-000000000023",
"班次管理",
"/health/shifts",
"ClockCircleOutlined",
20,
),
(
"b0000003-0000-7000-8000-000000000024",
"用药记录",
"/health/medications",
"MedicineBoxOutlined",
21,
),
(
"b0000003-0000-7000-8000-000000000025",
"BLE 网关",
"/health/ble-gateways",
"WifiOutlined",
22,
),
(
"b0000003-0000-7000-8000-000000000026",
"危急值阈值",
"/health/critical-value-thresholds",
"SafetyCertificateOutlined",
23,
),
(
"b0000003-0000-7000-8000-000000000027",
"诊断记录",
"/health/diagnoses",
"FileSearchOutlined",
24,
),
(
"b0000003-0000-7000-8000-000000000028",
"家庭健康代理",
"/health/family-proxy",
"TeamOutlined",
25,
),
(
"b0000003-0000-7000-8000-000000000029",
"知情同意",
"/health/consents",
"AuditOutlined",
26,
),
(
"b0000003-0000-7000-8000-000000000030",
"实时监控",
"/health/realtime-monitor",
"MonitorOutlined",
27,
),
(
"b0000003-0000-7000-8000-000000000031",
"OAuth 合作方",
"/health/oauth-clients",
"ApiOutlined",
28,
),
(
"b0000003-0000-7000-8000-000000000032",
"随访模板管理",
"/health/follow-up-templates",
"FormOutlined",
29,
),
];
for &(id, title, path, icon, sort) in menus {

View File

@@ -11,7 +11,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiTenantConfig::Table)
.if_not_exists()
.col(ColumnDef::new(AiTenantConfig::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(AiTenantConfig::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AiTenantConfig::TenantId).uuid().not_null())
.col(
ColumnDef::new(AiTenantConfig::DefaultProvider)
@@ -19,8 +24,16 @@ impl MigrationTrait for Migration {
.not_null()
.default("claude"),
)
.col(ColumnDef::new(AiTenantConfig::FallbackProvider).string_len(50).null())
.col(ColumnDef::new(AiTenantConfig::AnalysisTypeOverrides).json().null())
.col(
ColumnDef::new(AiTenantConfig::FallbackProvider)
.string_len(50)
.null(),
)
.col(
ColumnDef::new(AiTenantConfig::AnalysisTypeOverrides)
.json()
.null(),
)
.col(
ColumnDef::new(AiTenantConfig::MonthlyTokenBudget)
.big_integer()
@@ -53,7 +66,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(AiTenantConfig::CreatedBy).uuid().null())
.col(ColumnDef::new(AiTenantConfig::UpdatedBy).uuid().null())
.col(ColumnDef::new(AiTenantConfig::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(AiTenantConfig::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(AiTenantConfig::VersionLock)
.integer()

View File

@@ -11,7 +11,12 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiAnalysisQueue::Table)
.if_not_exists()
.col(ColumnDef::new(AiAnalysisQueue::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(AiAnalysisQueue::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AiAnalysisQueue::TenantId).uuid().not_null())
.col(ColumnDef::new(AiAnalysisQueue::PatientId).uuid().not_null())
.col(
@@ -31,7 +36,11 @@ impl MigrationTrait for Migration {
.not_null()
.default("pending"),
)
.col(ColumnDef::new(AiAnalysisQueue::SourceEvent).string_len(100).null())
.col(
ColumnDef::new(AiAnalysisQueue::SourceEvent)
.string_len(100)
.null(),
)
.col(
ColumnDef::new(AiAnalysisQueue::SourceRef)
.string_len(200)
@@ -44,9 +53,21 @@ impl MigrationTrait for Migration {
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(AiAnalysisQueue::StartedAt).timestamp_with_time_zone().null())
.col(ColumnDef::new(AiAnalysisQueue::CompletedAt).timestamp_with_time_zone().null())
.col(ColumnDef::new(AiAnalysisQueue::ResultAnalysisId).uuid().null())
.col(
ColumnDef::new(AiAnalysisQueue::StartedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(AiAnalysisQueue::CompletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(AiAnalysisQueue::ResultAnalysisId)
.uuid()
.null(),
)
.col(ColumnDef::new(AiAnalysisQueue::ErrorMessage).text().null())
.col(
ColumnDef::new(AiAnalysisQueue::RetryCount)
@@ -74,7 +95,11 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(AiAnalysisQueue::CreatedBy).uuid().null())
.col(ColumnDef::new(AiAnalysisQueue::UpdatedBy).uuid().null())
.col(ColumnDef::new(AiAnalysisQueue::DeletedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(AiAnalysisQueue::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(AiAnalysisQueue::VersionLock)
.integer()

View File

@@ -30,20 +30,66 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiKnowledgeRules::Table)
.if_not_exists()
.col(ColumnDef::new(AiKnowledgeRules::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(AiKnowledgeRules::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AiKnowledgeRules::TenantId).uuid().not_null())
.col(ColumnDef::new(AiKnowledgeRules::RuleName).string().not_null())
.col(ColumnDef::new(AiKnowledgeRules::AnalysisType).string().not_null())
.col(ColumnDef::new(AiKnowledgeRules::ConditionExpr).string().not_null())
.col(ColumnDef::new(AiKnowledgeRules::ActionText).string().not_null())
.col(ColumnDef::new(AiKnowledgeRules::Priority).integer().not_null().default(0))
.col(ColumnDef::new(AiKnowledgeRules::IsEnabled).boolean().not_null().default(true))
.col(ColumnDef::new(AiKnowledgeRules::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(AiKnowledgeRules::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(AiKnowledgeRules::RuleName)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeRules::AnalysisType)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeRules::ConditionExpr)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeRules::ActionText)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeRules::Priority)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(AiKnowledgeRules::IsEnabled)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(AiKnowledgeRules::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(AiKnowledgeRules::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(AiKnowledgeRules::CreatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeRules::UpdatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeRules::DeletedAt).timestamp_with_time_zone())
.col(ColumnDef::new(AiKnowledgeRules::VersionLock).integer().not_null().default(1))
.col(
ColumnDef::new(AiKnowledgeRules::VersionLock)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;

View File

@@ -31,22 +31,69 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiKnowledgeReferences::Table)
.if_not_exists()
.col(ColumnDef::new(AiKnowledgeReferences::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(AiKnowledgeReferences::TenantId).uuid().not_null())
.col(ColumnDef::new(AiKnowledgeReferences::Title).string().not_null())
.col(ColumnDef::new(AiKnowledgeReferences::AnalysisType).string().not_null())
.col(ColumnDef::new(AiKnowledgeReferences::SourceName).string().not_null())
.col(ColumnDef::new(AiKnowledgeReferences::ContentSummary).text().not_null())
.col(
ColumnDef::new(AiKnowledgeReferences::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::Title)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::AnalysisType)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::SourceName)
.string()
.not_null(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::ContentSummary)
.text()
.not_null(),
)
// vector(1536) — 兼容 OpenAI text-embedding-ada-002
.col(ColumnDef::new(AiKnowledgeReferences::Embedding).custom("vector"))
.col(ColumnDef::new(AiKnowledgeReferences::Tags).json())
.col(ColumnDef::new(AiKnowledgeReferences::IsEnabled).boolean().not_null().default(true))
.col(ColumnDef::new(AiKnowledgeReferences::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(AiKnowledgeReferences::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(AiKnowledgeReferences::IsEnabled)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(AiKnowledgeReferences::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(AiKnowledgeReferences::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(AiKnowledgeReferences::CreatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeReferences::UpdatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeReferences::DeletedAt).timestamp_with_time_zone())
.col(ColumnDef::new(AiKnowledgeReferences::VersionLock).integer().not_null().default(1))
.col(
ColumnDef::new(AiKnowledgeReferences::DeletedAt).timestamp_with_time_zone(),
)
.col(
ColumnDef::new(AiKnowledgeReferences::VersionLock)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;

View File

@@ -30,20 +30,53 @@ impl MigrationTrait for Migration {
Table::create()
.table(AiKnowledgeGuides::Table)
.if_not_exists()
.col(ColumnDef::new(AiKnowledgeGuides::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(AiKnowledgeGuides::TenantId).uuid().not_null())
.col(
ColumnDef::new(AiKnowledgeGuides::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(AiKnowledgeGuides::TenantId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(AiKnowledgeGuides::Title).string().not_null())
.col(ColumnDef::new(AiKnowledgeGuides::AnalysisType).string().not_null())
.col(
ColumnDef::new(AiKnowledgeGuides::AnalysisType)
.string()
.not_null(),
)
.col(ColumnDef::new(AiKnowledgeGuides::Content).text().not_null())
.col(ColumnDef::new(AiKnowledgeGuides::Category).string())
.col(ColumnDef::new(AiKnowledgeGuides::Embedding).custom("vector"))
.col(ColumnDef::new(AiKnowledgeGuides::IsEnabled).boolean().not_null().default(true))
.col(ColumnDef::new(AiKnowledgeGuides::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(AiKnowledgeGuides::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
.col(
ColumnDef::new(AiKnowledgeGuides::IsEnabled)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(AiKnowledgeGuides::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(AiKnowledgeGuides::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(AiKnowledgeGuides::CreatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeGuides::UpdatedBy).uuid())
.col(ColumnDef::new(AiKnowledgeGuides::DeletedAt).timestamp_with_time_zone())
.col(ColumnDef::new(AiKnowledgeGuides::VersionLock).integer().not_null().default(1))
.col(
ColumnDef::new(AiKnowledgeGuides::VersionLock)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;

View File

@@ -11,7 +11,8 @@ impl MigrationTrait for Migration {
let db = manager.get_connection();
// 化验单解读 — 强化系统指令
let sys_lab = esc(r#"你是一名专业的医学检验解读助手。这是由健康管理系统自动触发的分析任务,不是对话。
let sys_lab = esc(
r#"你是一名专业的医学检验解读助手。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出结构化的分析结果,格式如下:
@@ -30,10 +31,12 @@ impl MigrationTrait for Migration {
要求:
1. 直接输出结果,不要寒暄或询问
2. 使用通俗易懂的语言
3. 异常指标要重点标注"#);
3. 异常指标要重点标注"#,
);
// 趋势分析 — 保持已有的 v2 格式,只加非对话指令前缀
let sys_trend = esc(r#"你是一名健康数据分析专家。你将收到经过预处理的结构化统计摘要数据,包括线性回归趋势、异常检测结果等。
let sys_trend = esc(
r#"你是一名健康数据分析专家。你将收到经过预处理的结构化统计摘要数据,包括线性回归趋势、异常检测结果等。
这是由健康管理系统自动触发的分析任务,不是对话。请直接输出结构化的分析结果。
要求:
@@ -42,10 +45,12 @@ impl MigrationTrait for Migration {
3. **综合分析** — 考虑各指标间的关联性(如血压和体重、血糖和心率)
4. **临床建议** — 给出切实可行的健康管理建议,不替代医生诊断
5. **风险评级** — 对整体健康风险给出低/中/高评估并说明理由
6. **关注重点** — 用简洁的语言总结最需要关注的 2-3 个问题"#);
6. **关注重点** — 用简洁的语言总结最需要关注的 2-3 个问题"#,
);
// 体检方案 — 加非对话指令
let sys_checkup = esc(r#"你是一名健康管理顾问。这是由健康管理系统自动触发的分析任务,不是对话。
let sys_checkup = esc(
r#"你是一名健康管理顾问。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出个性化的体检方案,格式如下:
## 推荐检查项目
@@ -60,10 +65,12 @@ impl MigrationTrait for Migration {
要求:
1. 直接输出结果,不要寒暄或询问
2. 基于患者年龄、性别、既往病史推荐
3. 按优先级排序"#);
3. 按优先级排序"#,
);
// 报告摘要 — 加非对话指令
let sys_summary = esc(r#"你是一名医疗报告摘要撰写专家。这是由健康管理系统自动触发的分析任务,不是对话。
let sys_summary = esc(
r#"你是一名医疗报告摘要撰写专家。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出结构化的报告摘要,格式如下:
## 关键发现
@@ -81,7 +88,8 @@ impl MigrationTrait for Migration {
要求:
1. 直接输出结果,不要寒暄或询问
2. 控制在 500 字以内
3. 语言简洁专业"#);
3. 语言简洁专业"#,
);
for (name, sys) in [
("lab_report_interpretation", sys_lab),

View File

@@ -23,10 +23,7 @@ impl MigrationTrait for Migration {
for path in &frozen_paths {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
format!(
"UPDATE menus SET visible = false WHERE path = '{}'",
path
),
format!("UPDATE menus SET visible = false WHERE path = '{}'", path),
))
.await?;
}
@@ -50,10 +47,7 @@ impl MigrationTrait for Migration {
for path in &frozen_paths {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
format!(
"UPDATE menus SET visible = true WHERE path = '{}'",
path
),
format!("UPDATE menus SET visible = true WHERE path = '{}'", path),
))
.await?;
}

View File

@@ -64,7 +64,8 @@ impl MigrationTrait for Migration {
for &(path, sort) in sys_sort {
db.execute_unprepared(&format!(
"UPDATE menus SET sort_order = {sort} WHERE path = '{path}' AND deleted_at IS NULL"
)).await?;
))
.await?;
}
// 调整"健康业务"目录下的排序 — 按功能域分组
@@ -106,7 +107,8 @@ impl MigrationTrait for Migration {
for &(path, sort) in health_sort {
db.execute_unprepared(&format!(
"UPDATE menus SET sort_order = {sort} WHERE path = '{path}' AND deleted_at IS NULL"
)).await?;
))
.await?;
}
// ================================================================
@@ -117,7 +119,11 @@ impl MigrationTrait for Migration {
let roles: &[(&str, &str, &str)] = &[
("doctor", "医生", "负责患者诊疗、随访管理、AI辅助诊断"),
("nurse", "护士", "负责患者护理、体征监测、用药管理"),
("health_manager", "健康管理师", "负责健康管理计划、随访协调、运营统计"),
(
"health_manager",
"健康管理师",
"负责健康管理计划、随访协调、运营统计",
),
("operator", "运营人员", "负责内容运营、积分商城、活动管理"),
];
@@ -140,81 +146,138 @@ impl MigrationTrait for Migration {
// ================================================================
// doctor 权限: 患者 + 医护 + 随访 + 咨询 + AI + 告警 + 日常监测 + 诊断 + 知情同意 + 行动收件箱 + 消息(只读)
assign_perms_by_codes(db, "doctor", &[
"health.patient.list", "health.patient.manage",
"health.doctor.list", "health.doctor.manage",
"health.follow-up.list", "health.follow-up.manage",
"health.consultation.list", "health.consultation.manage",
"health.action-inbox.list", "health.action-inbox.manage",
"health.daily-monitoring.list", "health.daily-monitoring.manage",
"health.alerts.list", "health.alerts.manage",
"health.alert-rules.list",
"health.critical-alerts.list",
"health.diagnosis.list", "health.diagnosis.manage",
"health.consent.list", "health.consent.manage",
"health.health-data.list",
"ai.analysis.list", "ai.suggestion.list",
"message.list",
"workflow.list", "workflow.read",
]).await?;
assign_perms_by_codes(
db,
"doctor",
&[
"health.patient.list",
"health.patient.manage",
"health.doctor.list",
"health.doctor.manage",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.consultation.manage",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.alerts.manage",
"health.alert-rules.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.diagnosis.manage",
"health.consent.list",
"health.consent.manage",
"health.health-data.list",
"ai.analysis.list",
"ai.suggestion.list",
"message.list",
"workflow.list",
"workflow.read",
],
)
.await?;
// nurse 权限: 患者 + 随访 + 咨询 + 告警 + 日常监测 + 诊断 + 知情同意 + 行动收件箱 + 消息(只读)
assign_perms_by_codes(db, "nurse", &[
"health.patient.list", "health.patient.manage",
"health.follow-up.list", "health.follow-up.manage",
"health.consultation.list",
"health.action-inbox.list", "health.action-inbox.manage",
"health.daily-monitoring.list", "health.daily-monitoring.manage",
"health.alerts.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.consent.list", "health.consent.manage",
"health.health-data.list",
"health.device-readings.list",
"message.list",
]).await?;
assign_perms_by_codes(
db,
"nurse",
&[
"health.patient.list",
"health.patient.manage",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.consent.list",
"health.consent.manage",
"health.health-data.list",
"health.device-readings.list",
"message.list",
],
)
.await?;
// health_manager 权限: 统计 + 患者 + 医护 + 随访 + 咨询 + AI + 告警 + 设备 + 日常监测 + 标签 + 诊断 + 知情同意 + 行动收件箱 + 随访模板
assign_perms_by_codes(db, "health_manager", &[
"health.patient.list", "health.patient.manage",
"health.doctor.list",
"health.follow-up.list", "health.follow-up.manage",
"health.consultation.list", "health.consultation.manage",
"health.action-inbox.list", "health.action-inbox.manage", "health.action-inbox.team",
"health.daily-monitoring.list", "health.daily-monitoring.manage",
"health.alerts.list", "health.alerts.manage",
"health.alert-rules.list", "health.alert-rules.manage",
"health.critical-alerts.list",
"health.critical-value-thresholds.list",
"health.devices.list",
"health.tags.list", "health.tags.manage",
"health.diagnosis.list", "health.diagnosis.manage",
"health.consent.list", "health.consent.manage",
"health.health-data.list", "health.health-data.manage",
"health.follow-up-templates.list", "health.follow-up-templates.manage",
"health.dashboard.manage",
"ai.analysis.list", "ai.analysis.manage",
"ai.prompt.list",
"ai.suggestion.list", "ai.suggestion.manage",
"ai.usage.list",
"message.list",
"workflow.list", "workflow.read", "workflow.start",
]).await?;
assign_perms_by_codes(
db,
"health_manager",
&[
"health.patient.list",
"health.patient.manage",
"health.doctor.list",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.consultation.manage",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.action-inbox.team",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.alerts.manage",
"health.alert-rules.list",
"health.alert-rules.manage",
"health.critical-alerts.list",
"health.critical-value-thresholds.list",
"health.devices.list",
"health.tags.list",
"health.tags.manage",
"health.diagnosis.list",
"health.diagnosis.manage",
"health.consent.list",
"health.consent.manage",
"health.health-data.list",
"health.health-data.manage",
"health.follow-up-templates.list",
"health.follow-up-templates.manage",
"health.dashboard.manage",
"ai.analysis.list",
"ai.analysis.manage",
"ai.prompt.list",
"ai.suggestion.list",
"ai.suggestion.manage",
"ai.usage.list",
"message.list",
"workflow.list",
"workflow.read",
"workflow.start",
],
)
.await?;
// operator 权限: 统计 + 标签 + 内容 + 积分 + 活动 + 设备 + 告警(只读)
assign_perms_by_codes(db, "operator", &[
"health.patient.list",
"health.tags.list", "health.tags.manage",
"health.articles.list", "health.articles.manage",
"health.articles.review",
"health.points.list", "health.points.manage",
"health.offline-events.list", "health.offline-events.manage",
"health.devices.list",
"health.alerts.list",
"health.dashboard.manage",
"ai.usage.list",
"message.list",
]).await?;
assign_perms_by_codes(
db,
"operator",
&[
"health.patient.list",
"health.tags.list",
"health.tags.manage",
"health.articles.list",
"health.articles.manage",
"health.articles.review",
"health.points.list",
"health.points.manage",
"health.offline-events.list",
"health.offline-events.manage",
"health.devices.list",
"health.alerts.list",
"health.dashboard.manage",
"ai.usage.list",
"message.list",
],
)
.await?;
// ================================================================
// Part 5: 菜单-角色关联menu_roles
@@ -224,39 +287,64 @@ impl MigrationTrait for Migration {
// doctor 可见菜单路径
let doctor_paths: &[&str] = &[
"/", "/health/statistics",
"/health/patients", "/health/doctors",
"/health/follow-up-tasks", "/health/consultations",
"/health/action-inbox", "/health/follow-up-templates",
"/health/daily-monitoring", "/health/consents", "/health/diagnoses",
"/health/alert-dashboard", "/health/alerts",
"/health/ai-analysis", "/health/ai-usage",
"/",
"/health/statistics",
"/health/patients",
"/health/doctors",
"/health/follow-up-tasks",
"/health/consultations",
"/health/action-inbox",
"/health/follow-up-templates",
"/health/daily-monitoring",
"/health/consents",
"/health/diagnoses",
"/health/alert-dashboard",
"/health/alerts",
"/health/ai-analysis",
"/health/ai-usage",
"/messages",
];
assign_menus_for_role(db, "doctor", doctor_paths).await?;
// nurse 可见菜单路径
let nurse_paths: &[&str] = &[
"/", "/health/statistics",
"/",
"/health/statistics",
"/health/patients",
"/health/follow-up-tasks", "/health/consultations",
"/health/follow-up-tasks",
"/health/consultations",
"/health/action-inbox",
"/health/daily-monitoring", "/health/consents", "/health/diagnoses",
"/health/alert-dashboard", "/health/alerts",
"/health/daily-monitoring",
"/health/consents",
"/health/diagnoses",
"/health/alert-dashboard",
"/health/alerts",
"/messages",
];
assign_menus_for_role(db, "nurse", nurse_paths).await?;
// health_manager 可见菜单路径
let hm_paths: &[&str] = &[
"/", "/health/statistics",
"/health/patients", "/health/doctors", "/health/tags",
"/health/follow-up-tasks", "/health/consultations",
"/health/action-inbox", "/health/follow-up-templates",
"/health/daily-monitoring", "/health/consents", "/health/diagnoses",
"/health/alert-dashboard", "/health/alerts", "/health/alert-rules",
"/health/devices", "/health/critical-value-thresholds",
"/health/ai-prompts", "/health/ai-analysis", "/health/ai-usage",
"/",
"/health/statistics",
"/health/patients",
"/health/doctors",
"/health/tags",
"/health/follow-up-tasks",
"/health/consultations",
"/health/action-inbox",
"/health/follow-up-templates",
"/health/daily-monitoring",
"/health/consents",
"/health/diagnoses",
"/health/alert-dashboard",
"/health/alerts",
"/health/alert-rules",
"/health/devices",
"/health/critical-value-thresholds",
"/health/ai-prompts",
"/health/ai-analysis",
"/health/ai-usage",
"/health/realtime-monitor",
"/messages",
];
@@ -264,13 +352,18 @@ impl MigrationTrait for Migration {
// operator 可见菜单路径
let op_paths: &[&str] = &[
"/", "/health/statistics",
"/health/patients", "/health/tags",
"/",
"/health/statistics",
"/health/patients",
"/health/tags",
"/health/articles",
"/health/points-rules", "/health/points-products", "/health/points-orders",
"/health/points-rules",
"/health/points-products",
"/health/points-orders",
"/health/offline-events",
"/health/devices",
"/health/alert-dashboard", "/health/alerts",
"/health/alert-dashboard",
"/health/alerts",
"/health/ai-usage",
"/messages",
];
@@ -302,7 +395,8 @@ impl MigrationTrait for Migration {
// 软删除角色
db.execute_unprepared(&format!(
"UPDATE roles SET deleted_at = NOW() WHERE code = '{code}' AND is_system = false"
)).await?;
))
.await?;
}
// 恢复目录名
@@ -314,17 +408,20 @@ impl MigrationTrait for Migration {
for &(old, new) in dir_renames {
db.execute_unprepared(&format!(
"UPDATE menus SET title = '{new}' WHERE title = '{old}' AND menu_type = 'directory'"
)).await?;
))
.await?;
}
// 恢复"系统"目录
db.execute_unprepared(
"UPDATE menus SET deleted_at = NULL WHERE title = '配置' AND menu_type = 'directory'"
).await?;
"UPDATE menus SET deleted_at = NULL WHERE title = '配置' AND menu_type = 'directory'",
)
.await?;
// 恢复"系统"名称
db.execute_unprepared(
"UPDATE menus SET title = '系统' WHERE title = '配置' AND menu_type = 'directory'"
).await?;
"UPDATE menus SET title = '系统' WHERE title = '配置' AND menu_type = 'directory'",
)
.await?;
Ok(())
}

View File

@@ -50,10 +50,7 @@ impl MigrationTrait for Migration {
}
// === Nurse 角色权限清理 ===
let nurse_remove = vec![
"health.doctor.list",
"health.alerts.manage",
];
let nurse_remove = vec!["health.doctor.list", "health.alerts.manage"];
for code in &nurse_remove {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
@@ -93,7 +90,8 @@ impl MigrationTrait for Migration {
WHERE menu_roles.role_id = r.id AND menu_roles.menu_id = m.id \
AND r.code = 'operator' AND m.title = '{title}'",
),
)).await?;
))
.await?;
}
Ok(())

View File

@@ -11,10 +11,7 @@ impl MigrationTrait for Migration {
// Doctor 移除 health.health-data.manage 和 ai.analysis.manage
// 000125 正确分配了 health.health-data.list 和 ai.analysis.list
// 但早期迁移分配了 .manage 权限且未被 000126 清理
let doctor_remove = vec![
"health.health-data.manage",
"ai.analysis.manage",
];
let doctor_remove = vec!["health.health-data.manage", "ai.analysis.manage"];
for code in &doctor_remove {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,

View File

@@ -33,13 +33,19 @@ impl MigrationTrait for Migration {
("/health/shifts", "health.shifts.list"),
("/health/medications", "health.medication-records.manage"),
("/health/ble-gateways", "health.ble-gateways.list"),
("/health/critical-value-thresholds", "health.critical-value-thresholds.list"),
(
"/health/critical-value-thresholds",
"health.critical-value-thresholds.list",
),
("/health/diagnoses", "health.health-data.list"),
("/health/family-proxy", "health.patient.list"),
("/health/consents", "health.consent.list"),
("/health/realtime-monitor", "health.device-readings.list"),
("/health/oauth-clients", "health.oauth.list"),
("/health/follow-up-templates", "health.follow-up-templates.list"),
(
"/health/follow-up-templates",
"health.follow-up-templates.list",
),
];
for &(path, perm) in menu_perms {

View File

@@ -0,0 +1,62 @@
//! 修复护士设备权限 + 运营积分/线下活动权限
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();
// === 1. 护士角色:添加 health.devices.list 权限 ===
// 护士有 health.device-readings.list设备读数但缺少 health.devices.list设备绑定列表
db.execute_unprepared(
"INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'nurse' AND p.code = 'health.devices.list'
AND NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)",
)
.await?;
// === 2. 运营角色:添加 health.points.list 和 health.points.manage ===
// 运营需要管理积分商城,之前患者端端点错误使用了 health.health-data.list
// 已修正为 health.points.list需给运营角色补充对应权限
db.execute_unprepared(
"INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'operator' AND p.code IN ('health.points.list', 'health.points.manage')
AND NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)",
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DELETE FROM role_permissions
WHERE role_id = (SELECT id FROM roles WHERE name = 'nurse')
AND permission_id = (SELECT id FROM permissions WHERE code = 'health.devices.list')",
)
.await?;
db.execute_unprepared(
"DELETE FROM role_permissions
WHERE role_id = (SELECT id FROM roles WHERE name = 'operator')
AND permission_id IN (SELECT id FROM permissions WHERE code IN ('health.points.list', 'health.points.manage'))",
)
.await?;
Ok(())
}
}