use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreatePositionReq, PositionResp}; use crate::entity::department; use crate::entity::position; use crate::error::{AuthError, AuthResult}; use erp_core::events::EventBus; /// Position CRUD service -- create, read, update, soft-delete positions /// within a department. pub struct PositionService; impl PositionService { /// List all positions for a department within the given tenant. pub async fn list( dept_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { // Verify the department exists let _dept = department::Entity::find_by_id(dept_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()))?; let items = position::Entity::find() .filter(position::Column::TenantId.eq(tenant_id)) .filter(position::Column::DeptId.eq(dept_id)) .filter(position::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(items .iter() .map(|p| PositionResp { id: p.id, dept_id: p.dept_id, name: p.name.clone(), code: p.code.clone(), level: p.level, sort_order: p.sort_order, }) .collect()) } /// Create a new position under the specified department. pub async fn create( dept_id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &CreatePositionReq, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult { // Verify the department exists let _dept = department::Entity::find_by_id(dept_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 code uniqueness within tenant if code is provided if let Some(ref code) = req.code { let existing = position::Entity::find() .filter(position::Column::TenantId.eq(tenant_id)) .filter(position::Column::Code.eq(code.as_str())) .filter(position::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 now = Utc::now(); let id = Uuid::now_v7(); let model = position::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), dept_id: Set(dept_id), name: Set(req.name.clone()), code: Set(req.code.clone()), level: Set(req.level.unwrap_or(1)), 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( "position.created", tenant_id, serde_json::json!({ "position_id": id, "dept_id": dept_id, "name": req.name }), )); Ok(PositionResp { id, dept_id, name: req.name.clone(), code: req.code.clone(), level: req.level.unwrap_or(1), sort_order: req.sort_order.unwrap_or(0), }) } /// Update editable position fields (name, code, level, sort_order). pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, name: &Option, code: &Option, level: &Option, sort_order: &Option, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = position::Entity::find_by_id(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()))?; // If code is being changed, check uniqueness if let Some(new_code) = code { if Some(new_code) != model.code.as_ref() { let existing = position::Entity::find() .filter(position::Column::TenantId.eq(tenant_id)) .filter(position::Column::Code.eq(new_code.as_str())) .filter(position::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 mut active: position::ActiveModel = model.into(); if let Some(n) = name { active.name = Set(n.clone()); } if let Some(c) = code { active.code = Set(Some(c.clone())); } if let Some(l) = level { active.level = Set(*l); } if let Some(so) = sort_order { active.sort_order = Set(*so); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); let updated = active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(PositionResp { id: updated.id, dept_id: updated.dept_id, name: updated.name.clone(), code: updated.code.clone(), level: updated.level, sort_order: updated.sort_order, }) } /// Soft-delete a position 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 = position::Entity::find_by_id(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 mut active: position::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| AuthError::Validation(e.to_string()))?; event_bus.publish(erp_core::events::DomainEvent::new( "position.deleted", tenant_id, serde_json::json!({ "position_id": id }), )); Ok(()) } }