use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::SettingResp; use crate::entity::setting; use crate::error::{ConfigError, ConfigResult}; use erp_core::audit::AuditLog; use erp_core::audit_service; use erp_core::error::check_version; use erp_core::events::EventBus; use erp_core::types::Pagination; /// Setting scope hierarchy constants. const SCOPE_PLATFORM: &str = "platform"; const SCOPE_TENANT: &str = "tenant"; const SCOPE_ORG: &str = "org"; const SCOPE_USER: &str = "user"; /// Setting CRUD service — manage hierarchical configuration values. /// /// Settings support a 4-level inheritance hierarchy: /// `user -> org -> tenant -> platform` /// /// When reading a setting, if the exact scope+scope_id match is not found, /// the service walks up the hierarchy to find the nearest ancestor value. pub struct SettingService; impl SettingService { /// Get a setting value with hierarchical fallback. /// /// Resolution order: /// 1. Exact match at (scope, scope_id) /// 2. Walk up the hierarchy based on scope: /// - `user` -> org -> tenant -> platform /// - `org` -> tenant -> platform /// - `tenant` -> platform /// - `platform` -> NotFound pub async fn get( key: &str, scope: &str, scope_id: &Option, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { // 1. Try exact match if let Some(resp) = Self::find_exact(key, scope, scope_id, tenant_id, db).await? { return Ok(resp); } // 2. Walk up the hierarchy based on scope let fallback_chain = Self::fallback_chain(scope, scope_id, tenant_id)?; for (fb_scope, fb_scope_id) in fallback_chain { if let Some(resp) = Self::find_exact(key, &fb_scope, &fb_scope_id, tenant_id, db).await? { return Ok(resp); } } Err(ConfigError::NotFound(format!( "设置 '{}' 在 '{}' 作用域下不存在", key, scope ))) } /// Set a setting value. Creates or updates. /// /// If a record with the same (scope, scope_id, key) exists and is not /// soft-deleted, it will be updated. Otherwise a new record is inserted. pub async fn set( params: crate::dto::SetSettingParams, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> ConfigResult { // Look for an existing non-deleted record let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(¶ms.scope)) .filter(setting::Column::SettingKey.eq(¶ms.key)) .filter(setting::Column::DeletedAt.is_null()); query = match params.scope_id { Some(id) => query.filter(setting::Column::ScopeId.eq(id)), None => query.filter(setting::Column::ScopeId.is_null()), }; let existing = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; if let Some(model) = existing { // Update existing record — 乐观锁校验 let next_version = match params.version { Some(v) => { check_version(v, model.version).map_err(|_| ConfigError::VersionMismatch)? } None => model.version + 1, }; let mut active: setting::ActiveModel = model.into(); active.setting_value = Set(params.value.clone()); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_version); let updated = active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "setting.updated", tenant_id, serde_json::json!({ "setting_id": updated.id, "key": params.key, "scope": params.scope, }), ), db, ) .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "setting.upsert", "setting") .with_resource_id(updated.id), db, ) .await; Ok(Self::model_to_resp(&updated)) } else { // Insert new record let now = Utc::now(); let id = Uuid::now_v7(); let model = setting::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), scope: Set(params.scope.clone()), scope_id: Set(params.scope_id), setting_key: Set(params.key.clone()), setting_value: Set(params.value), created_at: Set(now), updated_at: Set(now), created_by: Set(operator_id), updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), }; let inserted = model .insert(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "setting.created", tenant_id, serde_json::json!({ "setting_id": id, "key": params.key, "scope": params.scope, }), ), db, ) .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "setting.upsert", "setting") .with_resource_id(id), db, ) .await; Ok(Self::model_to_resp(&inserted)) } } /// List all settings for a specific scope and scope_id, with pagination. pub async fn list_by_scope( scope: &str, scope_id: &Option, tenant_id: Uuid, pagination: &Pagination, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<(Vec, u64)> { let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) .filter(setting::Column::DeletedAt.is_null()); query = match scope_id { Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), None => query.filter(setting::Column::ScopeId.is_null()), }; let paginator = query.paginate(db, pagination.limit()); let total = paginator .num_items() .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let page_index = pagination.page.unwrap_or(1).saturating_sub(1); let models = paginator .fetch_page(page_index) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let resps: Vec = models.iter().map(Self::model_to_resp).collect(); Ok((resps, total)) } /// Soft-delete a setting by setting the `deleted_at` timestamp. /// Performs optimistic locking via version check. pub async fn delete( key: &str, scope: &str, scope_id: &Option, tenant_id: Uuid, operator_id: Uuid, version: i32, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<()> { let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) .filter(setting::Column::SettingKey.eq(key)) .filter(setting::Column::DeletedAt.is_null()); query = match scope_id { Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), None => query.filter(setting::Column::ScopeId.is_null()), }; let model = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .ok_or_else(|| { ConfigError::NotFound(format!("设置 '{}' 在 '{}' 作用域下不存在", key, scope)) })?; let next_version = check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?; let setting_id = model.id; let mut active: setting::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_version); active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "setting.delete", "setting") .with_resource_id(setting_id), db, ) .await; Ok(()) } // ---- 内部辅助方法 ---- /// Find an exact setting match by key, scope, and scope_id. async fn find_exact( key: &str, scope: &str, scope_id: &Option, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult> { let mut query = setting::Entity::find() .filter(setting::Column::TenantId.eq(tenant_id)) .filter(setting::Column::Scope.eq(scope)) .filter(setting::Column::SettingKey.eq(key)) .filter(setting::Column::DeletedAt.is_null()); // SQL 中 `= NULL` 永远返回 false,必须用 IS NULL 匹配 NULL 值 query = match scope_id { Some(id) => query.filter(setting::Column::ScopeId.eq(*id)), None => query.filter(setting::Column::ScopeId.is_null()), }; let model = query .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; Ok(model.as_ref().map(Self::model_to_resp)) } /// Build the fallback chain for hierarchical lookup. /// /// Returns a list of (scope, scope_id) tuples to try in order. fn fallback_chain( scope: &str, _scope_id: &Option, tenant_id: Uuid, ) -> ConfigResult)>> { match scope { SCOPE_USER => { // user -> org -> tenant -> platform // Note: We cannot resolve the actual org_id from user scope here // without a dependency on auth module. The caller should handle // org-level resolution externally if needed. We skip org fallback // and go directly to tenant. Ok(vec![ (SCOPE_TENANT.to_string(), Some(tenant_id)), (SCOPE_PLATFORM.to_string(), None), ]) } SCOPE_ORG => Ok(vec![ (SCOPE_TENANT.to_string(), Some(tenant_id)), (SCOPE_PLATFORM.to_string(), None), ]), SCOPE_TENANT => Ok(vec![(SCOPE_PLATFORM.to_string(), None)]), SCOPE_PLATFORM => Ok(vec![]), _ => Err(ConfigError::Validation(format!( "不支持的作用域类型: '{}'", scope ))), } } /// Convert a SeaORM model to a response DTO. fn model_to_resp(model: &setting::Model) -> SettingResp { SettingResp { id: model.id, scope: model.scope.clone(), scope_id: model.scope_id, setting_key: model.setting_key.clone(), setting_value: model.setting_value.clone(), version: model.version, } } }