fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复: 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:
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -37,7 +37,8 @@ impl MigrationTrait for Migration {
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -66,7 +67,8 @@ impl MigrationTrait for Migration {
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ impl MigrationTrait for Migration {
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -73,7 +74,8 @@ impl MigrationTrait for Migration {
|
||||
END;
|
||||
$$;
|
||||
"#,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -145,22 +145,8 @@ impl StorageConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RateLimitConfig {
|
||||
/// Redis 不可达时是否拒绝请求。默认 true(安全优先)。
|
||||
#[serde(default = "default_fail_close")]
|
||||
pub fail_close: bool,
|
||||
}
|
||||
|
||||
fn default_fail_close() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for RateLimitConfig {
|
||||
fn default() -> Self {
|
||||
Self { fail_close: true }
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct RateLimitConfig {}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
@@ -178,20 +164,3 @@ impl AppConfig {
|
||||
Ok(app_config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rate_limit_default_is_fail_close() {
|
||||
let config = RateLimitConfig::default();
|
||||
assert!(config.fail_close, "RateLimitConfig 默认应为 fail_close = true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_default_uses_custom_fn() {
|
||||
let config: RateLimitConfig = serde_json::from_str("{}").unwrap();
|
||||
assert!(config.fail_close, "serde 反序列化缺失字段时应使用 default_fail_close() = true");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,14 @@ use uuid::Uuid;
|
||||
|
||||
/// 启动透析会话工作流编排器
|
||||
/// 订阅 dialysis.record.created → 自动查找并启动 dialysis_session BPMN 工作流
|
||||
pub fn start_dialysis_workflow_orchestrator(
|
||||
db: sea_orm::DatabaseConnection,
|
||||
event_bus: EventBus,
|
||||
) {
|
||||
pub fn start_dialysis_workflow_orchestrator(db: sea_orm::DatabaseConnection, event_bus: EventBus) {
|
||||
let (mut receiver, _handle) = event_bus.subscribe_filtered("dialysis.".to_string());
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Some(event) if event.event_type == "dialysis.record.created" => {
|
||||
if let Err(e) =
|
||||
handle_dialysis_record_created(&db, &event_bus, &event).await
|
||||
{
|
||||
if let Err(e) = handle_dialysis_record_created(&db, &event_bus, &event).await {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
record_id = ?event.payload.get("record_id"),
|
||||
@@ -48,24 +43,13 @@ async fn handle_dialysis_record_created(
|
||||
let record_uuid = Uuid::parse_str(record_id)?;
|
||||
|
||||
// 查找 dialysis_session 流程定义
|
||||
let definition =
|
||||
erp_workflow::entity::process_definition::Entity::find()
|
||||
.filter(
|
||||
erp_workflow::entity::process_definition::Column::Key
|
||||
.eq("dialysis_session"),
|
||||
)
|
||||
.filter(
|
||||
erp_workflow::entity::process_definition::Column::TenantId
|
||||
.eq(event.tenant_id),
|
||||
)
|
||||
.filter(
|
||||
erp_workflow::entity::process_definition::Column::Status.eq("published"),
|
||||
)
|
||||
.filter(
|
||||
erp_workflow::entity::process_definition::Column::DeletedAt.is_null(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
let definition = erp_workflow::entity::process_definition::Entity::find()
|
||||
.filter(erp_workflow::entity::process_definition::Column::Key.eq("dialysis_session"))
|
||||
.filter(erp_workflow::entity::process_definition::Column::TenantId.eq(event.tenant_id))
|
||||
.filter(erp_workflow::entity::process_definition::Column::Status.eq("published"))
|
||||
.filter(erp_workflow::entity::process_definition::Column::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let definition = match definition {
|
||||
Some(d) => d,
|
||||
|
||||
@@ -20,9 +20,7 @@ pub struct BatchRequest {
|
||||
|
||||
/// 接收小程序批量埋点事件。
|
||||
/// 当前为日志记录模式 — 后续可接入 ClickHouse/PostgreSQL 分析表。
|
||||
pub async fn batch(
|
||||
Json(req): Json<BatchRequest>,
|
||||
) -> Json<ApiResponse<()>> {
|
||||
pub async fn batch(Json(req): Json<BatchRequest>) -> Json<ApiResponse<()>> {
|
||||
for evt in &req.events {
|
||||
tracing::info!(
|
||||
event = %evt.event,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use axum::extract::{FromRef, Path, State};
|
||||
use axum::Extension;
|
||||
use axum::Json;
|
||||
use sea_orm::{ConnectionTrait, Statement, DatabaseBackend};
|
||||
use serde_json::{json, Value};
|
||||
use axum::extract::{FromRef, Path, State};
|
||||
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement};
|
||||
use serde_json::{Value, json};
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
|
||||
@@ -56,10 +56,8 @@ pub async fn readiness_check(State(state): State<AppState>) -> Json<ReadyRespons
|
||||
.map(|m| m.name().to_string())
|
||||
.collect();
|
||||
|
||||
let (db_status, redis_status) = tokio::join!(
|
||||
check_database(&state.db),
|
||||
check_redis(&state.redis),
|
||||
);
|
||||
let (db_status, redis_status) =
|
||||
tokio::join!(check_database(&state.db), check_redis(&state.redis),);
|
||||
|
||||
let overall = if db_status.status == "ok" && redis_status.status == "ok" {
|
||||
"ok"
|
||||
@@ -81,10 +79,8 @@ pub async fn readiness_check(State(state): State<AppState>) -> Json<ReadyRespons
|
||||
async fn check_database(db: &sea_orm::DatabaseConnection) -> ComponentStatus {
|
||||
use sea_orm::ConnectionTrait;
|
||||
let start = std::time::Instant::now();
|
||||
let stmt = sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"SELECT 1".to_string(),
|
||||
);
|
||||
let stmt =
|
||||
sea_orm::Statement::from_string(sea_orm::DatabaseBackend::Postgres, "SELECT 1".to_string());
|
||||
match db.query_one(stmt).await {
|
||||
Ok(_) => ComponentStatus {
|
||||
status: "ok".to_string(),
|
||||
@@ -105,26 +101,21 @@ async fn check_database(db: &sea_orm::DatabaseConnection) -> ComponentStatus {
|
||||
async fn check_redis(client: &redis::Client) -> ComponentStatus {
|
||||
let start = std::time::Instant::now();
|
||||
match client.get_multiplexed_async_connection().await {
|
||||
Ok(mut conn) => {
|
||||
match redis::cmd("PING")
|
||||
.query_async::<String>(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_) => ComponentStatus {
|
||||
status: "ok".to_string(),
|
||||
Ok(mut conn) => match redis::cmd("PING").query_async::<String>(&mut conn).await {
|
||||
Ok(_) => ComponentStatus {
|
||||
status: "ok".to_string(),
|
||||
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Redis PING failed");
|
||||
ComponentStatus {
|
||||
status: "error".to_string(),
|
||||
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Redis PING failed");
|
||||
ComponentStatus {
|
||||
status: "error".to_string(),
|
||||
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||
error: Some("connection failed".to_string()),
|
||||
}
|
||||
error: Some("connection failed".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Redis connection failed");
|
||||
ComponentStatus {
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::response::Json;
|
||||
use serde_json::Value;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::{ApiDoc, AuthApiDoc, ConfigApiDoc, WorkflowApiDoc, MessageApiDoc};
|
||||
use crate::{ApiDoc, AuthApiDoc, ConfigApiDoc, MessageApiDoc, WorkflowApiDoc};
|
||||
|
||||
/// GET /docs/openapi.json
|
||||
///
|
||||
|
||||
@@ -46,9 +46,9 @@ where
|
||||
// 确保上传目录存在
|
||||
let base_dir = std::path::Path::new(upload_dir);
|
||||
let tenant_dir = base_dir.join(ctx.tenant_id.to_string());
|
||||
tokio::fs::create_dir_all(&tenant_dir).await.map_err(|e| {
|
||||
AppError::Internal(format!("创建上传目录失败: {}", e))
|
||||
})?;
|
||||
tokio::fs::create_dir_all(&tenant_dir)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("创建上传目录失败: {}", e)))?;
|
||||
|
||||
// 读取第一个 field 作为上传文件
|
||||
let field = multipart
|
||||
@@ -65,10 +65,7 @@ where
|
||||
// 验证文件类型
|
||||
validate_content_type(&content_type)?;
|
||||
|
||||
let original_name = field
|
||||
.name()
|
||||
.unwrap_or("file")
|
||||
.to_string();
|
||||
let original_name = field.name().unwrap_or("file").to_string();
|
||||
|
||||
let data = field
|
||||
.bytes()
|
||||
|
||||
@@ -39,22 +39,20 @@ struct ApiDoc;
|
||||
erp_auth::handler::role_handler::get_role_permissions,
|
||||
erp_auth::handler::role_handler::list_permissions,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_auth::dto::LoginReq,
|
||||
erp_auth::dto::LoginResp,
|
||||
erp_auth::dto::RefreshReq,
|
||||
erp_auth::dto::UserResp,
|
||||
erp_auth::dto::CreateUserReq,
|
||||
erp_auth::dto::UpdateUserReq,
|
||||
erp_auth::dto::RoleResp,
|
||||
erp_auth::dto::CreateRoleReq,
|
||||
erp_auth::dto::UpdateRoleReq,
|
||||
erp_auth::dto::PermissionResp,
|
||||
erp_auth::dto::AssignPermissionsReq,
|
||||
erp_auth::dto::ChangePasswordReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_auth::dto::LoginReq,
|
||||
erp_auth::dto::LoginResp,
|
||||
erp_auth::dto::RefreshReq,
|
||||
erp_auth::dto::UserResp,
|
||||
erp_auth::dto::CreateUserReq,
|
||||
erp_auth::dto::UpdateUserReq,
|
||||
erp_auth::dto::RoleResp,
|
||||
erp_auth::dto::CreateRoleReq,
|
||||
erp_auth::dto::UpdateRoleReq,
|
||||
erp_auth::dto::PermissionResp,
|
||||
erp_auth::dto::AssignPermissionsReq,
|
||||
erp_auth::dto::ChangePasswordReq,
|
||||
))
|
||||
)]
|
||||
struct AuthApiDoc;
|
||||
|
||||
@@ -86,23 +84,21 @@ struct AuthApiDoc;
|
||||
erp_config::handler::setting_handler::update_setting,
|
||||
erp_config::handler::setting_handler::delete_setting,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_config::dto::DictionaryResp,
|
||||
erp_config::dto::CreateDictionaryReq,
|
||||
erp_config::dto::UpdateDictionaryReq,
|
||||
erp_config::dto::DictionaryItemResp,
|
||||
erp_config::dto::CreateDictionaryItemReq,
|
||||
erp_config::dto::UpdateDictionaryItemReq,
|
||||
erp_config::dto::MenuResp,
|
||||
erp_config::dto::CreateMenuReq,
|
||||
erp_config::dto::UpdateMenuReq,
|
||||
erp_config::dto::NumberingRuleResp,
|
||||
erp_config::dto::CreateNumberingRuleReq,
|
||||
erp_config::dto::UpdateNumberingRuleReq,
|
||||
erp_config::dto::ThemeResp,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_config::dto::DictionaryResp,
|
||||
erp_config::dto::CreateDictionaryReq,
|
||||
erp_config::dto::UpdateDictionaryReq,
|
||||
erp_config::dto::DictionaryItemResp,
|
||||
erp_config::dto::CreateDictionaryItemReq,
|
||||
erp_config::dto::UpdateDictionaryItemReq,
|
||||
erp_config::dto::MenuResp,
|
||||
erp_config::dto::CreateMenuReq,
|
||||
erp_config::dto::UpdateMenuReq,
|
||||
erp_config::dto::NumberingRuleResp,
|
||||
erp_config::dto::CreateNumberingRuleReq,
|
||||
erp_config::dto::UpdateNumberingRuleReq,
|
||||
erp_config::dto::ThemeResp,
|
||||
))
|
||||
)]
|
||||
struct ConfigApiDoc;
|
||||
|
||||
@@ -126,18 +122,16 @@ struct ConfigApiDoc;
|
||||
erp_workflow::handler::task_handler::complete_task,
|
||||
erp_workflow::handler::task_handler::delegate_task,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_workflow::dto::ProcessDefinitionResp,
|
||||
erp_workflow::dto::CreateProcessDefinitionReq,
|
||||
erp_workflow::dto::UpdateProcessDefinitionReq,
|
||||
erp_workflow::dto::ProcessInstanceResp,
|
||||
erp_workflow::dto::StartInstanceReq,
|
||||
erp_workflow::dto::TaskResp,
|
||||
erp_workflow::dto::CompleteTaskReq,
|
||||
erp_workflow::dto::DelegateTaskReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_workflow::dto::ProcessDefinitionResp,
|
||||
erp_workflow::dto::CreateProcessDefinitionReq,
|
||||
erp_workflow::dto::UpdateProcessDefinitionReq,
|
||||
erp_workflow::dto::ProcessInstanceResp,
|
||||
erp_workflow::dto::StartInstanceReq,
|
||||
erp_workflow::dto::TaskResp,
|
||||
erp_workflow::dto::CompleteTaskReq,
|
||||
erp_workflow::dto::DelegateTaskReq,
|
||||
))
|
||||
)]
|
||||
struct WorkflowApiDoc;
|
||||
|
||||
@@ -155,18 +149,16 @@ struct WorkflowApiDoc;
|
||||
erp_message::handler::template_handler::create_template,
|
||||
erp_message::handler::subscription_handler::update_subscription,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_message::dto::MessageResp,
|
||||
erp_message::dto::SendMessageReq,
|
||||
erp_message::dto::MessageQuery,
|
||||
erp_message::dto::UnreadCountResp,
|
||||
erp_message::dto::MessageTemplateResp,
|
||||
erp_message::dto::CreateTemplateReq,
|
||||
erp_message::dto::MessageSubscriptionResp,
|
||||
erp_message::dto::UpdateSubscriptionReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_message::dto::MessageResp,
|
||||
erp_message::dto::SendMessageReq,
|
||||
erp_message::dto::MessageQuery,
|
||||
erp_message::dto::UnreadCountResp,
|
||||
erp_message::dto::MessageTemplateResp,
|
||||
erp_message::dto::CreateTemplateReq,
|
||||
erp_message::dto::MessageSubscriptionResp,
|
||||
erp_message::dto::UpdateSubscriptionReq,
|
||||
))
|
||||
)]
|
||||
struct MessageApiDoc;
|
||||
|
||||
@@ -190,31 +182,31 @@ async fn main() -> anyhow::Result<()> {
|
||||
let config = AppConfig::load()?;
|
||||
|
||||
// ── 安全检查:拒绝默认密钥 ──────────────────────────
|
||||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
|
||||
tracing::error!(
|
||||
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
|
||||
);
|
||||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production"
|
||||
{
|
||||
tracing::error!("JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.database.url == "__MUST_SET_VIA_ENV__" {
|
||||
tracing::error!(
|
||||
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
|
||||
);
|
||||
tracing::error!("数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.redis.url == "__MUST_SET_VIA_ENV__" {
|
||||
tracing::error!(
|
||||
"Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL"
|
||||
);
|
||||
tracing::error!("Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if !config.wechat.dev_mode && (config.wechat.appid == "__MUST_SET_VIA_ENV__" || config.wechat.secret == "__MUST_SET_VIA_ENV__") {
|
||||
if !config.wechat.dev_mode
|
||||
&& (config.wechat.appid == "__MUST_SET_VIA_ENV__"
|
||||
|| config.wechat.secret == "__MUST_SET_VIA_ENV__")
|
||||
{
|
||||
tracing::error!(
|
||||
"微信凭据为默认占位值,拒绝启动。请设置环境变量 ERP__WECHAT__APPID 和 ERP__WECHAT__SECRET"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.health.aes_key == "__MUST_SET_VIA_ENV__" || config.health.hmac_key == "__MUST_SET_VIA_ENV__" {
|
||||
if config.health.aes_key == "__MUST_SET_VIA_ENV__"
|
||||
|| config.health.hmac_key == "__MUST_SET_VIA_ENV__"
|
||||
{
|
||||
// 注: health 密钥已被统一 KEK (ERP__CRYPTO__KEK) 替代,此处仅保留兼容性检查
|
||||
tracing::warn!(
|
||||
"ERP__HEALTH__AES_KEY/HMAC_KEY 未设置(已迁移到 ERP__CRYPTO__KEK 统一密钥体系)"
|
||||
@@ -292,12 +284,21 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(tenant_id = %new_tenant_id, "Default tenant ready with auth seed data");
|
||||
|
||||
// Seed AI workflow definitions
|
||||
if let Err(e) = erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id).await {
|
||||
if let Err(e) =
|
||||
erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to seed AI workflow definitions");
|
||||
}
|
||||
|
||||
// Seed dialysis session workflow definition
|
||||
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(&db, new_tenant_id, new_tenant_id).await {
|
||||
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(
|
||||
&db,
|
||||
new_tenant_id,
|
||||
new_tenant_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to seed dialysis session workflow");
|
||||
}
|
||||
|
||||
@@ -363,7 +364,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Points module 已统一到 erp-health(/health/points/* 路由)
|
||||
|
||||
|
||||
// Initialize dialysis module
|
||||
let dialysis_module = erp_dialysis::DialysisModule;
|
||||
tracing::info!(
|
||||
@@ -388,11 +388,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Initialize plugin engine
|
||||
let plugin_config = erp_plugin::engine::PluginEngineConfig::default();
|
||||
let plugin_engine = erp_plugin::engine::PluginEngine::new(
|
||||
db.clone(),
|
||||
event_bus.clone(),
|
||||
plugin_config,
|
||||
)?;
|
||||
let plugin_engine =
|
||||
erp_plugin::engine::PluginEngine::new(db.clone(), event_bus.clone(), plugin_config)?;
|
||||
tracing::info!("Plugin engine initialized");
|
||||
|
||||
// Register plugin module
|
||||
@@ -466,7 +463,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
panic!("ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes).");
|
||||
panic!(
|
||||
"ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes)."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
erp_core::crypto::PiiCrypto::from_kek_hex(&config.crypto.kek)
|
||||
@@ -480,9 +479,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// 始终注册默认 Claude provider(兼容旧配置)
|
||||
{
|
||||
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
|
||||
config.ai.api_key.clone(),
|
||||
);
|
||||
let mut claude =
|
||||
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
@@ -496,22 +494,31 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
match pcfg.provider_type.as_str() {
|
||||
"openai" => {
|
||||
let api_key = pcfg.api_key_env.as_ref()
|
||||
let api_key = pcfg
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg.base_url.clone()
|
||||
let base_url = pcfg
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let provider = erp_ai::provider::openai::OpenAIProvider::new(
|
||||
api_key, base_url, pcfg.default_model.clone(),
|
||||
api_key,
|
||||
base_url,
|
||||
pcfg.default_model.clone(),
|
||||
);
|
||||
registry.register(name.clone(), std::sync::Arc::new(provider));
|
||||
tracing::info!(provider = %name, "已注册 OpenAI 兼容提供商");
|
||||
}
|
||||
"ollama" => {
|
||||
let base_url = pcfg.base_url.clone()
|
||||
let base_url = pcfg
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let provider = erp_ai::provider::ollama::OllamaProvider::new(
|
||||
base_url, pcfg.default_model.clone(),
|
||||
base_url,
|
||||
pcfg.default_model.clone(),
|
||||
);
|
||||
registry.register(name.clone(), std::sync::Arc::new(provider));
|
||||
tracing::info!(provider = %name, "已注册 Ollama 本地提供商");
|
||||
@@ -528,46 +535,58 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成");
|
||||
|
||||
// 根据 default_provider 配置构建 AnalysisService 的默认 provider
|
||||
let default_provider: Box<dyn erp_ai::provider::AiProvider> =
|
||||
match config.ai.default_provider.as_str() {
|
||||
"ollama" => {
|
||||
let pcfg = config.ai.providers.get("ollama");
|
||||
let base_url = pcfg.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let model = pcfg.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
|
||||
Box::new(erp_ai::provider::ollama::OllamaProvider::new(base_url, model))
|
||||
let default_provider: Box<dyn erp_ai::provider::AiProvider> = match config
|
||||
.ai
|
||||
.default_provider
|
||||
.as_str()
|
||||
{
|
||||
"ollama" => {
|
||||
let pcfg = config.ai.providers.get("ollama");
|
||||
let base_url = pcfg
|
||||
.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let model = pcfg
|
||||
.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
|
||||
Box::new(erp_ai::provider::ollama::OllamaProvider::new(
|
||||
base_url, model,
|
||||
))
|
||||
}
|
||||
"openai" => {
|
||||
let pcfg = config.ai.providers.get("openai");
|
||||
let api_key = pcfg
|
||||
.and_then(|c| c.api_key_env.as_ref())
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg
|
||||
.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let model = pcfg
|
||||
.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
Box::new(erp_ai::provider::openai::OpenAIProvider::new(
|
||||
api_key, base_url, model,
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
// 默认 Claude
|
||||
let mut claude =
|
||||
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
"openai" => {
|
||||
let pcfg = config.ai.providers.get("openai");
|
||||
let api_key = pcfg.and_then(|c| c.api_key_env.as_ref())
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let model = pcfg.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
Box::new(erp_ai::provider::openai::OpenAIProvider::new(api_key, base_url, model))
|
||||
}
|
||||
_ => {
|
||||
// 默认 Claude
|
||||
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
|
||||
config.ai.api_key.clone(),
|
||||
);
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
Box::new(claude)
|
||||
}
|
||||
};
|
||||
Box::new(claude)
|
||||
}
|
||||
};
|
||||
|
||||
let analysis_svc = erp_ai::service::analysis::AnalysisService::new(
|
||||
default_provider,
|
||||
db.clone(),
|
||||
).with_knowledge_source(std::sync::Arc::new(
|
||||
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(db.clone()),
|
||||
));
|
||||
let analysis_svc =
|
||||
erp_ai::service::analysis::AnalysisService::new(default_provider, db.clone())
|
||||
.with_knowledge_source(std::sync::Arc::new(
|
||||
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(
|
||||
db.clone(),
|
||||
),
|
||||
));
|
||||
let analysis = std::sync::Arc::new(analysis_svc);
|
||||
let prompt = std::sync::Arc::new(erp_ai::service::prompt::PromptService::new(db.clone()));
|
||||
let usage = std::sync::Arc::new(erp_ai::service::usage::UsageService::new(db.clone()));
|
||||
@@ -684,6 +703,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
"/analytics/batch",
|
||||
axum::routing::post(handlers::analytics::batch),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(
|
||||
middleware::frozen_module::frozen_module_middleware,
|
||||
))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
middleware::rate_limit::rate_limit_by_user,
|
||||
@@ -716,9 +738,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
let secret = secret_for_uploads.clone();
|
||||
async move { upload_auth_middleware(secret, req, next).await }
|
||||
}));
|
||||
let fhir_routes = erp_health::HealthModule::fhir_routes().with_state(state.clone());
|
||||
let app = Router::new()
|
||||
.nest("/api/v1", unthrottled_routes.merge(public_routes).merge(protected_routes))
|
||||
.nest("/fhir", erp_health::HealthModule::fhir_routes().with_state(state.clone()))
|
||||
.nest(
|
||||
"/api/v1",
|
||||
unthrottled_routes
|
||||
.merge(public_routes)
|
||||
.merge(protected_routes)
|
||||
.nest("/fhir", fhir_routes),
|
||||
)
|
||||
.nest(
|
||||
"/health/gateway",
|
||||
erp_health::HealthModule::gateway_routes()
|
||||
@@ -729,7 +757,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
.with_state(state.clone()),
|
||||
)
|
||||
.nest("/uploads", uploads_router)
|
||||
.layer(axum::middleware::from_fn(middleware::metrics::metrics_middleware))
|
||||
.layer(axum::middleware::from_fn(
|
||||
middleware::metrics::metrics_middleware,
|
||||
))
|
||||
.layer(cors);
|
||||
|
||||
// Start Prometheus metrics exporter on a separate port
|
||||
@@ -811,7 +841,9 @@ fn build_cors_layer(allowed_origins: &str) -> tower_http::cors::CorsLayer {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
tracing::error!("CORS wildcard '*' is not allowed in production builds");
|
||||
panic!("Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains.");
|
||||
panic!(
|
||||
"Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains."
|
||||
);
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -879,6 +911,7 @@ async fn shutdown_signal() {
|
||||
/// 对每个模块的 `permissions()` 返回的权限执行 upsert:
|
||||
/// - 新权限:INSERT
|
||||
/// - 已有权限(同 tenant_id + code):跳过
|
||||
///
|
||||
/// 同时将新权限分配给 admin 角色。
|
||||
async fn sync_module_permissions(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
@@ -906,7 +939,7 @@ async fn sync_module_permissions(
|
||||
perm.code.clone().into(),
|
||||
perm.name.clone().into(),
|
||||
perm.module.clone().into(),
|
||||
perm.code.split('.').last().unwrap_or("manage").into(),
|
||||
perm.code.split('.').next_back().unwrap_or("manage").into(),
|
||||
perm.description.clone().into(),
|
||||
system_user_id.into(),
|
||||
],
|
||||
@@ -932,7 +965,10 @@ async fn sync_module_permissions(
|
||||
)).await?;
|
||||
|
||||
if total_new > 0 {
|
||||
tracing::info!(total_new, "New module permissions synced and bound to admin role");
|
||||
tracing::info!(
|
||||
total_new,
|
||||
"New module permissions synced and bound to admin role"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
37
crates/erp-server/src/middleware/frozen_module.rs
Normal file
37
crates/erp-server/src/middleware/frozen_module.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use axum::Json;
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
/// 冻结模块路径前缀列表。
|
||||
///
|
||||
/// 这些模块前端已通过 FROZEN_ROUTES 守卫拦截,后端也需同步拦截,
|
||||
/// 防止直接调 API 绕过限制。
|
||||
const FROZEN_PREFIXES: &[&str] = &[
|
||||
"/api/v1/health/care-plans",
|
||||
"/api/v1/health/shifts",
|
||||
"/api/v1/health/family-proxy",
|
||||
"/api/v1/health/medications",
|
||||
"/api/v1/health/dialysis",
|
||||
"/api/v1/health/schedules",
|
||||
];
|
||||
|
||||
pub async fn frozen_module_middleware(req: Request<Body>, next: Next) -> Response {
|
||||
let path = req.uri().path();
|
||||
|
||||
for prefix in FROZEN_PREFIXES {
|
||||
if path.starts_with(prefix) {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({
|
||||
"success": false,
|
||||
"error": "该功能正在优化中,暂不可用"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
next.run(req).await
|
||||
}
|
||||
@@ -75,9 +75,13 @@ pub fn start_metrics_server(port: u16) {
|
||||
let handle = handle.clone();
|
||||
async move {
|
||||
let body = handle.render();
|
||||
axum::response::IntoResponse::into_response(
|
||||
([(axum::http::header::CONTENT_TYPE, "text/plain; version=0.0.4")], body),
|
||||
)
|
||||
axum::response::IntoResponse::into_response((
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/plain; version=0.0.4",
|
||||
)],
|
||||
body,
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod frozen_module;
|
||||
pub mod metrics;
|
||||
pub mod rate_limit;
|
||||
pub mod tenant_rls;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::State;
|
||||
use axum::http::{Request, StatusCode};
|
||||
@@ -8,7 +5,6 @@ use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use redis::AsyncCommands;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -23,64 +19,6 @@ struct RateLimitResponse {
|
||||
const ACCOUNT_LOCKOUT_MAX_FAILURES: i64 = 5;
|
||||
const ACCOUNT_LOCKOUT_TTL_SECS: i64 = 900; // 15 分钟
|
||||
|
||||
/// 限流参数(预留配置化扩展)。
|
||||
#[allow(dead_code)]
|
||||
pub struct RateLimitConfig {
|
||||
/// 窗口内最大请求数。
|
||||
pub max_requests: u64,
|
||||
/// 窗口大小(秒)。
|
||||
pub window_secs: u64,
|
||||
/// Redis key 前缀。
|
||||
pub key_prefix: String,
|
||||
}
|
||||
|
||||
/// Redis 可用性状态缓存,避免重复连接失败时阻塞。
|
||||
struct RedisAvailability {
|
||||
available: AtomicBool,
|
||||
last_check: Mutex<Instant>,
|
||||
}
|
||||
|
||||
impl RedisAvailability {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
available: AtomicBool::new(true),
|
||||
last_check: Mutex::new(Instant::now() - std::time::Duration::from_secs(60)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否应该尝试连接 Redis。
|
||||
/// 如果上次连接失败且冷却期未过,返回 false。
|
||||
async fn should_try(&self) -> bool {
|
||||
if self.available.load(Ordering::Relaxed) {
|
||||
return true;
|
||||
}
|
||||
let mut last = self.last_check.lock().await;
|
||||
// 连接失败后冷却 30 秒再重试
|
||||
if last.elapsed() > std::time::Duration::from_secs(30) {
|
||||
*last = Instant::now();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_ok(&self) {
|
||||
self.available.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
async fn mark_failed(&self) {
|
||||
self.available.store(false, Ordering::Relaxed);
|
||||
*self.last_check.lock().await = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局 Redis 可用性缓存
|
||||
static REDIS_AVAIL: std::sync::OnceLock<RedisAvailability> = std::sync::OnceLock::new();
|
||||
|
||||
fn redis_avail() -> &'static RedisAvailability {
|
||||
REDIS_AVAIL.get_or_init(RedisAvailability::new)
|
||||
}
|
||||
|
||||
/// 基于 Redis 的 IP 限流中间件。
|
||||
///
|
||||
/// 使用 INCR + EXPIRE 实现固定窗口计数器。
|
||||
@@ -91,8 +29,7 @@ pub async fn rate_limit_by_ip(
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let identifier = extract_client_ip(req.headers());
|
||||
let fail_close = state.config.rate_limit.fail_close;
|
||||
apply_rate_limit(&state.redis, &identifier, 5, 60, "login", fail_close, req, next).await
|
||||
apply_rate_limit(&state.redis, &identifier, 5, 60, "login", req, next).await
|
||||
}
|
||||
|
||||
/// 基于 Redis 的用户限流中间件。
|
||||
@@ -108,8 +45,7 @@ pub async fn rate_limit_by_user(
|
||||
.get::<erp_core::types::TenantContext>()
|
||||
.map(|ctx| ctx.user_id.to_string())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
let fail_close = state.config.rate_limit.fail_close;
|
||||
apply_rate_limit(&state.redis, &identifier, 100, 60, "write", fail_close, req, next).await
|
||||
apply_rate_limit(&state.redis, &identifier, 300, 60, "api", req, next).await
|
||||
}
|
||||
|
||||
/// 执行限流检查。
|
||||
@@ -119,42 +55,15 @@ async fn apply_rate_limit(
|
||||
max_requests: u64,
|
||||
window_secs: u64,
|
||||
prefix: &str,
|
||||
fail_close: bool,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let avail = redis_avail();
|
||||
|
||||
// Redis 不可达时根据 fail_close 配置决定行为
|
||||
if !avail.should_try().await {
|
||||
if fail_close {
|
||||
tracing::error!("Redis 不可达,fail-close 拒绝请求 [{}]", prefix);
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
|
||||
error: "service_unavailable".to_string(),
|
||||
message: "安全服务暂不可用,请稍后重试".to_string(),
|
||||
})).into_response();
|
||||
}
|
||||
tracing::warn!("Redis 不可达,fail-open 限流放行 [{}]", prefix);
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
let key = format!("rate_limit:{}:{}", prefix, identifier);
|
||||
|
||||
let mut conn = match redis_client.get_multiplexed_async_connection().await {
|
||||
Ok(c) => {
|
||||
avail.mark_ok();
|
||||
c
|
||||
}
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
avail.mark_failed().await;
|
||||
if fail_close {
|
||||
tracing::error!(error = %e, "Redis 连接失败,fail-close 拒绝请求 [{}]", prefix);
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
|
||||
error: "service_unavailable".to_string(),
|
||||
message: "安全服务暂不可用,请稍后重试".to_string(),
|
||||
})).into_response();
|
||||
}
|
||||
tracing::warn!(error = %e, "Redis 连接失败,fail-open 限流放行 [{}]", prefix);
|
||||
tracing::error!(error = %e, "Redis 连接失败,限流放行 [{}]", prefix);
|
||||
return next.run(req).await;
|
||||
}
|
||||
};
|
||||
@@ -162,14 +71,7 @@ async fn apply_rate_limit(
|
||||
let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
if fail_close {
|
||||
tracing::error!(error = %e, "Redis INCR 失败,fail-close 拒绝请求 [{}]", prefix);
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
|
||||
error: "service_unavailable".to_string(),
|
||||
message: "安全服务暂不可用,请稍后重试".to_string(),
|
||||
})).into_response();
|
||||
}
|
||||
tracing::warn!(error = %e, "Redis INCR 失败,fail-open 限流放行 [{}]", prefix);
|
||||
tracing::error!(error = %e, "Redis INCR 失败,限流放行 [{}]", prefix);
|
||||
return next.run(req).await;
|
||||
}
|
||||
};
|
||||
@@ -202,39 +104,11 @@ pub async fn account_lockout_middleware(
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let avail = redis_avail();
|
||||
|
||||
// Redis 可达性检查:生产环境 fail-close,开发环境 fail-open
|
||||
let fail_close = state.config.rate_limit.fail_close;
|
||||
|
||||
if !avail.should_try().await {
|
||||
if fail_close {
|
||||
tracing::error!("Redis 不可达,fail-close 拒绝登录请求");
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
|
||||
error: "service_unavailable".to_string(),
|
||||
message: "安全服务暂不可用,请稍后重试".to_string(),
|
||||
})).into_response();
|
||||
}
|
||||
tracing::error!("Redis 不可达,fail-open 放行(非生产模式,建议设置 ERP__RATE_LIMIT__FAIL_CLOSE=true)");
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
// 获取 Redis 连接
|
||||
let mut conn = match state.redis.get_multiplexed_async_connection().await {
|
||||
Ok(c) => {
|
||||
avail.mark_ok();
|
||||
c
|
||||
}
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
avail.mark_failed().await;
|
||||
if fail_close {
|
||||
tracing::error!(error = %e, "Redis 连接失败,fail-close 拒绝登录请求");
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
|
||||
error: "service_unavailable".to_string(),
|
||||
message: "安全服务暂不可用,请稍后重试".to_string(),
|
||||
})).into_response();
|
||||
}
|
||||
tracing::error!(error = %e, "Redis 连接失败,fail-open 放行(非生产模式)");
|
||||
tracing::error!(error = %e, "Redis 连接失败,登录锁定放行");
|
||||
return next.run(req).await;
|
||||
}
|
||||
};
|
||||
@@ -245,7 +119,6 @@ pub async fn account_lockout_middleware(
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "读取登录请求体失败,放行");
|
||||
// 无法读取 body,重建请求放行
|
||||
let req = Request::from_parts(parts, Body::from(Vec::new()));
|
||||
return next.run(req).await;
|
||||
}
|
||||
@@ -259,7 +132,6 @@ pub async fn account_lockout_middleware(
|
||||
let username = match username {
|
||||
Some(u) if !u.is_empty() => u,
|
||||
_ => {
|
||||
// 无法解析 username,用原始 body 重建请求放行
|
||||
let req = Request::from_parts(parts, Body::from(bytes.to_vec()));
|
||||
return next.run(req).await;
|
||||
}
|
||||
@@ -290,7 +162,6 @@ pub async fn account_lockout_middleware(
|
||||
let status = response.status();
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
// 需要读取 body 以重建响应(因为 into_parts 消费了 body)
|
||||
let body_bytes = axum::body::to_bytes(body, 1024 * 1024)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -305,7 +176,6 @@ pub async fn account_lockout_middleware(
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Redis INCR 失败计数失败");
|
||||
// 即使计数失败,也返回原始 401 响应
|
||||
let resp = Response::from_parts(parts, Body::from(body_bytes.to_vec()));
|
||||
return resp;
|
||||
}
|
||||
@@ -329,8 +199,8 @@ pub async fn account_lockout_middleware(
|
||||
}
|
||||
|
||||
// 重建并返回原始响应
|
||||
let resp = Response::from_parts(parts, Body::from(body_bytes.to_vec()));
|
||||
resp
|
||||
|
||||
Response::from_parts(parts, Body::from(body_bytes.to_vec()))
|
||||
}
|
||||
|
||||
/// 从请求头中提取客户端 IP。
|
||||
|
||||
@@ -18,7 +18,10 @@ pub async fn tenant_rls_middleware(
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let tenant_id = req.extensions().get::<TenantContext>().map(|ctx| ctx.tenant_id);
|
||||
let tenant_id = req
|
||||
.extensions()
|
||||
.get::<TenantContext>()
|
||||
.map(|ctx| ctx.tenant_id);
|
||||
|
||||
if let Some(tid) = tenant_id {
|
||||
// SET app.current_tenant_id — RLS 策略读取此值(参数化查询防止注入)
|
||||
@@ -37,13 +40,10 @@ pub async fn tenant_rls_middleware(
|
||||
let response = next.run(req).await;
|
||||
|
||||
// RESET — 防止连接池复用时泄漏租户上下文
|
||||
if tenant_id.is_some() {
|
||||
if let Err(e) = db
|
||||
.execute_unprepared("RESET app.current_tenant_id")
|
||||
.await
|
||||
{
|
||||
tracing::debug!(error = %e, "RESET app.current_tenant_id 失败(非致命)");
|
||||
}
|
||||
if tenant_id.is_some()
|
||||
&& let Err(e) = db.execute_unprepared("RESET app.current_tenant_id").await
|
||||
{
|
||||
tracing::debug!(error = %e, "RESET app.current_tenant_id 失败(非致命)");
|
||||
}
|
||||
|
||||
response
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
|
||||
};
|
||||
use sqlx::postgres::PgListener;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ async fn run_cleanup(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::Db
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
tracing::info!(
|
||||
rows_affected = result.rows_affected(),
|
||||
"已发布事件归档完成"
|
||||
);
|
||||
tracing::info!(rows_affected = result.rows_affected(), "已发布事件归档完成");
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "已发布事件归档失败"),
|
||||
}
|
||||
@@ -41,10 +38,7 @@ async fn run_cleanup(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::Db
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
tracing::info!(
|
||||
rows_affected = result.rows_affected(),
|
||||
"去重记录清理完成"
|
||||
);
|
||||
tracing::info!(rows_affected = result.rows_affected(), "去重记录清理完成");
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "去重记录清理失败"),
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
#[path = "integration/test_db.rs"]
|
||||
mod test_db;
|
||||
#[path = "integration/test_fixture.rs"]
|
||||
mod test_fixture;
|
||||
#[path = "integration/ai_prompt_tests.rs"]
|
||||
mod ai_prompt_tests;
|
||||
#[path = "integration/auth_tests.rs"]
|
||||
mod auth_tests;
|
||||
#[path = "integration/plugin_tests.rs"]
|
||||
mod plugin_tests;
|
||||
#[path = "integration/workflow_tests.rs"]
|
||||
mod workflow_tests;
|
||||
#[path = "integration/health_patient_tests.rs"]
|
||||
mod health_patient_tests;
|
||||
#[path = "integration/health_alert_tests.rs"]
|
||||
mod health_alert_tests;
|
||||
#[path = "integration/health_appointment_tests.rs"]
|
||||
mod health_appointment_tests;
|
||||
#[path = "integration/health_article_tests.rs"]
|
||||
mod health_article_tests;
|
||||
#[path = "integration/health_consent_tests.rs"]
|
||||
mod health_consent_tests;
|
||||
#[path = "integration/health_consultation_tests.rs"]
|
||||
mod health_consultation_tests;
|
||||
#[path = "integration/health_daily_monitoring_tests.rs"]
|
||||
mod health_daily_monitoring_tests;
|
||||
#[path = "integration/health_data_tests.rs"]
|
||||
mod health_data_tests;
|
||||
#[path = "integration/health_device_reading_tests.rs"]
|
||||
mod health_device_reading_tests;
|
||||
#[path = "integration/health_diagnosis_tests.rs"]
|
||||
mod health_diagnosis_tests;
|
||||
#[path = "integration/health_dialysis_prescription_tests.rs"]
|
||||
mod health_dialysis_prescription_tests;
|
||||
#[path = "integration/health_dialysis_tests.rs"]
|
||||
mod health_dialysis_tests;
|
||||
#[path = "integration/health_doctor_tests.rs"]
|
||||
mod health_doctor_tests;
|
||||
#[path = "integration/health_follow_up_template_tests.rs"]
|
||||
mod health_follow_up_template_tests;
|
||||
#[path = "integration/health_follow_up_tests.rs"]
|
||||
mod health_follow_up_tests;
|
||||
#[path = "integration/health_medication_tests.rs"]
|
||||
mod health_medication_tests;
|
||||
#[path = "integration/health_patient_tests.rs"]
|
||||
mod health_patient_tests;
|
||||
#[path = "integration/health_pii_encryption_tests.rs"]
|
||||
mod health_pii_encryption_tests;
|
||||
#[path = "integration/health_points_tests.rs"]
|
||||
mod health_points_tests;
|
||||
#[path = "integration/health_dialysis_tests.rs"]
|
||||
mod health_dialysis_tests;
|
||||
#[path = "integration/health_alert_tests.rs"]
|
||||
mod health_alert_tests;
|
||||
#[path = "integration/health_device_reading_tests.rs"]
|
||||
mod health_device_reading_tests;
|
||||
#[path = "integration/health_follow_up_tests.rs"]
|
||||
mod health_follow_up_tests;
|
||||
#[path = "integration/health_consultation_tests.rs"]
|
||||
mod health_consultation_tests;
|
||||
#[path = "integration/health_data_tests.rs"]
|
||||
mod health_data_tests;
|
||||
#[path = "integration/health_article_tests.rs"]
|
||||
mod health_article_tests;
|
||||
#[path = "integration/health_doctor_tests.rs"]
|
||||
mod health_doctor_tests;
|
||||
#[path = "integration/health_diagnosis_tests.rs"]
|
||||
mod health_diagnosis_tests;
|
||||
#[path = "integration/health_consent_tests.rs"]
|
||||
mod health_consent_tests;
|
||||
#[path = "integration/health_medication_tests.rs"]
|
||||
mod health_medication_tests;
|
||||
#[path = "integration/health_dialysis_prescription_tests.rs"]
|
||||
mod health_dialysis_prescription_tests;
|
||||
#[path = "integration/health_follow_up_template_tests.rs"]
|
||||
mod health_follow_up_template_tests;
|
||||
#[path = "integration/health_daily_monitoring_tests.rs"]
|
||||
mod health_daily_monitoring_tests;
|
||||
#[path = "integration/ai_prompt_tests.rs"]
|
||||
mod ai_prompt_tests;
|
||||
#[path = "integration/plugin_tests.rs"]
|
||||
mod plugin_tests;
|
||||
#[path = "integration/test_db.rs"]
|
||||
mod test_db;
|
||||
#[path = "integration/test_fixture.rs"]
|
||||
mod test_fixture;
|
||||
#[path = "integration/workflow_tests.rs"]
|
||||
mod workflow_tests;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use erp_ai::service::prompt::PromptService;
|
||||
use erp_ai::service::usage::UsageService;
|
||||
use erp_ai::service::analysis::AnalysisService;
|
||||
use erp_ai::provider::AiProvider;
|
||||
use erp_ai::dto::GenerateRequest;
|
||||
use erp_ai::error::{AiError, AiResult};
|
||||
use erp_ai::provider::AiProvider;
|
||||
use erp_ai::service::analysis::AnalysisService;
|
||||
use erp_ai::service::prompt::PromptService;
|
||||
use erp_ai::service::usage::UsageService;
|
||||
use erp_core::types::Pagination;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sha2::Digest;
|
||||
@@ -97,7 +97,14 @@ async fn prompt_list_with_category_filter() {
|
||||
}
|
||||
|
||||
let (items, total) = svc
|
||||
.list_prompts(tenant_id, Some("analysis".into()), &Pagination { page: Some(1), page_size: Some(10) })
|
||||
.list_prompts(
|
||||
tenant_id,
|
||||
Some("analysis".into()),
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
|
||||
@@ -113,25 +120,49 @@ async fn prompt_activate_switches_version() {
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
let v1 = svc
|
||||
.create_prompt(tenant_id, user_id, "my_prompt".into(), "sys_v1".into(), "usr".into(), serde_json::json!({}), "cat".into())
|
||||
.create_prompt(
|
||||
tenant_id,
|
||||
user_id,
|
||||
"my_prompt".into(),
|
||||
"sys_v1".into(),
|
||||
"usr".into(),
|
||||
serde_json::json!({}),
|
||||
"cat".into(),
|
||||
)
|
||||
.await
|
||||
.expect("v1");
|
||||
|
||||
let v2 = svc
|
||||
.update_prompt(v1.id, tenant_id, user_id, Some("sys_v2".into()), None, None, None)
|
||||
.update_prompt(
|
||||
v1.id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
Some("sys_v2".into()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("v2");
|
||||
|
||||
assert_eq!(v2.version, 2);
|
||||
|
||||
// v1 仍然激活(update 继承 is_active)
|
||||
let active_before = svc.get_active_prompt(tenant_id, "my_prompt").await.expect("active");
|
||||
let active_before = svc
|
||||
.get_active_prompt(tenant_id, "my_prompt")
|
||||
.await
|
||||
.expect("active");
|
||||
assert_eq!(active_before.system_prompt, "sys_v1");
|
||||
|
||||
// 激活 v2
|
||||
svc.activate_prompt(v2.id, tenant_id).await.expect("activate");
|
||||
svc.activate_prompt(v2.id, tenant_id)
|
||||
.await
|
||||
.expect("activate");
|
||||
|
||||
let active_after = svc.get_active_prompt(tenant_id, "my_prompt").await.expect("active");
|
||||
let active_after = svc
|
||||
.get_active_prompt(tenant_id, "my_prompt")
|
||||
.await
|
||||
.expect("active");
|
||||
assert_eq!(active_after.id, v2.id);
|
||||
assert_eq!(active_after.system_prompt, "sys_v2");
|
||||
|
||||
@@ -148,21 +179,44 @@ async fn prompt_rollback_equals_activate() {
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
let v1 = svc
|
||||
.create_prompt(tenant_id, user_id, "rb_test".into(), "sys_v1".into(), "usr".into(), serde_json::json!({}), "cat".into())
|
||||
.create_prompt(
|
||||
tenant_id,
|
||||
user_id,
|
||||
"rb_test".into(),
|
||||
"sys_v1".into(),
|
||||
"usr".into(),
|
||||
serde_json::json!({}),
|
||||
"cat".into(),
|
||||
)
|
||||
.await
|
||||
.expect("v1");
|
||||
|
||||
let v2 = svc
|
||||
.update_prompt(v1.id, tenant_id, user_id, Some("sys_v2".into()), None, None, None)
|
||||
.update_prompt(
|
||||
v1.id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
Some("sys_v2".into()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("v2");
|
||||
|
||||
svc.activate_prompt(v2.id, tenant_id).await.expect("activate v2");
|
||||
svc.activate_prompt(v2.id, tenant_id)
|
||||
.await
|
||||
.expect("activate v2");
|
||||
|
||||
// 回滚到 v1
|
||||
svc.rollback_prompt(v1.id, tenant_id).await.expect("rollback");
|
||||
svc.rollback_prompt(v1.id, tenant_id)
|
||||
.await
|
||||
.expect("rollback");
|
||||
|
||||
let active = svc.get_active_prompt(tenant_id, "rb_test").await.expect("active");
|
||||
let active = svc
|
||||
.get_active_prompt(tenant_id, "rb_test")
|
||||
.await
|
||||
.expect("active");
|
||||
assert_eq!(active.id, v1.id);
|
||||
}
|
||||
|
||||
@@ -174,9 +228,17 @@ async fn prompt_cross_tenant_isolation() {
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
svc.create_prompt(tenant_a, user_id, "shared_name".into(), "sys".into(), "usr".into(), serde_json::json!({}), "cat".into())
|
||||
.await
|
||||
.expect("create");
|
||||
svc.create_prompt(
|
||||
tenant_a,
|
||||
user_id,
|
||||
"shared_name".into(),
|
||||
"sys".into(),
|
||||
"usr".into(),
|
||||
serde_json::json!({}),
|
||||
"cat".into(),
|
||||
)
|
||||
.await
|
||||
.expect("create");
|
||||
|
||||
let result = svc.get_active_prompt(tenant_b, "shared_name").await;
|
||||
assert!(result.is_err());
|
||||
@@ -224,10 +286,16 @@ async fn usage_by_type_aggregation() {
|
||||
let by_type = svc.get_by_type(tenant_id).await.expect("by_type");
|
||||
assert_eq!(by_type.len(), 2);
|
||||
|
||||
let lab = by_type.iter().find(|t| t.analysis_type == "lab_report").expect("lab");
|
||||
let lab = by_type
|
||||
.iter()
|
||||
.find(|t| t.analysis_type == "lab_report")
|
||||
.expect("lab");
|
||||
assert_eq!(lab.count, 2);
|
||||
|
||||
let trends = by_type.iter().find(|t| t.analysis_type == "trends").expect("trends");
|
||||
let trends = by_type
|
||||
.iter()
|
||||
.find(|t| t.analysis_type == "trends")
|
||||
.expect("trends");
|
||||
assert_eq!(trends.count, 1);
|
||||
}
|
||||
|
||||
@@ -238,7 +306,17 @@ async fn usage_log_creates_record() {
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
|
||||
let record = svc
|
||||
.log_usage(tenant_id, "claude", "claude-3", "lab_report", 100, 200, 3000, 50, false)
|
||||
.log_usage(
|
||||
tenant_id,
|
||||
"claude",
|
||||
"claude-3",
|
||||
"lab_report",
|
||||
100,
|
||||
200,
|
||||
3000,
|
||||
50,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("log");
|
||||
|
||||
@@ -275,11 +353,23 @@ async fn analysis_complete_updates_status() {
|
||||
|
||||
// 通过内部方法创建 streaming 记录(直接插入 DB)
|
||||
let analysis_id = uuid::Uuid::now_v7();
|
||||
insert_streaming_analysis(&test_db, analysis_id, tenant_id, user_id, patient_id, "lab_report").await;
|
||||
insert_streaming_analysis(
|
||||
&test_db,
|
||||
analysis_id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_id,
|
||||
"lab_report",
|
||||
)
|
||||
.await;
|
||||
|
||||
svc.complete_analysis(analysis_id, "分析结果文本".into(), serde_json::json!({"tokens": 100}))
|
||||
.await
|
||||
.expect("complete");
|
||||
svc.complete_analysis(
|
||||
analysis_id,
|
||||
"分析结果文本".into(),
|
||||
serde_json::json!({"tokens": 100}),
|
||||
)
|
||||
.await
|
||||
.expect("complete");
|
||||
|
||||
let record = svc.get_analysis(analysis_id, tenant_id).await.expect("get");
|
||||
assert_eq!(record.status, "completed");
|
||||
@@ -295,7 +385,15 @@ async fn analysis_fail_updates_status() {
|
||||
let patient_id = uuid::Uuid::new_v4();
|
||||
|
||||
let analysis_id = uuid::Uuid::now_v7();
|
||||
insert_streaming_analysis(&test_db, analysis_id, tenant_id, user_id, patient_id, "trends").await;
|
||||
insert_streaming_analysis(
|
||||
&test_db,
|
||||
analysis_id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_id,
|
||||
"trends",
|
||||
)
|
||||
.await;
|
||||
|
||||
svc.fail_analysis(analysis_id, "API 超时".into())
|
||||
.await
|
||||
@@ -319,14 +417,30 @@ async fn analysis_find_cached() {
|
||||
|
||||
// 插入 completed 记录
|
||||
let analysis_id = uuid::Uuid::now_v7();
|
||||
insert_completed_analysis_with_hash(&test_db, analysis_id, tenant_id, user_id, patient_id, "lab_report", &hash, 1).await;
|
||||
insert_completed_analysis_with_hash(
|
||||
&test_db,
|
||||
analysis_id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_id,
|
||||
"lab_report",
|
||||
&hash,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let cached = svc.find_cached(tenant_id, &hash, 1).await.expect("find_cached");
|
||||
let cached = svc
|
||||
.find_cached(tenant_id, &hash, 1)
|
||||
.await
|
||||
.expect("find_cached");
|
||||
assert!(cached.is_some());
|
||||
assert_eq!(cached.unwrap().id, analysis_id);
|
||||
|
||||
// 不同 hash 不命中
|
||||
let miss = svc.find_cached(tenant_id, "wrong_hash", 1).await.expect("find_cached");
|
||||
let miss = svc
|
||||
.find_cached(tenant_id, "wrong_hash", 1)
|
||||
.await
|
||||
.expect("find_cached");
|
||||
assert!(miss.is_none());
|
||||
}
|
||||
|
||||
@@ -339,27 +453,93 @@ async fn analysis_list_with_filters() {
|
||||
let patient_a = uuid::Uuid::new_v4();
|
||||
let patient_b = uuid::Uuid::new_v4();
|
||||
|
||||
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_a, "lab_report", "h1", 1).await;
|
||||
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_a, "trends", "h2", 1).await;
|
||||
insert_completed_analysis_with_hash(&test_db, uuid::Uuid::now_v7(), tenant_id, user_id, patient_b, "lab_report", "h3", 1).await;
|
||||
insert_completed_analysis_with_hash(
|
||||
&test_db,
|
||||
uuid::Uuid::now_v7(),
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_a,
|
||||
"lab_report",
|
||||
"h1",
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
insert_completed_analysis_with_hash(
|
||||
&test_db,
|
||||
uuid::Uuid::now_v7(),
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_a,
|
||||
"trends",
|
||||
"h2",
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
insert_completed_analysis_with_hash(
|
||||
&test_db,
|
||||
uuid::Uuid::now_v7(),
|
||||
tenant_id,
|
||||
user_id,
|
||||
patient_b,
|
||||
"lab_report",
|
||||
"h3",
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
// 按 patient 筛选
|
||||
let (items, total) = svc.list_analysis(tenant_id, Some(patient_a), None, &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
|
||||
let (items, total) = svc
|
||||
.list_analysis(
|
||||
tenant_id,
|
||||
Some(patient_a),
|
||||
None,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("list");
|
||||
assert_eq!(total, 2);
|
||||
|
||||
// 按 type 筛选
|
||||
let (items, total) = svc.list_analysis(tenant_id, None, Some("lab_report".into()), &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
|
||||
let (items, total) = svc
|
||||
.list_analysis(
|
||||
tenant_id,
|
||||
None,
|
||||
Some("lab_report".into()),
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("list");
|
||||
assert_eq!(total, 2);
|
||||
|
||||
// 跨租户
|
||||
let (items, total) = svc.list_analysis(uuid::Uuid::new_v4(), None, None, &Pagination { page: Some(1), page_size: Some(10) }).await.expect("list");
|
||||
let (items, total) = svc
|
||||
.list_analysis(
|
||||
uuid::Uuid::new_v4(),
|
||||
None,
|
||||
None,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("list");
|
||||
assert_eq!(total, 0);
|
||||
assert!(items.is_empty());
|
||||
}
|
||||
|
||||
// ---- 辅助函数 ----
|
||||
|
||||
async fn ai_prompt_find_by_id(test_db: &TestDb, id: uuid::Uuid) -> erp_ai::entity::ai_prompt::Model {
|
||||
async fn ai_prompt_find_by_id(
|
||||
test_db: &TestDb,
|
||||
id: uuid::Uuid,
|
||||
) -> erp_ai::entity::ai_prompt::Model {
|
||||
use sea_orm::EntityTrait;
|
||||
erp_ai::entity::ai_prompt::Entity::find_by_id(id)
|
||||
.one(test_db.db())
|
||||
|
||||
@@ -5,15 +5,21 @@
|
||||
use erp_health::dto::alert_dto::*;
|
||||
use erp_health::entity::{alert_rules, vital_signs_hourly};
|
||||
use erp_health::service::{alert_engine, alert_rule_service, alert_service};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use super::test_fixture::TestApp;
|
||||
|
||||
/// 创建告警规则(单阈值)
|
||||
async fn seed_threshold_rule(app: &TestApp, device_type: &str, threshold: f64) -> alert_rules::Model {
|
||||
async fn seed_threshold_rule(
|
||||
app: &TestApp,
|
||||
device_type: &str,
|
||||
threshold: f64,
|
||||
) -> alert_rules::Model {
|
||||
alert_rule_service::create_rule(
|
||||
app.health_state(), app.tenant_id(), app.operator_id(),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
app.operator_id(),
|
||||
CreateAlertRuleRequest {
|
||||
name: "高收缩压".to_string(),
|
||||
description: Some("收缩压超过阈值".to_string()),
|
||||
@@ -31,9 +37,7 @@ async fn seed_threshold_rule(app: &TestApp, device_type: &str, threshold: f64) -
|
||||
}
|
||||
|
||||
/// 插入一条 hourly 汇总记录
|
||||
async fn seed_hourly(
|
||||
app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64,
|
||||
) {
|
||||
async fn seed_hourly(app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64) {
|
||||
let model = vital_signs_hourly::ActiveModel {
|
||||
id: Set(uuid::Uuid::now_v7()),
|
||||
tenant_id: Set(app.tenant_id()),
|
||||
@@ -64,11 +68,10 @@ async fn test_alert_rule_create_and_list() {
|
||||
assert_eq!(rule.condition_type, "single_threshold");
|
||||
assert!(rule.is_active);
|
||||
|
||||
let (rules, total) = alert_rule_service::list_rules(
|
||||
app.health_state(), app.tenant_id(), None, 1, 20,
|
||||
)
|
||||
.await
|
||||
.expect("列表应成功");
|
||||
let (rules, total) =
|
||||
alert_rule_service::list_rules(app.health_state(), app.tenant_id(), None, 1, 20)
|
||||
.await
|
||||
.expect("列表应成功");
|
||||
assert_eq!(total, 1);
|
||||
assert_eq!(rules[0].id, rule.id);
|
||||
}
|
||||
@@ -84,7 +87,10 @@ async fn test_alert_rule_deactivate() {
|
||||
assert!(rule.is_active);
|
||||
|
||||
let deactivated = alert_rule_service::deactivate_rule(
|
||||
app.health_state(), app.tenant_id(), rule.id, rule.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
rule.id,
|
||||
rule.version,
|
||||
)
|
||||
.await
|
||||
.expect("停用应成功");
|
||||
@@ -103,7 +109,10 @@ async fn test_alert_engine_single_threshold_trigger() {
|
||||
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
|
||||
|
||||
let triggered = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.expect("评估应成功");
|
||||
@@ -125,7 +134,10 @@ async fn test_alert_engine_single_threshold_no_trigger() {
|
||||
seed_hourly(&app, patient_id, "heart_rate", 120.0).await;
|
||||
|
||||
let triggered = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.expect("评估应成功");
|
||||
@@ -145,7 +157,10 @@ async fn test_alert_engine_cooldown_suppresses() {
|
||||
seed_hourly(&app, patient_id, "heart_rate", 160.0).await;
|
||||
|
||||
let first = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.expect("首次评估应成功");
|
||||
@@ -153,7 +168,10 @@ async fn test_alert_engine_cooldown_suppresses() {
|
||||
|
||||
// 再次评估,cooldown 内不应重复
|
||||
let second = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.expect("二次评估应成功");
|
||||
@@ -172,7 +190,10 @@ async fn test_alert_status_flow() {
|
||||
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
|
||||
|
||||
let triggered = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -181,7 +202,11 @@ async fn test_alert_status_flow() {
|
||||
|
||||
// pending → acknowledged
|
||||
let acked = alert_service::acknowledge_alert(
|
||||
app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
alert.id,
|
||||
app.operator_id(),
|
||||
alert.version,
|
||||
)
|
||||
.await
|
||||
.expect("确认应成功");
|
||||
@@ -189,11 +214,10 @@ async fn test_alert_status_flow() {
|
||||
assert!(acked.acknowledged_by.is_some());
|
||||
|
||||
// acknowledged → resolved
|
||||
let resolved = alert_service::resolve_alert(
|
||||
app.health_state(), app.tenant_id(), acked.id, acked.version,
|
||||
)
|
||||
.await
|
||||
.expect("解决应成功");
|
||||
let resolved =
|
||||
alert_service::resolve_alert(app.health_state(), app.tenant_id(), acked.id, acked.version)
|
||||
.await
|
||||
.expect("解决应成功");
|
||||
assert_eq!(resolved.status, "resolved");
|
||||
assert!(resolved.resolved_at.is_some());
|
||||
}
|
||||
@@ -210,14 +234,21 @@ async fn test_alert_dismiss_from_pending() {
|
||||
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
|
||||
|
||||
let triggered = alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate",
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let alert = &triggered[0];
|
||||
|
||||
let dismissed = alert_service::dismiss_alert(
|
||||
app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
alert.id,
|
||||
app.operator_id(),
|
||||
alert.version,
|
||||
)
|
||||
.await
|
||||
.expect("忽略应成功");
|
||||
@@ -237,20 +268,22 @@ async fn test_alert_list_filter_and_tenant_isolation() {
|
||||
seed_hourly(&app, patient_a, "heart_rate", 155.0).await;
|
||||
seed_hourly(&app, patient_b, "heart_rate", 160.0).await;
|
||||
|
||||
alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_a, "heart_rate",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
alert_engine::evaluate_rules(
|
||||
app.health_state(), app.tenant_id(), patient_b, "heart_rate",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_a, "heart_rate")
|
||||
.await
|
||||
.unwrap();
|
||||
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_b, "heart_rate")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 按患者 A 过滤
|
||||
let (alerts_a, total_a) = alert_service::list_alerts(
|
||||
app.health_state(), app.tenant_id(), Some(patient_a), None, None, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(patient_a),
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -260,7 +293,13 @@ async fn test_alert_list_filter_and_tenant_isolation() {
|
||||
// 租户隔离
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let (_alerts_other, total_other) = alert_service::list_alerts(
|
||||
app.health_state(), other_tenant, Some(patient_a), None, None, 1, 20,
|
||||
app.health_state(),
|
||||
other_tenant,
|
||||
Some(patient_a),
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
|
||||
//! 预约创建依赖患者 + 医护档案 + 排班三条前置数据。
|
||||
|
||||
use erp_core::crypto::PiiCrypto;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_health::dto::appointment_dto::{CreateAppointmentReq, UpdateAppointmentStatusReq};
|
||||
use erp_health::dto::doctor_dto::CreateDoctorReq;
|
||||
use erp_health::dto::patient_dto::CreatePatientReq;
|
||||
use erp_health::service::{
|
||||
appointment_service, doctor_service, patient_service,
|
||||
};
|
||||
use erp_health::service::{appointment_service, doctor_service, patient_service};
|
||||
use erp_health::state::HealthState;
|
||||
use erp_core::crypto::PiiCrypto;
|
||||
|
||||
use super::test_db::TestDb;
|
||||
|
||||
@@ -26,11 +24,7 @@ fn make_state(db: &sea_orm::DatabaseConnection) -> HealthState {
|
||||
}
|
||||
|
||||
/// 创建患者并返回其 ID
|
||||
async fn seed_patient(
|
||||
state: &HealthState,
|
||||
tenant_id: uuid::Uuid,
|
||||
name: &str,
|
||||
) -> uuid::Uuid {
|
||||
async fn seed_patient(state: &HealthState, tenant_id: uuid::Uuid, name: &str) -> uuid::Uuid {
|
||||
let req = CreatePatientReq {
|
||||
name: name.to_string(),
|
||||
gender: Some("male".to_string()),
|
||||
@@ -51,11 +45,7 @@ async fn seed_patient(
|
||||
}
|
||||
|
||||
/// 创建医护档案并返回其 ID
|
||||
async fn seed_doctor(
|
||||
state: &HealthState,
|
||||
tenant_id: uuid::Uuid,
|
||||
name: &str,
|
||||
) -> uuid::Uuid {
|
||||
async fn seed_doctor(state: &HealthState, tenant_id: uuid::Uuid, name: &str) -> uuid::Uuid {
|
||||
let req = CreateDoctorReq {
|
||||
user_id: None,
|
||||
name: name.to_string(),
|
||||
@@ -129,10 +119,7 @@ async fn test_create_appointment() {
|
||||
assert_eq!(appointment.appointment_type, "outpatient");
|
||||
assert_eq!(appointment.status, "pending");
|
||||
assert_eq!(appointment.version, 1);
|
||||
assert_eq!(
|
||||
appointment.notes,
|
||||
Some("首次就诊".to_string())
|
||||
);
|
||||
assert_eq!(appointment.notes, Some("首次就诊".to_string()));
|
||||
|
||||
// 通过 get_appointment 验证存储正确
|
||||
let found = appointment_service::get_appointment(&state, tenant_id, appointment.id)
|
||||
@@ -174,18 +161,10 @@ async fn test_list_appointments() {
|
||||
.expect("创建预约应成功");
|
||||
}
|
||||
|
||||
let result = appointment_service::list_appointments(
|
||||
&state,
|
||||
tenant_id,
|
||||
1,
|
||||
10,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("列表查询应成功");
|
||||
let result =
|
||||
appointment_service::list_appointments(&state, tenant_id, 1, 10, None, None, None, None)
|
||||
.await
|
||||
.expect("列表查询应成功");
|
||||
|
||||
assert_eq!(result.total, 2, "应有 2 条预约记录");
|
||||
assert_eq!(result.data.len(), 2, "当前页应返回 2 条");
|
||||
@@ -217,34 +196,22 @@ async fn test_appointment_tenant_isolation() {
|
||||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||||
notes: None,
|
||||
};
|
||||
let appointment_a =
|
||||
appointment_service::create_appointment(&state, tenant_a, None, req)
|
||||
.await
|
||||
.expect("租户 A 创建预约应成功");
|
||||
let appointment_a = appointment_service::create_appointment(&state, tenant_a, None, req)
|
||||
.await
|
||||
.expect("租户 A 创建预约应成功");
|
||||
|
||||
// 租户 B 列表查询应看不到租户 A 的预约
|
||||
let result_b = appointment_service::list_appointments(
|
||||
&state,
|
||||
tenant_b,
|
||||
1,
|
||||
10,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("租户 B 列表查询应成功");
|
||||
let result_b =
|
||||
appointment_service::list_appointments(&state, tenant_b, 1, 10, None, None, None, None)
|
||||
.await
|
||||
.expect("租户 B 列表查询应成功");
|
||||
assert_eq!(result_b.total, 0, "租户 B 不应看到租户 A 的预约");
|
||||
assert!(result_b.data.is_empty());
|
||||
|
||||
// 租户 B 通过 ID 查询租户 A 的预约应返回错误
|
||||
let lookup_result =
|
||||
appointment_service::get_appointment(&state, tenant_b, appointment_a.id).await;
|
||||
assert!(
|
||||
lookup_result.is_err(),
|
||||
"跨租户查询预约应返回错误"
|
||||
);
|
||||
assert!(lookup_result.is_err(), "跨租户查询预约应返回错误");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -264,7 +231,9 @@ async fn test_appointment_status_flow() {
|
||||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||||
|
||||
let appt = appointment_service::create_appointment(
|
||||
&state, tenant_id, Some(operator_id),
|
||||
&state,
|
||||
tenant_id,
|
||||
Some(operator_id),
|
||||
CreateAppointmentReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
@@ -281,8 +250,14 @@ async fn test_appointment_status_flow() {
|
||||
|
||||
// pending → confirmed
|
||||
let confirmed = appointment_service::update_appointment_status(
|
||||
&state, tenant_id, appt.id, Some(operator_id),
|
||||
UpdateAppointmentStatusReq { status: "confirmed".to_string(), cancel_reason: None },
|
||||
&state,
|
||||
tenant_id,
|
||||
appt.id,
|
||||
Some(operator_id),
|
||||
UpdateAppointmentStatusReq {
|
||||
status: "confirmed".to_string(),
|
||||
cancel_reason: None,
|
||||
},
|
||||
appt.version,
|
||||
)
|
||||
.await
|
||||
@@ -291,8 +266,14 @@ async fn test_appointment_status_flow() {
|
||||
|
||||
// confirmed → completed
|
||||
let completed = appointment_service::update_appointment_status(
|
||||
&state, tenant_id, appt.id, Some(operator_id),
|
||||
UpdateAppointmentStatusReq { status: "completed".to_string(), cancel_reason: None },
|
||||
&state,
|
||||
tenant_id,
|
||||
appt.id,
|
||||
Some(operator_id),
|
||||
UpdateAppointmentStatusReq {
|
||||
status: "completed".to_string(),
|
||||
cancel_reason: None,
|
||||
},
|
||||
confirmed.version,
|
||||
)
|
||||
.await
|
||||
@@ -316,7 +297,9 @@ async fn test_appointment_cancel() {
|
||||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||||
|
||||
let appt = appointment_service::create_appointment(
|
||||
&state, tenant_id, None,
|
||||
&state,
|
||||
tenant_id,
|
||||
None,
|
||||
CreateAppointmentReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
@@ -331,7 +314,10 @@ async fn test_appointment_cancel() {
|
||||
.expect("创建应成功");
|
||||
|
||||
let cancelled = appointment_service::update_appointment_status(
|
||||
&state, tenant_id, appt.id, None,
|
||||
&state,
|
||||
tenant_id,
|
||||
appt.id,
|
||||
None,
|
||||
UpdateAppointmentStatusReq {
|
||||
status: "cancelled".to_string(),
|
||||
cancel_reason: Some("患者临时有事".to_string()),
|
||||
@@ -360,7 +346,9 @@ async fn test_appointment_version_conflict() {
|
||||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||||
|
||||
let appt = appointment_service::create_appointment(
|
||||
&state, tenant_id, Some(operator_id),
|
||||
&state,
|
||||
tenant_id,
|
||||
Some(operator_id),
|
||||
CreateAppointmentReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
@@ -376,8 +364,14 @@ async fn test_appointment_version_conflict() {
|
||||
|
||||
// 正确版本确认
|
||||
let _confirmed = appointment_service::update_appointment_status(
|
||||
&state, tenant_id, appt.id, Some(operator_id),
|
||||
UpdateAppointmentStatusReq { status: "confirmed".to_string(), cancel_reason: None },
|
||||
&state,
|
||||
tenant_id,
|
||||
appt.id,
|
||||
Some(operator_id),
|
||||
UpdateAppointmentStatusReq {
|
||||
status: "confirmed".to_string(),
|
||||
cancel_reason: None,
|
||||
},
|
||||
appt.version,
|
||||
)
|
||||
.await
|
||||
@@ -385,8 +379,14 @@ async fn test_appointment_version_conflict() {
|
||||
|
||||
// 用旧版本再更新应失败
|
||||
let result = appointment_service::update_appointment_status(
|
||||
&state, tenant_id, appt.id, Some(operator_id),
|
||||
UpdateAppointmentStatusReq { status: "cancelled".to_string(), cancel_reason: None },
|
||||
&state,
|
||||
tenant_id,
|
||||
appt.id,
|
||||
Some(operator_id),
|
||||
UpdateAppointmentStatusReq {
|
||||
status: "cancelled".to_string(),
|
||||
cancel_reason: None,
|
||||
},
|
||||
appt.version, // 旧版本
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 验证文章 CRUD + 状态流、分类 CRUD、标签 CRUD、租户隔离、乐观锁。
|
||||
|
||||
use erp_health::dto::article_dto::*;
|
||||
use erp_health::service::{article_service, article_category_service, article_tag_service};
|
||||
use erp_health::service::{article_category_service, article_service, article_tag_service};
|
||||
|
||||
use super::test_fixture::TestApp;
|
||||
|
||||
@@ -29,7 +29,9 @@ fn default_create_article_req() -> CreateArticleReq {
|
||||
|
||||
async fn seed_article(app: &TestApp) -> ArticleResp {
|
||||
article_service::create_article(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
default_create_article_req(),
|
||||
)
|
||||
.await
|
||||
@@ -38,7 +40,9 @@ async fn seed_article(app: &TestApp) -> ArticleResp {
|
||||
|
||||
async fn seed_category(app: &TestApp, name: &str) -> CategoryResp {
|
||||
article_category_service::create_category(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
CreateCategoryReq {
|
||||
name: name.to_string(),
|
||||
slug: None,
|
||||
@@ -53,8 +57,12 @@ async fn seed_category(app: &TestApp, name: &str) -> CategoryResp {
|
||||
|
||||
async fn seed_tag(app: &TestApp, name: &str) -> TagResp {
|
||||
article_tag_service::create_tag(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
CreateTagReq { name: name.to_string() },
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
CreateTagReq {
|
||||
name: name.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("创建标签应成功")
|
||||
@@ -86,8 +94,11 @@ async fn test_article_status_flow() {
|
||||
|
||||
// draft → pending_review
|
||||
let submitted = article_service::submit_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
Some(app.operator_id()), article.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
article.version,
|
||||
)
|
||||
.await
|
||||
.expect("提交审核应成功");
|
||||
@@ -95,9 +106,14 @@ async fn test_article_status_flow() {
|
||||
|
||||
// pending_review → published
|
||||
let published = article_service::approve_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
ReviewArticleReq { note: Some("通过".to_string()), version: Some(submitted.version) },
|
||||
ReviewArticleReq {
|
||||
note: Some("通过".to_string()),
|
||||
version: Some(submitted.version),
|
||||
},
|
||||
submitted.version,
|
||||
)
|
||||
.await
|
||||
@@ -106,8 +122,11 @@ async fn test_article_status_flow() {
|
||||
|
||||
// published → draft(取消发布)
|
||||
let unpublished = article_service::unpublish_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
Some(app.operator_id()), published.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
published.version,
|
||||
)
|
||||
.await
|
||||
.expect("取消发布应成功");
|
||||
@@ -123,17 +142,25 @@ async fn test_article_reject_and_resubmit() {
|
||||
let article = seed_article(&app).await;
|
||||
|
||||
let submitted = article_service::submit_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
Some(app.operator_id()), article.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
article.version,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// pending_review → rejected
|
||||
let rejected = article_service::reject_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
ReviewArticleReq { note: Some("内容需修改".to_string()), version: Some(submitted.version) },
|
||||
ReviewArticleReq {
|
||||
note: Some("内容需修改".to_string()),
|
||||
version: Some(submitted.version),
|
||||
},
|
||||
submitted.version,
|
||||
)
|
||||
.await
|
||||
@@ -142,8 +169,11 @@ async fn test_article_reject_and_resubmit() {
|
||||
|
||||
// rejected → pending_review
|
||||
let resubmitted = article_service::submit_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
Some(app.operator_id()), rejected.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
rejected.version,
|
||||
)
|
||||
.await
|
||||
.expect("重新提交应成功");
|
||||
@@ -159,7 +189,9 @@ async fn test_article_update() {
|
||||
let article = seed_article(&app).await;
|
||||
|
||||
let updated = article_service::update_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateArticleReq {
|
||||
title: Some("更新标题".to_string()),
|
||||
@@ -196,24 +228,41 @@ async fn test_article_list_filter() {
|
||||
|
||||
// 提交 a1 到 pending_review
|
||||
article_service::submit_article(
|
||||
app.health_state(), app.tenant_id(), a1.id,
|
||||
Some(app.operator_id()), a1.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
a1.id,
|
||||
Some(app.operator_id()),
|
||||
a1.version,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 按状态过滤
|
||||
let pending = article_service::list_articles(
|
||||
app.health_state(), app.tenant_id(), 1, 20,
|
||||
None, Some("pending_review".to_string()), None, None, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
1,
|
||||
20,
|
||||
None,
|
||||
Some("pending_review".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(pending.total, 1);
|
||||
|
||||
let drafts = article_service::list_articles(
|
||||
app.health_state(), app.tenant_id(), 1, 20,
|
||||
None, Some("draft".to_string()), None, None, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
1,
|
||||
20,
|
||||
None,
|
||||
Some("draft".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -229,16 +278,17 @@ async fn test_article_soft_delete() {
|
||||
let article = seed_article(&app).await;
|
||||
|
||||
article_service::delete_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
Some(app.operator_id()), article.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
article.version,
|
||||
)
|
||||
.await
|
||||
.expect("删除应成功");
|
||||
|
||||
let result = article_service::get_article(
|
||||
app.health_state(), app.tenant_id(), article.id, true,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
article_service::get_article(app.health_state(), app.tenant_id(), article.id, true).await;
|
||||
assert!(result.is_err(), "软删除后查询应失败");
|
||||
}
|
||||
|
||||
@@ -251,10 +301,8 @@ async fn test_article_tenant_isolation() {
|
||||
let article = seed_article(&app).await;
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let result = article_service::get_article(
|
||||
app.health_state(), other_tenant, article.id, true,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
article_service::get_article(app.health_state(), other_tenant, article.id, true).await;
|
||||
assert!(result.is_err(), "不同租户不应看到此文章");
|
||||
}
|
||||
|
||||
@@ -271,16 +319,16 @@ async fn test_category_crud_and_isolation() {
|
||||
assert_eq!(cat.version, 1);
|
||||
|
||||
// 列表
|
||||
let list = article_category_service::list_categories(
|
||||
app.health_state(), app.tenant_id(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list = article_category_service::list_categories(app.health_state(), app.tenant_id())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
// 更新
|
||||
let updated = article_category_service::update_category(
|
||||
app.health_state(), app.tenant_id(), cat.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
cat.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateCategoryReq {
|
||||
name: Some("透析护理".to_string()),
|
||||
@@ -297,27 +345,26 @@ async fn test_category_crud_and_isolation() {
|
||||
|
||||
// 删除
|
||||
article_category_service::delete_category(
|
||||
app.health_state(), app.tenant_id(), cat.id,
|
||||
Some(app.operator_id()), updated.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
cat.id,
|
||||
Some(app.operator_id()),
|
||||
updated.version,
|
||||
)
|
||||
.await
|
||||
.expect("删除分类应成功");
|
||||
|
||||
let list_after = article_category_service::list_categories(
|
||||
app.health_state(), app.tenant_id(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list_after = article_category_service::list_categories(app.health_state(), app.tenant_id())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_after.len(), 0, "删除后列表应为空");
|
||||
|
||||
// 租户隔离
|
||||
let cat2 = seed_category(&app, "隔离分类").await;
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let other_list = article_category_service::list_categories(
|
||||
app.health_state(), other_tenant,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let other_list = article_category_service::list_categories(app.health_state(), other_tenant)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(other_list.len(), 0, "不同租户不应看到分类");
|
||||
// 防止 unused warning
|
||||
let _ = cat2;
|
||||
@@ -337,7 +384,9 @@ async fn test_tag_crud_and_article_association() {
|
||||
|
||||
// 创建文章并关联标签
|
||||
let article = article_service::create_article(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
CreateArticleReq {
|
||||
title: "带标签的文章".to_string(),
|
||||
tag_ids: vec![tag1.id, tag2.id],
|
||||
@@ -350,14 +399,24 @@ async fn test_tag_crud_and_article_association() {
|
||||
|
||||
// 更新标签(替换为只有 tag1)
|
||||
let updated = article_service::update_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateArticleReq {
|
||||
tag_ids: Some(vec![tag1.id]),
|
||||
version: article.version,
|
||||
title: None, summary: None, content: None, cover_image: None,
|
||||
category: None, author: None, published_at: None, slug: None,
|
||||
content_type: None, category_id: None, sort_order: None,
|
||||
title: None,
|
||||
summary: None,
|
||||
content: None,
|
||||
cover_image: None,
|
||||
category: None,
|
||||
author: None,
|
||||
published_at: None,
|
||||
slug: None,
|
||||
content_type: None,
|
||||
category_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -365,18 +424,21 @@ async fn test_tag_crud_and_article_association() {
|
||||
assert_eq!(updated.tags.len(), 1);
|
||||
|
||||
// 标签列表
|
||||
let tags = article_tag_service::list_tags(
|
||||
app.health_state(), app.tenant_id(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let tags = article_tag_service::list_tags(app.health_state(), app.tenant_id())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(tags.len(), 2);
|
||||
|
||||
// 更新标签名称
|
||||
let renamed = article_tag_service::update_tag(
|
||||
app.health_state(), app.tenant_id(), tag1.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
tag1.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateTagReq { name: "血压高".to_string(), version: tag1.version },
|
||||
UpdateTagReq {
|
||||
name: "血压高".to_string(),
|
||||
version: tag1.version,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("更新标签应成功");
|
||||
@@ -384,17 +446,18 @@ async fn test_tag_crud_and_article_association() {
|
||||
|
||||
// 删除标签
|
||||
article_tag_service::delete_tag(
|
||||
app.health_state(), app.tenant_id(), tag2.id,
|
||||
Some(app.operator_id()), tag2.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
tag2.id,
|
||||
Some(app.operator_id()),
|
||||
tag2.version,
|
||||
)
|
||||
.await
|
||||
.expect("删除标签应成功");
|
||||
|
||||
let tags_after = article_tag_service::list_tags(
|
||||
app.health_state(), app.tenant_id(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let tags_after = article_tag_service::list_tags(app.health_state(), app.tenant_id())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(tags_after.len(), 1);
|
||||
}
|
||||
|
||||
@@ -408,14 +471,24 @@ async fn test_article_version_conflict() {
|
||||
|
||||
// 先更新一次,version 变为 2
|
||||
article_service::update_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateArticleReq {
|
||||
title: Some("第一次更新".to_string()),
|
||||
version: article.version,
|
||||
summary: None, content: None, cover_image: None, category: None,
|
||||
author: None, published_at: None, slug: None, content_type: None,
|
||||
category_id: None, tag_ids: None, sort_order: None,
|
||||
summary: None,
|
||||
content: None,
|
||||
cover_image: None,
|
||||
category: None,
|
||||
author: None,
|
||||
published_at: None,
|
||||
slug: None,
|
||||
content_type: None,
|
||||
category_id: None,
|
||||
tag_ids: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -423,14 +496,24 @@ async fn test_article_version_conflict() {
|
||||
|
||||
// 用旧 version 再次更新应失败
|
||||
let result = article_service::update_article(
|
||||
app.health_state(), app.tenant_id(), article.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
article.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateArticleReq {
|
||||
title: Some("冲突更新".to_string()),
|
||||
version: article.version, // 旧版本号
|
||||
summary: None, content: None, cover_image: None, category: None,
|
||||
author: None, published_at: None, slug: None, content_type: None,
|
||||
category_id: None, tag_ids: None, sort_order: None,
|
||||
summary: None,
|
||||
content: None,
|
||||
cover_image: None,
|
||||
category: None,
|
||||
author: None,
|
||||
published_at: None,
|
||||
slug: None,
|
||||
content_type: None,
|
||||
category_id: None,
|
||||
tag_ids: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -21,7 +21,9 @@ fn default_create_consent_req(patient_id: uuid::Uuid) -> CreateConsentReq {
|
||||
|
||||
async fn seed_consent(app: &TestApp, patient_id: uuid::Uuid) -> ConsentResp {
|
||||
consent_service::grant_consent(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
default_create_consent_req(patient_id),
|
||||
)
|
||||
.await
|
||||
@@ -56,7 +58,9 @@ async fn test_consent_revoke() {
|
||||
assert_eq!(consent.status, "granted");
|
||||
|
||||
let revoked = consent_service::revoke_consent(
|
||||
app.health_state(), app.tenant_id(), consent.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
consent.id,
|
||||
Some(app.operator_id()),
|
||||
RevokeConsentReq {
|
||||
notes: Some("患者要求撤销".to_string()),
|
||||
@@ -84,18 +88,16 @@ async fn test_consent_list_by_patient() {
|
||||
seed_consent(&app, patient_a).await;
|
||||
seed_consent(&app, patient_b).await;
|
||||
|
||||
let list_a = consent_service::list_consents(
|
||||
app.health_state(), app.tenant_id(), patient_a, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list_a =
|
||||
consent_service::list_consents(app.health_state(), app.tenant_id(), patient_a, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_a.total, 2);
|
||||
|
||||
let list_b = consent_service::list_consents(
|
||||
app.health_state(), app.tenant_id(), patient_b, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list_b =
|
||||
consent_service::list_consents(app.health_state(), app.tenant_id(), patient_b, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_b.total, 1);
|
||||
}
|
||||
|
||||
@@ -109,11 +111,9 @@ async fn test_consent_tenant_isolation() {
|
||||
seed_consent(&app, patient_id).await;
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let list = consent_service::list_consents(
|
||||
app.health_state(), other_tenant, patient_id, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list = consent_service::list_consents(app.health_state(), other_tenant, patient_id, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.total, 0, "不同租户不应看到同意记录");
|
||||
}
|
||||
|
||||
@@ -126,7 +126,9 @@ async fn test_consent_invalid_patient() {
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
|
||||
let result = consent_service::grant_consent(
|
||||
app.health_state(), app.tenant_id(), None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
None,
|
||||
default_create_consent_req(fake_patient),
|
||||
)
|
||||
.await;
|
||||
@@ -144,18 +146,28 @@ async fn test_consent_revoke_version_conflict() {
|
||||
|
||||
// 先撤销一次
|
||||
consent_service::revoke_consent(
|
||||
app.health_state(), app.tenant_id(), consent.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
consent.id,
|
||||
Some(app.operator_id()),
|
||||
RevokeConsentReq { notes: None, version: consent.version },
|
||||
RevokeConsentReq {
|
||||
notes: None,
|
||||
version: consent.version,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 用旧 version 再撤销应失败
|
||||
let result = consent_service::revoke_consent(
|
||||
app.health_state(), app.tenant_id(), consent.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
consent.id,
|
||||
Some(app.operator_id()),
|
||||
RevokeConsentReq { notes: None, version: consent.version },
|
||||
RevokeConsentReq {
|
||||
notes: None,
|
||||
version: consent.version,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err(), "乐观锁冲突应返回错误");
|
||||
|
||||
@@ -10,8 +10,14 @@ use super::test_fixture::TestApp;
|
||||
/// 创建测试用会话(无医护)
|
||||
async fn seed_session(app: &TestApp, patient_id: uuid::Uuid) -> SessionResp {
|
||||
consultation_service::create_session(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
CreateSessionReq { patient_id, doctor_id: None, consultation_type: None },
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
CreateSessionReq {
|
||||
patient_id,
|
||||
doctor_id: None,
|
||||
consultation_type: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("创建会话应成功")
|
||||
@@ -27,7 +33,9 @@ async fn test_consultation_session_create() {
|
||||
let doctor_id = app.create_doctor("咨询医生").await;
|
||||
|
||||
let session = consultation_service::create_session(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
CreateSessionReq {
|
||||
patient_id,
|
||||
doctor_id: Some(doctor_id),
|
||||
@@ -53,11 +61,10 @@ async fn test_consultation_session_get() {
|
||||
|
||||
let session = seed_session(&app, patient_id).await;
|
||||
|
||||
let fetched = consultation_service::get_session(
|
||||
app.health_state(), app.tenant_id(), session.id,
|
||||
)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
let fetched =
|
||||
consultation_service::get_session(app.health_state(), app.tenant_id(), session.id)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
assert_eq!(fetched.id, session.id);
|
||||
assert_eq!(fetched.status, "waiting");
|
||||
}
|
||||
@@ -76,14 +83,26 @@ async fn test_consultation_session_list_by_patient() {
|
||||
seed_session(&app, patient_b).await;
|
||||
|
||||
let list_a = consultation_service::list_sessions(
|
||||
app.health_state(), app.tenant_id(), 1, 20, None, Some(patient_a), None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
1,
|
||||
20,
|
||||
None,
|
||||
Some(patient_a),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_a.total, 2);
|
||||
|
||||
let list_b = consultation_service::list_sessions(
|
||||
app.health_state(), app.tenant_id(), 1, 20, None, Some(patient_b), None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
1,
|
||||
20,
|
||||
None,
|
||||
Some(patient_b),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -101,8 +120,11 @@ async fn test_consultation_message_send() {
|
||||
let session = seed_session(&app, patient_id).await;
|
||||
|
||||
let msg = consultation_service::create_message(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.operator_id(), "doctor".to_string(),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
app.operator_id(),
|
||||
"doctor".to_string(),
|
||||
CreateMessageReq {
|
||||
session_id: session.id,
|
||||
content_type: Some("text".to_string()),
|
||||
@@ -130,8 +152,11 @@ async fn test_consultation_message_list() {
|
||||
|
||||
for i in 0..3 {
|
||||
consultation_service::create_message(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.operator_id(), "doctor".to_string(),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
app.operator_id(),
|
||||
"doctor".to_string(),
|
||||
CreateMessageReq {
|
||||
session_id: session.id,
|
||||
content_type: None,
|
||||
@@ -143,7 +168,12 @@ async fn test_consultation_message_list() {
|
||||
}
|
||||
|
||||
let messages = consultation_service::list_messages(
|
||||
app.health_state(), app.tenant_id(), session.id, 1, 20, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
session.id,
|
||||
1,
|
||||
20,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("查询消息应成功");
|
||||
@@ -163,7 +193,11 @@ async fn test_consultation_session_close() {
|
||||
assert_eq!(session.status, "waiting");
|
||||
|
||||
let closed = consultation_service::close_session(
|
||||
app.health_state(), app.tenant_id(), session.id, Some(app.operator_id()), session.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
session.id,
|
||||
Some(app.operator_id()),
|
||||
session.version,
|
||||
)
|
||||
.await
|
||||
.expect("关闭应成功");
|
||||
@@ -181,10 +215,8 @@ async fn test_consultation_session_tenant_isolation() {
|
||||
let session = seed_session(&app, patient_id).await;
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let result = consultation_service::get_session(
|
||||
app.health_state(), other_tenant, session.id,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
consultation_service::get_session(app.health_state(), other_tenant, session.id).await;
|
||||
assert!(result.is_err(), "不同租户不应看到此会话");
|
||||
}
|
||||
|
||||
@@ -197,7 +229,9 @@ async fn test_consultation_session_invalid_patient() {
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
|
||||
let result = consultation_service::create_session(
|
||||
app.health_state(), app.tenant_id(), None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
None,
|
||||
CreateSessionReq {
|
||||
patient_id: fake_patient,
|
||||
doctor_id: None,
|
||||
|
||||
@@ -25,7 +25,9 @@ fn default_create_req(patient_id: uuid::Uuid) -> CreateDailyMonitoringReq {
|
||||
|
||||
async fn seed_monitoring(app: &TestApp, patient_id: uuid::Uuid) -> DailyMonitoringResp {
|
||||
daily_monitoring_service::create_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
Some(app.operator_id()),
|
||||
default_create_req(patient_id),
|
||||
)
|
||||
.await
|
||||
@@ -57,11 +59,10 @@ async fn test_daily_monitoring_get() {
|
||||
let patient_id = app.create_patient("监测查询患者").await;
|
||||
let dm = seed_monitoring(&app, patient_id).await;
|
||||
|
||||
let fetched = daily_monitoring_service::get_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), dm.id,
|
||||
)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
let fetched =
|
||||
daily_monitoring_service::get_daily_monitoring(app.health_state(), app.tenant_id(), dm.id)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
assert_eq!(fetched.id, dm.id);
|
||||
assert_eq!(fetched.blood_sugar, Some(5.2));
|
||||
}
|
||||
@@ -76,7 +77,9 @@ async fn test_daily_monitoring_update() {
|
||||
let dm = seed_monitoring(&app, patient_id).await;
|
||||
|
||||
let updated = daily_monitoring_service::update_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), dm.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
dm.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateDailyMonitoringReq {
|
||||
weight: Some(67.0),
|
||||
@@ -113,14 +116,22 @@ async fn test_daily_monitoring_list_by_patient() {
|
||||
seed_monitoring(&app, patient_b).await;
|
||||
|
||||
let list_a = daily_monitoring_service::list_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), patient_a, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_a,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_a.total, 1);
|
||||
|
||||
let list_b = daily_monitoring_service::list_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), patient_b, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_b,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -137,16 +148,18 @@ async fn test_daily_monitoring_soft_delete() {
|
||||
let dm = seed_monitoring(&app, patient_id).await;
|
||||
|
||||
daily_monitoring_service::delete_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), dm.id,
|
||||
Some(app.operator_id()), dm.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
dm.id,
|
||||
Some(app.operator_id()),
|
||||
dm.version,
|
||||
)
|
||||
.await
|
||||
.expect("删除应成功");
|
||||
|
||||
let result = daily_monitoring_service::get_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), dm.id,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
daily_monitoring_service::get_daily_monitoring(app.health_state(), app.tenant_id(), dm.id)
|
||||
.await;
|
||||
assert!(result.is_err(), "软删除后查询应失败");
|
||||
}
|
||||
|
||||
@@ -161,7 +174,11 @@ async fn test_daily_monitoring_tenant_isolation() {
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let list = daily_monitoring_service::list_daily_monitoring(
|
||||
app.health_state(), other_tenant, patient_id, 1, 20,
|
||||
app.health_state(),
|
||||
other_tenant,
|
||||
patient_id,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -177,7 +194,9 @@ async fn test_daily_monitoring_invalid_patient() {
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
|
||||
let result = daily_monitoring_service::create_daily_monitoring(
|
||||
app.health_state(), app.tenant_id(), None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
None,
|
||||
default_create_req(fake_patient),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -36,7 +36,10 @@ async fn test_vital_signs_create() {
|
||||
let patient_id = app.create_patient("体征患者").await;
|
||||
|
||||
let vs = health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
Some(app.operator_id()),
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await
|
||||
@@ -58,27 +61,41 @@ async fn test_vital_signs_list() {
|
||||
let patient_b = app.create_patient("列表B").await;
|
||||
|
||||
health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_a, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_a,
|
||||
None,
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_b, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_b,
|
||||
None,
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list_a = health_data_service::list_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_a, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_a,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_a.total, 1);
|
||||
|
||||
let list_b = health_data_service::list_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_b, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_b,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -94,20 +111,35 @@ async fn test_vital_signs_update() {
|
||||
let patient_id = app.create_patient("更新患者").await;
|
||||
|
||||
let vs = health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
Some(app.operator_id()),
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = health_data_service::update_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_id, vs.id, Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
vs.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateVitalSignsReq {
|
||||
record_date: None, systolic_bp_morning: None, diastolic_bp_morning: None,
|
||||
systolic_bp_evening: None, diastolic_bp_evening: None,
|
||||
heart_rate: Some(65), weight: Some(67.0),
|
||||
blood_sugar: None, body_temperature: None, spo2: None,
|
||||
blood_sugar_type: None, water_intake_ml: None, urine_output_ml: None,
|
||||
record_date: None,
|
||||
systolic_bp_morning: None,
|
||||
diastolic_bp_morning: None,
|
||||
systolic_bp_evening: None,
|
||||
diastolic_bp_evening: None,
|
||||
heart_rate: Some(65),
|
||||
weight: Some(67.0),
|
||||
blood_sugar: None,
|
||||
body_temperature: None,
|
||||
spo2: None,
|
||||
blood_sugar_type: None,
|
||||
water_intake_ml: None,
|
||||
urine_output_ml: None,
|
||||
notes: None,
|
||||
},
|
||||
vs.version,
|
||||
@@ -128,18 +160,20 @@ async fn test_vital_signs_tenant_isolation() {
|
||||
let patient_id = app.create_patient("隔离患者").await;
|
||||
|
||||
health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), patient_id, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
None,
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let list = health_data_service::list_vital_signs(
|
||||
app.health_state(), other_tenant, patient_id, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list =
|
||||
health_data_service::list_vital_signs(app.health_state(), other_tenant, patient_id, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.total, 0, "不同租户不应看到体征记录");
|
||||
}
|
||||
|
||||
@@ -183,11 +217,17 @@ async fn test_lab_report_review() {
|
||||
let patient_id = app.create_patient("审阅患者").await;
|
||||
|
||||
let report = health_data_service::create_lab_report(
|
||||
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
Some(app.operator_id()),
|
||||
CreateLabReportReq {
|
||||
report_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 3).unwrap(),
|
||||
report_type: "blood_routine".to_string(),
|
||||
source: None, items: None, image_urls: None, doctor_notes: None,
|
||||
source: None,
|
||||
items: None,
|
||||
image_urls: None,
|
||||
doctor_notes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -195,8 +235,15 @@ async fn test_lab_report_review() {
|
||||
assert_eq!(report.status, "pending");
|
||||
|
||||
let reviewed = health_data_service::review_lab_report(
|
||||
app.health_state(), app.tenant_id(), patient_id, report.id, app.operator_id(),
|
||||
ReviewLabReportReq { doctor_notes: Some("复查确认".to_string()), items: None },
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
report.id,
|
||||
app.operator_id(),
|
||||
ReviewLabReportReq {
|
||||
doctor_notes: Some("复查确认".to_string()),
|
||||
items: None,
|
||||
},
|
||||
report.version,
|
||||
)
|
||||
.await
|
||||
@@ -216,11 +263,17 @@ async fn test_lab_report_list() {
|
||||
|
||||
for pid in &[patient_a, patient_b] {
|
||||
health_data_service::create_lab_report(
|
||||
app.health_state(), app.tenant_id(), *pid, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
*pid,
|
||||
None,
|
||||
CreateLabReportReq {
|
||||
report_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 4).unwrap(),
|
||||
report_type: "blood_routine".to_string(),
|
||||
source: None, items: None, image_urls: None, doctor_notes: None,
|
||||
source: None,
|
||||
items: None,
|
||||
image_urls: None,
|
||||
doctor_notes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -228,7 +281,11 @@ async fn test_lab_report_list() {
|
||||
}
|
||||
|
||||
let list_a = health_data_service::list_lab_reports(
|
||||
app.health_state(), app.tenant_id(), patient_a, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_a,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -245,7 +302,10 @@ async fn test_vital_signs_invalid_patient() {
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
|
||||
let result = health_data_service::create_vital_signs(
|
||||
app.health_state(), app.tenant_id(), fake_patient, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
fake_patient,
|
||||
None,
|
||||
default_vital_signs_req(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
//!
|
||||
//! 验证批量摄入、设备绑定自动创建、hourly 聚合、查询过滤、参数校验、租户隔离。
|
||||
|
||||
use erp_health::service::device_reading_service::{
|
||||
BatchReadingRequest, ReadingInput,
|
||||
};
|
||||
use erp_health::service::device_reading_service;
|
||||
use chrono::Datelike;
|
||||
use erp_health::service::device_reading_service;
|
||||
use erp_health::service::device_reading_service::{BatchReadingRequest, ReadingInput};
|
||||
use sea_orm::ConnectionTrait;
|
||||
|
||||
use super::test_fixture::TestApp;
|
||||
@@ -27,10 +25,13 @@ async fn ensure_current_month_partition(app: &TestApp) {
|
||||
let sql = format!(
|
||||
"CREATE TABLE IF NOT EXISTS device_readings_{suffix} PARTITION OF device_readings FOR VALUES FROM ('{start}') TO ('{next_month}');"
|
||||
);
|
||||
app.db().execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
)).await.expect("创建分区应成功");
|
||||
app.db()
|
||||
.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
))
|
||||
.await
|
||||
.expect("创建分区应成功");
|
||||
}
|
||||
|
||||
/// 构建一条心率读数(measured_at 用几分钟前的时间)
|
||||
@@ -53,7 +54,9 @@ async fn test_device_reading_batch_single() {
|
||||
let patient_id = app.create_patient("读数患者").await;
|
||||
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-001".to_string(),
|
||||
device_model: Some("Apple Watch".to_string()),
|
||||
@@ -79,7 +82,9 @@ async fn test_device_reading_batch_multiple() {
|
||||
let patient_id = app.create_patient("批量患者").await;
|
||||
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-002".to_string(),
|
||||
device_model: None,
|
||||
@@ -106,7 +111,9 @@ async fn test_device_reading_creates_device_binding() {
|
||||
let patient_id = app.create_patient("绑定患者").await;
|
||||
|
||||
device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "band-001".to_string(),
|
||||
device_model: Some("Mi Band".to_string()),
|
||||
@@ -118,7 +125,9 @@ async fn test_device_reading_creates_device_binding() {
|
||||
|
||||
// 再次使用同一设备,应更新 last_sync_at 而非重复创建
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "band-001".to_string(),
|
||||
device_model: Some("Mi Band".to_string()),
|
||||
@@ -141,7 +150,9 @@ async fn test_device_reading_hourly_aggregation() {
|
||||
let patient_id = app.create_patient("聚合患者").await;
|
||||
|
||||
device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-003".to_string(),
|
||||
device_model: None,
|
||||
@@ -157,7 +168,13 @@ async fn test_device_reading_hourly_aggregation() {
|
||||
|
||||
// 查询 hourly 聚合
|
||||
let hourly = device_reading_service::query_hourly_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id, "heart_rate", 1, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
"heart_rate",
|
||||
1,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.expect("查询 hourly 应成功");
|
||||
@@ -179,21 +196,26 @@ async fn test_device_reading_query_filter() {
|
||||
let patient_id = app.create_patient("查询患者").await;
|
||||
|
||||
device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-004".to_string(),
|
||||
device_model: None,
|
||||
readings: vec![
|
||||
heart_rate_reading(72, 5),
|
||||
heart_rate_reading(74, 3),
|
||||
],
|
||||
readings: vec![heart_rate_reading(72, 5), heart_rate_reading(74, 3)],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let readings = device_reading_service::query_device_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id, Some("heart_rate"), None, 1, 20,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
Some("heart_rate"),
|
||||
None,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
@@ -211,7 +233,9 @@ async fn test_device_reading_invalid_device_type() {
|
||||
let patient_id = app.create_patient("校验患者").await;
|
||||
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "bad-001".to_string(),
|
||||
device_model: None,
|
||||
@@ -237,7 +261,9 @@ async fn test_device_reading_future_time_rejected() {
|
||||
|
||||
let future_time = (chrono::Utc::now() + chrono::Duration::hours(1)).to_rfc3339();
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-005".to_string(),
|
||||
device_model: None,
|
||||
@@ -264,7 +290,9 @@ async fn test_device_reading_invalid_patient_and_isolation() {
|
||||
// 无效患者
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
let result = device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), fake_patient,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
fake_patient,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-006".to_string(),
|
||||
device_model: None,
|
||||
@@ -277,7 +305,9 @@ async fn test_device_reading_invalid_patient_and_isolation() {
|
||||
// 租户隔离:创建患者并摄入数据,用不同租户查询
|
||||
let patient_id = app.create_patient("隔离患者").await;
|
||||
device_reading_service::batch_create_readings(
|
||||
app.health_state(), app.tenant_id(), patient_id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
BatchReadingRequest {
|
||||
device_id: "watch-007".to_string(),
|
||||
device_model: None,
|
||||
@@ -289,7 +319,13 @@ async fn test_device_reading_invalid_patient_and_isolation() {
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let readings = device_reading_service::query_device_readings(
|
||||
app.health_state(), other_tenant, patient_id, None, None, 1, 20,
|
||||
app.health_state(),
|
||||
other_tenant,
|
||||
patient_id,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
20,
|
||||
)
|
||||
.await
|
||||
.expect("查询应成功");
|
||||
|
||||
@@ -20,9 +20,17 @@ fn default_create_diagnosis_req() -> CreateDiagnosisReq {
|
||||
}
|
||||
}
|
||||
|
||||
async fn seed_diagnosis(app: &TestApp, patient_id: uuid::Uuid, icd_code: &str, name: &str) -> DiagnosisResp {
|
||||
async fn seed_diagnosis(
|
||||
app: &TestApp,
|
||||
patient_id: uuid::Uuid,
|
||||
icd_code: &str,
|
||||
name: &str,
|
||||
) -> DiagnosisResp {
|
||||
diagnosis_service::create_diagnosis(
|
||||
app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()),
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
patient_id,
|
||||
Some(app.operator_id()),
|
||||
CreateDiagnosisReq {
|
||||
icd_code: icd_code.to_string(),
|
||||
diagnosis_name: name.to_string(),
|
||||
@@ -60,7 +68,9 @@ async fn test_diagnosis_update() {
|
||||
let diag = seed_diagnosis(&app, patient_id, "N18.8", "CKD更新").await;
|
||||
|
||||
let updated = diagnosis_service::update_diagnosis(
|
||||
app.health_state(), app.tenant_id(), diag.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
diag.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateDiagnosisReq {
|
||||
status: Some("chronic".to_string()),
|
||||
@@ -94,18 +104,16 @@ async fn test_diagnosis_list_by_patient() {
|
||||
seed_diagnosis(&app, patient_a, "N18.2", "CKD 2期").await;
|
||||
seed_diagnosis(&app, patient_b, "E11.9", "2型糖尿病").await;
|
||||
|
||||
let list_a = diagnosis_service::list_diagnoses(
|
||||
app.health_state(), app.tenant_id(), patient_a, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list_a =
|
||||
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_a, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_a.total, 2);
|
||||
|
||||
let list_b = diagnosis_service::list_diagnoses(
|
||||
app.health_state(), app.tenant_id(), patient_b, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list_b =
|
||||
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_b, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list_b.total, 1);
|
||||
}
|
||||
|
||||
@@ -119,17 +127,19 @@ async fn test_diagnosis_soft_delete() {
|
||||
let diag = seed_diagnosis(&app, patient_id, "N18.3", "CKD删除").await;
|
||||
|
||||
diagnosis_service::delete_diagnosis(
|
||||
app.health_state(), app.tenant_id(), diag.id,
|
||||
Some(app.operator_id()), diag.version,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
diag.id,
|
||||
Some(app.operator_id()),
|
||||
diag.version,
|
||||
)
|
||||
.await
|
||||
.expect("删除应成功");
|
||||
|
||||
let list = diagnosis_service::list_diagnoses(
|
||||
app.health_state(), app.tenant_id(), patient_id, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list =
|
||||
diagnosis_service::list_diagnoses(app.health_state(), app.tenant_id(), patient_id, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.total, 0);
|
||||
}
|
||||
|
||||
@@ -143,11 +153,10 @@ async fn test_diagnosis_tenant_isolation() {
|
||||
seed_diagnosis(&app, patient_id, "N18.4", "CKD隔离").await;
|
||||
|
||||
let other_tenant = uuid::Uuid::new_v4();
|
||||
let list = diagnosis_service::list_diagnoses(
|
||||
app.health_state(), other_tenant, patient_id, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let list =
|
||||
diagnosis_service::list_diagnoses(app.health_state(), other_tenant, patient_id, 1, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.total, 0, "不同租户不应看到诊断记录");
|
||||
}
|
||||
|
||||
@@ -160,7 +169,10 @@ async fn test_diagnosis_invalid_patient() {
|
||||
let fake_patient = uuid::Uuid::new_v4();
|
||||
|
||||
let result = diagnosis_service::create_diagnosis(
|
||||
app.health_state(), app.tenant_id(), fake_patient, None,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
fake_patient,
|
||||
None,
|
||||
default_create_diagnosis_req(),
|
||||
)
|
||||
.await;
|
||||
@@ -178,13 +190,19 @@ async fn test_diagnosis_version_conflict() {
|
||||
|
||||
// 先更新一次
|
||||
diagnosis_service::update_diagnosis(
|
||||
app.health_state(), app.tenant_id(), diag.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
diag.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateDiagnosisReq {
|
||||
status: Some("resolved".to_string()),
|
||||
icd_code: None, diagnosis_name: None, diagnosis_type: None,
|
||||
diagnosed_date: None, health_record_id: None,
|
||||
diagnosed_by: None, notes: None,
|
||||
icd_code: None,
|
||||
diagnosis_name: None,
|
||||
diagnosis_type: None,
|
||||
diagnosed_date: None,
|
||||
health_record_id: None,
|
||||
diagnosed_by: None,
|
||||
notes: None,
|
||||
},
|
||||
diag.version,
|
||||
)
|
||||
@@ -193,13 +211,19 @@ async fn test_diagnosis_version_conflict() {
|
||||
|
||||
// 用旧 version 再更新应失败
|
||||
let result = diagnosis_service::update_diagnosis(
|
||||
app.health_state(), app.tenant_id(), diag.id,
|
||||
app.health_state(),
|
||||
app.tenant_id(),
|
||||
diag.id,
|
||||
Some(app.operator_id()),
|
||||
UpdateDiagnosisReq {
|
||||
status: Some("chronic".to_string()),
|
||||
icd_code: None, diagnosis_name: None, diagnosis_type: None,
|
||||
diagnosed_date: None, health_record_id: None,
|
||||
diagnosed_by: None, notes: None,
|
||||
icd_code: None,
|
||||
diagnosis_name: None,
|
||||
diagnosis_type: None,
|
||||
diagnosed_date: None,
|
||||
health_record_id: None,
|
||||
diagnosed_by: None,
|
||||
notes: None,
|
||||
},
|
||||
diag.version,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user