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. ai_prompts — Prompt 模板存储 manager .create_table( Table::create() .table(AiPrompt::Table) .if_not_exists() .col(ColumnDef::new(AiPrompt::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(AiPrompt::TenantId).uuid().not_null()) .col(ColumnDef::new(AiPrompt::Name).string_len(100).not_null()) .col( ColumnDef::new(AiPrompt::Description) .text() .not_null() .default(""), ) .col(ColumnDef::new(AiPrompt::SystemPrompt).text().not_null()) .col( ColumnDef::new(AiPrompt::UserPromptTemplate) .text() .not_null(), ) .col(ColumnDef::new(AiPrompt::VariablesSchema).json().null()) .col(ColumnDef::new(AiPrompt::ModelConfig).json().not_null()) .col( ColumnDef::new(AiPrompt::Version) .integer() .not_null() .default(1), ) .col( ColumnDef::new(AiPrompt::IsActive) .boolean() .not_null() .default(true), ) .col( ColumnDef::new(AiPrompt::Category) .string_len(50) .not_null() .default("analysis"), ) .col(ColumnDef::new(AiPrompt::Tags).json().null()) .col( ColumnDef::new(AiPrompt::CreatedAt) .timestamp_with_time_zone() .not_null() .default(Expr::current_timestamp()), ) .col( ColumnDef::new(AiPrompt::UpdatedAt) .timestamp_with_time_zone() .not_null() .default(Expr::current_timestamp()), ) .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::VersionLock) .integer() .not_null() .default(1), ) .to_owned(), ) .await?; manager .create_index( Index::create() .if_not_exists() .name("idx_ai_prompts_tenant_name") .table(AiPrompt::Table) .col(AiPrompt::TenantId) .col(AiPrompt::Name) .col(AiPrompt::IsActive) .to_owned(), ) .await?; // 2. ai_analysis_results — 分析结果存储 manager .create_table( Table::create() .table(AiAnalysis::Table) .if_not_exists() .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( ColumnDef::new(AiAnalysis::AnalysisType) .string_len(50) .not_null(), ) .col( ColumnDef::new(AiAnalysis::SourceRef) .string_len(200) .not_null(), ) .col(ColumnDef::new(AiAnalysis::PromptId).uuid().not_null()) .col(ColumnDef::new(AiAnalysis::PromptVersion).integer().not_null()) .col( ColumnDef::new(AiAnalysis::ModelUsed) .string_len(100) .not_null(), ) .col( ColumnDef::new(AiAnalysis::InputDataHash) .string_len(64) .not_null(), ) .col(ColumnDef::new(AiAnalysis::SanitizedInput).json().null()) .col(ColumnDef::new(AiAnalysis::ResultContent).text().null()) .col(ColumnDef::new(AiAnalysis::ResultMetadata).json().null()) .col( ColumnDef::new(AiAnalysis::Status) .string_len(20) .not_null() .default("pending"), ) .col(ColumnDef::new(AiAnalysis::ErrorMessage).text().null()) .col( ColumnDef::new(AiAnalysis::CreatedAt) .timestamp_with_time_zone() .not_null() .default(Expr::current_timestamp()), ) .col( ColumnDef::new(AiAnalysis::UpdatedAt) .timestamp_with_time_zone() .not_null() .default(Expr::current_timestamp()), ) .col(ColumnDef::new(AiAnalysis::CreatedBy).uuid().null()) .col(ColumnDef::new(AiAnalysis::UpdatedBy).uuid().null()) .col( ColumnDef::new(AiAnalysis::DeletedAt) .timestamp_with_time_zone() .null(), ) .col( ColumnDef::new(AiAnalysis::VersionLock) .integer() .not_null() .default(1), ) .to_owned(), ) .await?; manager .create_index( Index::create() .if_not_exists() .name("idx_ai_analysis_tenant_patient") .table(AiAnalysis::Table) .col(AiAnalysis::TenantId) .col(AiAnalysis::PatientId) .to_owned(), ) .await?; manager .create_index( Index::create() .if_not_exists() .name("idx_ai_analysis_cache_hash") .table(AiAnalysis::Table) .col(AiAnalysis::InputDataHash) .to_owned(), ) .await?; // 3. ai_usage_logs — AI 调用计量 manager .create_table( Table::create() .table(AiUsage::Table) .if_not_exists() .col(ColumnDef::new(AiUsage::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(AiUsage::TenantId).uuid().not_null()) .col(ColumnDef::new(AiUsage::Provider).string_len(50).not_null()) .col(ColumnDef::new(AiUsage::Model).string_len(100).not_null()) .col( ColumnDef::new(AiUsage::AnalysisType) .string_len(50) .not_null(), ) .col( ColumnDef::new(AiUsage::InputTokens) .integer() .not_null() .default(0), ) .col( ColumnDef::new(AiUsage::OutputTokens) .integer() .not_null() .default(0), ) .col( ColumnDef::new(AiUsage::DurationMs) .integer() .not_null() .default(0), ) .col( ColumnDef::new(AiUsage::CostCents) .integer() .not_null() .default(0), ) .col( ColumnDef::new(AiUsage::IsCacheHit) .boolean() .not_null() .default(false), ) .col( ColumnDef::new(AiUsage::CreatedAt) .timestamp_with_time_zone() .not_null() .default(Expr::current_timestamp()), ) .to_owned(), ) .await?; manager .create_index( Index::create() .if_not_exists() .name("idx_ai_usage_tenant_created") .table(AiUsage::Table) .col(AiUsage::TenantId) .col(AiUsage::CreatedAt) .to_owned(), ) .await?; Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(AiUsage::Table).to_owned()) .await?; manager .drop_table(Table::drop().table(AiAnalysis::Table).to_owned()) .await?; manager .drop_table(Table::drop().table(AiPrompt::Table).to_owned()) .await?; Ok(()) } } #[derive(DeriveIden)] enum AiPrompt { Table, Id, TenantId, Name, Description, SystemPrompt, UserPromptTemplate, VariablesSchema, ModelConfig, Version, IsActive, Category, Tags, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, DeletedAt, VersionLock, } #[derive(DeriveIden)] enum AiAnalysis { Table, Id, TenantId, PatientId, AnalysisType, SourceRef, PromptId, PromptVersion, ModelUsed, InputDataHash, SanitizedInput, ResultContent, ResultMetadata, Status, ErrorMessage, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, DeletedAt, VersionLock, } #[derive(DeriveIden)] enum AiUsage { Table, Id, TenantId, Provider, Model, AnalysisType, InputTokens, OutputTokens, DurationMs, CostCents, IsCacheHit, CreatedAt, }