Files
hms/crates/erp-auth/src/service/dept_service.rs
iven 39a12500e3 fix(security): Q2 Chunk 1 — 密钥外部化与启动强制检查
- default.toml 敏感值改为占位符,强制通过环境变量注入
- 启动时拒绝默认 JWT 密钥和数据库 URL
- 移除 super_admin_password 硬编码 fallback
- 移除 From<AppError> for AuthError 反向映射,5 处调用点改为显式 map_err
- .gitignore 添加 .test_token 和测试产物
2026-04-17 17:42:19 +08:00

354 lines
12 KiB
Rust

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<Vec<DepartmentResp>> {
// 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<DepartmentResp> {
// 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<DepartmentResp> {
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<DepartmentResp> {
let mut children_map: HashMap<Option<Uuid>, 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<Option<Uuid>, 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()
}