Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Permission Service - 权限服务层
|
||||
"""
|
||||
from typing import Tuple, Dict, Any, Optional, List
|
||||
|
||||
from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from core.permission.model import Permission
|
||||
from core.permission.schema import PermissionCreate, PermissionUpdate
|
||||
|
||||
|
||||
class PermissionService(BaseService[Permission, PermissionCreate, PermissionUpdate]):
|
||||
"""
|
||||
权限服务层
|
||||
继承BaseService,自动获得增删改查功能
|
||||
"""
|
||||
|
||||
model = Permission
|
||||
|
||||
@classmethod
|
||||
async def check_code_unique(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
menu_id: str,
|
||||
code: str,
|
||||
exclude_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
检查同一菜单下权限编码是否唯一
|
||||
|
||||
:return: True表示唯一,False表示已存在
|
||||
"""
|
||||
query = select(Permission).where(
|
||||
Permission.menu_id == menu_id,
|
||||
Permission.code == code,
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
)
|
||||
if exclude_id:
|
||||
query = query.where(Permission.id != exclude_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar_one_or_none() is None
|
||||
|
||||
@classmethod
|
||||
async def get_by_menu(cls, db: AsyncSession, menu_id: str) -> List[Permission]:
|
||||
"""根据菜单ID获取权限列表"""
|
||||
result = await db.execute(
|
||||
select(Permission).where(
|
||||
Permission.menu_id == menu_id,
|
||||
Permission.is_active == True, # noqa: E712
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
).order_by(Permission.sort, Permission.sys_create_datetime)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_by_type(cls, db: AsyncSession, permission_type: int) -> List[Permission]:
|
||||
"""根据权限类型获取权限列表"""
|
||||
result = await db.execute(
|
||||
select(Permission).where(
|
||||
Permission.permission_type == permission_type,
|
||||
Permission.is_active == True, # noqa: E712
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
).order_by(Permission.menu_id, Permission.sort)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def batch_update_status(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
ids: List[str],
|
||||
is_active: bool
|
||||
) -> int:
|
||||
"""批量更新权限状态"""
|
||||
count = 0
|
||||
for perm_id in ids:
|
||||
perm = await cls.get_by_id(db, perm_id)
|
||||
if perm:
|
||||
perm.is_active = is_active
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
await db.commit()
|
||||
|
||||
return count
|
||||
|
||||
@classmethod
|
||||
async def batch_delete(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
ids: List[str],
|
||||
hard: bool = False
|
||||
) -> int:
|
||||
"""批量删除权限"""
|
||||
count = 0
|
||||
for perm_id in ids:
|
||||
if await cls.delete(db, perm_id, hard=hard):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@classmethod
|
||||
async def search(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
keyword: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Permission], int]:
|
||||
"""搜索权限"""
|
||||
if not keyword:
|
||||
return [], 0
|
||||
|
||||
search_filter = or_(
|
||||
Permission.name.ilike(f"%{keyword}%"),
|
||||
Permission.code.ilike(f"%{keyword}%"),
|
||||
Permission.description.ilike(f"%{keyword}%")
|
||||
)
|
||||
|
||||
count_result = await db.execute(
|
||||
select(func.count(Permission.id)).where(
|
||||
search_filter,
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
result = await db.execute(
|
||||
select(Permission).where(
|
||||
search_filter,
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
)
|
||||
.order_by(Permission.sort, Permission.sys_create_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[Permission]:
|
||||
"""获取所有启用的权限"""
|
||||
result = await db.execute(
|
||||
select(Permission).where(
|
||||
Permission.is_active == True, # noqa: E712
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
).order_by(Permission.menu_id, Permission.sort)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_by_ids(cls, db: AsyncSession, ids: List[str]) -> List[Permission]:
|
||||
"""根据ID列表批量获取权限"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
result = await db.execute(
|
||||
select(Permission).where(
|
||||
Permission.id.in_(ids),
|
||||
Permission.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
def get_all_routes_from_app(cls, app) -> List[Dict[str, Any]]:
|
||||
"""从FastAPI应用获取所有已注册的路由"""
|
||||
routes = []
|
||||
|
||||
# HTTP方法映射
|
||||
METHOD_MAP = {
|
||||
'GET': 0,
|
||||
'POST': 1,
|
||||
'PUT': 2,
|
||||
'DELETE': 3,
|
||||
'PATCH': 4,
|
||||
}
|
||||
|
||||
for route in app.routes:
|
||||
# 只处理APIRoute
|
||||
if hasattr(route, 'methods') and hasattr(route, 'path'):
|
||||
path = route.path
|
||||
methods = route.methods or {'GET'}
|
||||
|
||||
# 跳过文档路由
|
||||
if path in ['/docs', '/redoc', '/openapi.json']:
|
||||
continue
|
||||
|
||||
for method in methods:
|
||||
if method in ['HEAD', 'OPTIONS']:
|
||||
continue
|
||||
|
||||
# 生成权限编码
|
||||
# 将路径转换为编码格式: /api/core/user -> api:core:user
|
||||
code_parts = [p for p in path.split('/') if p and not p.startswith('{')]
|
||||
code = ':'.join(code_parts)
|
||||
if not code:
|
||||
code = 'root'
|
||||
code = f"{code}:{method.lower()}"
|
||||
|
||||
# 获取summary
|
||||
summary = getattr(route, 'summary', None) or getattr(route, 'name', None) or ''
|
||||
|
||||
routes.append({
|
||||
'path': path,
|
||||
'method': method,
|
||||
'name': summary or f"{method} {path}",
|
||||
'code': code,
|
||||
'summary': summary,
|
||||
'permission_type': 1, # API权限
|
||||
'http_method': METHOD_MAP.get(method, 0),
|
||||
'is_active': True,
|
||||
})
|
||||
|
||||
return routes
|
||||
|
||||
@classmethod
|
||||
async def batch_create_from_routes(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
menu_id: str,
|
||||
routes: List[Dict[str, Any]]
|
||||
) -> Tuple[int, int, int, List[str]]:
|
||||
"""
|
||||
从路由批量创建权限
|
||||
|
||||
:return: (created, skipped, failed, errors)
|
||||
"""
|
||||
created_count = 0
|
||||
skipped_count = 0
|
||||
failed_count = 0
|
||||
errors = []
|
||||
|
||||
for route in routes:
|
||||
try:
|
||||
# 检查权限是否已存在
|
||||
is_unique = await cls.check_code_unique(db, menu_id, route['code'])
|
||||
|
||||
if not is_unique:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# 创建权限
|
||||
permission = Permission(
|
||||
menu_id=menu_id,
|
||||
name=route['name'],
|
||||
code=route['code'],
|
||||
permission_type=route.get('permission_type', 1),
|
||||
api_path=route['path'],
|
||||
http_method=route.get('http_method', 0),
|
||||
description=route.get('summary') or f"{route['name']}权限",
|
||||
is_active=route.get('is_active', True),
|
||||
)
|
||||
db.add(permission)
|
||||
created_count += 1
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
errors.append(f"创建权限 {route.get('code', 'unknown')} 失败: {str(e)}")
|
||||
|
||||
if created_count > 0:
|
||||
await db.commit()
|
||||
|
||||
return created_count, skipped_count, failed_count, errors
|
||||
|
||||
@classmethod
|
||||
async def auto_generate_permissions(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
app,
|
||||
dry_run: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
自动扫描并生成权限
|
||||
|
||||
:param dry_run: 如果为True,只预览不实际创建
|
||||
:return: 生成结果
|
||||
"""
|
||||
from core.menu.model import Menu
|
||||
|
||||
# 获取所有路由
|
||||
routes = cls.get_all_routes_from_app(app)
|
||||
|
||||
if dry_run:
|
||||
return {
|
||||
'created': 0,
|
||||
'skipped': 0,
|
||||
'failed': 0,
|
||||
'permissions': routes,
|
||||
'dry_run': True,
|
||||
}
|
||||
|
||||
# 按路径前缀分组,尝试匹配菜单
|
||||
created_total = 0
|
||||
skipped_total = 0
|
||||
failed_total = 0
|
||||
all_errors = []
|
||||
|
||||
# 获取所有菜单
|
||||
result = await db.execute(select(Menu).where(Menu.is_deleted == False)) # noqa: E712
|
||||
all_menus = list(result.scalars().all())
|
||||
|
||||
# 创建菜单路径映射
|
||||
menu_path_map = {}
|
||||
for menu in all_menus:
|
||||
if menu.path:
|
||||
# 标准化路径
|
||||
path = menu.path.strip('/')
|
||||
menu_path_map[path] = menu.id
|
||||
|
||||
# 按路由路径匹配菜单
|
||||
routes_by_menu = {}
|
||||
unmatched_routes = []
|
||||
|
||||
for route in routes:
|
||||
path = route['path'].strip('/')
|
||||
matched_menu_id = None
|
||||
|
||||
# 尝试匹配菜单
|
||||
for menu_path, menu_id in menu_path_map.items():
|
||||
if path.startswith(menu_path) or menu_path in path:
|
||||
matched_menu_id = menu_id
|
||||
break
|
||||
|
||||
if matched_menu_id:
|
||||
if matched_menu_id not in routes_by_menu:
|
||||
routes_by_menu[matched_menu_id] = []
|
||||
routes_by_menu[matched_menu_id].append(route)
|
||||
else:
|
||||
unmatched_routes.append(route)
|
||||
|
||||
# 为每个菜单创建权限
|
||||
for menu_id, menu_routes in routes_by_menu.items():
|
||||
created, skipped, failed, errors = await cls.batch_create_from_routes(
|
||||
db, menu_id, menu_routes
|
||||
)
|
||||
created_total += created
|
||||
skipped_total += skipped
|
||||
failed_total += failed
|
||||
all_errors.extend(errors)
|
||||
|
||||
return {
|
||||
'created': created_total,
|
||||
'skipped': skipped_total,
|
||||
'failed': failed_total,
|
||||
'unmatched_routes': len(unmatched_routes),
|
||||
'errors': all_errors,
|
||||
'dry_run': False,
|
||||
}
|
||||
Reference in New Issue
Block a user