Files
hms/crates/erp-server/migration/src/m20260423_000042_create_health_tables.rs
iven 6d5a711d2c
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
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 统一格式化
2026-05-07 23:43:14 +08:00

1834 lines
66 KiB
Rust

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> {
// 1. patient — 患者档案
manager
.create_table(
Table::create()
.table(Patient::Table)
.if_not_exists()
.col(ColumnDef::new(Patient::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Patient::TenantId).uuid().not_null())
.col(ColumnDef::new(Patient::UserId).uuid().null())
.col(ColumnDef::new(Patient::Name).string_len(100).not_null())
.col(ColumnDef::new(Patient::Gender).string_len(10).null())
.col(ColumnDef::new(Patient::BirthDate).date().null())
.col(ColumnDef::new(Patient::BloodType).string_len(10).null())
.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::Status)
.string_len(20)
.not_null()
.default("active"),
)
.col(
ColumnDef::new(Patient::VerificationStatus)
.string_len(20)
.not_null()
.default("pending"),
)
.col(ColumnDef::new(Patient::Source).string_len(100).null())
.col(ColumnDef::new(Patient::Notes).text().null())
.col(
ColumnDef::new(Patient::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Patient::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tenant_name")
.table(Patient::Table)
.col(Patient::TenantId)
.col(Patient::Name)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tenant_status")
.table(Patient::Table)
.col(Patient::TenantId)
.col(Patient::Status)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tenant_id_number")
.table(Patient::Table)
.col(Patient::TenantId)
.col(Patient::IdNumber)
.unique()
.to_owned(),
)
.await?;
// 2. patient_family_member — 家庭成员
manager
.create_table(
Table::create()
.table(PatientFamilyMember::Table)
.if_not_exists()
.col(
ColumnDef::new(PatientFamilyMember::Id)
.uuid()
.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::BirthDate).date().null())
.col(ColumnDef::new(PatientFamilyMember::Notes).text().null())
.col(
ColumnDef::new(PatientFamilyMember::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(PatientFamilyMember::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(PatientFamilyMember::CreatedBy).uuid().null())
.col(ColumnDef::new(PatientFamilyMember::UpdatedBy).uuid().null())
.col(
ColumnDef::new(PatientFamilyMember::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(PatientFamilyMember::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(PatientFamilyMember::Table, PatientFamilyMember::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// 3. patient_tag — 患者标签
manager
.create_table(
Table::create()
.table(PatientTag::Table)
.if_not_exists()
.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())
.col(ColumnDef::new(PatientTag::Description).text().null())
.col(
ColumnDef::new(PatientTag::IsSystem)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(PatientTag::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(PatientTag::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
// 4. patient_tag_relation — 患者-标签关联
manager
.create_table(
Table::create()
.table(PatientTagRelation::Table)
.if_not_exists()
.col(
ColumnDef::new(PatientTagRelation::Id)
.uuid()
.not_null()
.primary_key(),
)
.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)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(PatientTagRelation::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(PatientTagRelation::CreatedBy).uuid().null())
.col(ColumnDef::new(PatientTagRelation::UpdatedBy).uuid().null())
.col(
ColumnDef::new(PatientTagRelation::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.foreign_key(
ForeignKey::create()
.from(PatientTagRelation::Table, PatientTagRelation::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(PatientTagRelation::Table, PatientTagRelation::TagId)
.to(PatientTag::Table, PatientTag::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tag_rel_tenant_patient")
.table(PatientTagRelation::Table)
.col(PatientTagRelation::TenantId)
.col(PatientTagRelation::PatientId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tag_rel_tenant_tag")
.table(PatientTagRelation::Table)
.col(PatientTagRelation::TenantId)
.col(PatientTagRelation::TagId)
.to_owned(),
)
.await?;
// patient_tag 唯一名称索引
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_tag_tenant_name_unique")
.table(PatientTag::Table)
.col(PatientTag::TenantId)
.col(PatientTag::Name)
.unique()
.to_owned(),
)
.await?;
// 5. doctor_profile — 医护档案
manager
.create_table(
Table::create()
.table(DoctorProfile::Table)
.if_not_exists()
.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::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::Bio).text().null())
.col(
ColumnDef::new(DoctorProfile::OnlineStatus)
.string_len(20)
.not_null()
.default("offline"),
)
.col(
ColumnDef::new(DoctorProfile::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(DoctorProfile::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
// doctor_profile 名称搜索索引
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_doctor_profile_tenant_name")
.table(DoctorProfile::Table)
.col(DoctorProfile::TenantId)
.col(DoctorProfile::Name)
.to_owned(),
)
.await?;
// 6. patient_doctor_relation — 医患关系
manager
.create_table(
Table::create()
.table(PatientDoctorRelation::Table)
.if_not_exists()
.col(
ColumnDef::new(PatientDoctorRelation::Id)
.uuid()
.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::RelationshipType)
.string_len(20)
.not_null()
.default("primary"),
)
.col(
ColumnDef::new(PatientDoctorRelation::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(PatientDoctorRelation::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(PatientDoctorRelation::CreatedBy)
.uuid()
.null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::UpdatedBy)
.uuid()
.null(),
)
.col(
ColumnDef::new(PatientDoctorRelation::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.foreign_key(
ForeignKey::create()
.from(
PatientDoctorRelation::Table,
PatientDoctorRelation::PatientId,
)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(
PatientDoctorRelation::Table,
PatientDoctorRelation::DoctorId,
)
.to(DoctorProfile::Table, DoctorProfile::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_doctor_rel_patient")
.table(PatientDoctorRelation::Table)
.col(PatientDoctorRelation::TenantId)
.col(PatientDoctorRelation::PatientId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_patient_doctor_rel_doctor")
.table(PatientDoctorRelation::Table)
.col(PatientDoctorRelation::TenantId)
.col(PatientDoctorRelation::DoctorId)
.to_owned(),
)
.await?;
// 7. health_record — 体检/就诊记录
manager
.create_table(
Table::create()
.table(HealthRecord::Table)
.if_not_exists()
.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(
ColumnDef::new(HealthRecord::RecordType)
.string_len(20)
.not_null()
.default("checkup"),
)
.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::Notes).text().null())
.col(
ColumnDef::new(HealthRecord::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(HealthRecord::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(HealthRecord::Table, HealthRecord::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_health_record_tenant_patient_date")
.table(HealthRecord::Table)
.col(HealthRecord::TenantId)
.col(HealthRecord::PatientId)
.col(HealthRecord::RecordDate)
.to_owned(),
)
.await?;
// 8. vital_signs — 日常监测数据
manager
.create_table(
Table::create()
.table(VitalSigns::Table)
.if_not_exists()
.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::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::WaterIntakeMl).integer().null())
.col(ColumnDef::new(VitalSigns::UrineOutputMl).integer().null())
.col(ColumnDef::new(VitalSigns::Notes).text().null())
.col(
ColumnDef::new(VitalSigns::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(VitalSigns::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(VitalSigns::Table, VitalSigns::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_vital_signs_tenant_patient_date")
.table(VitalSigns::Table)
.col(VitalSigns::TenantId)
.col(VitalSigns::PatientId)
.col(VitalSigns::RecordDate)
.to_owned(),
)
.await?;
// 9. lab_report — 化验报告
manager
.create_table(
Table::create()
.table(LabReport::Table)
.if_not_exists()
.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::Indicators).json_binary().null())
.col(ColumnDef::new(LabReport::ImageUrls).json_binary().null())
.col(
ColumnDef::new(LabReport::DoctorInterpretation)
.text()
.null(),
)
.col(
ColumnDef::new(LabReport::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(LabReport::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(LabReport::Table, LabReport::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_lab_report_tenant_patient_date")
.table(LabReport::Table)
.col(LabReport::TenantId)
.col(LabReport::PatientId)
.col(LabReport::ReportDate)
.to_owned(),
)
.await?;
// 10. health_trend — 健康趋势报告
manager
.create_table(
Table::create()
.table(HealthTrend::Table)
.if_not_exists()
.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::GenerationType)
.string_len(20)
.not_null()
.default("auto"),
)
.col(
ColumnDef::new(HealthTrend::ReportFileUrl)
.string_len(500)
.null(),
)
.col(
ColumnDef::new(HealthTrend::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(HealthTrend::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(HealthTrend::Table, HealthTrend::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// 11. appointment — 预约记录
manager
.create_table(
Table::create()
.table(Appointment::Table)
.if_not_exists()
.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())
.col(
ColumnDef::new(Appointment::AppointmentType)
.string_len(20)
.not_null()
.default("outpatient"),
)
.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(
ColumnDef::new(Appointment::Status)
.string_len(20)
.not_null()
.default("pending"),
)
.col(ColumnDef::new(Appointment::CancelReason).text().null())
.col(ColumnDef::new(Appointment::Notes).text().null())
.col(
ColumnDef::new(Appointment::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Appointment::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(Appointment::Table, Appointment::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(Appointment::Table, Appointment::DoctorId)
.to(DoctorProfile::Table, DoctorProfile::Id)
.on_delete(ForeignKeyAction::SetNull),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_appointment_tenant_date_status")
.table(Appointment::Table)
.col(Appointment::TenantId)
.col(Appointment::AppointmentDate)
.col(Appointment::Status)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_appointment_tenant_doctor_date")
.table(Appointment::Table)
.col(Appointment::TenantId)
.col(Appointment::DoctorId)
.col(Appointment::AppointmentDate)
.to_owned(),
)
.await?;
// 12. doctor_schedule — 医生排班
manager
.create_table(
Table::create()
.table(DoctorSchedule::Table)
.if_not_exists()
.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::PeriodType)
.string_len(20)
.not_null()
.default("am"),
)
.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::CurrentAppointments)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(DoctorSchedule::Status)
.string_len(20)
.not_null()
.default("enabled"),
)
.col(
ColumnDef::new(DoctorSchedule::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(DoctorSchedule::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(DoctorSchedule::CreatedBy).uuid().null())
.col(ColumnDef::new(DoctorSchedule::UpdatedBy).uuid().null())
.col(
ColumnDef::new(DoctorSchedule::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(DoctorSchedule::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(DoctorSchedule::Table, DoctorSchedule::DoctorId)
.to(DoctorProfile::Table, DoctorProfile::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_doctor_schedule_tenant_doctor_date")
.table(DoctorSchedule::Table)
.col(DoctorSchedule::TenantId)
.col(DoctorSchedule::DoctorId)
.col(DoctorSchedule::ScheduleDate)
.to_owned(),
)
.await?;
// doctor_schedule 唯一约束:同一医生同一天同一时段不能重复排班
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_doctor_schedule_unique_slot")
.table(DoctorSchedule::Table)
.col(DoctorSchedule::TenantId)
.col(DoctorSchedule::DoctorId)
.col(DoctorSchedule::ScheduleDate)
.col(DoctorSchedule::StartTime)
.unique()
.to_owned(),
)
.await?;
// 13. follow_up_task — 随访任务
manager
.create_table(
Table::create()
.table(FollowUpTask::Table)
.if_not_exists()
.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())
.col(
ColumnDef::new(FollowUpTask::FollowUpType)
.string_len(20)
.not_null()
.default("phone"),
)
.col(ColumnDef::new(FollowUpTask::PlannedDate).date().not_null())
.col(
ColumnDef::new(FollowUpTask::Status)
.string_len(20)
.not_null()
.default("pending"),
)
.col(ColumnDef::new(FollowUpTask::ContentTemplate).text().null())
.col(
ColumnDef::new(FollowUpTask::RelatedAppointmentId)
.uuid()
.null(),
)
.col(
ColumnDef::new(FollowUpTask::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(FollowUpTask::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(FollowUpTask::Table, FollowUpTask::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_follow_up_task_tenant_assigned_status")
.table(FollowUpTask::Table)
.col(FollowUpTask::TenantId)
.col(FollowUpTask::AssignedTo)
.col(FollowUpTask::Status)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_follow_up_task_tenant_date_status")
.table(FollowUpTask::Table)
.col(FollowUpTask::TenantId)
.col(FollowUpTask::PlannedDate)
.col(FollowUpTask::Status)
.to_owned(),
)
.await?;
// 14. follow_up_record — 随访记录
manager
.create_table(
Table::create()
.table(FollowUpRecord::Table)
.if_not_exists()
.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::Result)
.string_len(20)
.not_null()
.default("followed_up"),
)
.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::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(FollowUpRecord::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(FollowUpRecord::CreatedBy).uuid().null())
.col(ColumnDef::new(FollowUpRecord::UpdatedBy).uuid().null())
.col(
ColumnDef::new(FollowUpRecord::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(FollowUpRecord::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(FollowUpRecord::Table, FollowUpRecord::TaskId)
.to(FollowUpTask::Table, FollowUpTask::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_follow_up_record_tenant_task")
.table(FollowUpRecord::Table)
.col(FollowUpRecord::TenantId)
.col(FollowUpRecord::TaskId)
.to_owned(),
)
.await?;
// 15. consultation_session — 咨询会话
manager
.create_table(
Table::create()
.table(ConsultationSession::Table)
.if_not_exists()
.col(
ColumnDef::new(ConsultationSession::Id)
.uuid()
.not_null()
.primary_key(),
)
.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)
.string_len(20)
.not_null()
.default("customer_service"),
)
.col(
ColumnDef::new(ConsultationSession::Status)
.string_len(20)
.not_null()
.default("waiting"),
)
.col(
ColumnDef::new(ConsultationSession::LastMessageAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ConsultationSession::UnreadCountPatient)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(ConsultationSession::UnreadCountDoctor)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(ConsultationSession::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ConsultationSession::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(ConsultationSession::CreatedBy).uuid().null())
.col(ColumnDef::new(ConsultationSession::UpdatedBy).uuid().null())
.col(
ColumnDef::new(ConsultationSession::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ConsultationSession::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(ConsultationSession::Table, ConsultationSession::PatientId)
.to(Patient::Table, Patient::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(ConsultationSession::Table, ConsultationSession::DoctorId)
.to(DoctorProfile::Table, DoctorProfile::Id)
.on_delete(ForeignKeyAction::SetNull),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_consultation_session_tenant_doctor_status")
.table(ConsultationSession::Table)
.col(ConsultationSession::TenantId)
.col(ConsultationSession::DoctorId)
.col(ConsultationSession::Status)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_consultation_session_tenant_patient_status")
.table(ConsultationSession::Table)
.col(ConsultationSession::TenantId)
.col(ConsultationSession::PatientId)
.col(ConsultationSession::Status)
.to_owned(),
)
.await?;
// 16. consultation_message — 咨询消息
manager
.create_table(
Table::create()
.table(ConsultationMessage::Table)
.if_not_exists()
.col(
ColumnDef::new(ConsultationMessage::Id)
.uuid()
.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::SenderRole)
.string_len(20)
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::ContentType)
.string_len(20)
.not_null()
.default("text"),
)
.col(
ColumnDef::new(ConsultationMessage::Content)
.text()
.not_null(),
)
.col(
ColumnDef::new(ConsultationMessage::IsRead)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(ConsultationMessage::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ConsultationMessage::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(ConsultationMessage::CreatedBy).uuid().null())
.col(ColumnDef::new(ConsultationMessage::UpdatedBy).uuid().null())
.col(
ColumnDef::new(ConsultationMessage::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ConsultationMessage::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.from(ConsultationMessage::Table, ConsultationMessage::SessionId)
.to(ConsultationSession::Table, ConsultationSession::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_consultation_message_tenant_session_created")
.table(ConsultationMessage::Table)
.col(ConsultationMessage::TenantId)
.col(ConsultationMessage::SessionId)
.col(ConsultationMessage::CreatedAt)
.to_owned(),
)
.await?;
// health_trend 索引
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_health_trend_tenant_patient")
.table(HealthTrend::Table)
.col(HealthTrend::TenantId)
.col(HealthTrend::PatientId)
.to_owned(),
)
.await?;
Ok(())
}
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?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Patient {
Table,
Id,
TenantId,
UserId,
Name,
Gender,
BirthDate,
BloodType,
IdNumber,
AllergyHistory,
MedicalHistorySummary,
EmergencyContactName,
EmergencyContactPhone,
Status,
VerificationStatus,
Source,
Notes,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum PatientFamilyMember {
Table,
Id,
TenantId,
PatientId,
Name,
Relationship,
Phone,
BirthDate,
Notes,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum PatientTag {
Table,
Id,
TenantId,
Name,
Color,
Description,
IsSystem,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum PatientTagRelation {
Table,
Id,
TenantId,
PatientId,
TagId,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
}
#[derive(DeriveIden)]
enum DoctorProfile {
Table,
Id,
TenantId,
UserId,
Name,
Department,
Title,
Specialty,
LicenseNumber,
Bio,
OnlineStatus,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum PatientDoctorRelation {
Table,
Id,
TenantId,
PatientId,
DoctorId,
RelationshipType,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
}
#[derive(DeriveIden)]
enum HealthRecord {
Table,
Id,
TenantId,
PatientId,
RecordType,
RecordDate,
Source,
OverallAssessment,
ReportFileUrl,
Notes,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum VitalSigns {
Table,
Id,
TenantId,
PatientId,
RecordDate,
SystolicBpMorning,
DiastolicBpMorning,
SystolicBpEvening,
DiastolicBpEvening,
HeartRate,
Weight,
BloodSugar,
WaterIntakeMl,
UrineOutputMl,
Notes,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum LabReport {
Table,
Id,
TenantId,
PatientId,
ReportDate,
ReportType,
Indicators,
ImageUrls,
DoctorInterpretation,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum HealthTrend {
Table,
Id,
TenantId,
PatientId,
PeriodStart,
PeriodEnd,
IndicatorSummary,
AbnormalItems,
GenerationType,
ReportFileUrl,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Appointment {
Table,
Id,
TenantId,
PatientId,
DoctorId,
AppointmentType,
AppointmentDate,
StartTime,
EndTime,
Status,
CancelReason,
Notes,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum DoctorSchedule {
Table,
Id,
TenantId,
DoctorId,
ScheduleDate,
PeriodType,
StartTime,
EndTime,
MaxAppointments,
CurrentAppointments,
Status,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum FollowUpTask {
Table,
Id,
TenantId,
PatientId,
AssignedTo,
FollowUpType,
PlannedDate,
Status,
ContentTemplate,
RelatedAppointmentId,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum FollowUpRecord {
Table,
Id,
TenantId,
TaskId,
ExecutedBy,
ExecutedDate,
Result,
PatientCondition,
MedicalAdvice,
NextFollowUpDate,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum ConsultationSession {
Table,
Id,
TenantId,
PatientId,
DoctorId,
ConsultationType,
Status,
LastMessageAt,
UnreadCountPatient,
UnreadCountDoctor,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum ConsultationMessage {
Table,
Id,
TenantId,
SessionId,
SenderId,
SenderRole,
ContentType,
Content,
IsRead,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}