Files
erp/crates/erp-auth/src/service/position_service.rs
iven 8a012f6c6a feat(auth): add org/dept/position management, user page, and Phase 2 completion
Complete Phase 2 identity & authentication module:
- Organization CRUD with tree structure (parent_id + materialized path)
- Department CRUD nested under organizations with tree support
- Position CRUD nested under departments
- User management page with table, create/edit modal, role assignment
- Organization architecture page with 3-panel tree layout
- Frontend API layer for orgs/depts/positions
- Sidebar navigation updated with organization menu item
- Fix parse_ttl edge case for strings ending in 'd' (e.g. "invalid")
2026-04-11 04:00:32 +08:00

219 lines
7.4 KiB
Rust

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<Vec<PositionResp>> {
// 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<PositionResp> {
// 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<String>,
code: &Option<String>,
level: &Option<i32>,
sort_order: &Option<i32>,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<PositionResp> {
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(())
}
}