feat: initialize ERP base platform (extracted from HMS)

- Stripped 11 business crates (health, ai, dialysis, plugins)
- Cleaned AppState, AppConfig, main.rs from business coupling
- Reduced migrations from 169 to 53 (base-only)
- Removed health_provider trait from erp-core
- Removed business integration tests
- Removed gateway rate limiting middleware
- Base capabilities: auth, RBAC, JWT, config, workflow, message, plugin, audit, crypto, RLS, multi-tenant

Cargo check: OK
Cargo test: OK
This commit is contained in:
iven
2026-05-31 20:35:57 +08:00
commit 59856ac2fc
639 changed files with 124710 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
[package]
name = "erp-server-migration"
version = "0.1.0"
edition = "2024"
[dependencies]
sea-orm-migration.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,120 @@
#![allow(clippy::too_many_arguments)]
pub use sea_orm_migration::prelude::*;
mod m20260410_000001_create_tenant;
mod m20260411_000002_create_users;
mod m20260411_000003_create_user_credentials;
mod m20260411_000004_create_user_tokens;
mod m20260411_000005_create_roles;
mod m20260411_000006_create_permissions;
mod m20260411_000007_create_role_permissions;
mod m20260411_000008_create_user_roles;
mod m20260411_000009_create_organizations;
mod m20260411_000010_create_departments;
mod m20260411_000011_create_positions;
mod m20260412_000012_create_dictionaries;
mod m20260412_000013_create_dictionary_items;
mod m20260412_000014_create_menus;
mod m20260412_000015_create_menu_roles;
mod m20260412_000016_create_settings;
mod m20260412_000017_create_numbering_rules;
mod m20260412_000018_create_process_definitions;
mod m20260412_000019_create_process_instances;
mod m20260412_000020_create_tokens;
mod m20260412_000021_create_tasks;
mod m20260412_000022_create_process_variables;
mod m20260413_000023_create_message_templates;
mod m20260413_000024_create_messages;
mod m20260413_000025_create_message_subscriptions;
mod m20260413_000026_create_audit_logs;
mod m20260414_000027_fix_unique_indexes_soft_delete;
mod m20260414_000028_add_standard_fields_to_tokens;
mod m20260414_000029_add_standard_fields_to_process_variables;
mod m20260415_000030_add_version_to_message_tables;
mod m20260416_000031_create_domain_events;
mod m20260414_000032_fix_settings_unique_index_null;
mod m20260417_000033_create_plugins;
mod m20260417_000034_seed_plugin_permissions;
mod m20260418_000035_pg_trgm_and_entity_columns;
mod m20260418_000036_add_data_scope_to_role_permissions;
mod m20260419_000037_create_user_departments;
mod m20260419_000039_entity_registry_columns;
mod m20260419_000040_plugin_market;
mod m20260419_000041_plugin_user_views;
mod m20260423_000043_create_wechat_users;
mod m20260427_000062_create_tenant_crypto_keys;
mod m20260427_000084_domain_events_cleanup;
mod m20260427_000085_processed_events;
mod m20260427_000086_enable_rls_all_tables;
mod m20260427_000087_audit_logs_hash_chain;
mod m20260428_000088_rls_policy_strict;
mod m20260428_000089_blind_indexes;
mod m20260428_000091_dead_letter_events;
mod m20260504_000106_create_api_clients;
mod m20260513_000144_enforce_version_optimistic_lock;
mod m20260518_000149_fix_admin_permissions;
mod m20260529_000169_supplement_rls_for_new_tables;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20260410_000001_create_tenant::Migration),
Box::new(m20260411_000002_create_users::Migration),
Box::new(m20260411_000003_create_user_credentials::Migration),
Box::new(m20260411_000004_create_user_tokens::Migration),
Box::new(m20260411_000005_create_roles::Migration),
Box::new(m20260411_000006_create_permissions::Migration),
Box::new(m20260411_000007_create_role_permissions::Migration),
Box::new(m20260411_000008_create_user_roles::Migration),
Box::new(m20260411_000009_create_organizations::Migration),
Box::new(m20260411_000010_create_departments::Migration),
Box::new(m20260411_000011_create_positions::Migration),
Box::new(m20260412_000012_create_dictionaries::Migration),
Box::new(m20260412_000013_create_dictionary_items::Migration),
Box::new(m20260412_000014_create_menus::Migration),
Box::new(m20260412_000015_create_menu_roles::Migration),
Box::new(m20260412_000016_create_settings::Migration),
Box::new(m20260412_000017_create_numbering_rules::Migration),
Box::new(m20260412_000018_create_process_definitions::Migration),
Box::new(m20260412_000019_create_process_instances::Migration),
Box::new(m20260412_000020_create_tokens::Migration),
Box::new(m20260412_000021_create_tasks::Migration),
Box::new(m20260412_000022_create_process_variables::Migration),
Box::new(m20260413_000023_create_message_templates::Migration),
Box::new(m20260413_000024_create_messages::Migration),
Box::new(m20260413_000025_create_message_subscriptions::Migration),
Box::new(m20260413_000026_create_audit_logs::Migration),
Box::new(m20260414_000027_fix_unique_indexes_soft_delete::Migration),
Box::new(m20260414_000028_add_standard_fields_to_tokens::Migration),
Box::new(m20260414_000029_add_standard_fields_to_process_variables::Migration),
Box::new(m20260415_000030_add_version_to_message_tables::Migration),
Box::new(m20260416_000031_create_domain_events::Migration),
Box::new(m20260414_000032_fix_settings_unique_index_null::Migration),
Box::new(m20260417_000033_create_plugins::Migration),
Box::new(m20260417_000034_seed_plugin_permissions::Migration),
Box::new(m20260418_000035_pg_trgm_and_entity_columns::Migration),
Box::new(m20260418_000036_add_data_scope_to_role_permissions::Migration),
Box::new(m20260419_000037_create_user_departments::Migration),
Box::new(m20260419_000039_entity_registry_columns::Migration),
Box::new(m20260419_000040_plugin_market::Migration),
Box::new(m20260419_000041_plugin_user_views::Migration),
Box::new(m20260423_000043_create_wechat_users::Migration),
Box::new(m20260427_000062_create_tenant_crypto_keys::Migration),
Box::new(m20260427_000084_domain_events_cleanup::Migration),
Box::new(m20260427_000085_processed_events::Migration),
Box::new(m20260427_000086_enable_rls_all_tables::Migration),
Box::new(m20260427_000087_audit_logs_hash_chain::Migration),
Box::new(m20260428_000088_rls_policy_strict::Migration),
Box::new(m20260428_000089_blind_indexes::Migration),
Box::new(m20260428_000091_dead_letter_events::Migration),
Box::new(m20260504_000106_create_api_clients::Migration),
Box::new(m20260513_000144_enforce_version_optimistic_lock::Migration),
Box::new(m20260518_000149_fix_admin_permissions::Migration),
Box::new(m20260529_000169_supplement_rls_for_new_tables::Migration),
]
}
}

View File

