use std::collections::HashMap; use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreateOrganizationReq, OrganizationResp, UpdateOrganizationReq}; 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; /// Organization CRUD service -- create, read, update, soft-delete organizations /// within a tenant, supporting tree-structured hierarchy with path and level. pub struct OrgService; impl OrgService { /// Fetch all organizations for a tenant as a flat list (not deleted). pub async fn list_flat( tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { let items = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(items) } /// Fetch all organizations for a tenant as a nested tree. /// /// Root nodes have `parent_id = None`. Children are grouped by `parent_id`. pub async fn get_tree( tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { let items = Self::list_flat(tenant_id, db).await?; Ok(build_org_tree(&items)) } /// Create a new organization within the current tenant. /// /// If `parent_id` is provided, computes `path` from the parent's path and id, /// and sets `level = parent.level + 1`. Otherwise, level defaults to 1. pub async fn create( tenant_id: Uuid, operator_id: Uuid, req: &CreateOrganizationReq, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult { // Check code uniqueness within tenant if code is provided if let Some(ref code) = req.code { let existing = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Code.eq(code.as_str())) .filter(organization::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())); } } // Check name uniqueness within tenant let name_exists = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Name.eq(&req.name)) .filter(organization::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if name_exists.is_some() { return Err(AuthError::Validation("组织名称已存在".to_string())); } let (path, level) = if let Some(parent_id) = req.parent_id { let parent = organization::Entity::find_by_id(parent_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|p| p.tenant_id == tenant_id && p.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("父级组织不存在".to_string()))?; let parent_path = parent.path.clone().unwrap_or_default(); let computed_path = format!("{}{}/", parent_path, parent.id); (Some(computed_path), parent.level + 1) } else { (None, 1) }; let now = Utc::now(); let id = Uuid::now_v7(); let model = organization::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(req.name.clone()), code: Set(req.code.clone()), parent_id: Set(req.parent_id), path: Set(path), level: Set(level), 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( "organization.created", tenant_id, serde_json::json!({ "org_id": id, "name": req.name }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "organization.create", "organization", ) .with_resource_id(id), db, ) .await; Ok(OrganizationResp { id, name: req.name.clone(), code: req.code.clone(), parent_id: req.parent_id, path: None, level, sort_order: req.sort_order.unwrap_or(0), children: vec![], version: 1, }) } /// Update editable organization fields (name, code, sort_order). pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &UpdateOrganizationReq, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = organization::Entity::find_by_id(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()))?; // If code is being changed, check uniqueness if let Some(ref new_code) = req.code && Some(new_code) != model.code.as_ref() { let existing = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Code.eq(new_code.as_str())) .filter(organization::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())); } } // If name is being changed, check uniqueness (exclude self) if let Some(ref new_name) = req.name && new_name != &model.name { let name_exists = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::Name.eq(new_name.as_str())) .filter(organization::Column::DeletedAt.is_null()) .filter(organization::Column::Id.ne(id)) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if name_exists.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: organization::ActiveModel = model.into(); if let Some(ref name) = req.name { active.name = Set(name.clone()); } if let Some(ref code) = req.code { active.code = Set(Some(code.clone())); } if let Some(sort_order) = req.sort_order { active.sort_order = Set(sort_order); } 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), "organization.update", "organization", ) .with_resource_id(id), db, ) .await; Ok(OrganizationResp { id: updated.id, name: updated.name.clone(), code: updated.code.clone(), parent_id: updated.parent_id, path: updated.path.clone(), level: updated.level, sort_order: updated.sort_order, children: vec![], version: updated.version, }) } /// Soft-delete an organization 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, ) -> AuthResult<()> { let model = organization::Entity::find_by_id(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 for child organizations let children = organization::Entity::find() .filter(organization::Column::TenantId.eq(tenant_id)) .filter(organization::Column::ParentId.eq(id)) .filter(organization::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(), )); } // Check for departments under this organization let depts = department::Entity::find() .filter(department::Column::TenantId.eq(tenant_id)) .filter(department::Column::OrgId.eq(id)) .filter(department::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; if depts.is_some() { return Err(AuthError::Validation( "该组织下存在部门,无法删除".to_string(), )); } let current_version = model.version; let mut active: organization::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( "organization.deleted", tenant_id, serde_json::json!({ "org_id": id }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "organization.delete", "organization", ) .with_resource_id(id), db, ) .await; Ok(()) } } /// Build a nested tree of `OrganizationResp` from a flat list of models. /// /// Root nodes (parent_id = None) form the top level. Each node recursively /// includes its children grouped by parent_id. pub(crate) fn build_org_tree(items: &[organization::Model]) -> Vec { let mut children_map: HashMap, Vec<&organization::Model>> = HashMap::new(); for item in items { children_map.entry(item.parent_id).or_default().push(item); } fn build_node( item: &organization::Model, map: &HashMap, Vec<&organization::Model>>, ) -> OrganizationResp { let children = map .get(&Some(item.id)) .map(|items| items.iter().map(|i| build_node(i, map)).collect()) .unwrap_or_default(); OrganizationResp { id: item.id, name: item.name.clone(), code: item.code.clone(), parent_id: item.parent_id, path: item.path.clone(), level: item.level, 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() } #[cfg(test)] mod tests { use chrono::Utc; use uuid::Uuid; use crate::entity::organization; use super::*; fn make_org( id: Uuid, tenant_id: Uuid, name: &str, parent_id: Option, level: i32, version: i32, ) -> organization::Model { organization::Model { id, tenant_id, name: name.to_string(), code: None, parent_id, path: None, level, sort_order: 0, created_at: Utc::now(), updated_at: Utc::now(), created_by: Uuid::now_v7(), updated_by: Uuid::now_v7(), deleted_at: None, version, } } #[test] fn build_org_tree_empty() { let tree = build_org_tree(&[]); assert!(tree.is_empty()); } #[test] fn build_org_tree_single_root() { let tid = Uuid::now_v7(); let root_id = Uuid::now_v7(); let items = vec![make_org(root_id, tid, "总公司", None, 1, 1)]; let tree = build_org_tree(&items); assert_eq!(tree.len(), 1); assert_eq!(tree[0].name, "总公司"); assert!(tree[0].children.is_empty()); } #[test] fn build_org_tree_multiple_roots() { let tid = Uuid::now_v7(); let items = vec![ make_org(Uuid::now_v7(), tid, "公司A", None, 1, 1), make_org(Uuid::now_v7(), tid, "公司B", None, 1, 1), ]; let tree = build_org_tree(&items); assert_eq!(tree.len(), 2); } #[test] fn build_org_tree_nested_children() { let tid = Uuid::now_v7(); let root_id = Uuid::now_v7(); let child1_id = Uuid::now_v7(); let child2_id = Uuid::now_v7(); let grandchild_id = Uuid::now_v7(); let items = vec![ make_org(root_id, tid, "总公司", None, 1, 1), make_org(child1_id, tid, "分公司A", Some(root_id), 2, 1), make_org(child2_id, tid, "分公司B", Some(root_id), 2, 1), make_org(grandchild_id, tid, "部门A1", Some(child1_id), 3, 1), ]; let tree = build_org_tree(&items); assert_eq!(tree.len(), 1); // one root assert_eq!(tree[0].children.len(), 2); // two children assert_eq!(tree[0].children[0].children.len(), 1); // one grandchild assert_eq!(tree[0].children[0].children[0].name, "部门A1"); } #[test] fn build_org_tree_deep_nesting() { let tid = Uuid::now_v7(); let l1 = Uuid::now_v7(); let l2 = Uuid::now_v7(); let l3 = Uuid::now_v7(); let l4 = Uuid::now_v7(); let items = vec![ make_org(l1, tid, "L1", None, 1, 1), make_org(l2, tid, "L2", Some(l1), 2, 1), make_org(l3, tid, "L3", Some(l2), 3, 1), make_org(l4, tid, "L4", Some(l3), 4, 1), ]; let tree = build_org_tree(&items); assert_eq!(tree.len(), 1); assert_eq!(tree[0].children[0].children[0].children[0].name, "L4"); } #[test] fn build_org_tree_preserves_version() { let tid = Uuid::now_v7(); let root_id = Uuid::now_v7(); let items = vec![make_org(root_id, tid, "测试", None, 1, 5)]; let tree = build_org_tree(&items); assert_eq!(tree[0].version, 5); } }