use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{PermissionResp, RoleResp}; use crate::entity::{permission, role, role_permission}; use crate::error::AuthError; use crate::error::AuthResult; use erp_core::audit::AuditLog; use erp_core::audit_service; use erp_core::error::check_version; use erp_core::events::EventBus; use erp_core::types::Pagination; /// Role CRUD service — create, read, update, soft-delete roles within a tenant, /// and manage role-permission assignments. pub struct RoleService; impl RoleService { /// List roles within a tenant with pagination. /// /// Returns `(roles, total_count)`. pub async fn list( tenant_id: Uuid, pagination: &Pagination, db: &sea_orm::DatabaseConnection, ) -> AuthResult<(Vec, u64)> { let paginator = role::Entity::find() .filter(role::Column::TenantId.eq(tenant_id)) .filter(role::Column::DeletedAt.is_null()) .paginate(db, pagination.limit()); let total = paginator .num_items() .await .map_err(|e| AuthError::Validation(e.to_string()))?; let page_index = pagination.page.unwrap_or(1).saturating_sub(1); let models = paginator .fetch_page(page_index) .await .map_err(|e| AuthError::Validation(e.to_string()))?; let resps: Vec = models .iter() .map(|m| RoleResp { id: m.id, name: m.name.clone(), code: m.code.clone(), description: m.description.clone(), is_system: m.is_system, version: m.version, }) .collect(); Ok((resps, total)) } /// Fetch a single role by ID, scoped to the given tenant. pub async fn get_by_id( id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = role::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?; Ok(RoleResp { id: model.id, name: model.name.clone(), code: model.code.clone(), description: model.description.clone(), is_system: model.is_system, version: model.version, }) } /// Create a new role within the current tenant. /// /// Validates code uniqueness, then inserts the record and publishes /// a `role.created` domain event. pub async fn create( tenant_id: Uuid, operator_id: Uuid, name: &str, code: &str, description: &Option, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult { // Check code uniqueness within tenant let existing = role::Entity::find() .filter(role::Column::TenantId.eq(tenant_id)) .filter(role::Column::Code.eq(code)) .filter(role::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 = role::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(name.to_string()), code: Set(code.to_string()), description: Set(description.clone()), is_system: Set(false), 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( "role.created", tenant_id, serde_json::json!({ "role_id": id, "code": code }), ), db, ) .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "role.create", "role").with_resource_id(id), db, ) .await; Ok(RoleResp { id, name: name.to_string(), code: code.to_string(), description: description.clone(), is_system: false, version: 1, }) } /// Update editable role fields (name and description). /// /// Code and is_system cannot be changed after creation. pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, name: &Option, description: &Option, version: i32, db: &sea_orm::DatabaseConnection, ) -> AuthResult { let model = role::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?; let old_json = serde_json::to_value(&model).unwrap_or(serde_json::Value::Null); let next_ver = check_version(version, model.version) .map_err(|e| AuthError::Validation(e.to_string()))?; let mut active: role::ActiveModel = model.into(); if let Some(name) = name { active.name = Set(name.clone()); } if let Some(desc) = description { active.description = Set(Some(desc.clone())); } 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()))?; let new_json = serde_json::to_value(&updated).unwrap_or(serde_json::Value::Null); audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "role.update", "role") .with_resource_id(id) .with_changes(Some(old_json), Some(new_json)), db, ) .await; Ok(RoleResp { id: updated.id, name: updated.name.clone(), code: updated.code.clone(), description: updated.description.clone(), is_system: updated.is_system, version: updated.version, }) } /// Soft-delete a role by setting the `deleted_at` timestamp. /// /// System roles cannot be deleted. pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> AuthResult<()> { let model = role::Entity::find_by_id(id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?; if model.is_system { return Err(AuthError::Validation("系统角色不可删除".to_string())); } let current_version = model.version; let mut active: role::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( "role.deleted", tenant_id, serde_json::json!({ "role_id": id }), ), db, ) .await; audit_service::record( AuditLog::new(tenant_id, Some(operator_id), "role.delete", "role").with_resource_id(id), db, ) .await; Ok(()) } /// Replace all permission assignments for a role. /// /// Soft-deletes existing assignments and creates new ones. pub async fn assign_permissions( role_id: Uuid, tenant_id: Uuid, operator_id: Uuid, permission_ids: &[Uuid], db: &sea_orm::DatabaseConnection, ) -> AuthResult<()> { // Verify the role exists and belongs to this tenant let _role = role::Entity::find_by_id(role_id) .one(db) .await .map_err(|e| AuthError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?; // Soft-delete existing role_permission rows let existing = role_permission::Entity::find() .filter(role_permission::Column::RoleId.eq(role_id)) .filter(role_permission::Column::TenantId.eq(tenant_id)) .filter(role_permission::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; let now = Utc::now(); for rp in existing { let mut active: role_permission::ActiveModel = rp.into(); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(operator_id); active.version = Set(active.version.take().unwrap_or(0) + 1); active .update(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; } // Insert new role_permission rows for perm_id in permission_ids { let rp = role_permission::ActiveModel { role_id: Set(role_id), permission_id: Set(*perm_id), tenant_id: Set(tenant_id), data_scope: Set("all".to_string()), 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), }; rp.insert(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; } Ok(()) } /// Fetch all permissions assigned to a role. /// /// Resolves through the role_permission join table. pub async fn get_role_permissions( role_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AuthResult> { let rp_rows = role_permission::Entity::find() .filter(role_permission::Column::RoleId.eq(role_id)) .filter(role_permission::Column::TenantId.eq(tenant_id)) .filter(role_permission::Column::DeletedAt.is_null()) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; let perm_ids: Vec = rp_rows.iter().map(|rp| rp.permission_id).collect(); if perm_ids.is_empty() { return Ok(vec![]); } let perms = permission::Entity::find() .filter(permission::Column::Id.is_in(perm_ids)) .filter(permission::Column::TenantId.eq(tenant_id)) .all(db) .await .map_err(|e| AuthError::Validation(e.to_string()))?; Ok(perms .iter() .map(|p| PermissionResp { id: p.id, code: p.code.clone(), name: p.name.clone(), resource: p.resource.clone(), action: p.action.clone(), description: p.description.clone(), }) .collect()) } }