Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Permission Module - 权限管理模块
|
||||
"""
|
||||
from core.permission.model import Permission
|
||||
from core.permission.service import PermissionService
|
||||
from core.permission.api import router
|
||||
|
||||
__all__ = ["Permission", "PermissionService", "router"]
|
||||
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Permission API - 权限管理接口
|
||||
提供权限的 CRUD 操作
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.permission.schema import (
|
||||
PermissionCreate, PermissionUpdate, PermissionResponse, PermissionSimple,
|
||||
PermissionBatchDeleteIn, PermissionBatchDeleteOut,
|
||||
PermissionBatchUpdateStatusIn, PermissionBatchUpdateStatusOut,
|
||||
PermissionSearchRequest, PermissionBatchCreateFromRoutesIn, PermissionBatchCreateFromRoutesOut
|
||||
)
|
||||
from core.permission.service import PermissionService
|
||||
|
||||
router = APIRouter(prefix="/permission", tags=["权限管理"])
|
||||
|
||||
|
||||
@router.post("", response_model=PermissionResponse, summary="创建权限")
|
||||
async def create_permission(data: PermissionCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建权限"""
|
||||
# 检查同一菜单下权限编码唯一性
|
||||
if not await PermissionService.check_code_unique(db, data.menu_id, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"该菜单下已存在权限编码: {data.code}")
|
||||
|
||||
permission = await PermissionService.create(db=db, data=data)
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return await _build_permission_response(db, permission)
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[PermissionResponse], summary="获取所有权限")
|
||||
async def get_all_permissions(db: AsyncSession = Depends(get_db)):
|
||||
"""获取所有启用的权限"""
|
||||
permissions = await PermissionService.get_all_active(db)
|
||||
return [await _build_permission_response(db, p) for p in permissions]
|
||||
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[PermissionResponse], summary="获取权限列表")
|
||||
async def get_permission_list(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
name: Optional[str] = Query(None, description="权限名称"),
|
||||
code: Optional[str] = Query(None, description="权限编码"),
|
||||
menu_id: Optional[str] = Query(None, alias="menu_id", description="菜单ID"),
|
||||
permission_type: Optional[int] = Query(None, alias="permission_type", description="权限类型"),
|
||||
is_active: Optional[bool] = Query(None, alias="is_active", description="是否启用"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取权限列表(分页)"""
|
||||
from core.permission.model import Permission
|
||||
|
||||
filters = []
|
||||
if name:
|
||||
filters.append(Permission.name.ilike(f"%{name}%"))
|
||||
if code:
|
||||
filters.append(Permission.code.ilike(f"%{code}%"))
|
||||
if menu_id:
|
||||
filters.append(Permission.menu_id == menu_id)
|
||||
if permission_type is not None:
|
||||
filters.append(Permission.permission_type == permission_type)
|
||||
if is_active is not None:
|
||||
filters.append(Permission.is_active == is_active)
|
||||
|
||||
items, total = await PermissionService.get_list(db, page=page, page_size=page_size, filters=filters)
|
||||
response_items = [await _build_permission_response(db, item) for item in items]
|
||||
return PaginatedResponse(items=response_items, total=total)
|
||||
|
||||
|
||||
@router.get("/check/unique", response_model=ResponseModel, summary="检查权限唯一性")
|
||||
async def check_permission_unique(
|
||||
menu_id: str = Query(..., alias="menu_id", description="菜单ID"),
|
||||
code: str = Query(..., description="权限编码"),
|
||||
exclude_id: Optional[str] = Query(None, alias="excludeId", description="排除ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""检查同一菜单下权限编码唯一性"""
|
||||
is_unique = await PermissionService.check_code_unique(db, menu_id, code, exclude_id)
|
||||
return ResponseModel(message="可用" if is_unique else "已存在", data={"unique": is_unique})
|
||||
|
||||
|
||||
@router.post("/batch/delete", response_model=PermissionBatchDeleteOut, summary="批量删除权限")
|
||||
async def batch_delete_permissions(
|
||||
data: PermissionBatchDeleteIn,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除权限"""
|
||||
count = await PermissionService.batch_delete(db, data.ids, hard=hard)
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return PermissionBatchDeleteOut(count=count)
|
||||
|
||||
|
||||
@router.post("/batch/status", response_model=PermissionBatchUpdateStatusOut, summary="批量更新权限状态")
|
||||
async def batch_update_permission_status(
|
||||
data: PermissionBatchUpdateStatusIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量更新权限状态"""
|
||||
count = await PermissionService.batch_update_status(db, data.ids, data.is_active)
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return PermissionBatchUpdateStatusOut(count=count)
|
||||
|
||||
|
||||
@router.post("/search", response_model=PaginatedResponse[PermissionResponse], summary="搜索权限")
|
||||
async def search_permission(
|
||||
data: PermissionSearchRequest,
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""搜索权限"""
|
||||
items, total = await PermissionService.search(db, data.keyword, page, page_size)
|
||||
response_items = [await _build_permission_response(db, item) for item in items]
|
||||
return PaginatedResponse(items=response_items, total=total)
|
||||
|
||||
|
||||
@router.get("/by/menu/{menu_id}", response_model=List[PermissionResponse], summary="根据菜单ID获取权限")
|
||||
async def get_permissions_by_menu(
|
||||
menu_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""根据菜单ID获取该菜单下的所有权限"""
|
||||
permissions = await PermissionService.get_by_menu(db, menu_id)
|
||||
return [await _build_permission_response(db, p) for p in permissions]
|
||||
|
||||
|
||||
@router.get("/by/type/{permission_type}", response_model=List[PermissionResponse], summary="根据类型获取权限")
|
||||
async def get_permissions_by_type(
|
||||
permission_type: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""根据权限类型获取权限列表"""
|
||||
if permission_type not in [0, 1, 2, 3]:
|
||||
raise HTTPException(status_code=400, detail="权限类型必须在 0-3 之间")
|
||||
|
||||
permissions = await PermissionService.get_by_type(db, permission_type)
|
||||
return [await _build_permission_response(db, p) for p in permissions]
|
||||
|
||||
|
||||
@router.get("/{permission_id}", response_model=PermissionResponse, summary="获取权限详情")
|
||||
async def get_permission_by_id(permission_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取权限详情"""
|
||||
permission = await PermissionService.get_by_id(db, permission_id)
|
||||
if permission is None:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
return await _build_permission_response(db, permission)
|
||||
|
||||
|
||||
@router.put("/{permission_id}", response_model=PermissionResponse, summary="更新权限")
|
||||
async def update_permission(permission_id: str, data: PermissionUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""更新权限"""
|
||||
permission = await PermissionService.get_by_id(db, permission_id)
|
||||
if permission is None:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
# 检查权限编码唯一性
|
||||
menu_id = data.menu_id if data.menu_id else permission.menu_id
|
||||
if data.code:
|
||||
if not await PermissionService.check_code_unique(db, menu_id, data.code, permission_id):
|
||||
raise HTTPException(status_code=400, detail=f"该菜单下已存在权限编码: {data.code}")
|
||||
|
||||
permission = await PermissionService.update(db, record_id=permission_id, data=data)
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return await _build_permission_response(db, permission)
|
||||
|
||||
|
||||
@router.patch("/{permission_id}", response_model=PermissionResponse, summary="部分更新权限")
|
||||
async def patch_permission(permission_id: str, data: PermissionUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""部分更新权限(只更新提供的字段)"""
|
||||
permission = await PermissionService.get_by_id(db, permission_id)
|
||||
if permission is None:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
# 检查权限编码唯一性
|
||||
menu_id = data.menu_id if data.menu_id else permission.menu_id
|
||||
if data.code:
|
||||
if not await PermissionService.check_code_unique(db, menu_id, data.code, permission_id):
|
||||
raise HTTPException(status_code=400, detail=f"该菜单下已存在权限编码: {data.code}")
|
||||
|
||||
permission = await PermissionService.update(db, record_id=permission_id, data=data)
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return await _build_permission_response(db, permission)
|
||||
|
||||
|
||||
@router.delete("/{permission_id}", response_model=ResponseModel, summary="删除权限")
|
||||
async def delete_permission(
|
||||
permission_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除权限"""
|
||||
success = await PermissionService.delete(db, record_id=permission_id, hard=hard)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
@router.get("/all/routes", response_model=List[dict], summary="获取所有可用的API路由")
|
||||
async def get_all_routes():
|
||||
"""
|
||||
获取所有已注册的API路由
|
||||
用于前端在创建权限时选择
|
||||
"""
|
||||
from main import app
|
||||
routes = PermissionService.get_all_routes_from_app(app)
|
||||
return routes
|
||||
|
||||
|
||||
@router.post("/batch/create-from-routes", response_model=PermissionBatchCreateFromRoutesOut, summary="从路由批量创建权限")
|
||||
async def batch_create_permissions_from_routes(
|
||||
data: PermissionBatchCreateFromRoutesIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
从前端选择的路由批量创建权限
|
||||
|
||||
前端会传递:
|
||||
- menu_id: 菜单ID
|
||||
- routes: 选中的路由列表(已编辑过的权限信息)
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from core.menu.model import Menu
|
||||
|
||||
# 验证菜单是否存在
|
||||
result = await db.execute(
|
||||
select(Menu).where(Menu.id == data.menu_id)
|
||||
)
|
||||
menu = result.scalar_one_or_none()
|
||||
if not menu:
|
||||
return PermissionBatchCreateFromRoutesOut(
|
||||
created=0,
|
||||
skipped=0,
|
||||
failed=len(data.routes),
|
||||
errors=[f"菜单 {data.menu_id} 不存在"]
|
||||
)
|
||||
|
||||
# 转换routes为dict列表
|
||||
routes_dict = [route.model_dump() for route in data.routes]
|
||||
|
||||
created, skipped, failed, errors = await PermissionService.batch_create_from_routes(
|
||||
db, data.menu_id, routes_dict
|
||||
)
|
||||
|
||||
return PermissionBatchCreateFromRoutesOut(
|
||||
created=created,
|
||||
skipped=skipped,
|
||||
failed=failed,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auto/scan", response_model=dict, summary="自动扫描并生成权限")
|
||||
async def auto_scan_and_generate_permissions(
|
||||
dry_run: bool = Query(default=False, description="如果为true,只预览不实际创建"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
从FastAPI Router自动扫描所有API端点并生成权限
|
||||
|
||||
参数:
|
||||
- dry_run: 如果为true,只预览将要生成的权限,不实际创建
|
||||
"""
|
||||
from main import app
|
||||
|
||||
result = await PermissionService.auto_generate_permissions(db, app, dry_run=dry_run)
|
||||
return result
|
||||
|
||||
|
||||
async def _build_permission_response(db: AsyncSession, permission) -> PermissionResponse:
|
||||
"""构建权限响应"""
|
||||
# 获取菜单名称
|
||||
menu_name = None
|
||||
if permission.menu_id:
|
||||
from core.menu.service import MenuService
|
||||
menu = await MenuService.get_by_id(db, permission.menu_id)
|
||||
menu_name = menu.name if menu else None
|
||||
|
||||
return PermissionResponse(
|
||||
id=permission.id,
|
||||
menu_id=permission.menu_id,
|
||||
menu_name=menu_name,
|
||||
name=permission.name,
|
||||
code=permission.code,
|
||||
permission_type=permission.permission_type,
|
||||
permission_type_display=permission.get_permission_type_display(),
|
||||
api_path=permission.api_path,
|
||||
http_method=permission.http_method,
|
||||
http_method_display=permission.get_http_method_display(),
|
||||
description=permission.description,
|
||||
is_active=permission.is_active,
|
||||
sort=permission.sort,
|
||||
is_deleted=permission.is_deleted,
|
||||
sys_create_datetime=permission.sys_create_datetime,
|
||||
sys_update_datetime=permission.sys_update_datetime,
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Permission Model - 权限模型
|
||||
用于管理系统中的操作权限(如按钮权限、接口权限等)
|
||||
"""
|
||||
from sqlalchemy import Column, String, Integer, Boolean, Text, UniqueConstraint
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class Permission(BaseModel):
|
||||
"""
|
||||
权限模型 - 用于细粒度的权限控制
|
||||
|
||||
字段说明:
|
||||
- menu_id: 关联的菜单ID(逻辑外键)
|
||||
- name: 权限名称
|
||||
- code: 权限编码
|
||||
- permission_type: 权限类型(0-按钮权限, 1-API权限, 3-其他权限)
|
||||
- api_path: API路径
|
||||
- http_method: HTTP方法(0-GET, 1-POST, 2-PUT, 3-DELETE, 4-PATCH, 5-ALL)
|
||||
- description: 权限描述
|
||||
- is_active: 是否启用
|
||||
|
||||
注意:数据权限已迁移到 ResourceDataScopeConfig 模型,按资源类型进行配置
|
||||
"""
|
||||
__tablename__ = "core_permission"
|
||||
|
||||
# 权限类型选择
|
||||
PERMISSION_TYPE_CHOICES = {
|
||||
0: '按钮权限',
|
||||
1: 'API权限',
|
||||
3: '其他权限',
|
||||
}
|
||||
|
||||
# HTTP方法选择
|
||||
HTTP_METHOD_CHOICES = {
|
||||
0: 'GET',
|
||||
1: 'POST',
|
||||
2: 'PUT',
|
||||
3: 'DELETE',
|
||||
4: 'PATCH',
|
||||
5: 'ALL',
|
||||
}
|
||||
|
||||
# 关联的菜单ID(逻辑外键)
|
||||
menu_id = Column(String(21), nullable=False, index=True, comment="关联菜单ID(逻辑外键关联core_menu)")
|
||||
|
||||
# 权限名称
|
||||
name = Column(String(64), nullable=False, index=True, comment="权限名称")
|
||||
|
||||
# 权限编码
|
||||
code = Column(String(64), nullable=False, index=True, comment="权限编码")
|
||||
|
||||
# 权限类型
|
||||
permission_type = Column(Integer, default=0, index=True, comment="权限类型(1-API权限")
|
||||
|
||||
# API路径
|
||||
api_path = Column(String(200), nullable=True, comment="API路径")
|
||||
|
||||
# HTTP方法
|
||||
http_method = Column(Integer, default=0, comment="HTTP方法(0-GET, 1-POST, 2-PUT, 3-DELETE, 4-PATCH, 5-ALL)")
|
||||
|
||||
# 权限描述
|
||||
description = Column(Text, nullable=True, comment="权限描述")
|
||||
|
||||
# 是否启用
|
||||
is_active = Column(Boolean, default=True, index=True, comment="是否启用")
|
||||
|
||||
# 联合唯一约束:同一个菜单下的权限编码必须唯一
|
||||
__table_args__ = (
|
||||
UniqueConstraint('menu_id', 'code', name='uq_permission_menu_code'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
def get_http_method_display(self) -> str:
|
||||
"""获取HTTP方法的显示名称"""
|
||||
return self.HTTP_METHOD_CHOICES.get(self.http_method, 'UNKNOWN')
|
||||
|
||||
def get_permission_type_display(self) -> str:
|
||||
"""获取权限类型的显示名称"""
|
||||
return self.PERMISSION_TYPE_CHOICES.get(self.permission_type, '未知')
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Permission Schema - 权限数据验证模式
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class PermissionBase(BaseModel):
|
||||
"""权限基础Schema"""
|
||||
menu_id: str = Field(..., description="菜单ID")
|
||||
name: str = Field(..., min_length=1, max_length=64, description="权限名称")
|
||||
code: str = Field(..., min_length=1, max_length=64, description="权限编码")
|
||||
permission_type: int = Field(default=0, description="权限类型(0-按钮权限, 1-API权限, 3-其他权限)")
|
||||
api_path: Optional[str] = Field(None, max_length=200, description="API路径")
|
||||
http_method: int = Field(default=0, description="HTTP方法(0-GET, 1-POST, 2-PUT, 3-DELETE, 4-PATCH, 5-ALL)")
|
||||
description: Optional[str] = Field(None, description="权限描述")
|
||||
is_active: bool = Field(default=True, description="是否启用")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证权限编码格式"""
|
||||
if not v:
|
||||
raise ValueError("权限编码不能为空")
|
||||
if not all(c.isalnum() or c in '_:' for c in v):
|
||||
raise ValueError("权限编码只能包含字母、数字、下划线和冒号")
|
||||
return v
|
||||
|
||||
@field_validator("http_method")
|
||||
@classmethod
|
||||
def validate_http_method(cls, v):
|
||||
"""验证HTTP方法"""
|
||||
if v not in [0, 1, 2, 3, 4, 5]:
|
||||
raise ValueError("HTTP方法必须在 0-5 之间")
|
||||
return v
|
||||
|
||||
@field_validator("permission_type")
|
||||
@classmethod
|
||||
def validate_permission_type(cls, v):
|
||||
"""验证权限类型"""
|
||||
if v not in [0, 1, 3]:
|
||||
raise ValueError("权限类型必须在 0, 1, 3 之间")
|
||||
return v
|
||||
|
||||
|
||||
class PermissionCreate(PermissionBase):
|
||||
"""权限创建Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class PermissionUpdate(BaseModel):
|
||||
"""权限更新Schema - 所有字段可选"""
|
||||
menu_id: Optional[str] = Field(None, description="菜单ID")
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=64, description="权限名称")
|
||||
code: Optional[str] = Field(None, min_length=1, max_length=64, description="权限编码")
|
||||
permission_type: Optional[int] = Field(None, description="权限类型")
|
||||
api_path: Optional[str] = Field(None, max_length=200, description="API路径")
|
||||
http_method: Optional[int] = Field(None, description="HTTP方法")
|
||||
description: Optional[str] = Field(None, description="权限描述")
|
||||
is_active: Optional[bool] = Field(None, description="是否启用")
|
||||
sort: Optional[int] = Field(None, description="排序")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证权限编码格式"""
|
||||
if v is not None:
|
||||
if not v:
|
||||
raise ValueError("权限编码不能为空")
|
||||
if not all(c.isalnum() or c in '_:' for c in v):
|
||||
raise ValueError("权限编码只能包含字母、数字、下划线和冒号")
|
||||
return v
|
||||
|
||||
@field_validator("http_method")
|
||||
@classmethod
|
||||
def validate_http_method(cls, v):
|
||||
"""验证HTTP方法"""
|
||||
if v is not None and v not in [0, 1, 2, 3, 4, 5]:
|
||||
raise ValueError("HTTP方法必须在 0-5 之间")
|
||||
return v
|
||||
|
||||
@field_validator("permission_type")
|
||||
@classmethod
|
||||
def validate_permission_type(cls, v):
|
||||
"""验证权限类型"""
|
||||
if v is not None and v not in [0, 1, 3]:
|
||||
raise ValueError("权限类型必须在 0, 1, 3 之间")
|
||||
return v
|
||||
|
||||
|
||||
class PermissionResponse(BaseModel):
|
||||
"""权限响应Schema"""
|
||||
id: str
|
||||
menu_id: str
|
||||
menu_name: Optional[str] = None
|
||||
name: str
|
||||
code: str
|
||||
permission_type: int
|
||||
permission_type_display: Optional[str] = None
|
||||
api_path: Optional[str] = None
|
||||
http_method: int
|
||||
http_method_display: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
is_active: bool
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PermissionSimple(BaseModel):
|
||||
"""权限简单输出"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
permission_type: int
|
||||
is_active: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PermissionBatchDeleteIn(BaseModel):
|
||||
"""批量删除权限输入"""
|
||||
ids: List[str] = Field(..., description="要删除的权限ID列表")
|
||||
|
||||
|
||||
class PermissionBatchDeleteOut(BaseModel):
|
||||
"""批量删除权限输出"""
|
||||
count: int = Field(..., description="删除的记录数")
|
||||
|
||||
|
||||
class PermissionBatchUpdateStatusIn(BaseModel):
|
||||
"""批量更新权限状态输入"""
|
||||
ids: List[str] = Field(..., description="权限ID列表")
|
||||
is_active: bool = Field(..., description="是否启用")
|
||||
|
||||
|
||||
class PermissionBatchUpdateStatusOut(BaseModel):
|
||||
"""批量更新权限状态输出"""
|
||||
count: int = Field(..., description="更新的记录数")
|
||||
|
||||
|
||||
class PermissionSearchRequest(BaseModel):
|
||||
"""搜索权限请求"""
|
||||
keyword: str = Field(..., description="搜索关键词")
|
||||
|
||||
|
||||
class PermissionRouteItem(BaseModel):
|
||||
"""单个路由权限项"""
|
||||
path: str = Field(..., description="API路径")
|
||||
method: str = Field(..., description="HTTP方法")
|
||||
name: str = Field(..., description="权限名称")
|
||||
code: str = Field(..., description="权限编码")
|
||||
summary: Optional[str] = Field(None, description="权限描述")
|
||||
permission_type: int = Field(default=1, description="权限类型")
|
||||
http_method: int = Field(..., description="HTTP方法编码")
|
||||
is_active: bool = Field(default=True, description="是否启用")
|
||||
|
||||
|
||||
class PermissionBatchCreateFromRoutesIn(BaseModel):
|
||||
"""从路由批量创建权限输入"""
|
||||
menu_id: str = Field(..., description="菜单ID")
|
||||
routes: List[PermissionRouteItem] = Field(..., description="要创建的权限列表")
|
||||
|
||||
|
||||
class PermissionBatchCreateFromRoutesOut(BaseModel):
|
||||
"""从路由批量创建权限输出"""
|
||||
created: int = Field(..., description="创建的权限数")
|
||||
skipped: int = Field(..., description="跳过的权限数")
|
||||
failed: int = Field(..., description="失败的权限数")
|
||||
errors: List[str] = Field(default=[], description="错误信息列表")
|
||||
@@ -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