diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 5749100..6f5b6ca 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -149,6 +149,7 @@ mod m20260513_000144_enforce_version_optimistic_lock; mod m20260513_000145_seed_missing_permissions; mod m20260515_000146_seed_menu_permissions_phase2; mod m20260516_000147_seed_ai_chat_permission; +mod m20260518_000148_create_ai_chat_tables; pub struct Migrator; @@ -305,6 +306,7 @@ impl MigratorTrait for Migrator { Box::new(m20260513_000145_seed_missing_permissions::Migration), Box::new(m20260515_000146_seed_menu_permissions_phase2::Migration), Box::new(m20260516_000147_seed_ai_chat_permission::Migration), + Box::new(m20260518_000148_create_ai_chat_tables::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260518_000148_create_ai_chat_tables.rs b/crates/erp-server/migration/src/m20260518_000148_create_ai_chat_tables.rs new file mode 100644 index 0000000..754eed3 --- /dev/null +++ b/crates/erp-server/migration/src/m20260518_000148_create_ai_chat_tables.rs @@ -0,0 +1,335 @@ +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> { + // ai_chat_sessions — AI 会话表 + manager + .create_table( + Table::create() + .table(AiChatSessions::Table) + .col( + ColumnDef::new(AiChatSessions::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AiChatSessions::TenantId).uuid().not_null()) + .col(ColumnDef::new(AiChatSessions::UserId).uuid().not_null()) + .col(ColumnDef::new(AiChatSessions::PatientId).uuid().null()) + .col(ColumnDef::new(AiChatSessions::Title).string_len(255).null()) + .col( + ColumnDef::new(AiChatSessions::Status) + .string_len(20) + .not_null() + .default("active"), + ) + .col(ColumnDef::new(AiChatSessions::Metadata).json().null()) + .col( + ColumnDef::new(AiChatSessions::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(AiChatSessions::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col(ColumnDef::new(AiChatSessions::CreatedBy).uuid().null()) + .col(ColumnDef::new(AiChatSessions::UpdatedBy).uuid().null()) + .col( + ColumnDef::new(AiChatSessions::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(AiChatSessions::VersionLock) + .integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_ai_chat_sessions_tenant_user") + .table(AiChatSessions::Table) + .col(AiChatSessions::TenantId) + .col(AiChatSessions::UserId) + .to_owned(), + ) + .await?; + + // ai_chat_messages — AI 聊天消息表 + manager + .create_table( + Table::create() + .table(AiChatMessages::Table) + .col( + ColumnDef::new(AiChatMessages::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AiChatMessages::TenantId).uuid().not_null()) + .col(ColumnDef::new(AiChatMessages::SessionId).uuid().not_null()) + .col( + ColumnDef::new(AiChatMessages::Role) + .string_len(20) + .not_null(), + ) + .col(ColumnDef::new(AiChatMessages::Content).text().null()) + .col(ColumnDef::new(AiChatMessages::ToolCalls).json().null()) + .col( + ColumnDef::new(AiChatMessages::ToolCallId) + .string_len(100) + .null(), + ) + .col(ColumnDef::new(AiChatMessages::TokenCount).integer().null()) + .col( + ColumnDef::new(AiChatMessages::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(AiChatMessages::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col(ColumnDef::new(AiChatMessages::CreatedBy).uuid().null()) + .col(ColumnDef::new(AiChatMessages::UpdatedBy).uuid().null()) + .col( + ColumnDef::new(AiChatMessages::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(AiChatMessages::VersionLock) + .integer() + .not_null() + .default(1), + ) + .foreign_key( + ForeignKey::create() + .name("fk_ai_chat_messages_session") + .from(AiChatMessages::Table, AiChatMessages::SessionId) + .to(AiChatSessions::Table, AiChatSessions::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_ai_chat_messages_session") + .table(AiChatMessages::Table) + .col(AiChatMessages::TenantId) + .col(AiChatMessages::SessionId) + .col(AiChatMessages::CreatedAt) + .to_owned(), + ) + .await?; + + // ai_tool_call_logs — AI 工具调用日志(append-only) + manager + .create_table( + Table::create() + .table(AiToolCallLogs::Table) + .col( + ColumnDef::new(AiToolCallLogs::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AiToolCallLogs::TenantId).uuid().not_null()) + .col(ColumnDef::new(AiToolCallLogs::SessionId).uuid().not_null()) + .col(ColumnDef::new(AiToolCallLogs::MessageId).uuid().not_null()) + .col( + ColumnDef::new(AiToolCallLogs::ToolName) + .string_len(100) + .not_null(), + ) + .col(ColumnDef::new(AiToolCallLogs::Parameters).json().null()) + .col(ColumnDef::new(AiToolCallLogs::ResultSummary).text().null()) + .col(ColumnDef::new(AiToolCallLogs::ExecutionMs).integer().null()) + .col(ColumnDef::new(AiToolCallLogs::Success).boolean().not_null()) + .col( + ColumnDef::new(AiToolCallLogs::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col(ColumnDef::new(AiToolCallLogs::CreatedBy).uuid().null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_ai_tool_call_logs_session") + .table(AiToolCallLogs::Table) + .col(AiToolCallLogs::TenantId) + .col(AiToolCallLogs::SessionId) + .to_owned(), + ) + .await?; + + // ai_user_profiles — 用户长期画像 + manager + .create_table( + Table::create() + .table(AiUserProfiles::Table) + .col( + ColumnDef::new(AiUserProfiles::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AiUserProfiles::TenantId).uuid().not_null()) + .col(ColumnDef::new(AiUserProfiles::UserId).uuid().not_null()) + .col(ColumnDef::new(AiUserProfiles::Preferences).json().null()) + .col(ColumnDef::new(AiUserProfiles::HealthInterests).array(ColumnType::Text)) + .col(ColumnDef::new(AiUserProfiles::FrequentTopics).array(ColumnType::Text)) + .col( + ColumnDef::new(AiUserProfiles::PersonalitySummary) + .text() + .null(), + ) + .col( + ColumnDef::new(AiUserProfiles::LastUpdatedAt) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(AiUserProfiles::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(AiUserProfiles::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(AiUserProfiles::DeletedAt) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(AiUserProfiles::VersionLock) + .integer() + .not_null() + .default(1), + ) + .index( + Index::create() + .name("uq_ai_user_profiles_tenant_user") + .col(AiUserProfiles::TenantId) + .col(AiUserProfiles::UserId) + .unique(), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(AiUserProfiles::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AiToolCallLogs::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AiChatMessages::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AiChatSessions::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum AiChatSessions { + Table, + Id, + TenantId, + UserId, + PatientId, + Title, + Status, + Metadata, + CreatedAt, + UpdatedAt, + CreatedBy, + UpdatedBy, + DeletedAt, + VersionLock, +} + +#[derive(DeriveIden)] +enum AiChatMessages { + Table, + Id, + TenantId, + SessionId, + Role, + Content, + ToolCalls, + ToolCallId, + TokenCount, + CreatedAt, + UpdatedAt, + CreatedBy, + UpdatedBy, + DeletedAt, + VersionLock, +} + +#[derive(DeriveIden)] +enum AiToolCallLogs { + Table, + Id, + TenantId, + SessionId, + MessageId, + ToolName, + Parameters, + ResultSummary, + ExecutionMs, + Success, + CreatedAt, + CreatedBy, +} + +#[derive(DeriveIden)] +enum AiUserProfiles { + Table, + Id, + TenantId, + UserId, + Preferences, + HealthInterests, + FrequentTopics, + PersonalitySummary, + LastUpdatedAt, + CreatedAt, + UpdatedAt, + DeletedAt, + VersionLock, +}