@@ -0,0 +1,69 @@
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> {
manager
.create_table(
Table::create()
.table(Tenant::Table)
.if_not_exists()
.col(ColumnDef::new(Tenant::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Tenant::Name).string().not_null())
.col(
ColumnDef::new(Tenant::Code)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(Tenant::Status)
.string()
.not_null()
.default("active"),
)
.col(ColumnDef::new(Tenant::Settings).json().null())
.col(
ColumnDef::new(Tenant::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Tenant::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Tenant::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Tenant::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Tenant {
Table,
Id,
Name,
Code,
Status,
Settings,
CreatedAt,
UpdatedAt,
DeletedAt,
}

View File

@@ -0,0 +1,104 @@
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> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(ColumnDef::new(Users::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Users::TenantId).uuid().not_null())
.col(ColumnDef::new(Users::Username).string().not_null())
.col(ColumnDef::new(Users::Email).string().null())
.col(ColumnDef::new(Users::Phone).string().null())
.col(ColumnDef::new(Users::DisplayName).string().null())
.col(ColumnDef::new(Users::AvatarUrl).string().null())
.col(
ColumnDef::new(Users::Status)
.string()
.not_null()
.default("active"),
)
.col(
ColumnDef::new(Users::LastLoginAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Users::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Users::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Users::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Users::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Users::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Users::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_users_tenant_id")
.table(Users::Table)
.col(Users::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_users_tenant_username ON users (tenant_id, username) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
TenantId,
Username,
Email,
Phone,
DisplayName,
AvatarUrl,
Status,
LastLoginAt,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,127 @@
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> {
manager
.create_table(
Table::create()
.table(UserCredentials::Table)
.if_not_exists()
.col(
ColumnDef::new(UserCredentials::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(UserCredentials::TenantId).uuid().not_null())
.col(ColumnDef::new(UserCredentials::UserId).uuid().not_null())
.col(
ColumnDef::new(UserCredentials::CredentialType)
.string()
.not_null()
.default("password"),
)
.col(
ColumnDef::new(UserCredentials::CredentialData)
.json()
.null(),
)
.col(
ColumnDef::new(UserCredentials::Verified)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(UserCredentials::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(UserCredentials::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(UserCredentials::CreatedBy).uuid().not_null())
.col(ColumnDef::new(UserCredentials::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(UserCredentials::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(UserCredentials::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_user_credentials_user_id")
.from(UserCredentials::Table, UserCredentials::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_credentials_tenant_id")
.table(UserCredentials::Table)
.col(UserCredentials::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_credentials_user_id")
.table(UserCredentials::Table)
.col(UserCredentials::UserId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(UserCredentials::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum UserCredentials {
Table,
Id,
TenantId,
UserId,
CredentialType,
CredentialData,
Verified,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}

View File

@@ -0,0 +1,140 @@
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> {
manager
.create_table(
Table::create()
.table(UserTokens::Table)
.if_not_exists()
.col(
ColumnDef::new(UserTokens::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(UserTokens::TenantId).uuid().not_null())
.col(ColumnDef::new(UserTokens::UserId).uuid().not_null())
.col(
ColumnDef::new(UserTokens::TokenHash)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(UserTokens::TokenType).string().not_null())
.col(
ColumnDef::new(UserTokens::ExpiresAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(UserTokens::RevokedAt)
.timestamp_with_time_zone()
.null(),
)
.col(ColumnDef::new(UserTokens::DeviceInfo).string().null())
.col(
ColumnDef::new(UserTokens::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(UserTokens::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(UserTokens::CreatedBy).uuid().not_null())
.col(ColumnDef::new(UserTokens::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(UserTokens::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(UserTokens::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_user_tokens_user_id")
.from(UserTokens::Table, UserTokens::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_tokens_tenant_id")
.table(UserTokens::Table)
.col(UserTokens::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_tokens_user_id")
.table(UserTokens::Table)
.col(UserTokens::UserId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_tokens_token_hash")
.table(UserTokens::Table)
.col(UserTokens::TokenHash)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(UserTokens::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum UserTokens {
Table,
Id,
TenantId,
UserId,
TokenHash,
TokenType,
ExpiresAt,
RevokedAt,
DeviceInfo,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}

View File

@@ -0,0 +1,101 @@
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> {
manager
.create_table(
Table::create()
.table(Roles::Table)
.if_not_exists()
.col(ColumnDef::new(Roles::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Roles::TenantId).uuid().not_null())
.col(ColumnDef::new(Roles::Name).string().not_null())
.col(ColumnDef::new(Roles::Code).string().not_null())
.col(ColumnDef::new(Roles::Description).text().null())
.col(
ColumnDef::new(Roles::IsSystem)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(Roles::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Roles::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Roles::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Roles::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Roles::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Roles::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_roles_tenant_id")
.table(Roles::Table)
.col(Roles::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_roles_tenant_code")
.table(Roles::Table)
.col(Roles::TenantId)
.col(Roles::Code)
.unique()
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Roles::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Roles {
Table,
Id,
TenantId,
Name,
Code,
Description,
IsSystem,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,103 @@
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> {
manager
.create_table(
Table::create()
.table(Permissions::Table)
.if_not_exists()
.col(
ColumnDef::new(Permissions::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Permissions::TenantId).uuid().not_null())
.col(ColumnDef::new(Permissions::Code).string().not_null())
.col(ColumnDef::new(Permissions::Name).string().not_null())
.col(ColumnDef::new(Permissions::Resource).string().not_null())
.col(ColumnDef::new(Permissions::Action).string().not_null())
.col(ColumnDef::new(Permissions::Description).text().null())
.col(
ColumnDef::new(Permissions::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Permissions::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Permissions::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Permissions::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Permissions::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Permissions::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_permissions_tenant_id")
.table(Permissions::Table)
.col(Permissions::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_permissions_tenant_code")
.table(Permissions::Table)
.col(Permissions::TenantId)
.col(Permissions::Code)
.unique()
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Permissions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Permissions {
Table,
Id,
TenantId,
Code,
Name,
Resource,
Action,
Description,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,115 @@
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> {
manager
.create_table(
Table::create()
.table(RolePermissions::Table)
.if_not_exists()
.col(ColumnDef::new(RolePermissions::RoleId).uuid().not_null())
.col(
ColumnDef::new(RolePermissions::PermissionId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(RolePermissions::TenantId).uuid().not_null())
.col(
ColumnDef::new(RolePermissions::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(RolePermissions::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(RolePermissions::CreatedBy).uuid().not_null())
.col(ColumnDef::new(RolePermissions::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(RolePermissions::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(RolePermissions::Version)
.integer()
.not_null()
.default(1),
)
.primary_key(
Index::create()
.col(RolePermissions::RoleId)
.col(RolePermissions::PermissionId),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_role_permissions_role_id")
.from(RolePermissions::Table, RolePermissions::RoleId)
.to(Roles::Table, Roles::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_role_permissions_permission_id")
.from(RolePermissions::Table, RolePermissions::PermissionId)
.to(Permissions::Table, Permissions::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_role_permissions_tenant_id")
.table(RolePermissions::Table)
.col(RolePermissions::TenantId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(RolePermissions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum RolePermissions {
Table,
RoleId,
PermissionId,
TenantId,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Roles {
Table,
Id,
}
#[derive(DeriveIden)]
enum Permissions {
Table,
Id,
}

View File

@@ -0,0 +1,111 @@
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> {
manager
.create_table(
Table::create()
.table(UserRoles::Table)
.if_not_exists()
.col(ColumnDef::new(UserRoles::UserId).uuid().not_null())
.col(ColumnDef::new(UserRoles::RoleId).uuid().not_null())
.col(ColumnDef::new(UserRoles::TenantId).uuid().not_null())
.col(
ColumnDef::new(UserRoles::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(UserRoles::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(UserRoles::CreatedBy).uuid().not_null())
.col(ColumnDef::new(UserRoles::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(UserRoles::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(UserRoles::Version)
.integer()
.not_null()
.default(1),
)
.primary_key(
Index::create()
.col(UserRoles::UserId)
.col(UserRoles::RoleId),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_user_roles_user_id")
.from(UserRoles::Table, UserRoles::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_user_roles_role_id")
.from(UserRoles::Table, UserRoles::RoleId)
.to(Roles::Table, Roles::Id)
.on_delete(ForeignKeyAction::Cascade)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_user_roles_tenant_id")
.table(UserRoles::Table)
.col(UserRoles::TenantId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(UserRoles::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum UserRoles {
Table,
UserId,
RoleId,
TenantId,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}
#[derive(DeriveIden)]
enum Roles {
Table,
Id,
}

View File

@@ -0,0 +1,116 @@
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> {
manager
.create_table(
Table::create()
.table(Organizations::Table)
.if_not_exists()
.col(
ColumnDef::new(Organizations::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Organizations::TenantId).uuid().not_null())
.col(ColumnDef::new(Organizations::Name).string().not_null())
.col(ColumnDef::new(Organizations::Code).string().null())
.col(ColumnDef::new(Organizations::ParentId).uuid().null())
.col(ColumnDef::new(Organizations::Path).string().null())
.col(
ColumnDef::new(Organizations::Level)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Organizations::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Organizations::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Organizations::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Organizations::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Organizations::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Organizations::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Organizations::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_organizations_parent_id")
.from(Organizations::Table, Organizations::ParentId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::Restrict)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_organizations_tenant_id")
.table(Organizations::Table)
.col(Organizations::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_organizations_tenant_code ON organizations (tenant_id, code) WHERE code IS NOT NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Organizations::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
TenantId,
Name,
Code,
ParentId,
Path,
Level,
SortOrder,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,146 @@
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> {
manager
.create_table(
Table::create()
.table(Departments::Table)
.if_not_exists()
.col(
ColumnDef::new(Departments::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Departments::TenantId).uuid().not_null())
.col(ColumnDef::new(Departments::OrgId).uuid().not_null())
.col(ColumnDef::new(Departments::Name).string().not_null())
.col(ColumnDef::new(Departments::Code).string().null())
.col(ColumnDef::new(Departments::ParentId).uuid().null())
.col(ColumnDef::new(Departments::ManagerId).uuid().null())
.col(ColumnDef::new(Departments::Path).string().null())
.col(
ColumnDef::new(Departments::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Departments::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Departments::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Departments::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Departments::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Departments::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Departments::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_departments_org_id")
.from(Departments::Table, Departments::OrgId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::Restrict)
.to_owned(),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_departments_parent_id")
.from(Departments::Table, Departments::ParentId)
.to(Departments::Table, Departments::Id)
.on_delete(ForeignKeyAction::Restrict)
.to_owned(),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_departments_manager_id")
.from(Departments::Table, Departments::ManagerId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::SetNull)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_departments_tenant_id")
.table(Departments::Table)
.col(Departments::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_departments_org_id")
.table(Departments::Table)
.col(Departments::OrgId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Departments::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Departments {
Table,
Id,
TenantId,
OrgId,
Name,
Code,
ParentId,
ManagerId,
Path,
SortOrder,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}

View File

@@ -0,0 +1,125 @@
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> {
manager
.create_table(
Table::create()
.table(Positions::Table)
.if_not_exists()
.col(
ColumnDef::new(Positions::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Positions::TenantId).uuid().not_null())
.col(ColumnDef::new(Positions::DeptId).uuid().not_null())
.col(ColumnDef::new(Positions::Name).string().not_null())
.col(ColumnDef::new(Positions::Code).string().null())
.col(
ColumnDef::new(Positions::Level)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Positions::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Positions::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Positions::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Positions::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Positions::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Positions::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Positions::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
&mut ForeignKey::create()
.name("fk_positions_dept_id")
.from(Positions::Table, Positions::DeptId)
.to(Departments::Table, Departments::Id)
.on_delete(ForeignKeyAction::Restrict)
.to_owned(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_positions_tenant_id")
.table(Positions::Table)
.col(Positions::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_positions_dept_id")
.table(Positions::Table)
.col(Positions::DeptId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Positions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Positions {
Table,
Id,
TenantId,
DeptId,
Name,
Code,
Level,
SortOrder,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum Departments {
Table,
Id,
}

View File

@@ -0,0 +1,92 @@
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> {
manager
.create_table(
Table::create()
.table(Dictionaries::Table)
.if_not_exists()
.col(
ColumnDef::new(Dictionaries::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Dictionaries::TenantId).uuid().not_null())
.col(ColumnDef::new(Dictionaries::Name).string().not_null())
.col(ColumnDef::new(Dictionaries::Code).string().not_null())
.col(ColumnDef::new(Dictionaries::Description).text().null())
.col(
ColumnDef::new(Dictionaries::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Dictionaries::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Dictionaries::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Dictionaries::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Dictionaries::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Dictionaries::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_dictionaries_tenant_id")
.table(Dictionaries::Table)
.col(Dictionaries::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_dictionaries_tenant_code ON dictionaries (tenant_id, code) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Dictionaries::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Dictionaries {
Table,
Id,
TenantId,
Name,
Code,
Description,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,118 @@
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> {
manager
.create_table(
Table::create()
.table(DictionaryItems::Table)
.if_not_exists()
.col(
ColumnDef::new(DictionaryItems::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(DictionaryItems::TenantId).uuid().not_null())
.col(
ColumnDef::new(DictionaryItems::DictionaryId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(DictionaryItems::Label).string().not_null())
.col(ColumnDef::new(DictionaryItems::Value).string().not_null())
.col(
ColumnDef::new(DictionaryItems::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(ColumnDef::new(DictionaryItems::Color).string().null())
.col(
ColumnDef::new(DictionaryItems::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(DictionaryItems::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(DictionaryItems::CreatedBy).uuid().not_null())
.col(ColumnDef::new(DictionaryItems::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(DictionaryItems::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(DictionaryItems::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.name("fk_dict_items_dictionary")
.from(DictionaryItems::Table, DictionaryItems::DictionaryId)
.to(Dictionaries::Table, Dictionaries::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_dict_items_dictionary_id")
.table(DictionaryItems::Table)
.col(DictionaryItems::DictionaryId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_dict_items_dict_value ON dictionary_items (dictionary_id, value) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(DictionaryItems::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Dictionaries {
Table,
Id,
}
#[derive(DeriveIden)]
enum DictionaryItems {
Table,
Id,
TenantId,
DictionaryId,
Label,
Value,
SortOrder,
Color,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,124 @@
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> {
manager
.create_table(
Table::create()
.table(Menus::Table)
.if_not_exists()
.col(ColumnDef::new(Menus::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Menus::TenantId).uuid().not_null())
.col(ColumnDef::new(Menus::ParentId).uuid().null())
.col(ColumnDef::new(Menus::Title).string().not_null())
.col(ColumnDef::new(Menus::Path).string().null())
.col(ColumnDef::new(Menus::Icon).string().null())
.col(
ColumnDef::new(Menus::SortOrder)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Menus::Visible)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(Menus::MenuType)
.string()
.not_null()
.default("page"),
)
.col(ColumnDef::new(Menus::Permission).string().null())
.col(
ColumnDef::new(Menus::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Menus::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Menus::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Menus::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Menus::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Menus::Version)
.integer()
.not_null()
.default(1),
)
.foreign_key(
ForeignKey::create()
.name("fk_menus_parent")
.from(Menus::Table, Menus::ParentId)
.to(Menus::Table, Menus::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_menus_tenant_id")
.table(Menus::Table)
.col(Menus::TenantId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_menus_parent_id")
.table(Menus::Table)
.col(Menus::ParentId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Menus::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Menus {
Table,
Id,
TenantId,
ParentId,
Title,
Path,
Icon,
SortOrder,
Visible,
MenuType,
Permission,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,96 @@
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> {
manager
.create_table(
Table::create()
.table(MenuRoles::Table)
.if_not_exists()
.col(ColumnDef::new(MenuRoles::MenuId).uuid().not_null())
.col(ColumnDef::new(MenuRoles::RoleId).uuid().not_null())
.col(ColumnDef::new(MenuRoles::TenantId).uuid().not_null())
.col(ColumnDef::new(MenuRoles::Id).uuid().not_null())
.col(
ColumnDef::new(MenuRoles::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(MenuRoles::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(MenuRoles::CreatedBy).uuid().not_null())
.col(ColumnDef::new(MenuRoles::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(MenuRoles::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(MenuRoles::Version)
.integer()
.not_null()
.default(1),
)
.primary_key(Index::create().col(MenuRoles::Id))
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_menu_roles_unique ON menu_roles (menu_id, role_id) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
manager
.create_index(
Index::create()
.name("idx_menu_roles_menu_id")
.table(MenuRoles::Table)
.col(MenuRoles::MenuId)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_menu_roles_role_id")
.table(MenuRoles::Table)
.col(MenuRoles::RoleId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(MenuRoles::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum MenuRoles {
Table,
Id,
MenuId,
RoleId,
TenantId,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,94 @@
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> {
manager
.create_table(
Table::create()
.table(Settings::Table)
.if_not_exists()
.col(ColumnDef::new(Settings::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Settings::TenantId).uuid().not_null())
.col(ColumnDef::new(Settings::Scope).string().not_null())
.col(ColumnDef::new(Settings::ScopeId).uuid().null())
.col(ColumnDef::new(Settings::SettingKey).string().not_null())
.col(
ColumnDef::new(Settings::SettingValue)
.json_binary()
.not_null()
.default(Expr::val("{}")),
)
.col(
ColumnDef::new(Settings::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Settings::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Settings::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Settings::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Settings::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Settings::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_settings_tenant_id")
.table(Settings::Table)
.col(Settings::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, scope_id, setting_key) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Settings::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Settings {
Table,
Id,
TenantId,
Scope,
ScopeId,
SettingKey,
SettingValue,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,136 @@
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> {
manager
.create_table(
Table::create()
.table(NumberingRules::Table)
.if_not_exists()
.col(
ColumnDef::new(NumberingRules::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(NumberingRules::TenantId).uuid().not_null())
.col(ColumnDef::new(NumberingRules::Name).string().not_null())
.col(ColumnDef::new(NumberingRules::Code).string().not_null())
.col(
ColumnDef::new(NumberingRules::Prefix)
.string()
.not_null()
.default(""),
)
.col(ColumnDef::new(NumberingRules::DateFormat).string().null())
.col(
ColumnDef::new(NumberingRules::SeqLength)
.integer()
.not_null()
.default(4),
)
.col(
ColumnDef::new(NumberingRules::SeqStart)
.integer()
.not_null()
.default(1),
)
.col(
ColumnDef::new(NumberingRules::SeqCurrent)
.big_integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(NumberingRules::Separator)
.string()
.not_null()
.default("-"),
)
.col(
ColumnDef::new(NumberingRules::ResetCycle)
.string()
.not_null()
.default("never"),
)
.col(ColumnDef::new(NumberingRules::LastResetDate).date().null())
.col(
ColumnDef::new(NumberingRules::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(NumberingRules::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(NumberingRules::CreatedBy).uuid().not_null())
.col(ColumnDef::new(NumberingRules::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(NumberingRules::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(NumberingRules::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_numbering_rules_tenant_id")
.table(NumberingRules::Table)
.col(NumberingRules::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_numbering_rules_tenant_code ON numbering_rules (tenant_id, code) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(NumberingRules::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum NumberingRules {
Table,
Id,
TenantId,
Name,
Code,
Prefix,
DateFormat,
SeqLength,
SeqStart,
SeqCurrent,
Separator,
ResetCycle,
LastResetDate,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,138 @@
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> {
manager
.create_table(
Table::create()
.table(ProcessDefinitions::Table)
.if_not_exists()
.col(
ColumnDef::new(ProcessDefinitions::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(ProcessDefinitions::TenantId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(ProcessDefinitions::Name).string().not_null())
.col(ColumnDef::new(ProcessDefinitions::Key).string().not_null())
.col(
ColumnDef::new(ProcessDefinitions::Version)
.integer()
.not_null()
.default(1),
)
.col(ColumnDef::new(ProcessDefinitions::Category).string().null())
.col(
ColumnDef::new(ProcessDefinitions::Description)
.text()
.null(),
)
.col(
ColumnDef::new(ProcessDefinitions::Nodes)
.json_binary()
.not_null()
.default(Expr::val("[]")),
)
.col(
ColumnDef::new(ProcessDefinitions::Edges)
.json_binary()
.not_null()
.default(Expr::val("[]")),
)
.col(
ColumnDef::new(ProcessDefinitions::Status)
.string()
.not_null()
.default("draft"),
)
.col(
ColumnDef::new(ProcessDefinitions::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ProcessDefinitions::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ProcessDefinitions::CreatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessDefinitions::UpdatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessDefinitions::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ProcessDefinitions::VersionField)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_process_definitions_tenant_id")
.table(ProcessDefinitions::Table)
.col(ProcessDefinitions::TenantId)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_process_definitions_key_version ON process_definitions (tenant_id, key, version) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(ProcessDefinitions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum ProcessDefinitions {
Table,
Id,
TenantId,
Name,
Key,
Version,
Category,
Description,
Nodes,
Edges,
Status,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
VersionField,
}

View File

@@ -0,0 +1,144 @@
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> {
manager
.create_table(
Table::create()
.table(ProcessInstances::Table)
.if_not_exists()
.col(
ColumnDef::new(ProcessInstances::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ProcessInstances::TenantId).uuid().not_null())
.col(
ColumnDef::new(ProcessInstances::DefinitionId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessInstances::BusinessKey)
.string()
.null(),
)
.col(
ColumnDef::new(ProcessInstances::Status)
.string()
.not_null()
.default("running"),
)
.col(
ColumnDef::new(ProcessInstances::StartedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessInstances::StartedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ProcessInstances::CompletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ProcessInstances::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ProcessInstances::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(ProcessInstances::CreatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessInstances::UpdatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(ProcessInstances::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ProcessInstances::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_instances_tenant_status")
.table(ProcessInstances::Table)
.col(ProcessInstances::TenantId)
.col(ProcessInstances::Status)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_instances_definition")
.from(ProcessInstances::Table, ProcessInstances::DefinitionId)
.to(ProcessDefinitions::Table, ProcessDefinitions::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(ProcessInstances::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum ProcessInstances {
Table,
Id,
TenantId,
DefinitionId,
BusinessKey,
Status,
StartedBy,
StartedAt,
CompletedAt,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum ProcessDefinitions {
Table,
Id,
}

View File

@@ -0,0 +1,85 @@
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> {
manager
.create_table(
Table::create()
.table(Tokens::Table)
.if_not_exists()
.col(ColumnDef::new(Tokens::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Tokens::TenantId).uuid().not_null())
.col(ColumnDef::new(Tokens::InstanceId).uuid().not_null())
.col(ColumnDef::new(Tokens::NodeId).string().not_null())
.col(
ColumnDef::new(Tokens::Status)
.string()
.not_null()
.default("active"),
)
.col(
ColumnDef::new(Tokens::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Tokens::ConsumedAt)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_tokens_instance")
.table(Tokens::Table)
.col(Tokens::InstanceId)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_tokens_instance")
.from(Tokens::Table, Tokens::InstanceId)
.to(ProcessInstances::Table, ProcessInstances::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Tokens::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Tokens {
Table,
Id,
TenantId,
InstanceId,
NodeId,
Status,
CreatedAt,
ConsumedAt,
}
#[derive(DeriveIden)]
enum ProcessInstances {
Table,
Id,
}

View File

@@ -0,0 +1,155 @@
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> {
manager
.create_table(
Table::create()
.table(Tasks::Table)
.if_not_exists()
.col(ColumnDef::new(Tasks::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Tasks::TenantId).uuid().not_null())
.col(ColumnDef::new(Tasks::InstanceId).uuid().not_null())
.col(ColumnDef::new(Tasks::TokenId).uuid().not_null())
.col(ColumnDef::new(Tasks::NodeId).string().not_null())
.col(ColumnDef::new(Tasks::NodeName).string().null())
.col(ColumnDef::new(Tasks::AssigneeId).uuid().null())
.col(ColumnDef::new(Tasks::CandidateGroups).json_binary().null())
.col(
ColumnDef::new(Tasks::Status)
.string()
.not_null()
.default("pending"),
)
.col(ColumnDef::new(Tasks::Outcome).string().null())
.col(ColumnDef::new(Tasks::FormData).json_binary().null())
.col(
ColumnDef::new(Tasks::DueDate)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Tasks::CompletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Tasks::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Tasks::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Tasks::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Tasks::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Tasks::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Tasks::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_tasks_assignee")
.table(Tasks::Table)
.col(Tasks::TenantId)
.col(Tasks::AssigneeId)
.col(Tasks::Status)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_tasks_instance")
.table(Tasks::Table)
.col(Tasks::InstanceId)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_tasks_instance")
.from(Tasks::Table, Tasks::InstanceId)
.to(ProcessInstances::Table, ProcessInstances::Id)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_tasks_token")
.from(Tasks::Table, Tasks::TokenId)
.to(Tokens::Table, Tokens::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Tasks::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Tasks {
Table,
Id,
TenantId,
InstanceId,
TokenId,
NodeId,
NodeName,
AssigneeId,
CandidateGroups,
Status,
Outcome,
FormData,
DueDate,
CompletedAt,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}
#[derive(DeriveIden)]
enum ProcessInstances {
Table,
Id,
}
#[derive(DeriveIden)]
enum Tokens {
Table,
Id,
}

View File

@@ -0,0 +1,96 @@
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> {
manager
.create_table(
Table::create()
.table(ProcessVariables::Table)
.if_not_exists()
.col(
ColumnDef::new(ProcessVariables::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ProcessVariables::TenantId).uuid().not_null())
.col(
ColumnDef::new(ProcessVariables::InstanceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(ProcessVariables::Name).string().not_null())
.col(
ColumnDef::new(ProcessVariables::VarType)
.string()
.not_null()
.default("string"),
)
.col(ColumnDef::new(ProcessVariables::ValueString).text().null())
.col(
ColumnDef::new(ProcessVariables::ValueNumber)
.double()
.null(),
)
.col(
ColumnDef::new(ProcessVariables::ValueBoolean)
.boolean()
.null(),
)
.col(
ColumnDef::new(ProcessVariables::ValueDate)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_process_variables_instance_name ON process_variables (instance_id, name)".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_variables_instance")
.from(ProcessVariables::Table, ProcessVariables::InstanceId)
.to(ProcessInstances::Table, ProcessInstances::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(ProcessVariables::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum ProcessVariables {
Table,
Id,
TenantId,
InstanceId,
Name,
VarType,
ValueString,
ValueNumber,
ValueBoolean,
ValueDate,
}
#[derive(DeriveIden)]
enum ProcessInstances {
Table,
Id,
}

View File

@@ -0,0 +1,105 @@
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> {
manager
.create_table(
Table::create()
.table(MessageTemplates::Table)
.if_not_exists()
.col(
ColumnDef::new(MessageTemplates::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(MessageTemplates::TenantId).uuid().not_null())
.col(ColumnDef::new(MessageTemplates::Name).string().not_null())
.col(ColumnDef::new(MessageTemplates::Code).string().not_null())
.col(
ColumnDef::new(MessageTemplates::Channel)
.string()
.not_null()
.default("in_app"),
)
.col(
ColumnDef::new(MessageTemplates::TitleTemplate)
.string()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::BodyTemplate)
.text()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::Language)
.string()
.not_null()
.default("zh-CN"),
)
.col(
ColumnDef::new(MessageTemplates::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::CreatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::UpdatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageTemplates::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_message_templates_tenant_code ON message_templates (tenant_id, code) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(MessageTemplates::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum MessageTemplates {
Table,
Id,
TenantId,
Name,
Code,
Channel,
TitleTemplate,
BodyTemplate,
Language,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
}

View File

@@ -0,0 +1,162 @@
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> {
manager
.create_table(
Table::create()
.table(Messages::Table)
.if_not_exists()
.col(ColumnDef::new(Messages::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Messages::TenantId).uuid().not_null())
.col(ColumnDef::new(Messages::TemplateId).uuid().null())
.col(ColumnDef::new(Messages::SenderId).uuid().null())
.col(
ColumnDef::new(Messages::SenderType)
.string()
.not_null()
.default("system"),
)
.col(ColumnDef::new(Messages::RecipientId).uuid().not_null())
.col(
ColumnDef::new(Messages::RecipientType)
.string()
.not_null()
.default("user"),
)
.col(ColumnDef::new(Messages::Title).string().not_null())
.col(ColumnDef::new(Messages::Body).text().not_null())
.col(
ColumnDef::new(Messages::Priority)
.string()
.not_null()
.default("normal"),
)
.col(ColumnDef::new(Messages::BusinessType).string().null())
.col(ColumnDef::new(Messages::BusinessId).uuid().null())
.col(
ColumnDef::new(Messages::IsRead)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(Messages::ReadAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Messages::IsArchived)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(Messages::ArchivedAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Messages::SentAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(Messages::Status)
.string()
.not_null()
.default("sent"),
)
.col(
ColumnDef::new(Messages::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Messages::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(ColumnDef::new(Messages::CreatedBy).uuid().not_null())
.col(ColumnDef::new(Messages::UpdatedBy).uuid().not_null())
.col(
ColumnDef::new(Messages::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE INDEX idx_messages_tenant_recipient ON messages (tenant_id, recipient_id) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE INDEX idx_messages_tenant_recipient_unread ON messages (tenant_id, recipient_id) WHERE deleted_at IS NULL AND is_read = false".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE INDEX idx_messages_tenant_business ON messages (tenant_id, business_type, business_id) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_messages_template")
.from(Messages::Table, Messages::TemplateId)
.to(MessageTemplates::Table, MessageTemplates::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Messages::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Messages {
Table,
Id,
TenantId,
TemplateId,
SenderId,
SenderType,
RecipientId,
RecipientType,
Title,
Body,
Priority,
BusinessType,
BusinessId,
IsRead,
ReadAt,
IsArchived,
ArchivedAt,
SentAt,
Status,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
}
#[derive(DeriveIden)]
enum MessageTemplates {
Table,
Id,
}

View File

@@ -0,0 +1,112 @@
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> {
manager
.create_table(
Table::create()
.table(MessageSubscriptions::Table)
.if_not_exists()
.col(
ColumnDef::new(MessageSubscriptions::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(MessageSubscriptions::TenantId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::UserId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::NotificationTypes)
.json()
.null(),
)
.col(
ColumnDef::new(MessageSubscriptions::ChannelPreferences)
.json()
.null(),
)
.col(
ColumnDef::new(MessageSubscriptions::DndEnabled)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(MessageSubscriptions::DndStart)
.string()
.null(),
)
.col(ColumnDef::new(MessageSubscriptions::DndEnd).string().null())
.col(
ColumnDef::new(MessageSubscriptions::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::CreatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::UpdatedBy)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(MessageSubscriptions::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_message_subscriptions_tenant_user ON message_subscriptions (tenant_id, user_id) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(MessageSubscriptions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum MessageSubscriptions {
Table,
Id,
TenantId,
UserId,
NotificationTypes,
ChannelPreferences,
DndEnabled,
DndStart,
DndEnd,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
}

View File

@@ -0,0 +1,81 @@
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> {
manager
.create_table(
Table::create()
.table(AuditLogs::Table)
.if_not_exists()
.col(
ColumnDef::new(AuditLogs::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AuditLogs::TenantId).uuid().not_null())
.col(ColumnDef::new(AuditLogs::UserId).uuid().null())
.col(ColumnDef::new(AuditLogs::Action).string().not_null())
.col(ColumnDef::new(AuditLogs::ResourceType).string().not_null())
.col(ColumnDef::new(AuditLogs::ResourceId).uuid().null())
.col(ColumnDef::new(AuditLogs::OldValue).json().null())
.col(ColumnDef::new(AuditLogs::NewValue).json().null())
.col(ColumnDef::new(AuditLogs::IpAddress).string().null())
.col(ColumnDef::new(AuditLogs::UserAgent).text().null())
.col(
ColumnDef::new(AuditLogs::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE INDEX idx_audit_logs_tenant ON audit_logs (tenant_id)".to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE INDEX idx_audit_logs_resource ON audit_logs (resource_type, resource_id)"
.to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(AuditLogs::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum AuditLogs {
Table,
Id,
TenantId,
UserId,
Action,
ResourceType,
ResourceId,
OldValue,
NewValue,
IpAddress,
UserAgent,
CreatedAt,
}

View File

@@ -0,0 +1,92 @@
use sea_orm::DatabaseBackend;
use sea_orm::Statement;
use sea_orm_migration::prelude::*;
/// Recreate unique indexes on roles and permissions to include soft-delete awareness.
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_roles_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_permissions_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_roles_tenant_code \
ON roles (tenant_id, code) \
WHERE deleted_at IS NULL"
.to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_permissions_tenant_code \
ON permissions (tenant_id, code) \
WHERE deleted_at IS NULL"
.to_string(),
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_roles_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_permissions_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_roles_tenant_code \
ON roles (tenant_id, code)"
.to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_permissions_tenant_code \
ON permissions (tenant_id, code)"
.to_string(),
))
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
/// 为 tokens 表添加缺失的标准字段: updated_at, created_by, updated_by, deleted_at, version。
///
/// tokens 表原始迁移缺少 ERP 标准要求的审计和乐观锁字段。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Tokens::Table)
.add_column(
ColumnDef::new(Tokens::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(Tokens::CreatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(Tokens::UpdatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(Tokens::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.add_column(
ColumnDef::new(Tokens::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Tokens::Table)
.drop_column(Tokens::UpdatedAt)
.drop_column(Tokens::CreatedBy)
.drop_column(Tokens::UpdatedBy)
.drop_column(Tokens::DeletedAt)
.drop_column(Tokens::Version)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Tokens {
Table,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,82 @@
use sea_orm_migration::prelude::*;
/// 为 process_variables 表添加缺失的标准字段: created_at, updated_at, created_by, updated_by, deleted_at, version。
///
/// process_variables 表原始迁移缺少 ERP 标准要求的审计和乐观锁字段。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProcessVariables::Table)
.add_column(
ColumnDef::new(ProcessVariables::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(ProcessVariables::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(ProcessVariables::CreatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(ProcessVariables::UpdatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(ProcessVariables::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.add_column(
ColumnDef::new(ProcessVariables::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProcessVariables::Table)
.drop_column(ProcessVariables::CreatedAt)
.drop_column(ProcessVariables::UpdatedAt)
.drop_column(ProcessVariables::CreatedBy)
.drop_column(ProcessVariables::UpdatedBy)
.drop_column(ProcessVariables::DeletedAt)
.drop_column(ProcessVariables::Version)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum ProcessVariables {
Table,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,65 @@
use sea_orm_migration::prelude::*;
/// 修复 settings 表唯一索引:原索引使用 scope_id 列,当 scope_id 为 NULL 时
/// PostgreSQL B-tree 不认为两行重复NULL != NULL导致可插入重复数据。
/// 修复方案:使用 COALESCE(scope_id, '00000000-0000-0000-0000-000000000000')
/// 将 NULL 转为固定 UUID使索引能正确约束 NULL scope_id 的行。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 1. 删除旧索引
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_settings_scope_key".to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
// 2. 先清理可能已存在的重复数据(保留每组最新的一条)
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
r#"
DELETE FROM settings a USING settings b
WHERE a.id < b.id
AND a.tenant_id = b.tenant_id
AND a.scope = b.scope
AND a.setting_key = b.setting_key
AND a.deleted_at IS NULL
AND b.deleted_at IS NULL
AND COALESCE(a.scope_id, '00000000-0000-0000-0000-000000000000') = COALESCE(b.scope_id, '00000000-0000-0000-0000-000000000000')
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
// 3. 创建新索引,使用 COALESCE 处理 NULL scope_id
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, COALESCE(scope_id, '00000000-0000-0000-0000-000000000000'), setting_key) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 回滚:删除新索引,恢复旧索引
manager
.get_connection()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_settings_scope_key".to_string(),
))
.await
.map_err(|e| DbErr::Custom(e.to_string()))?;
manager.get_connection().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, scope_id, setting_key) WHERE deleted_at IS NULL".to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
}

View File

@@ -0,0 +1,104 @@
use sea_orm_migration::prelude::*;
/// 为三个消息表添加缺失的 version 列(乐观锁字段)。
///
/// CLAUDE.md 要求所有表包含 version 字段用于乐观锁,但消息模块的三个表遗漏了。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// message_templates
manager
.alter_table(
Table::alter()
.table(MessageTemplates::Table)
.add_column(
ColumnDef::new(MessageTemplates::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
// messages
manager
.alter_table(
Table::alter()
.table(Messages::Table)
.add_column(
ColumnDef::new(Messages::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
// message_subscriptions
manager
.alter_table(
Table::alter()
.table(MessageSubscriptions::Table)
.add_column(
ColumnDef::new(MessageSubscriptions::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(MessageTemplates::Table)
.drop_column(MessageTemplates::Version)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(Messages::Table)
.drop_column(Messages::Version)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(MessageSubscriptions::Table)
.drop_column(MessageSubscriptions::Version)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum MessageTemplates {
Table,
Version,
}
#[derive(DeriveIden)]
enum Messages {
Table,
Version,
}
#[derive(DeriveIden)]
enum MessageSubscriptions {
Table,
Version,
}

View File

@@ -0,0 +1,84 @@
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> {
manager
.create_table(
Table::create()
.table(Alias::new("domain_events"))
.if_not_exists()
.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("payload")).json().null())
.col(ColumnDef::new(Alias::new("correlation_id")).uuid().null())
.col(
ColumnDef::new(Alias::new("status"))
.string_len(20)
.not_null()
.default("pending"),
)
.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(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_domain_events_status")
.table(Alias::new("domain_events"))
.col(Alias::new("status"))
.col(Alias::new("created_at"))
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_domain_events_tenant")
.table(Alias::new("domain_events"))
.col(Alias::new("tenant_id"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("domain_events")).to_owned())
.await
}
}

View File

@@ -0,0 +1,248 @@
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. plugins 表 — 插件注册与生命周期
manager
.create_table(
Table::create()
.table(Alias::new("plugins"))
.if_not_exists()
.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("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(
ColumnDef::new(Alias::new("status"))
.string_len(20)
.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("config_json"))
.json()
.not_null()
.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("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().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("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_plugins_tenant_status")
.table(Alias::new("plugins"))
.col(Alias::new("tenant_id"))
.col(Alias::new("status"))
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_plugins_name")
.table(Alias::new("plugins"))
.col(Alias::new("name"))
.to_owned(),
)
.await?;
// 2. plugin_entities 表 — 插件动态表注册
manager
.create_table(
Table::create()
.table(Alias::new("plugin_entities"))
.if_not_exists()
.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("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("schema_json")).json().not_null())
// 标准字段
.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().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("version"))
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_plugin_entities_plugin")
.table(Alias::new("plugin_entities"))
.col(Alias::new("plugin_id"))
.to_owned(),
)
.await?;
// 3. plugin_event_subscriptions 表 — 事件订阅
manager
.create_table(
Table::create()
.table(Alias::new("plugin_event_subscriptions"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.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("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_plugin_event_subs_plugin")
.table(Alias::new("plugin_event_subscriptions"))
.col(Alias::new("plugin_id"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.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(),
)
.await?;
manager
.drop_table(Table::drop().table(Alias::new("plugins")).to_owned())
.await
}
}

View File

@@ -0,0 +1,84 @@
use sea_orm_migration::prelude::*;
/// 为已存在的租户补充 plugin 模块权限,并分配给 admin 角色。
/// seed_tenant_auth 只在租户创建时执行,已存在的租户缺少 plugin 相关权限。
#[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();
// 插入 plugin 权限(如果不存在)
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
r#"
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id, 'plugin.admin', '插件管理', 'plugin', 'admin', '管理插件全生命周期', NOW(), NOW(), '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM permissions p WHERE p.code = 'plugin.admin' AND p.tenant_id = t.id AND p.deleted_at IS NULL
)
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
r#"
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id, 'plugin.list', '查看插件', 'plugin', 'list', '查看插件列表', NOW(), NOW(), '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM permissions p WHERE p.code = 'plugin.list' AND p.tenant_id = t.id AND p.deleted_at IS NULL
)
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
// 将 plugin 权限分配给 admin 角色(如果尚未分配)
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT r.id, p.id, r.tenant_id, NOW(), NOW(), '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', NULL, 1
FROM roles r
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code IN ('plugin.admin', 'plugin.list') AND p.deleted_at IS NULL
WHERE r.code = 'admin' AND r.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id AND rp.deleted_at IS NULL
)
"#.to_string(),
)).await.map_err(|e| DbErr::Custom(e.to_string()))?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
// 删除 plugin 权限的角色关联
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
r#"
DELETE FROM role_permissions
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()))?;
// 删除 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()))?;
Ok(())
}
}

View File

@@ -0,0 +1,85 @@
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> {
// 启用 pg_trgm 扩展(加速 ILIKE '%keyword%' 搜索)
manager
.get_connection()
.execute_unprepared("CREATE EXTENSION IF NOT EXISTS pg_trgm")
.await?;
// 插件实体列元数据表 — 记录哪些字段被提取为 Generated Column
manager
.create_table(
Table::create()
.table(Alias::new("plugin_entity_columns"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.default(Expr::cust("gen_random_uuid()"))
.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("field_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"))
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::cust("NOW()")),
)
.to_owned(),
)
.await?;
// plugin_entity_id 外键
manager
.create_foreign_key(
ForeignKey::create()
.name("fk_plugin_entity_columns_entity")
.from(
Alias::new("plugin_entity_columns"),
Alias::new("plugin_entity_id"),
)
.to(Alias::new("plugin_entities"), Alias::new("id"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(Alias::new("plugin_entity_columns"))
.to_owned(),
)
.await?;
// pg_trgm 不卸载(其他功能可能依赖)
Ok(())
}
}

View File

@@ -0,0 +1,37 @@
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> {
// 添加 data_scope 列 — 行级数据权限范围
// 可选值: all, self, department, department_tree
manager
.alter_table(
Table::alter()
.table(Alias::new("role_permissions"))
.add_column(
ColumnDef::new(Alias::new("data_scope"))
.string()
.not_null()
.default("all"),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Alias::new("role_permissions"))
.drop_column(Alias::new("data_scope"))
.to_owned(),
)
.await
}
}

View File

@@ -0,0 +1,94 @@
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> {
manager
.create_table(
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("department_id"))
.uuid()
.not_null(),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("is_primary"))
.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),
)
.primary_key(
Index::create()
.col(Alias::new("user_id"))
.col(Alias::new("department_id")),
)
.to_owned(),
)
.await?;
// 索引:按租户 + 用户查询部门
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_user_departments_tenant_user")
.table(Alias::new("user_departments"))
.col(Alias::new("tenant_id"))
.col(Alias::new("user_id"))
.to_owned(),
)
.await?;
// 索引:按部门查询成员
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_user_departments_dept")
.table(Alias::new("user_departments"))
.col(Alias::new("department_id"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(Alias::new("user_departments"))
.to_owned(),
)
.await
}
}

View File

@@ -0,0 +1,51 @@
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> {
// plugin_entities 新增 manifest_id 列 — 避免跨插件查询时 JOIN plugins 表
manager
.get_connection()
.execute_unprepared(
r#"
ALTER TABLE plugin_entities
ADD COLUMN IF NOT EXISTS manifest_id TEXT NOT NULL DEFAULT '';
ALTER TABLE plugin_entities
ADD COLUMN IF NOT EXISTS is_public BOOLEAN NOT NULL DEFAULT false;
-- 回填 manifest_id从 plugins.manifest_json 提取 metadata.id
UPDATE plugin_entities pe
SET manifest_id = COALESCE(p.manifest_json->'metadata'->>'id', '')
FROM plugins p
WHERE pe.plugin_id = p.id AND pe.deleted_at IS NULL;
-- 跨插件实体查找索引
CREATE INDEX IF NOT EXISTS idx_plugin_entities_cross_ref
ON plugin_entities (manifest_id, entity_name, tenant_id)
WHERE deleted_at IS NULL;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared(
r#"
DROP INDEX IF EXISTS idx_plugin_entities_cross_ref;
ALTER TABLE plugin_entities DROP COLUMN IF EXISTS is_public;
ALTER TABLE plugin_entities DROP COLUMN IF EXISTS manifest_id;
"#,
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,146 @@
use sea_orm_migration::prelude::*;
/// 插件市场目录表 — P4 插件市场基础设施
#[derive(DeriveMigrationName)]
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("plugin_market_entries"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Alias::new("plugin_id")).string().not_null())
.col(ColumnDef::new(Alias::new("name")).string().not_null())
.col(ColumnDef::new(Alias::new("version")).string().not_null())
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("author")).string())
.col(ColumnDef::new(Alias::new("category")).string()) // 行业分类
.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_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("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()),
)
.to_owned(),
)
.await?;
// 插件市场评论/评分表
manager
.create_table(
Table::create()
.table(Alias::new("plugin_market_reviews"))
.if_not_exists()
.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("user_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()),
)
.to_owned(),
)
.await?;
// 唯一索引:每个用户对每个市场条目只能评一次
manager
.create_index(
Index::create()
.if_not_exists()
.unique()
.name("uq_market_review_tenant_user_entry")
.table(Alias::new("plugin_market_reviews"))
.col(Alias::new("tenant_id"))
.col(Alias::new("user_id"))
.col(Alias::new("market_entry_id"))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.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(),
)
.await
}
}

View File

@@ -0,0 +1,63 @@
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> {
manager
.create_table(
Table::create()
.table(Alias::new("plugin_user_views"))
.if_not_exists()
.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("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("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()),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(Alias::new("plugin_user_views"))
.to_owned(),
)
.await
}
}

View File

@@ -0,0 +1,88 @@
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> {
manager
.create_table(
Table::create()
.table(WechatUsers::Table)
.if_not_exists()
.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())
.col(ColumnDef::new(WechatUsers::UserId).uuid().not_null())
.col(ColumnDef::new(WechatUsers::Phone).string())
.col(
ColumnDef::new(WechatUsers::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(WechatUsers::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.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::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_wechat_users_openid")
.table(WechatUsers::Table)
.col(WechatUsers::Openid)
.col(WechatUsers::TenantId)
.unique()
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_wechat_users_openid").to_owned())
.await?;
manager
.drop_table(Table::drop().table(WechatUsers::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum WechatUsers {
Table,
Id,
TenantId,
Openid,
UnionId,
UserId,
Phone,
CreatedAt,
UpdatedAt,
DeletedAt,
CreatedBy,
UpdatedBy,
Version,
}

View File

@@ -0,0 +1,102 @@
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> {
manager
.create_table(
Table::create()
.table(TenantCryptoKey::Table)
.col(
ColumnDef::new(TenantCryptoKey::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(TenantCryptoKey::TenantId).uuid().not_null())
.col(
ColumnDef::new(TenantCryptoKey::EncryptedDek)
.string_len(128)
.not_null(),
)
.col(
ColumnDef::new(TenantCryptoKey::KeyVersion)
.integer()
.not_null()
.default(1),
)
.col(
ColumnDef::new(TenantCryptoKey::IsActive)
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(TenantCryptoKey::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(TenantCryptoKey::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(TenantCryptoKey::CreatedBy).uuid())
.col(ColumnDef::new(TenantCryptoKey::UpdatedBy).uuid())
.col(ColumnDef::new(TenantCryptoKey::DeletedAt).timestamp_with_time_zone())
.col(
ColumnDef::new(TenantCryptoKey::Version)
.integer()
.not_null()
.default(1),
)
.index(
Index::create()
.col(TenantCryptoKey::TenantId)
.col(TenantCryptoKey::KeyVersion)
.unique(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_tenant_crypto_keys_tenant")
.table(TenantCryptoKey::Table)
.col(TenantCryptoKey::TenantId)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(TenantCryptoKey::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum TenantCryptoKey {
Table,
Id,
TenantId,
EncryptedDek,
KeyVersion,
IsActive,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,128 @@
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> {
// 归档表 — 与 domain_events 结构相同,用于存放 >90 天的已发布事件
manager
.create_table(
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("tenant_id")).uuid().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("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()),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_domain_events_archive_created")
.table(Alias::new("domain_events_archive"))
.col(Alias::new("created_at"))
.to_owned(),
)
.await?;
// 清理函数:将 >90 天的已发布事件迁移到归档表
manager
.get_connection()
.execute_unprepared(
r#"
CREATE OR REPLACE FUNCTION cleanup_old_published_events(
retention_days INT DEFAULT 90,
batch_size INT DEFAULT 1000
) RETURNS INT AS $$
DECLARE
moved_count INT;
BEGIN
INSERT INTO domain_events_archive (id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at)
SELECT id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at
FROM domain_events
WHERE status = 'published'
AND published_at < NOW() - (retention_days || ' days')::INTERVAL
ORDER BY created_at ASC
LIMIT batch_size;
GET DIAGNOSTICS moved_count = ROW_COUNT;
DELETE FROM domain_events
WHERE ctid IN (
SELECT ctid FROM domain_events
WHERE status = 'published'
AND published_at < NOW() - (retention_days || ' days')::INTERVAL
ORDER BY created_at ASC
LIMIT batch_size
);
RETURN moved_count;
END;
$$ LANGUAGE plpgsql;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared("DROP FUNCTION IF EXISTS cleanup_old_published_events(INT, INT);")
.await?;
manager
.drop_table(
Table::drop()
.table(Alias::new("domain_events_archive"))
.to_owned(),
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,81 @@
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> {
manager
.create_table(
Table::create()
.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")),
)
.to_owned(),
)
.await?;
// 7 天 TTL 清理函数
manager
.get_connection()
.execute_unprepared(
r#"
CREATE OR REPLACE FUNCTION cleanup_old_processed_events(
retention_days INT DEFAULT 7,
batch_size INT DEFAULT 1000
) RETURNS INT AS $$
DECLARE
deleted_count INT;
BEGIN
DELETE FROM processed_events
WHERE ctid IN (
SELECT ctid FROM processed_events
WHERE processed_at < NOW() - (retention_days || ' days')::INTERVAL
LIMIT batch_size
);
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared("DROP FUNCTION IF EXISTS cleanup_old_processed_events(INT, INT);")
.await?;
manager
.drop_table(
Table::drop()
.table(Alias::new("processed_events"))
.to_owned(),
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,75 @@
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 conn = manager.get_connection();
// PL/pgSQL 动态为所有含 tenant_id 列的表启用 RLS
conn.execute_unprepared(
r#"
DO $$
DECLARE
tbl TEXT;
BEGIN
FOR tbl IN
SELECT c.table_name FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name AND c.table_schema = t.table_schema
WHERE c.column_name = 'tenant_id'
AND c.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
ORDER BY c.table_name
LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tbl);
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', tbl);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (
current_setting(''app.current_tenant_id'', true) = ''''
OR tenant_id = current_setting(''app.current_tenant_id'', true)::uuid
)',
tbl
);
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
conn.execute_unprepared(
r#"
DO $$
DECLARE
tbl TEXT;
BEGIN
FOR tbl IN
SELECT c.table_name FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name AND c.table_schema = t.table_schema
WHERE c.column_name = 'tenant_id'
AND c.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
ORDER BY c.table_name
LOOP
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', tbl);
EXECUTE format('ALTER TABLE %I DISABLE ROW LEVEL SECURITY', tbl);
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,51 @@
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 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 record_hash TEXT")
.await?;
// 为 record_hash 创建索引(用于快速查找最新哈希)
conn.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_audit_logs_record_hash
ON audit_logs (record_hash) WHERE record_hash IS NOT NULL",
)
.await?;
// 按 tenant_id + created_at DESC 查找最新哈希的索引
conn.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created
ON audit_logs (tenant_id, created_at DESC)",
)
.await?;
Ok(())
}
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_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?;
Ok(())
}
}

View File

@@ -0,0 +1,82 @@
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 conn = manager.get_connection();
// 替换所有表的 RLS 策略:移除空字符串绕过条件
// 原策略允许 current_setting(...) = '' 时通过(绕过 RLS现在要求变量已设置且匹配
conn.execute_unprepared(
r#"
DO $$
DECLARE
tbl TEXT;
BEGIN
FOR tbl IN
SELECT c.table_name FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name AND c.table_schema = t.table_schema
WHERE c.column_name = 'tenant_id'
AND c.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
ORDER BY c.table_name
LOOP
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', tbl);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (
current_setting(''app.current_tenant_id'', true) != ''''
AND tenant_id = current_setting(''app.current_tenant_id'', true)::uuid
)',
tbl
);
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
// 回滚:恢复允许空字符串绕过的原策略
conn.execute_unprepared(
r#"
DO $$
DECLARE
tbl TEXT;
BEGIN
FOR tbl IN
SELECT c.table_name FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name AND c.table_schema = t.table_schema
WHERE c.column_name = 'tenant_id'
AND c.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
ORDER BY c.table_name
LOOP
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', tbl);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (
current_setting(''app.current_tenant_id'', true) = ''''
OR tenant_id = current_setting(''app.current_tenant_id'', true)::uuid
)',
tbl
);
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,93 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(Iden)]
enum BlindIndex {
Table,
Id,
TenantId,
EntityType,
EntityId,
FieldName,
BlindHash,
CreatedAt,
UpdatedAt,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(BlindIndex::Table)
.col(
ColumnDef::new(BlindIndex::Id)
.uuid()
.not_null()
.primary_key()
.default(PgFunc::gen_random_uuid()),
)
.col(ColumnDef::new(BlindIndex::TenantId).uuid().not_null())
.col(
ColumnDef::new(BlindIndex::EntityType)
.string_len(64)
.not_null(),
)
.col(ColumnDef::new(BlindIndex::EntityId).uuid().not_null())
.col(
ColumnDef::new(BlindIndex::FieldName)
.string_len(64)
.not_null(),
)
.col(
ColumnDef::new(BlindIndex::BlindHash)
.string_len(64)
.not_null(),
)
.col(
ColumnDef::new(BlindIndex::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(BlindIndex::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.index(
Index::create()
.col(BlindIndex::TenantId)
.col(BlindIndex::EntityType)
.col(BlindIndex::FieldName)
.col(BlindIndex::BlindHash)
.unique(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_blind_hashes")
.table(BlindIndex::Table)
.col(BlindIndex::TenantId)
.col(BlindIndex::EntityType)
.col(BlindIndex::FieldName)
.col(BlindIndex::BlindHash)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(BlindIndex::Table).to_owned())
.await
}
}

View File

@@ -0,0 +1,76 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(Iden)]
enum DeadLetterEvent {
Table,
Id,
TenantId,
OriginalEventId,
EventType,
Payload,
ConsumerId,
Attempts,
LastError,
CreatedAt,
ResolvedAt,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(DeadLetterEvent::Table)
.col(
ColumnDef::new(DeadLetterEvent::Id)
.uuid()
.not_null()
.primary_key()
.default(PgFunc::gen_random_uuid()),
)
.col(ColumnDef::new(DeadLetterEvent::TenantId).uuid())
.col(
ColumnDef::new(DeadLetterEvent::OriginalEventId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(DeadLetterEvent::EventType)
.string_len(128)
.not_null(),
)
.col(ColumnDef::new(DeadLetterEvent::Payload).json_binary())
.col(
ColumnDef::new(DeadLetterEvent::ConsumerId)
.string_len(128)
.not_null(),
)
.col(
ColumnDef::new(DeadLetterEvent::Attempts)
.integer()
.not_null()
.default(0),
)
.col(ColumnDef::new(DeadLetterEvent::LastError).text())
.col(
ColumnDef::new(DeadLetterEvent::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(DeadLetterEvent::ResolvedAt).timestamp_with_time_zone())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(DeadLetterEvent::Table).to_owned())
.await
}
}

View File

@@ -0,0 +1,116 @@
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> {
manager
.create_table(
Table::create()
.table(Alias::new("api_clients"))
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.default(Expr::cust("gen_random_uuid()")),
)
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("client_id"))
.string_len(128)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("client_secret_hash"))
.string_len(256)
.not_null(),
)
.col(
ColumnDef::new(Alias::new("client_name"))
.string_len(200)
.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("rate_limit_per_minute"))
.integer()
.not_null()
.default(60),
)
.col(
ColumnDef::new(Alias::new("is_active"))
.boolean()
.not_null()
.default(true),
)
.col(
ColumnDef::new(Alias::new("token_lifetime_seconds"))
.integer()
.not_null()
.default(3600),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::cust("NOW()")),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::cust("NOW()")),
)
.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("version"))
.integer()
.not_null()
.default(1),
)
.primary_key(Index::create().col(Alias::new("id")))
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_api_clients_client_id_unique")
.table(Alias::new("api_clients"))
.col(Alias::new("client_id"))
.unique()
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_api_clients_tenant_id")
.table(Alias::new("api_clients"))
.col(Alias::new("tenant_id"))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Alias::new("api_clients")).to_owned())
.await
}
}

View File

@@ -0,0 +1,115 @@
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. 创建触发器函数:适用于 `version` 列erp-health / erp-auth / erp-config 等所有模块)
db.execute_unprepared(
r#"
CREATE OR REPLACE FUNCTION enforce_version() RETURNS trigger AS $$
BEGIN
IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
RAISE EXCEPTION 'Optimistic lock conflict on %: expected version %, got %',
TG_TABLE_NAME, OLD.version + 1, NEW.version;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"#,
)
.await?;
// 2. 创建触发器函数:适用于 `version_lock` 列erp-ai 模块)
db.execute_unprepared(
r#"
CREATE OR REPLACE FUNCTION enforce_version_lock() RETURNS trigger AS $$
BEGIN
IF NEW.version_lock IS DISTINCT FROM OLD.version_lock + 1 THEN
RAISE EXCEPTION 'Optimistic lock conflict on %: expected version_lock %, got %',
TG_TABLE_NAME, OLD.version_lock + 1, NEW.version_lock;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"#,
)
.await?;
// 3. 自动发现所有含 version / version_lock 的表并绑定触发器
// 排除 market_entryversion 是 String非乐观锁
db.execute_unprepared(
r#"
DO $$
DECLARE
rec RECORD;
trig_name text;
BEGIN
FOR rec IN
SELECT table_name, column_name
FROM information_schema.columns
WHERE column_name IN ('version', 'version_lock')
AND table_schema = 'public'
AND table_name NOT IN ('market_entry', 'process_definitions', 'ai_prompt')
LOOP
IF rec.column_name = 'version' THEN
trig_name := 'trg_enforce_version';
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE ON %I
FOR EACH ROW EXECUTE FUNCTION enforce_version()',
trig_name, rec.table_name
);
ELSE
trig_name := 'trg_enforce_version_lock';
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE ON %I
FOR EACH ROW EXECUTE FUNCTION enforce_version_lock()',
trig_name, rec.table_name
);
END IF;
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
// 1. 删除所有乐观锁触发器
db.execute_unprepared(
r#"
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT event_object_table AS table_name, trigger_name
FROM information_schema.triggers
WHERE trigger_name IN ('trg_enforce_version', 'trg_enforce_version_lock')
LOOP
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I', rec.trigger_name, rec.table_name);
END LOOP;
END;
$$;
"#,
)
.await?;
// 2. 删除触发器函数
db.execute_unprepared("DROP FUNCTION IF EXISTS enforce_version() CASCADE")
.await?;
db.execute_unprepared("DROP FUNCTION IF EXISTS enforce_version_lock() CASCADE")
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,78 @@
//! 修复 admin 角色权限绑定
//!
//! 根因链:
//! 1. m20260506_000126 对部分角色执行了软删除SET deleted_at = NOW()
//! 2. m20260508_000131 执行 `DELETE FROM role_permissions WHERE deleted_at IS NOT NULL`
//! 物理删除了所有被软删除的记录
//! 3. m20260508_000131 只重新分配了 doctor/nurse/operator 的权限,遗漏了 admin 角色
//! 4. 后续的 assign_permissions API 调用可能在内部先软删除再 INSERT
//! INSERT 失败时 admin 权限全部丢失
//!
//! 本迁移:
//! - Step 1: 恢复所有被软删除的 admin role_permissionsdeleted_at IS NOT NULL → NULL
//! - Step 2: 插入所有缺失的 admin role_permissionsON CONFLICT DO NOTHING 保证幂等)
//!
//! 覆盖范围:全系统 128 个权限码auth/config/workflow/message/plugin/health/ai/copilot/points
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();
// ================================================================
// Step 1: 恢复被软删除的 admin role_permissions
// ================================================================
// 如果 admin 的某些权限记录仍然存在但被软删除了,恢复它们
db.execute_unprepared(
r#"
UPDATE role_permissions rp
SET deleted_at = NULL, updated_at = NOW(), version = rp.version + 1
FROM roles r
WHERE rp.role_id = r.id
AND r.code = 'admin'
AND r.deleted_at IS NULL
AND rp.deleted_at IS NOT NULL
"#,
)
.await?;
// ================================================================
// Step 2: 插入缺失的 admin role_permissions
// ================================================================
// 将 permissions 表中所有未被软删除的权限绑定到 admin 角色
// ON CONFLICT (role_id, permission_id) DO NOTHING — 已存在(含刚恢复的)的跳过
db.execute_unprepared(
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope,
created_at, updated_at, created_by, updated_by,
deleted_at, version)
SELECT r.id, p.id, r.tenant_id, 'all',
NOW(), NOW(), r.id, r.id,
NULL, 1
FROM roles r
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.deleted_at IS NULL
WHERE r.code = 'admin' AND r.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id
AND rp.permission_id = p.id
AND rp.deleted_at IS NULL
)
ON CONFLICT (role_id, permission_id) DO NOTHING
"#,
)
.await?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// 不回滚 — 这是修复性迁移admin 应该始终拥有全部权限
Ok(())
}
}

View File

@@ -0,0 +1,65 @@
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 conn = manager.get_connection();
// 为 m000088 之后创建的新表补充 RLS 策略。
// 幂等操作:仅影响尚未启用 RLS 或缺少策略的表。
conn.execute_unprepared(
r#"
DO $$
DECLARE
tbl TEXT;
policy_exists BOOLEAN;
BEGIN
FOR tbl IN
SELECT c.table_name FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name AND c.table_schema = t.table_schema
WHERE c.column_name = 'tenant_id'
AND c.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
ORDER BY c.table_name
LOOP
-- 启用 RLS幂等
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tbl);
-- 检查是否已有 tenant_isolation 策略
SELECT EXISTS(
SELECT 1 FROM pg_policies
WHERE tablename = tbl
AND policyname = 'tenant_isolation'
) INTO policy_exists;
IF NOT policy_exists THEN
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (
current_setting(''app.current_tenant_id'', true) != ''''
AND tenant_id = current_setting(''app.current_tenant_id'', true)::uuid
)',
tbl
);
RAISE NOTICE 'Created RLS policy for table: %', tbl;
END IF;
END LOOP;
END;
$$;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 回滚不需要移除 RLS保持 m000088 的策略不变
// 此迁移补充的 RLS 策略在 down() 中保留,因为 m000088 已处理回滚
let _ = manager;
Ok(())
}
}