# ============================================================================= # 企微IT智能服务台 — 管理后台角色管理 API # ============================================================================= # 说明:管理后台的角色管理接口 # 包含: # 1. 角色管理(CRUD) # 2. 用户角色分配/撤销 # 3. 角色映射规则管理 # 所有接口需要 admin 角色权限 # ============================================================================= import logging from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, UserInfo, require_role from app.database import get_db from app.models.role import Role from app.models.role_mapping_rule import RoleMappingRule from app.models.user_role import UserRole from app.schemas.role import ( RoleAssignRequest, RoleMappingRuleRequest, RoleMappingRuleResponse, RoleRevokeRequest, RoleResponse, UserRoleResponse, ) from app.utils.response import AppException, success_response logger = logging.getLogger(__name__) def _mask_sensitive_data(value: str, visible_chars: int = 3) -> str: """脱敏处理敏感数据。 Args: value: 原始值 visible_chars: 开头保留的字符数 Returns: str: 脱敏后的值,如 "abc***def" """ if not value: return "" if len(value) <= visible_chars: return "*" * len(value) return f"{value[:visible_chars]}{'*' * (len(value) - visible_chars)}" # 创建路由器 router = APIRouter(prefix="/admin/roles") # -------------------------------------------------------------------------- # 管理后台权限校验依赖 # -------------------------------------------------------------------------- async def require_admin( current_user: UserInfo = Depends(get_current_user), ) -> UserInfo: """管理后台权限校验:仅 admin 角色可访问。 Args: current_user: 当前用户(通过认证依赖注入) Returns: UserInfo: 具有管理权限的用户信息 Raises: AppException: 非管理员角色(错误码 1004) """ if "admin" not in current_user.roles: raise AppException(1004, "无管理权限") return current_user # ========================================================================== # 1. 角色管理 # ========================================================================== # ---------- GET /api/admin/roles ---------- @router.get("") async def get_roles( admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """获取所有角色列表。 返回角色列表,包含每个角色的用户数量统计。 Args: admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式,包含角色列表 """ # 查询所有角色 stmt = select(Role).order_by(Role.is_default.desc(), Role.name) result = await db.execute(stmt) roles = result.scalars().all() # 构建响应,包含用户数量 role_list = [] for role in roles: # 统计拥有该角色的用户数 count_stmt = select(func.count()).select_from(UserRole).where(UserRole.role_id == role.id) count_result = await db.execute(count_stmt) user_count = count_result.scalar() or 0 role_list.append( RoleResponse( id=role.id, name=role.name, display_name=role.display_name, description=role.description, permissions=role.permissions or [], is_default=role.is_default, user_count=user_count, created_at=role.created_at, updated_at=role.updated_at, ) ) return success_response(data=[r.model_dump() for r in role_list]) # ========================================================================== # 2. 用户角色分配/撤销 # ========================================================================== # ---------- POST /api/admin/roles/assign ---------- @router.post("/assign") async def assign_role( body: RoleAssignRequest, admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """手动分配角色。 为指定用户分配角色,记录分配者和分配原因。 安全限制:禁止管理员给自己分配角色。 Args: body: 分配角色请求 admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式 """ # 安全限制:禁止管理员给自己分配角色 if body.employee_id == admin.employee_id: raise AppException(4014, "不能给自己分配角色") # 查询目标角色 role_stmt = select(Role).where(Role.name == body.role_name) role_result = await db.execute(role_stmt) role = role_result.scalars().first() if not role: raise AppException(4004, f"角色 {body.role_name} 不存在") # 检查是否已拥有该角色 existing_stmt = select(UserRole).where( UserRole.employee_id == body.employee_id, UserRole.role_id == role.id, ) existing_result = await db.execute(existing_stmt) existing = existing_result.scalars().first() if existing: raise AppException(4009, f"用户已拥有 {body.role_name} 角色") # 创建用户角色关联 user_role = UserRole( employee_id=body.employee_id, role_id=role.id, source="manual", assigned_by=admin.employee_id, ) db.add(user_role) await db.commit() logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 为用户 {_mask_sensitive_data(body.employee_id)} 分配角色 {body.role_name},原因:{body.reason}") return success_response(message=f"角色 {body.role_name} 分配成功") # ---------- POST /api/admin/roles/revoke ---------- @router.post("/revoke") async def revoke_role( body: RoleRevokeRequest, admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """撤销角色。 撤销指定用户的角色。 安全限制:禁止管理员撤销自己的角色。 Args: body: 撤销角色请求 admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式 """ # 安全限制:禁止管理员撤销自己的角色 if body.employee_id == admin.employee_id: raise AppException(4015, "不能撤销自己的角色") # 查询目标角色 role_stmt = select(Role).where(Role.name == body.role_name) role_result = await db.execute(role_stmt) role = role_result.scalars().first() if not role: raise AppException(4004, f"角色 {body.role_name} 不存在") # 不允许撤销默认角色 if role.is_default: raise AppException(4010, "不能撤销默认角色") # 查询用户角色关联 user_role_stmt = select(UserRole).where( UserRole.employee_id == body.employee_id, UserRole.role_id == role.id, ) user_role_result = await db.execute(user_role_stmt) user_role = user_role_result.scalars().first() if not user_role: raise AppException(4011, f"用户没有 {body.role_name} 角色") # 删除用户角色关联 await db.delete(user_role) await db.commit() logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 撤销用户 {_mask_sensitive_data(body.employee_id)} 的角色 {body.role_name},原因:{body.reason}") return success_response(message=f"角色 {body.role_name} 撤销成功") # ========================================================================== # 3. 角色映射规则管理 # ========================================================================== # ---------- GET /api/admin/roles/mapping-rules ---------- @router.get("/mapping-rules") async def get_mapping_rules( admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """获取所有角色映射规则。 Args: admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式,包含映射规则列表 """ # 查询所有映射规则 stmt = ( select(RoleMappingRule, Role) .join(Role, RoleMappingRule.role_id == Role.id) .order_by(RoleMappingRule.priority.desc(), RoleMappingRule.source_type) ) result = await db.execute(stmt) rules = result.all() # 构建响应 rule_list = [] for rule, role in rules: rule_list.append( RoleMappingRuleResponse( id=rule.id, role_id=rule.role_id, role_name=role.name, source_type=rule.source_type, source_value=rule.source_value, priority=rule.priority, is_active=rule.is_active, created_at=rule.created_at, ) ) return success_response(data=[r.model_dump() for r in rule_list]) # ---------- POST /api/admin/roles/mapping-rules ---------- @router.post("/mapping-rules") async def create_mapping_rule( body: RoleMappingRuleRequest, admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """创建角色映射规则。 Args: body: 创建映射规则请求 admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式,包含新创建的规则 ID """ # 查询目标角色 role_stmt = select(Role).where(Role.name == body.role_name) role_result = await db.execute(role_stmt) role = role_result.scalars().first() if not role: raise AppException(4004, f"角色 {body.role_name} 不存在") # 检查是否已存在相同的规则 existing_stmt = select(RoleMappingRule).where( RoleMappingRule.role_id == role.id, RoleMappingRule.source_type == body.source_type, RoleMappingRule.source_value == body.source_value, ) existing_result = await db.execute(existing_stmt) existing = existing_result.scalars().first() if existing: raise AppException(4012, "已存在相同的映射规则") # 创建映射规则 rule = RoleMappingRule( role_id=role.id, source_type=body.source_type, source_value=body.source_value, priority=body.priority, is_active=body.is_active, ) db.add(rule) await db.commit() logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 创建映射规则:{body.source_type}={body.source_value} → {body.role_name}") return success_response( message="映射规则创建成功", data={"id": rule.id}, ) # ---------- DELETE /api/admin/roles/mapping-rules/{rule_id} ---------- @router.delete("/mapping-rules/{rule_id}") async def delete_mapping_rule( rule_id: str, admin: UserInfo = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """删除角色映射规则。 Args: rule_id: 规则 ID admin: 管理员(权限校验) db: 数据库会话 Returns: Dict: 统一响应格式 """ # 查询规则 rule_stmt = select(RoleMappingRule).where(RoleMappingRule.id == rule_id) rule_result = await db.execute(rule_stmt) rule = rule_result.scalars().first() if not rule: raise AppException(4013, "映射规则不存在") # 删除规则 await db.delete(rule) await db.commit() logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 删除映射规则 {rule_id}") return success_response(message="映射规则删除成功")