use std::collections::HashMap; use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreateDepartmentReq, DepartmentResp, UpdateDepartmentReq}; use crate::entity::department; use crate::entity::organization; use crate::error::{AuthError, AuthResult}; use erp_core::audit::AuditLog; use erp_core::audit_service; use erp_core::error::check_version; use erp_core::events::EventBus; /// Department CRUD service -- create, read, update, soft-delete departments /// within an organization, supporting tree-structured hierarchy. pub struct DeptService; impl DeptService { /// Fetch all departments for an organization as a nested tree. /// /// Root departments (parent_id = None) form the top level. pub async fn list_tree( org_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { // Verify the organization exists let _org = organization::Entity::find_by_id(org_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|o| o.tenant_id == tenant_id && o.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("组织不存在".to_string()))?; let items = department::Entity::find() .filter(department::Column::TenantId.eq(tenant_id)) .filter(department::Column::OrgId.eq(org_id)) .filter(department::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(build_dept_tree(&items)) } /// Create a new department under the specified organization. /// /// If `parent_id` is provided, computes `path` from the parent department. /// Otherwise, path is computed from the organization root. pub async fn create( org_id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &CreateDepartmentReq, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult { // Verify the organization exists let org = organization::Entity::find_by_id(org_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|o| o.tenant_id == tenant_id && o.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("组织不存在".to_string()))?; // Check code uniqueness within tenant if code is provided if let Some(ref code) = req.code { let existing = department::Entity::find() .filter(department::Column::TenantId.eq(tenant_id)) .filter(department::Column::Code.eq(code.as_str())) .filter(department::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("部门编码已存在".to_string())); } } // Compute path from parent department or organization root let path = if let Some(parent_id) = req.parent_id { let parent = department::Entity::find_by_id(parent_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|d| { d.tenant_id == tenant_id && d.org_id == org_id && d.deleted_at.is_none() }) .ok_or_else(|| AuthError::Validation("父级部门不存在".to_string()))?; let parent_path = parent.path.clone().unwrap_or_default(); Some(format!("{}{}/", parent_path, parent.id)) } else { // Root department under the organization let org_path = org.path.clone().unwrap_or_default(); Some(format!("{}{}/", org_path, org.id)) }; let now = Utc::now(); let id = Uuid::now_v7(); let model = department::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), org_id: Set(org_id), name: Set(req.name.clone()), code: Set(req.code.clone()), parent_id: Set(req.parent_id), manager_id: Set(req.manager_id), path: Set(path), sort_order: Set(req.sort_order.unwrap_or(0)), 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| AuthError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "department.created", tenant_id, serde_json::json!({ "dept_id": id, "org_id": org_id, "name": req.name }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "department.create", "department", ) .with_resource_id(id), db, ) .await; Ok(DepartmentResp { id, org_id, name: req.name.clone(), code: req.code.clone(), parent_id: req.parent_id, manager_id: req.manager_id, path: None, sort_order: req.sort_order.unwrap_or(0), children: vec![], version: 1, }) } /// Update editable department fields (name, code, manager_id, sort_order). pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &UpdateDepartmentReq, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = department::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("部门不存在".to_string()))?; // If code is being changed, check uniqueness if let Some(new_code) = &req.code && Some(new_code) != model.code.as_ref() { let existing = department::Entity::find() .filter(department::Column::TenantId.eq(tenant_id)) .filter(department::Column::Code.eq(new_code.as_str())) .filter(department::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("部门编码已存在".to_string())); } } let next_ver = check_version(req.version, model.version) .map_err(|e| AuthError::Validation(e.to_string()))?; let mut active: department::ActiveModel = model.into(); if let Some(n) = &req.name { active.name = Set(n.clone()); } if let Some(c) = &req.code { active.code = Set(Some(c.clone())); } if let Some(mgr_id) = &req.manager_id { active.manager_id = Set(Some(*mgr_id)); } if let Some(so) = &req.sort_order { active.sort_order = Set(*so); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_ver); let updated = active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "department.update", "department", ) .with_resource_id(id), db, ) .await; Ok(DepartmentResp { id: updated.id, org_id: updated.org_id, name: updated.name.clone(), code: updated.code.clone(), parent_id: updated.parent_id, manager_id: updated.manager_id, path: updated.path.clone(), sort_order: updated.sort_order, children: vec![], version: updated.version, }) } /// Soft-delete a department by setting the `deleted_at` timestamp. /// /// Will not delete if child departments exist. pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult<()> { let model = department::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("部门不存在".to_string()))?; // Check for child departments let children = department::Entity::find() .filter(department::Column::TenantId.eq(tenant_id)) .filter(department::Column::ParentId.eq(id)) .filter(department::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if children.is_some() { return Err(AuthError::Validation( "该部门下存在子部门,无法删除".to_string(), )); } let current_version = model.version; let mut active: department::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(current_version + 1); active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "department.deleted", tenant_id, serde_json::json!({ "dept_id": id }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "department.delete", "department", ) .with_resource_id(id), db, ) .await; Ok(()) } } /// Build a nested tree of `DepartmentResp` from a flat list of models. fn build_dept_tree(items: &[department::Model]) -> Vec { let mut children_map: HashMap, Vec<&department::Model>> = HashMap::new(); for item in items { children_map.entry(item.parent_id).or_default().push(item); } fn build_node( item: &department::Model, map: &HashMap, Vec<&department::Model>>, ) -> DepartmentResp { let children = map .get(&Some(item.id)) .map(|items| items.iter().map(|i| build_node(i, map)).collect()) .unwrap_or_default(); DepartmentResp { id: item.id, org_id: item.org_id, name: item.name.clone(), code: item.code.clone(), parent_id: item.parent_id, manager_id: item.manager_id, path: item.path.clone(), sort_order: item.sort_order, children, version: item.version, } } children_map .get(&None) .map(|root_items| { root_items .iter() .map(|item| build_node(item, &children_map)) .collect() }) .unwrap_or_default() }