Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Role Module - 角色管理模块
|
||||
"""
|
||||
from core.role.model import Role
|
||||
from core.role.service import RoleService
|
||||
from core.role.api import router
|
||||
|
||||
__all__ = ["Role", "RoleService", "router"]
|
||||
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Role 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.role.schema import (
|
||||
RoleCreate, RoleUpdate, RoleResponse, RoleDetail, RoleSimple,
|
||||
RoleBatchDeleteIn, RoleBatchDeleteOut,
|
||||
RoleBatchUpdateStatusIn, RoleBatchUpdateStatusOut,
|
||||
RoleUserSchema, RoleUserIn, RoleMenuPermissionUpdateIn,
|
||||
RoleSearchRequest, RoleCopyRequest
|
||||
)
|
||||
from core.role.service import RoleService
|
||||
|
||||
router = APIRouter(prefix="/role", tags=["角色管理"])
|
||||
|
||||
|
||||
@router.post("", response_model=RoleResponse, summary="创建角色")
|
||||
async def create_role(data: RoleCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建角色"""
|
||||
# 编码唯一性校验
|
||||
if not await RoleService.check_unique(db, field="code", value=data.code):
|
||||
raise HTTPException(status_code=400, detail=f"角色编码已存在: {data.code}")
|
||||
|
||||
role = await RoleService.create(db=db, data=data)
|
||||
return await _build_role_response(db, role)
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[RoleSimple], summary="获取所有角色(简化版)")
|
||||
async def get_all_roles(db: AsyncSession = Depends(get_db)):
|
||||
"""获取所有启用的角色(不分页,简化版,用于选择器)"""
|
||||
roles = await RoleService.get_all_active(db)
|
||||
return [RoleSimple.model_validate(role) for role in roles]
|
||||
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[RoleResponse], summary="获取角色列表")
|
||||
async def get_role_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="角色编码"),
|
||||
status: Optional[bool] = Query(None, description="角色状态"),
|
||||
role_type: Optional[int] = Query(None, alias="role_type", description="角色类型"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取角色列表(分页)"""
|
||||
from core.role.model import Role
|
||||
|
||||
filters = []
|
||||
if name:
|
||||
filters.append(Role.name.ilike(f"%{name}%"))
|
||||
if code:
|
||||
filters.append(Role.code.ilike(f"%{code}%"))
|
||||
if status is not None:
|
||||
filters.append(Role.status == status)
|
||||
if role_type is not None:
|
||||
filters.append(Role.role_type == role_type)
|
||||
|
||||
items, total = await RoleService.get_list(db, page=page, page_size=page_size, filters=filters)
|
||||
response_items = [await _build_role_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_role_unique(
|
||||
field: str = Query(..., description="字段名"),
|
||||
value: str = Query(..., description="字段值"),
|
||||
exclude_id: Optional[str] = Query(None, alias="excludeId", description="排除ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""检查角色字段唯一性"""
|
||||
allowed_fields = ["code", "name"]
|
||||
if field not in allowed_fields:
|
||||
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
|
||||
|
||||
is_unique = await RoleService.check_unique(db, field=field, value=value, exclude_id=exclude_id)
|
||||
return ResponseModel(message="可用" if is_unique else "已存在", data={"unique": is_unique})
|
||||
|
||||
|
||||
@router.post("/batch/delete", response_model=RoleBatchDeleteOut, summary="批量删除角色")
|
||||
async def batch_delete_roles(
|
||||
data: RoleBatchDeleteIn,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除角色"""
|
||||
count, failed_ids = await RoleService.batch_delete(db, data.ids, hard=hard)
|
||||
return RoleBatchDeleteOut(count=count, failed_ids=failed_ids)
|
||||
|
||||
|
||||
@router.post("/batch/status", response_model=RoleBatchUpdateStatusOut, summary="批量更新角色状态")
|
||||
async def batch_update_role_status(
|
||||
data: RoleBatchUpdateStatusIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量更新角色状态"""
|
||||
count = await RoleService.batch_update_status(db, data.ids, data.status)
|
||||
return RoleBatchUpdateStatusOut(count=count)
|
||||
|
||||
|
||||
@router.post("/search", response_model=PaginatedResponse[RoleResponse], summary="搜索角色")
|
||||
async def search_role(
|
||||
data: RoleSearchRequest,
|
||||
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 RoleService.search(db, data.keyword, page, page_size)
|
||||
response_items = [await _build_role_response(db, item) for item in items]
|
||||
return PaginatedResponse(items=response_items, total=total)
|
||||
|
||||
|
||||
@router.get("/by/ids", response_model=List[RoleSimple], summary="根据ID列表获取角色")
|
||||
async def get_roles_by_ids(
|
||||
ids: str = Query(..., description="角色ID列表,逗号分隔"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""根据角色ID列表批量获取角色信息"""
|
||||
role_ids = [id.strip() for id in ids.split(',') if id.strip()]
|
||||
roles = await RoleService.get_by_ids(db, role_ids)
|
||||
return [RoleSimple.model_validate(role) for role in roles]
|
||||
|
||||
|
||||
@router.get("/users/by/role_id", response_model=PaginatedResponse[RoleUserSchema], summary="获取角色用户列表")
|
||||
async def get_role_users(
|
||||
role_id: str = Query(..., alias="role_id", description="角色ID"),
|
||||
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="每页数量"),
|
||||
username: str = Query(default=None, description="用户名/姓名搜索"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取角色下的用户列表"""
|
||||
role = await RoleService.get_by_id(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
items, total = await RoleService.get_role_users(
|
||||
db, role_id, page=page, page_size=page_size, username=username
|
||||
)
|
||||
return PaginatedResponse(
|
||||
items=[RoleUserSchema(**item) for item in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/users/by/role_id", response_model=ResponseModel, summary="为角色添加用户")
|
||||
async def add_user_to_role(
|
||||
data: RoleUserIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""将用户添加到角色"""
|
||||
role = await RoleService.get_by_id(db, data.role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
if not data.user_ids:
|
||||
raise HTTPException(status_code=400, detail="用户ID列表不能为空")
|
||||
|
||||
added_count = await RoleService.add_users_to_role(db, data.role_id, data.user_ids)
|
||||
return ResponseModel(message=f"成功添加 {added_count} 个用户")
|
||||
|
||||
|
||||
@router.delete("/users/by/role_id", response_model=ResponseModel, summary="从角色中移除用户")
|
||||
async def remove_user_from_role(
|
||||
data: RoleUserIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""从角色中移除用户"""
|
||||
role = await RoleService.get_by_id(db, data.role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
user_ids_to_remove = data.user_ids if data.user_ids else ([data.user_id] if data.user_id else [])
|
||||
|
||||
if not user_ids_to_remove:
|
||||
raise HTTPException(status_code=400, detail="用户ID不能为空")
|
||||
|
||||
removed_count = await RoleService.remove_users_from_role(db, data.role_id, user_ids_to_remove)
|
||||
return ResponseModel(message=f"成功移除 {removed_count} 个用户")
|
||||
|
||||
|
||||
@router.put("/{role_id}/menus-permissions", response_model=ResponseModel, summary="更新角色菜单和权限")
|
||||
async def update_role_menus_permissions(
|
||||
role_id: str,
|
||||
data: RoleMenuPermissionUpdateIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""同时更新角色关联的菜单和权限"""
|
||||
role = await RoleService.get_by_id(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
success = await RoleService.update_menus_permissions(
|
||||
db, role_id, data.menu_ids, data.permission_ids, data.loaded_menu_ids
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="更新失败")
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return ResponseModel(message=f"成功更新 {len(data.menu_ids)} 个菜单和 {len(data.permission_ids)} 个权限")
|
||||
|
||||
|
||||
@router.put("/{role_id}/permissions", response_model=ResponseModel, summary="更新角色权限")
|
||||
async def update_role_permissions(
|
||||
role_id: str,
|
||||
data: RoleMenuPermissionUpdateIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新角色关联的权限"""
|
||||
role = await RoleService.get_by_id(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
success = await RoleService.update_permissions(db, role_id, data.permission_ids)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="更新失败")
|
||||
|
||||
# 刷新权限缓存
|
||||
from utils.permission import clear_permission_cache
|
||||
await clear_permission_cache()
|
||||
|
||||
return ResponseModel(message=f"成功更新 {len(data.permission_ids)} 个权限")
|
||||
|
||||
|
||||
@router.get("/menu-permission-tree/{role_id}", response_model=dict, summary="获取角色的菜单权限树")
|
||||
async def get_role_menu_permission_tree(
|
||||
role_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取角色的菜单和权限树形结构,用于角色权限配置界面"""
|
||||
role = await RoleService.get_by_id_with_relations(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
return await RoleService.get_menu_permission_tree(db, role)
|
||||
|
||||
|
||||
@router.get("/{role_id}/menus", response_model=dict, summary="获取角色菜单列表")
|
||||
async def get_role_menus(
|
||||
role_id: str,
|
||||
application_id: Optional[str] = Query(None, alias="applicationId", description="应用ID,子应用访问时只显示该应用的菜单"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取角色的菜单列表(不包含权限)"""
|
||||
role = await RoleService.get_by_id_with_relations(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
return await RoleService.get_role_menus(db, role, application_id=application_id)
|
||||
|
||||
|
||||
@router.get("/{role_id}/menu/{menu_id}/permissions", response_model=dict, summary="获取菜单的权限列表")
|
||||
async def get_menu_permissions(
|
||||
role_id: str,
|
||||
menu_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定菜单的权限列表"""
|
||||
role = await RoleService.get_by_id_with_relations(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
return await RoleService.get_menu_permissions(db, role, menu_id)
|
||||
|
||||
|
||||
@router.patch("/{role_id}", response_model=RoleResponse, summary="部分更新角色")
|
||||
async def patch_role(role_id: str, data: RoleUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""部分更新角色(只更新提供的字段)"""
|
||||
role = await RoleService.get_by_id(db, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 编码唯一性校验
|
||||
if data.code:
|
||||
if not await RoleService.check_unique(db, field="code", value=data.code, exclude_id=role_id):
|
||||
raise HTTPException(status_code=400, detail=f"角色编码已存在: {data.code}")
|
||||
|
||||
# 系统角色不能修改角色类型和编码
|
||||
if role.is_system_role():
|
||||
if data.role_type is not None and data.role_type != 0:
|
||||
raise HTTPException(status_code=400, detail="系统角色不能修改角色类型")
|
||||
if data.code and data.code != role.code:
|
||||
raise HTTPException(status_code=400, detail="系统角色不能修改角色编码")
|
||||
|
||||
role = await RoleService.update(db, record_id=role_id, data=data)
|
||||
return await _build_role_response(db, role)
|
||||
|
||||
|
||||
@router.post("/copy/{role_id}", response_model=RoleResponse, summary="复制角色")
|
||||
async def copy_role(
|
||||
role_id: str,
|
||||
data: RoleCopyRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""复制现有角色"""
|
||||
# 检查新编码是否已存在
|
||||
if not await RoleService.check_unique(db, field="code", value=data.new_code):
|
||||
raise HTTPException(status_code=400, detail=f"角色编码已存在: {data.new_code}")
|
||||
|
||||
new_role = await RoleService.copy_role(db, role_id, data.new_name, data.new_code)
|
||||
if not new_role:
|
||||
raise HTTPException(status_code=404, detail="源角色不存在")
|
||||
|
||||
return await _build_role_response(db, new_role)
|
||||
|
||||
|
||||
@router.get("/{role_id}", response_model=RoleDetail, summary="获取角色详情")
|
||||
async def get_role_by_id(role_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取角色详情(包含关联数据)"""
|
||||
role = await RoleService.get_by_id_with_relations(db, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
return await _build_role_detail(db, role)
|
||||
|
||||
|
||||
@router.put("/{role_id}", response_model=RoleResponse, summary="更新角色")
|
||||
async def update_role(role_id: str, data: RoleUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""更新角色"""
|
||||
role = await RoleService.get_by_id(db, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 编码唯一性校验
|
||||
if data.code:
|
||||
if not await RoleService.check_unique(db, field="code", value=data.code, exclude_id=role_id):
|
||||
raise HTTPException(status_code=400, detail=f"角色编码已存在: {data.code}")
|
||||
|
||||
# 系统角色不能修改角色类型和编码
|
||||
if role.is_system_role():
|
||||
if data.role_type is not None and data.role_type != 0:
|
||||
raise HTTPException(status_code=400, detail="系统角色不能修改角色类型")
|
||||
if data.code and data.code != role.code:
|
||||
raise HTTPException(status_code=400, detail="系统角色不能修改角色编码")
|
||||
|
||||
role = await RoleService.update(db, record_id=role_id, data=data)
|
||||
return await _build_role_response(db, role)
|
||||
|
||||
|
||||
@router.delete("/{role_id}", response_model=ResponseModel, summary="删除角色")
|
||||
async def delete_role(
|
||||
role_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除角色"""
|
||||
can_del, reason = await RoleService.can_delete(db, role_id)
|
||||
if not can_del:
|
||||
raise HTTPException(status_code=400, detail=reason)
|
||||
|
||||
success = await RoleService.delete(db, record_id=role_id, hard=hard)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
async def _build_role_response(db: AsyncSession, role) -> RoleResponse:
|
||||
"""构建角色响应"""
|
||||
user_count = await RoleService.get_user_count(db, role.id)
|
||||
|
||||
return RoleResponse(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
code=role.code,
|
||||
role_type=role.role_type,
|
||||
role_type_display=role.get_role_type_display(),
|
||||
status=role.status,
|
||||
priority=role.priority,
|
||||
description=role.description,
|
||||
remark=role.remark,
|
||||
user_count=user_count,
|
||||
menu_count=len(role.menus) if hasattr(role, 'menus') and role.menus else 0,
|
||||
permission_count=len(role.permissions) if hasattr(role, 'permissions') and role.permissions else 0,
|
||||
can_delete=role.can_delete(),
|
||||
sort=role.sort,
|
||||
is_deleted=role.is_deleted,
|
||||
sys_create_datetime=role.sys_create_datetime,
|
||||
sys_update_datetime=role.sys_update_datetime,
|
||||
)
|
||||
|
||||
|
||||
async def _build_role_detail(db: AsyncSession, role) -> RoleDetail:
|
||||
"""构建角色详情响应"""
|
||||
user_count = await RoleService.get_user_count(db, role.id)
|
||||
|
||||
return RoleDetail(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
code=role.code,
|
||||
role_type=role.role_type,
|
||||
role_type_display=role.get_role_type_display(),
|
||||
status=role.status,
|
||||
priority=role.priority,
|
||||
description=role.description,
|
||||
remark=role.remark,
|
||||
user_count=user_count,
|
||||
menu_count=len(role.menus) if role.menus else 0,
|
||||
permission_count=len(role.permissions) if role.permissions else 0,
|
||||
can_delete=role.can_delete(),
|
||||
sort=role.sort,
|
||||
is_deleted=role.is_deleted,
|
||||
sys_create_datetime=role.sys_create_datetime,
|
||||
sys_update_datetime=role.sys_update_datetime,
|
||||
menu_ids=[m.id for m in role.menus] if role.menus else [],
|
||||
permission_ids=[p.id for p in role.permissions] if role.permissions else [],
|
||||
dept_ids=[d.id for d in role.depts] if role.depts else [],
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Role Model - 角色模型
|
||||
用于管理系统角色和权限分配
|
||||
"""
|
||||
from sqlalchemy import Column, String, Integer, Boolean, Text, Table, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.base_model import BaseModel
|
||||
from app.database import Base
|
||||
|
||||
|
||||
# 角色-菜单关联表
|
||||
role_menu = Table(
|
||||
'core_role_menu',
|
||||
Base.metadata,
|
||||
Column('role_id', String(21), ForeignKey('core_role.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('menu_id', String(21), ForeignKey('core_menu.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
# 角色-权限关联表
|
||||
role_permission = Table(
|
||||
'core_role_permission',
|
||||
Base.metadata,
|
||||
Column('role_id', String(21), ForeignKey('core_role.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('permission_id', String(21), ForeignKey('core_permission.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
# 角色-部门关联表
|
||||
role_dept = Table(
|
||||
'core_role_dept',
|
||||
Base.metadata,
|
||||
Column('role_id', String(21), ForeignKey('core_role.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('dept_id', String(21), ForeignKey('core_dept.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
"""
|
||||
角色模型 - 用于用户角色管理和权限分配
|
||||
|
||||
字段说明:
|
||||
- name: 角色名称
|
||||
- code: 角色编码(唯一)
|
||||
- role_type: 角色类型(0-系统角色, 1-自定义角色)
|
||||
- status: 角色状态
|
||||
- priority: 角色优先级
|
||||
- description: 角色描述
|
||||
- remark: 备注
|
||||
"""
|
||||
__tablename__ = "core_role"
|
||||
|
||||
# 角色类型选择
|
||||
ROLE_TYPE_CHOICES = {
|
||||
0: '系统角色',
|
||||
1: '自定义角色',
|
||||
}
|
||||
|
||||
# 角色名称
|
||||
name = Column(String(64), nullable=False, index=True, comment="角色名称")
|
||||
|
||||
# 角色编码
|
||||
code = Column(String(64), unique=True, nullable=False, index=True, comment="角色编码")
|
||||
|
||||
# 角色类型
|
||||
role_type = Column(Integer, default=1, index=True, comment="角色类型(0-系统角色, 1-自定义角色)")
|
||||
|
||||
# 角色状态
|
||||
status = Column(Boolean, default=True, index=True, comment="角色状态(启用/禁用)")
|
||||
|
||||
# 角色优先级
|
||||
priority = Column(Integer, default=0, index=True, comment="角色优先级(数字越大优先级越高)")
|
||||
|
||||
# 角色描述
|
||||
description = Column(Text, nullable=True, comment="角色描述")
|
||||
|
||||
# 备注
|
||||
remark = Column(Text, nullable=True, comment="备注信息")
|
||||
|
||||
# 多对多关系
|
||||
menus = relationship("Menu", secondary=role_menu, backref="roles", lazy="selectin")
|
||||
permissions = relationship("Permission", secondary=role_permission, backref="roles", lazy="selectin")
|
||||
depts = relationship("Dept", secondary=role_dept, backref="roles", lazy="selectin")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
def is_system_role(self) -> bool:
|
||||
"""判断是否为系统角色"""
|
||||
return self.role_type == 0
|
||||
|
||||
def can_delete(self) -> bool:
|
||||
"""判断是否可以删除(系统角色不可删除)"""
|
||||
return self.role_type != 0
|
||||
|
||||
def get_role_type_display(self) -> str:
|
||||
"""获取角色类型的显示名称"""
|
||||
return self.ROLE_TYPE_CHOICES.get(self.role_type, '未知')
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Role 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 RoleBase(BaseModel):
|
||||
"""角色基础Schema"""
|
||||
name: str = Field(..., min_length=1, max_length=64, description="角色名称")
|
||||
code: str = Field(..., min_length=1, max_length=64, description="角色编码")
|
||||
role_type: int = Field(default=1, description="角色类型(0-系统角色, 1-自定义角色)")
|
||||
status: bool = Field(default=True, description="角色状态")
|
||||
priority: int = Field(default=0, description="角色优先级")
|
||||
description: Optional[str] = Field(None, description="角色描述")
|
||||
remark: Optional[str] = Field(None, description="备注")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证角色编码格式"""
|
||||
if not v:
|
||||
raise ValueError("角色编码不能为空")
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("角色编码只能包含字母、数字和下划线")
|
||||
return v
|
||||
|
||||
@field_validator("role_type")
|
||||
@classmethod
|
||||
def validate_role_type(cls, v):
|
||||
"""验证角色类型"""
|
||||
if v not in [0, 1]:
|
||||
raise ValueError("角色类型必须为 0(系统角色) 或 1(自定义角色)")
|
||||
return v
|
||||
|
||||
@field_validator("priority")
|
||||
@classmethod
|
||||
def validate_priority(cls, v):
|
||||
"""验证优先级"""
|
||||
if v < 0:
|
||||
raise ValueError("优先级不能为负数")
|
||||
return v
|
||||
|
||||
|
||||
class RoleCreate(RoleBase):
|
||||
"""角色创建Schema"""
|
||||
menu_ids: List[str] = Field(default=[], description="菜单ID列表")
|
||||
permission_ids: List[str] = Field(default=[], description="权限ID列表")
|
||||
dept_ids: List[str] = Field(default=[], description="部门ID列表")
|
||||
|
||||
|
||||
class RoleUpdate(BaseModel):
|
||||
"""角色更新Schema - 所有字段可选"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=64, description="角色名称")
|
||||
code: Optional[str] = Field(None, min_length=1, max_length=64, description="角色编码")
|
||||
role_type: Optional[int] = Field(None, description="角色类型")
|
||||
status: Optional[bool] = Field(None, description="角色状态")
|
||||
priority: Optional[int] = Field(None, description="角色优先级")
|
||||
description: Optional[str] = Field(None, description="角色描述")
|
||||
remark: Optional[str] = Field(None, description="备注")
|
||||
sort: Optional[int] = Field(None, description="排序")
|
||||
menu_ids: Optional[List[str]] = Field(None, description="菜单ID列表")
|
||||
permission_ids: Optional[List[str]] = Field(None, description="权限ID列表")
|
||||
dept_ids: Optional[List[str]] = Field(None, description="部门ID列表")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证角色编码格式"""
|
||||
if v is not None:
|
||||
if not v:
|
||||
raise ValueError("角色编码不能为空")
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("角色编码只能包含字母、数字和下划线")
|
||||
return v
|
||||
|
||||
@field_validator("role_type")
|
||||
@classmethod
|
||||
def validate_role_type(cls, v):
|
||||
"""验证角色类型"""
|
||||
if v is not None and v not in [0, 1]:
|
||||
raise ValueError("角色类型必须为 0(系统角色) 或 1(自定义角色)")
|
||||
return v
|
||||
|
||||
@field_validator("priority")
|
||||
@classmethod
|
||||
def validate_priority(cls, v):
|
||||
"""验证优先级"""
|
||||
if v is not None and v < 0:
|
||||
raise ValueError("优先级不能为负数")
|
||||
return v
|
||||
|
||||
|
||||
class RoleResponse(BaseModel):
|
||||
"""角色响应Schema"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
role_type: int
|
||||
role_type_display: Optional[str] = None
|
||||
status: bool
|
||||
priority: int
|
||||
description: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
user_count: int = 0
|
||||
menu_count: int = 0
|
||||
permission_count: int = 0
|
||||
can_delete: bool = True
|
||||
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 RoleDetail(RoleResponse):
|
||||
"""角色详情响应Schema(包含关联数据)"""
|
||||
menu_ids: List[str] = []
|
||||
permission_ids: List[str] = []
|
||||
dept_ids: List[str] = []
|
||||
|
||||
|
||||
class RoleSimple(BaseModel):
|
||||
"""角色简单输出(用于选择器)"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
status: bool
|
||||
role_type: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RoleBatchDeleteIn(BaseModel):
|
||||
"""批量删除角色输入"""
|
||||
ids: List[str] = Field(..., description="要删除的角色ID列表")
|
||||
|
||||
|
||||
class RoleBatchDeleteOut(BaseModel):
|
||||
"""批量删除角色输出"""
|
||||
count: int = Field(..., description="删除的记录数")
|
||||
failed_ids: List[str] = Field(default=[], description="删除失败的ID列表")
|
||||
|
||||
|
||||
class RoleBatchUpdateStatusIn(BaseModel):
|
||||
"""批量更新角色状态输入"""
|
||||
ids: List[str] = Field(..., description="角色ID列表")
|
||||
status: bool = Field(..., description="角色状态")
|
||||
|
||||
|
||||
class RoleBatchUpdateStatusOut(BaseModel):
|
||||
"""批量更新角色状态输出"""
|
||||
count: int = Field(..., description="更新的记录数")
|
||||
|
||||
|
||||
class RoleUserSchema(BaseModel):
|
||||
"""角色关联的用户信息"""
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
dept_name: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RoleUserIn(BaseModel):
|
||||
"""角色用户操作输入"""
|
||||
role_id: str = Field(..., description="角色ID")
|
||||
user_ids: List[str] = Field(default=[], description="用户ID列表")
|
||||
user_id: Optional[str] = Field(None, description="单个用户ID")
|
||||
|
||||
|
||||
class RoleMenuPermissionUpdateIn(BaseModel):
|
||||
"""更新角色菜单和权限输入"""
|
||||
menu_ids: List[str] = Field(default=[], description="菜单ID列表")
|
||||
permission_ids: List[str] = Field(default=[], description="权限ID列表")
|
||||
loaded_menu_ids: List[str] = Field(default=[], description="前端已加载过权限的菜单ID列表,用于增量更新权限(只替换这些菜单下的权限,保留未加载菜单的原有权限)")
|
||||
|
||||
|
||||
class RoleSearchRequest(BaseModel):
|
||||
"""搜索角色请求"""
|
||||
keyword: str = Field(..., description="搜索关键词")
|
||||
|
||||
|
||||
class RoleCopyRequest(BaseModel):
|
||||
"""复制角色请求"""
|
||||
new_name: str = Field(..., description="新角色名称")
|
||||
new_code: str = Field(..., description="新角色编码")
|
||||
@@ -0,0 +1,729 @@
|
||||
#!/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
|
||||
|
||||
|
||||
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 = list(result.scalars().all())
|
||||
role.menus = menus
|
||||
|
||||
from core.permission.model import Permission
|
||||
|
||||
if loaded_menu_ids:
|
||||
# 增量更新:只替换已加载菜单下的权限
|
||||
loaded_menu_set = set(loaded_menu_ids)
|
||||
|
||||
# 保留未加载菜单下的原有权限
|
||||
kept_permissions = [
|
||||
p for p in (role.permissions or [])
|
||||
if 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))
|
||||
)
|
||||
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))
|
||||
)
|
||||
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 = list(result.scalars().all())
|
||||
|
||||
# 获取该角色已分配的权限ID和菜单ID
|
||||
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
||||
role_menu_ids = set(m.id for m in role.menus) if role.menus else set()
|
||||
|
||||
# 获取所有启用的权限
|
||||
result = await db.execute(
|
||||
select(Permission).where(Permission.is_active == True, Permission.is_deleted == False) # noqa: E712
|
||||
)
|
||||
all_permissions = list(result.scalars().all())
|
||||
|
||||
# 权限类型映射
|
||||
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 = list(result.scalars().all())
|
||||
|
||||
# 统计每个菜单的权限数量
|
||||
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.permission.model import Permission
|
||||
|
||||
# 获取该角色已选中的权限ID
|
||||
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
||||
|
||||
# 权限类型映射
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user