Files
ai-agent-admin/backend-fastapi/core/role/service.py
T

764 lines
26 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Role Service - 角色服务层
"""
from typing import Tuple, Dict, Any, Optional, List
from sqlalchemy import select, func, or_, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.base_service import BaseService
from core.role.model import Role
from core.role.schema import RoleCreate, RoleUpdate
def _filter_ai_agent_admin_role_menus(menus: List[Any]) -> List[Any]:
from core.menu.service import (
AI_AGENT_ADMIN_MENU_NAMES,
_normalize_ai_agent_admin_menu,
)
filtered = [menu for menu in menus if menu.name in AI_AGENT_ADMIN_MENU_NAMES]
for menu in filtered:
_normalize_ai_agent_admin_menu(menu)
return filtered
class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
"""
角色服务层
继承BaseService,自动获得增删改查功能
"""
model = Role
@classmethod
async def create(cls, db: AsyncSession, data: RoleCreate) -> Role:
"""创建角色,处理多对多关系"""
role_data = data.model_dump(exclude={'menu_ids', 'permission_ids', 'dept_ids'})
role = Role(**role_data)
db.add(role)
await db.flush()
# 设置多对多关系
if data.menu_ids:
from core.menu.model import Menu
result = await db.execute(select(Menu).where(Menu.id.in_(data.menu_ids)))
menus = list(result.scalars().all())
role.menus = menus
if data.permission_ids:
from core.permission.model import Permission
result = await db.execute(select(Permission).where(Permission.id.in_(data.permission_ids)))
permissions = list(result.scalars().all())
role.permissions = permissions
if data.dept_ids:
from core.dept.model import Dept
result = await db.execute(select(Dept).where(Dept.id.in_(data.dept_ids)))
depts = list(result.scalars().all())
role.depts = depts
await db.commit()
await db.refresh(role)
return role
@classmethod
async def update(
cls,
db: AsyncSession,
record_id: str,
data: RoleUpdate
) -> Optional[Role]:
"""更新角色,处理多对多关系"""
role = await cls.get_by_id(db, record_id)
if not role:
return None
update_data = data.model_dump(exclude_unset=True, exclude={'menu_ids', 'permission_ids', 'dept_ids'})
for field, value in update_data.items():
setattr(role, field, value)
# 更新多对多关系
menu_or_permission_updated = False
if data.menu_ids is not None:
from core.menu.model import Menu
result = await db.execute(select(Menu).where(Menu.id.in_(data.menu_ids)))
menus = list(result.scalars().all())
role.menus = menus
menu_or_permission_updated = True
if data.permission_ids is not None:
from core.permission.model import Permission
result = await db.execute(select(Permission).where(Permission.id.in_(data.permission_ids)))
permissions = list(result.scalars().all())
role.permissions = permissions
menu_or_permission_updated = True
if data.dept_ids is not None:
from core.dept.model import Dept
result = await db.execute(select(Dept).where(Dept.id.in_(data.dept_ids)))
depts = list(result.scalars().all())
role.depts = depts
await db.commit()
await db.refresh(role)
# 如果更新了菜单或权限,清除缓存
if menu_or_permission_updated:
await cls._invalidate_role_related_cache(record_id)
return role
@classmethod
async def _invalidate_role_related_cache(cls, role_id: str):
"""
清除角色相关的所有缓存,使权限变更立即生效
1. 清除菜单路由缓存
2. 清除 API 权限内存缓存(该角色)
3. 清除所有用户的 Redis 用户信息缓存(因为不知道哪些用户拥有该角色)
"""
# 1. 清除菜单路由缓存
from core.menu.service import MenuService
await MenuService.invalidate_cache()
# 2. 清除该角色的 API 权限 Redis 缓存
from utils.permission import clear_role_permission_cache
await clear_role_permission_cache(role_id)
# 3. 清除所有用户的 Redis 用户信息缓存
from utils.user_info_cache import delete_all_cached_user_info
await delete_all_cached_user_info()
@classmethod
async def get_by_id_with_relations(cls, db: AsyncSession, record_id: str) -> Optional[Role]:
"""获取角色详情(包含关联数据)"""
result = await db.execute(
select(Role)
.options(
selectinload(Role.menus),
selectinload(Role.permissions),
selectinload(Role.depts)
)
.where(Role.id == record_id, Role.is_deleted == False) # noqa: E712
)
return result.scalar_one_or_none()
@classmethod
async def get_user_count(cls, db: AsyncSession, role_id: str) -> int:
"""获取角色下的用户数量"""
from core.user.model import User
result = await db.execute(
select(func.count(User.id)).where(
User.role_id == role_id,
User.is_deleted == False # noqa: E712
)
)
return result.scalar() or 0
@classmethod
async def can_delete(cls, db: AsyncSession, role_id: str) -> Tuple[bool, str]:
"""检查角色是否可以删除"""
role = await cls.get_by_id(db, role_id)
if not role:
return False, "角色不存在"
if role.is_system_role():
return False, "系统角色不能删除"
user_count = await cls.get_user_count(db, role_id)
if user_count > 0:
return False, f"该角色下还有 {user_count} 个用户,无法删除"
return True, ""
@classmethod
async def batch_update_status(
cls,
db: AsyncSession,
ids: List[str],
status: bool
) -> int:
"""批量更新角色状态(系统角色不能禁用)"""
count = 0
for role_id in ids:
role = await cls.get_by_id(db, role_id)
if role and role.role_type == 1: # 只更新自定义角色
role.status = status
count += 1
if count > 0:
await db.commit()
return count
@classmethod
async def batch_delete(
cls,
db: AsyncSession,
ids: List[str],
hard: bool = False
) -> Tuple[int, List[str]]:
"""批量删除角色"""
success_count = 0
failed_ids = []
for role_id in ids:
can_del, reason = await cls.can_delete(db, role_id)
if can_del:
if await cls.delete(db, role_id, hard=hard):
success_count += 1
else:
failed_ids.append(role_id)
else:
failed_ids.append(role_id)
return success_count, failed_ids
@classmethod
async def search(
cls,
db: AsyncSession,
keyword: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Role], int]:
"""搜索角色"""
if not keyword:
return [], 0
search_filter = or_(
Role.name.ilike(f"%{keyword}%"),
Role.code.ilike(f"%{keyword}%"),
Role.description.ilike(f"%{keyword}%")
)
count_result = await db.execute(
select(func.count(Role.id)).where(
search_filter,
Role.is_deleted == False # noqa: E712
)
)
total = count_result.scalar() or 0
result = await db.execute(
select(Role).where(
search_filter,
Role.is_deleted == False # noqa: E712
)
.order_by(Role.priority.desc(), Role.sys_update_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_all_active(cls, db: AsyncSession) -> List[Role]:
"""获取所有启用的角色"""
result = await db.execute(
select(Role).where(
Role.status == True, # noqa: E712
Role.is_deleted == False # noqa: E712
).order_by(Role.priority.desc(), Role.name)
)
return list(result.scalars().all())
@classmethod
async def get_by_ids(cls, db: AsyncSession, ids: List[str]) -> List[Role]:
"""根据ID列表批量获取角色"""
if not ids:
return []
result = await db.execute(
select(Role).where(
Role.id.in_(ids),
Role.is_deleted == False # noqa: E712
)
)
return list(result.scalars().all())
@classmethod
async def get_role_users(
cls,
db: AsyncSession,
role_id: str,
page: int = 1,
page_size: int = 20,
username: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""获取角色下的用户列表(SQL 分页 + JOIN 部门)"""
from core.dept.model import Dept
from core.user.model import User
from core.user.user_role_model import UserRole
# 基础条件
base_conditions = [
UserRole.role_id == role_id,
UserRole.is_deleted == False, # noqa: E712
User.user_status == 1,
User.is_deleted == False, # noqa: E712
]
if username:
base_conditions.append(
or_(
User.username.ilike(f"%{username}%"),
User.name.ilike(f"%{username}%"),
)
)
# count
count_stmt = (
select(func.count(User.id))
.join(UserRole, UserRole.user_id == User.id)
.where(*base_conditions)
)
total = (await db.execute(count_stmt)).scalar() or 0
# 分页查询,LEFT JOIN Dept 一次拿到 dept_name
query_stmt = (
select(
User.id,
User.username,
User.name,
User.avatar,
User.email,
User.mobile,
Dept.name.label("dept_name"),
)
.join(UserRole, UserRole.user_id == User.id)
.outerjoin(Dept, and_(Dept.id == User.dept_id, Dept.is_deleted == False)) # noqa: E712
.where(*base_conditions)
.order_by(User.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
rows = (await db.execute(query_stmt)).all()
items = [
{
"id": row.id,
"username": row.username,
"name": row.name,
"avatar": row.avatar,
"email": row.email,
"mobile": row.mobile,
"dept_name": row.dept_name,
}
for row in rows
]
return items, total
@classmethod
async def add_users_to_role(
cls,
db: AsyncSession,
role_id: str,
user_ids: List[str]
) -> int:
"""将用户添加到角色(通过多对多关联表)"""
from core.user.user_role_model import UserRole
added_count = 0
for user_id in user_ids:
# 检查是否已存在关联(包括软删除的)
existing = await db.execute(
select(UserRole).where(
UserRole.user_id == user_id,
UserRole.role_id == role_id,
)
)
record = existing.scalar_one_or_none()
if record:
# 如果是软删除的,恢复
if record.is_deleted:
record.is_deleted = False
added_count += 1
else:
db.add(UserRole(user_id=user_id, role_id=role_id))
added_count += 1
if added_count > 0:
await db.commit()
return added_count
@classmethod
async def remove_users_from_role(
cls,
db: AsyncSession,
role_id: str,
user_ids: List[str]
) -> int:
"""从角色中移除用户(通过多对多关联表)"""
from core.user.user_role_model import UserRole
removed_count = 0
for user_id in user_ids:
result = await db.execute(
select(UserRole).where(
UserRole.user_id == user_id,
UserRole.role_id == role_id,
UserRole.is_deleted == False, # noqa: E712
)
)
record = result.scalar_one_or_none()
if record:
record.is_deleted = True
removed_count += 1
if removed_count > 0:
await db.commit()
return removed_count
@classmethod
async def update_menus_permissions(
cls,
db: AsyncSession,
role_id: str,
menu_ids: List[str],
permission_ids: List[str],
loaded_menu_ids: Optional[List[str]] = None
) -> bool:
"""更新角色的菜单和权限
增量更新策略:
- 菜单:全量替换(前端始终加载完整菜单树)
- 权限:只替换 loaded_menu_ids 对应菜单下的权限,保留未加载菜单的原有权限
如果 loaded_menu_ids 为空列表或 None,则全量替换权限(兼容旧逻辑)
"""
role = await cls.get_by_id_with_relations(db, role_id)
if not role:
return False
# 菜单始终全量替换
from core.menu.model import Menu
result = await db.execute(select(Menu).where(Menu.id.in_(menu_ids)))
menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
allowed_menu_ids = {menu.id for menu in menus}
role.menus = menus
from core.permission.model import Permission
if loaded_menu_ids:
# 增量更新:只替换已加载菜单下的权限
loaded_menu_set = set(loaded_menu_ids) & allowed_menu_ids
# 保留未加载菜单下的原有权限
kept_permissions = [
p for p in (role.permissions or [])
if p.menu_id in allowed_menu_ids and p.menu_id not in loaded_menu_set
]
# 获取前端提交的权限(已加载菜单下用户选择的权限)
new_permissions = []
if permission_ids:
result = await db.execute(
select(Permission).where(
Permission.id.in_(permission_ids),
Permission.menu_id.in_(allowed_menu_ids),
)
)
new_permissions = list(result.scalars().all())
# 合并:未加载菜单的旧权限 + 已加载菜单的新权限
role.permissions = kept_permissions + new_permissions
else:
# 兼容旧逻辑:全量替换
if permission_ids:
result = await db.execute(
select(Permission).where(
Permission.id.in_(permission_ids),
Permission.menu_id.in_(allowed_menu_ids),
)
)
permissions = list(result.scalars().all())
else:
permissions = []
role.permissions = permissions
await db.commit()
# 清除缓存,确保权限变更立即生效
await cls._invalidate_role_related_cache(role_id)
return True
@classmethod
async def copy_role(
cls,
db: AsyncSession,
source_role_id: str,
new_name: str,
new_code: str,
creator_id: Optional[str] = None
) -> Optional[Role]:
"""复制角色"""
source_role = await cls.get_by_id_with_relations(db, source_role_id)
if not source_role:
return None
# 创建新角色
new_role = Role(
name=new_name,
code=new_code,
role_type=1, # 复制的角色都是自定义角色
status=source_role.status,
data_scope=source_role.data_scope,
priority=source_role.priority,
description=source_role.description,
remark=f"复制自角色: {source_role.name}",
sys_creator_id=creator_id
)
db.add(new_role)
await db.flush()
# 复制关联关系
new_role.menus = source_role.menus
new_role.permissions = source_role.permissions
new_role.depts = source_role.depts
await db.commit()
await db.refresh(new_role)
return new_role
@classmethod
async def update_permissions(
cls,
db: AsyncSession,
role_id: str,
permission_ids: List[str]
) -> bool:
"""更新角色的权限"""
role = await cls.get_by_id_with_relations(db, role_id)
if not role:
return False
from core.permission.model import Permission
result = await db.execute(select(Permission).where(Permission.id.in_(permission_ids)))
permissions = list(result.scalars().all())
role.permissions = permissions
await db.commit()
# 清除缓存,确保权限变更立即生效
await cls._invalidate_role_related_cache(role_id)
return True
@classmethod
async def get_menu_permission_tree(cls, db: AsyncSession, role: Role) -> Dict[str, Any]:
"""获取角色的菜单权限树"""
from core.menu.model import Menu
from core.permission.model import Permission
# 获取所有菜单
result = await db.execute(select(Menu).where(Menu.is_deleted == False)) # noqa: E712
all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
allowed_menu_ids = {menu.id for menu in all_menus}
# 获取该角色已分配的权限ID和菜单ID
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
role_menu_ids = {m.id for m in (role.menus or []) if m.id in allowed_menu_ids}
# 获取所有启用的权限
result = await db.execute(
select(Permission).where(Permission.is_active == True, Permission.is_deleted == False) # noqa: E712
)
all_permissions = [
perm for perm in list(result.scalars().all()) if perm.menu_id in allowed_menu_ids
]
# 权限类型映射
PERMISSION_TYPE_MAP = {
0: '按钮权限',
1: 'API权限',
2: '数据权限',
3: '其他权限',
}
# 按菜单分组权限
permissions_by_menu = {}
for perm in all_permissions:
menu_id = perm.menu_id
if menu_id not in permissions_by_menu:
permissions_by_menu[menu_id] = []
permission_type = perm.permission_type if perm.permission_type is not None else 3
permission_type_display = PERMISSION_TYPE_MAP.get(permission_type, '其他权限')
permissions_by_menu[menu_id].append({
'id': perm.id,
'label': perm.name,
'name': perm.name,
'code': perm.code,
'permission_type': permission_type,
'permission_type_display': permission_type_display,
'checked': perm.id in role_permission_ids,
})
# 构建菜单树
menu_map = {}
root_menus = []
for menu in all_menus:
menu_node = {
'id': menu.id,
'label': menu.title or menu.name,
'name': menu.name,
'parent_id': menu.parent_id,
'checked': menu.id in role_menu_ids,
'children': [],
}
menu_map[menu.id] = menu_node
# 建立父子关系
for menu in all_menus:
if menu.parent_id and menu.parent_id in menu_map:
menu_map[menu.parent_id]['children'].append(menu_map[menu.id])
else:
root_menus.append(menu_map[menu.id])
# 为叶子菜单添加权限
for menu_id, menu_node in menu_map.items():
if not menu_node['children']:
menu_node['children'] = permissions_by_menu.get(menu_id, [])
return {
'menu_tree': root_menus,
'permission_tree': [],
'selected_menu_ids': list(role_menu_ids),
'selected_permission_ids': list(role_permission_ids),
}
@classmethod
async def get_role_menus(cls, db: AsyncSession, role: Role, application_id: Optional[str] = None) -> Dict[str, Any]:
"""获取角色的菜单列表
Args:
db: 数据库会话
role: 角色对象
application_id: 应用ID,如果指定则只返回该应用的菜单
"""
from core.menu.model import Menu
from core.permission.model import Permission
# 获取该角色已选中的菜单ID
role_menu_ids = set(m.id for m in role.menus) if role.menus else set()
# 获取菜单,支持按应用过滤
query = select(Menu).where(Menu.is_deleted == False) # noqa: E712
if application_id:
query = query.where(Menu.application_id == application_id)
result = await db.execute(query)
all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
allowed_menu_ids = {menu.id for menu in all_menus}
role_menu_ids = {menu_id for menu_id in role_menu_ids if menu_id in allowed_menu_ids}
# 统计每个菜单的权限数量
permission_counts = {}
for menu in all_menus:
count_result = await db.execute(
select(func.count(Permission.id)).where(
Permission.menu_id == menu.id,
Permission.is_active == True, # noqa: E712
Permission.is_deleted == False # noqa: E712
)
)
permission_counts[menu.id] = count_result.scalar() or 0
# 构建菜单树
menu_map = {}
root_menus = []
for menu in all_menus:
menu_node = {
'id': menu.id,
'label': menu.title or menu.name,
'name': menu.name,
'parent_id': menu.parent_id,
'application_id': menu.application_id,
'checked': menu.id in role_menu_ids,
'permission_count': permission_counts.get(menu.id, 0),
'children': [],
}
menu_map[menu.id] = menu_node
if not menu.parent_id:
root_menus.append(menu_node)
# 建立父子关系
for menu_id, menu_node in menu_map.items():
parent_id = menu_node['parent_id']
if parent_id and parent_id in menu_map:
menu_map[parent_id]['children'].append(menu_node)
return {
'menu_tree': root_menus,
'selected_menu_ids': list(role_menu_ids),
}
@classmethod
async def get_menu_permissions(cls, db: AsyncSession, role: Role, menu_id: str) -> Dict[str, Any]:
"""获取指定菜单的权限列表"""
from core.menu.model import Menu
from core.permission.model import Permission
# 获取该角色已选中的权限ID
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
menu_result = await db.execute(
select(Menu).where(Menu.id == menu_id, Menu.is_deleted == False) # noqa: E712
)
menu = menu_result.scalar_one_or_none()
if not menu or not _filter_ai_agent_admin_role_menus([menu]):
return {
'menu_id': menu_id,
'permissions': [],
}
# 权限类型映射
PERMISSION_TYPE_MAP = {
0: '按钮权限',
1: 'API权限',
2: '数据权限',
3: '其他权限',
}
# 获取该菜单的所有权限
result = await db.execute(
select(Permission).where(
Permission.menu_id == menu_id,
Permission.is_active == True, # noqa: E712
Permission.is_deleted == False # noqa: E712
)
)
permissions = list(result.scalars().all())
permission_list = []
for perm in permissions:
permission_type = perm.permission_type if perm.permission_type is not None else 3
permission_type_display = PERMISSION_TYPE_MAP.get(permission_type, '其他权限')
permission_list.append({
'id': perm.id,
'label': perm.name,
'name': perm.name,
'code': perm.code,
'permission_type': permission_type,
'permission_type_display': permission_type_display,
'checked': perm.id in role_permission_ids,
})
return {
'menu_id': menu_id,
'permissions': permission_list,
}