diff --git a/crates/erp-core/src/audit.rs b/crates/erp-core/src/audit.rs new file mode 100644 index 0000000..68a6275 --- /dev/null +++ b/crates/erp-core/src/audit.rs @@ -0,0 +1,67 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// 审计日志记录。 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditLog { + pub id: Uuid, + pub tenant_id: Uuid, + pub user_id: Option, + pub action: String, + pub resource_type: String, + pub resource_id: Option, + pub old_value: Option, + pub new_value: Option, + pub ip_address: Option, + pub user_agent: Option, + pub created_at: chrono::DateTime, +} + +impl AuditLog { + /// 创建一条审计日志记录。 + pub fn new( + tenant_id: Uuid, + user_id: Option, + action: impl Into, + resource_type: impl Into, + ) -> Self { + Self { + id: Uuid::now_v7(), + tenant_id, + user_id, + action: action.into(), + resource_type: resource_type.into(), + resource_id: None, + old_value: None, + new_value: None, + ip_address: None, + user_agent: None, + created_at: Utc::now(), + } + } + + /// 设置资源 ID。 + pub fn with_resource_id(mut self, id: Uuid) -> Self { + self.resource_id = Some(id); + self + } + + /// 设置变更前后的值。 + pub fn with_changes( + mut self, + old: Option, + new: Option, + ) -> Self { + self.old_value = old; + self.new_value = new; + self + } + + /// 设置请求来源信息。 + pub fn with_request_info(mut self, ip: Option, user_agent: Option) -> Self { + self.ip_address = ip; + self.user_agent = user_agent; + self + } +} diff --git a/crates/erp-core/src/lib.rs b/crates/erp-core/src/lib.rs index 982bcae..3fcf792 100644 --- a/crates/erp-core/src/lib.rs +++ b/crates/erp-core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod audit; pub mod error; pub mod events; pub mod module; diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index e830edc..3237272 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -25,6 +25,7 @@ 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; pub struct Migrator; @@ -57,6 +58,7 @@ impl MigratorTrait for Migrator { 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), ] } } diff --git a/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs b/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs new file mode 100644 index 0000000..b604ca8 --- /dev/null +++ b/crates/erp-server/migration/src/m20260413_000026_create_audit_logs.rs @@ -0,0 +1,68 @@ +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, +}