use chrono::Utc; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set, }; use uuid::Uuid; use crate::dto::{DictionaryItemResp, DictionaryResp}; use crate::entity::{dictionary, dictionary_item}; use crate::error::{ConfigError, ConfigResult}; use erp_core::events::EventBus; use erp_core::types::Pagination; /// Dictionary CRUD service — manage dictionaries and their items within a tenant. /// /// Dictionaries provide enumerated value sets (e.g. status codes, categories) /// that can be referenced throughout the system by their unique `code`. pub struct DictionaryService; impl DictionaryService { /// List dictionaries within a tenant with pagination. /// /// Each dictionary includes its associated items. /// Returns `(dictionaries, total_count)`. pub async fn list( tenant_id: Uuid, pagination: &Pagination, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<(Vec, u64)> { let paginator = dictionary::Entity::find() .filter(dictionary::Column::TenantId.eq(tenant_id)) .filter(dictionary::Column::DeletedAt.is_null()) .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) as u64; let models = paginator .fetch_page(page_index) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let mut resps = Vec::with_capacity(models.len()); for m in &models { let items = Self::fetch_items(m.id, tenant_id, db).await?; resps.push(DictionaryResp { id: m.id, name: m.name.clone(), code: m.code.clone(), description: m.description.clone(), items, }); } Ok((resps, total)) } /// Fetch a single dictionary by ID, scoped to the given tenant. /// /// Includes all associated items. pub async fn get_by_id( id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { let model = dictionary::Entity::find_by_id(id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?; let items = Self::fetch_items(model.id, tenant_id, db).await?; Ok(DictionaryResp { id: model.id, name: model.name.clone(), code: model.code.clone(), description: model.description.clone(), items, }) } /// Create a new dictionary within the current tenant. /// /// Validates code uniqueness, then inserts the record and publishes /// a `dictionary.created` domain event. pub async fn create( tenant_id: Uuid, operator_id: Uuid, name: &str, code: &str, description: &Option, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> ConfigResult { // Check code uniqueness within tenant let existing = dictionary::Entity::find() .filter(dictionary::Column::TenantId.eq(tenant_id)) .filter(dictionary::Column::Code.eq(code)) .filter(dictionary::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; if existing.is_some() { return Err(ConfigError::Validation("字典编码已存在".to_string())); } let now = Utc::now(); let id = Uuid::now_v7(); let model = dictionary::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(name.to_string()), code: Set(code.to_string()), description: Set(description.clone()), 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), }; model .insert(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus.publish(erp_core::events::DomainEvent::new( "dictionary.created", tenant_id, serde_json::json!({ "dictionary_id": id, "code": code }), )); Ok(DictionaryResp { id, name: name.to_string(), code: code.to_string(), description: description.clone(), items: vec![], }) } /// Update editable dictionary fields (name and description). /// /// Code cannot be changed after creation. pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, name: &Option, description: &Option, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { let model = dictionary::Entity::find_by_id(id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?; let mut active: dictionary::ActiveModel = model.into(); if let Some(n) = name { active.name = Set(n.clone()); } if let Some(d) = description { active.description = Set(Some(d.clone())); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); let updated = active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let items = Self::fetch_items(updated.id, tenant_id, db).await?; Ok(DictionaryResp { id: updated.id, name: updated.name.clone(), code: updated.code.clone(), description: updated.description.clone(), items, }) } /// Soft-delete a dictionary by setting the `deleted_at` timestamp. pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> ConfigResult<()> { let model = dictionary::Entity::find_by_id(id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?; let mut active: dictionary::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus.publish(erp_core::events::DomainEvent::new( "dictionary.deleted", tenant_id, serde_json::json!({ "dictionary_id": id }), )); Ok(()) } /// Add a new item to a dictionary. /// /// Validates that the item `value` is unique within the dictionary. pub async fn add_item( dictionary_id: Uuid, tenant_id: Uuid, operator_id: Uuid, label: &str, value: &str, sort_order: i32, color: &Option, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { // Verify the dictionary exists and belongs to this tenant let _dict = dictionary::Entity::find_by_id(dictionary_id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?; // Check value uniqueness within dictionary let existing = dictionary_item::Entity::find() .filter(dictionary_item::Column::DictionaryId.eq(dictionary_id)) .filter(dictionary_item::Column::TenantId.eq(tenant_id)) .filter(dictionary_item::Column::Value.eq(value)) .filter(dictionary_item::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; if existing.is_some() { return Err(ConfigError::Validation("字典项值已存在".to_string())); } let now = Utc::now(); let id = Uuid::now_v7(); let model = dictionary_item::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), dictionary_id: Set(dictionary_id), label: Set(label.to_string()), value: Set(value.to_string()), sort_order: Set(sort_order), color: Set(color.clone()), 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), }; model .insert(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; Ok(DictionaryItemResp { id, dictionary_id, label: label.to_string(), value: value.to_string(), sort_order, color: color.clone(), }) } /// Update editable dictionary item fields (label, value, sort_order, color). pub async fn update_item( item_id: Uuid, tenant_id: Uuid, operator_id: Uuid, label: &Option, value: &Option, sort_order: &Option, color: &Option, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { let model = dictionary_item::Entity::find_by_id(item_id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?; let mut active: dictionary_item::ActiveModel = model.into(); if let Some(l) = label { active.label = Set(l.clone()); } if let Some(v) = value { active.value = Set(v.clone()); } if let Some(s) = sort_order { active.sort_order = Set(*s); } if let Some(c) = color { active.color = Set(Some(c.clone())); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); let updated = active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; Ok(DictionaryItemResp { id: updated.id, dictionary_id: updated.dictionary_id, label: updated.label.clone(), value: updated.value.clone(), sort_order: updated.sort_order, color: updated.color.clone(), }) } /// Soft-delete a dictionary item by setting the `deleted_at` timestamp. pub async fn delete_item( item_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<()> { let model = dictionary_item::Entity::find_by_id(item_id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?; let mut active: dictionary_item::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; Ok(()) } /// Look up a dictionary by its `code` and return all items. /// /// Useful for frontend dropdowns and enum-like lookups. pub async fn list_items_by_code( code: &str, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult> { let dict = dictionary::Entity::find() .filter(dictionary::Column::TenantId.eq(tenant_id)) .filter(dictionary::Column::Code.eq(code)) .filter(dictionary::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .ok_or_else(|| ConfigError::NotFound(format!("字典编码 '{}' 不存在", code)))?; Self::fetch_items(dict.id, tenant_id, db).await } // ---- 内部辅助方法 ---- /// Fetch all non-deleted items for a given dictionary. async fn fetch_items( dictionary_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult> { let items = dictionary_item::Entity::find() .filter(dictionary_item::Column::DictionaryId.eq(dictionary_id)) .filter(dictionary_item::Column::TenantId.eq(tenant_id)) .filter(dictionary_item::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; Ok(items .iter() .map(|i| DictionaryItemResp { id: i.id, dictionary_id: i.dictionary_id, label: i.label.clone(), value: i.value.clone(), sort_order: i.sort_order, color: i.color.clone(), }) .collect()) } }