- Permission 模型新增 sort 字段
- 后端 API 按 sort 升序排列树形菜单
- 前端菜单管理页面增加排序列
- 初始数据添加排序号
- 修复 GET /permissions/{id} 详情接口 bug
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
521 lines
17 KiB
Python
521 lines
17 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, and_
|
||
from sqlalchemy.orm import selectinload
|
||
from fastcrud import FastCRUD
|
||
from typing import Dict, Any
|
||
|
||
from database import get_db
|
||
from models import Permission, Role
|
||
from schemas import (
|
||
PermissionCreate,
|
||
PermissionResponse,
|
||
PermissionTreeResponse,
|
||
RolePermissionAssign,
|
||
RolePermissionResponse,
|
||
)
|
||
from middleware import get_current_user
|
||
from models import User
|
||
from utils.like_filter import ilike_contains
|
||
from utils.log_utils import get_client_ip, get_user_agent, create_operation_log
|
||
|
||
router = APIRouter(prefix="/permissions", tags=["permissions"])
|
||
|
||
# 创建FastCRUD实例
|
||
permission_crud = FastCRUD(Permission, PermissionResponse)
|
||
|
||
|
||
@router.post("/", response_model=PermissionResponse)
|
||
async def create_permission(
|
||
permission_data: PermissionCreate,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""创建权限"""
|
||
# 检查权限编码是否已存在
|
||
result = await db.execute(select(Permission).filter(Permission.code == permission_data.code))
|
||
existing_permission = result.scalar_one_or_none()
|
||
if existing_permission:
|
||
raise HTTPException(status_code=400, detail="权限编码已存在")
|
||
|
||
# 创建权限实例
|
||
db_permission = Permission(
|
||
name=permission_data.name,
|
||
code=permission_data.code,
|
||
parentid=permission_data.parentid,
|
||
route=permission_data.route,
|
||
description=permission_data.description,
|
||
sort=permission_data.sort,
|
||
)
|
||
|
||
db.add(db_permission)
|
||
await db.flush()
|
||
await db.refresh(db_permission)
|
||
|
||
# 记录操作日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "create", "permission",
|
||
db_permission.id, db_permission.name,
|
||
{"name": permission_data.name, "code": permission_data.code},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
return db_permission
|
||
|
||
|
||
@router.post("/search", response_model=list[PermissionResponse])
|
||
async def search_permissions(
|
||
query: dict = {},
|
||
request: Request = None,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取权限列表(POST方式);支持按名称、编码的模糊查询与组合查询(AND)"""
|
||
stmt = select(Permission)
|
||
filters = []
|
||
for col, key in ((Permission.name, "name"), (Permission.code, "code")):
|
||
raw = query.get(key)
|
||
cond = ilike_contains(col, raw)
|
||
if cond is not None:
|
||
filters.append(cond)
|
||
if filters:
|
||
stmt = stmt.where(and_(*filters))
|
||
result = await db.execute(stmt)
|
||
permissions = result.scalars().all()
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
None, None, {"filters": {k: v for k, v in query.items() if v}},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
return [PermissionResponse.model_validate(p) for p in permissions]
|
||
|
||
|
||
@router.get("/tree", response_model=list[PermissionTreeResponse])
|
||
async def get_permission_tree(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取权限树形结构(基于 parentid)"""
|
||
# 获取所有权限
|
||
result = await db.execute(select(Permission))
|
||
permissions = result.scalars().all()
|
||
|
||
# 转换为字典,便于查找
|
||
perm_dict = {perm.id: {
|
||
"id": perm.id,
|
||
"name": perm.name,
|
||
"code": perm.code,
|
||
"parentid": perm.parentid,
|
||
"route": perm.route,
|
||
"description": perm.description,
|
||
"sort": perm.sort or 0,
|
||
"createtime": perm.createtime,
|
||
"updatetime": perm.updatetime,
|
||
"children": []
|
||
} for perm in permissions}
|
||
|
||
# 构建树形结构
|
||
tree = []
|
||
for perm_id, perm_data in perm_dict.items():
|
||
parentid = perm_data["parentid"]
|
||
if parentid == 0:
|
||
# 顶级节点
|
||
tree.append(perm_data)
|
||
elif parentid in perm_dict:
|
||
# 子节点,添加到父节点的 children
|
||
perm_dict[parentid]["children"].append(perm_data)
|
||
|
||
# 递归排序:按 sort 字段升序排列
|
||
def sort_tree(nodes):
|
||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||
for node in nodes:
|
||
if node.get("children"):
|
||
sort_tree(node["children"])
|
||
return nodes
|
||
|
||
tree = sort_tree(tree)
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
None, None, None,
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
return tree
|
||
|
||
|
||
@router.get("/menu", response_model=list[PermissionTreeResponse])
|
||
async def get_menu(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取当前用户的菜单树形结构(仅包含有路由的权限)"""
|
||
# 获取用户角色的权限
|
||
result = await db.execute(
|
||
select(Role)
|
||
.options(selectinload(Role.permissions))
|
||
.filter(Role.id == current_user.role_id)
|
||
)
|
||
role = result.scalar_one_or_none()
|
||
|
||
if not role:
|
||
raise HTTPException(status_code=404, detail="角色不存在")
|
||
|
||
# 获取用户有权限的权限编码集合
|
||
user_permission_codes = {perm.code for perm in role.permissions}
|
||
|
||
# 获取所有顶级模块和二级模块权限(有route字段的)
|
||
result = await db.execute(
|
||
select(Permission)
|
||
.filter(Permission.route.isnot(None))
|
||
)
|
||
all_menu_permissions = result.scalars().all()
|
||
|
||
# 过滤用户有权限的菜单
|
||
user_menu_permissions = []
|
||
for perm in all_menu_permissions:
|
||
# 检查用户是否有该权限或其子权限
|
||
has_access = False
|
||
for user_perm_code in user_permission_codes:
|
||
if user_perm_code.startswith(perm.code):
|
||
has_access = True
|
||
break
|
||
if has_access:
|
||
user_menu_permissions.append(perm)
|
||
|
||
# 转换为字典,便于查找
|
||
perm_dict = {perm.id: {
|
||
"id": perm.id,
|
||
"name": perm.name,
|
||
"code": perm.code,
|
||
"parentid": perm.parentid,
|
||
"route": perm.route,
|
||
"description": perm.description,
|
||
"sort": perm.sort or 0,
|
||
"createtime": perm.createtime,
|
||
"updatetime": perm.updatetime,
|
||
"children": []
|
||
} for perm in user_menu_permissions}
|
||
|
||
# 构建树形结构
|
||
tree = []
|
||
for perm_id, perm_data in perm_dict.items():
|
||
parentid = perm_data["parentid"]
|
||
if parentid == 0:
|
||
# 顶级节点
|
||
tree.append(perm_data)
|
||
elif parentid in perm_dict:
|
||
# 子节点,添加到父节点的 children
|
||
perm_dict[parentid]["children"].append(perm_data)
|
||
|
||
# 递归排序:按 sort 字段升序排列
|
||
def sort_tree(nodes):
|
||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||
for node in nodes:
|
||
if node.get("children"):
|
||
sort_tree(node["children"])
|
||
return nodes
|
||
|
||
tree = sort_tree(tree)
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
None, None, {"type": "menu_tree"},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
return tree
|
||
|
||
|
||
@router.get("/{permission_id}", response_model=PermissionResponse)
|
||
async def get_permission(
|
||
permission_id: int,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取单个权限"""
|
||
permission = await permission_crud.get(db, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True)
|
||
if not permission:
|
||
raise HTTPException(status_code=404, detail="权限不存在")
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
permission_id, permission.name, None,
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
return permission
|
||
|
||
|
||
@router.put("/{permission_id}", response_model=PermissionResponse)
|
||
async def update_permission(
|
||
permission_id: int,
|
||
permission_data: PermissionCreate,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""更新权限"""
|
||
# 获取原权限数据
|
||
result = await db.execute(select(Permission).filter(Permission.id == permission_id))
|
||
old_permission = result.scalar_one_or_none()
|
||
if not old_permission:
|
||
raise HTTPException(status_code=404, detail="权限不存在")
|
||
|
||
# 检查权限编码是否被其他权限使用
|
||
result = await db.execute(
|
||
select(Permission).filter(Permission.code == permission_data.code, Permission.id != permission_id)
|
||
)
|
||
existing_permission = result.scalar_one_or_none()
|
||
if existing_permission:
|
||
raise HTTPException(status_code=400, detail="权限编码已存在")
|
||
|
||
# 准备更新数据
|
||
update_data = permission_data.model_dump()
|
||
|
||
# 使用FastCRUD更新权限
|
||
updated_permission = await permission_crud.update(
|
||
db, update_data, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True
|
||
)
|
||
if not updated_permission:
|
||
raise HTTPException(status_code=404, detail="权限不存在")
|
||
# 记录操作日志
|
||
changed = {}
|
||
for field in ["name", "code", "parentid", "route", "sort", "description"]:
|
||
old_val = getattr(old_permission, field, None)
|
||
new_val = update_data.get(field)
|
||
if old_val != new_val:
|
||
changed[field] = {"old": old_val, "new": new_val}
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "update", "permission",
|
||
permission_id, old_permission.name, changed if changed else None,
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
return updated_permission
|
||
|
||
|
||
@router.delete("/{permission_id}")
|
||
async def delete_permission(
|
||
permission_id: int,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""删除权限(自动清理角色关联和子权限关联)"""
|
||
# 检查权限是否存在
|
||
result = await db.execute(select(Permission).filter(Permission.id == permission_id))
|
||
existing_permission = result.scalar_one_or_none()
|
||
if not existing_permission:
|
||
raise HTTPException(status_code=404, detail="权限不存在")
|
||
# 记录操作日志(删除前)
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "delete", "permission",
|
||
permission_id, existing_permission.name,
|
||
{"name": existing_permission.name, "code": existing_permission.code},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
|
||
# 1. 删除角色-权限关联表中的记录
|
||
from sqlalchemy import delete as sql_delete
|
||
from models import role_permission
|
||
|
||
stmt = sql_delete(role_permission).where(role_permission.c.permission_id == permission_id)
|
||
await db.execute(stmt)
|
||
|
||
# 2. 处理子权限:如果有子权限,将子权限的父ID置为0(提升为顶级节点)
|
||
from sqlalchemy import update as sql_update
|
||
|
||
stmt = (
|
||
sql_update(Permission)
|
||
.where(Permission.parentid == permission_id)
|
||
.values(parentid=0)
|
||
)
|
||
await db.execute(stmt)
|
||
|
||
# 3. 使用FastCRUD删除权限本身(不提交,与前述操作在同一事务中)
|
||
await permission_crud.delete(db, id=permission_id, commit=False)
|
||
await db.commit()
|
||
return {"message": "权限删除成功"}
|
||
|
||
|
||
# 角色权限关联接口
|
||
@router.post("/assign", response_model=RolePermissionResponse)
|
||
async def assign_permissions_to_role(
|
||
data: RolePermissionAssign,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""为角色分配权限"""
|
||
# 查找角色,同时加载权限关系
|
||
result = await db.execute(select(Role).options(selectinload(Role.permissions)).filter(Role.id == data.role_id))
|
||
role = result.scalar_one_or_none()
|
||
if not role:
|
||
raise HTTPException(status_code=404, detail="角色不存在")
|
||
|
||
# 查找要分配的权限
|
||
permissions = []
|
||
for permission_id in data.permission_ids:
|
||
result = await db.execute(select(Permission).filter(Permission.id == permission_id))
|
||
permission = result.scalar_one_or_none()
|
||
if permission:
|
||
permissions.append(permission)
|
||
|
||
# 更新角色的权限
|
||
role.permissions = permissions
|
||
|
||
# 记录操作日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "update", "role",
|
||
role.id, role.name,
|
||
{"permission_ids": data.permission_ids},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
await db.refresh(role)
|
||
|
||
# 返回角色权限信息
|
||
return RolePermissionResponse(
|
||
role_id=role.id,
|
||
role_name=role.name,
|
||
permissions=[PermissionResponse.from_orm(p) for p in permissions]
|
||
)
|
||
|
||
|
||
@router.get("/role/{role_id}", response_model=RolePermissionResponse)
|
||
async def get_role_permissions(
|
||
role_id: int,
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取角色的权限列表"""
|
||
# 查找角色,同时加载权限关系
|
||
result = await db.execute(select(Role).options(selectinload(Role.permissions)).filter(Role.id == role_id))
|
||
role = result.scalar_one_or_none()
|
||
if not role:
|
||
raise HTTPException(status_code=404, detail="角色不存在")
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
role_id, role.name, {"type": "role_permissions"},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
# 返回角色权限信息
|
||
return RolePermissionResponse(
|
||
role_id=role.id,
|
||
role_name=role.name,
|
||
permissions=[PermissionResponse.from_orm(p) for p in role.permissions]
|
||
)
|
||
|
||
|
||
@router.get("/user/me", response_model=RolePermissionResponse)
|
||
async def get_current_user_permissions(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""获取当前登录用户的权限列表和菜单"""
|
||
# 获取用户角色的权限
|
||
result = await db.execute(
|
||
select(Role)
|
||
.options(selectinload(Role.permissions))
|
||
.filter(Role.id == current_user.role_id)
|
||
)
|
||
role = result.scalar_one_or_none()
|
||
|
||
if not role:
|
||
raise HTTPException(status_code=404, detail="角色不存在")
|
||
|
||
# 获取用户有权限的权限编码集合
|
||
user_permission_codes = {perm.code for perm in role.permissions}
|
||
|
||
# 获取所有顶级模块和二级模块权限(有route字段的)
|
||
result = await db.execute(
|
||
select(Permission)
|
||
.filter(Permission.route.isnot(None))
|
||
)
|
||
all_menu_permissions = result.scalars().all()
|
||
|
||
# 过滤用户有权限的菜单
|
||
user_menu_permissions = []
|
||
for perm in all_menu_permissions:
|
||
# 检查用户是否有该权限或其子权限
|
||
has_access = False
|
||
for user_perm_code in user_permission_codes:
|
||
if user_perm_code.startswith(perm.code):
|
||
has_access = True
|
||
break
|
||
if has_access:
|
||
user_menu_permissions.append(perm)
|
||
|
||
# 转换为字典,便于查找
|
||
perm_dict = {perm.id: {
|
||
"id": perm.id,
|
||
"name": perm.name,
|
||
"code": perm.code,
|
||
"parentid": perm.parentid,
|
||
"route": perm.route,
|
||
"description": perm.description,
|
||
"sort": perm.sort or 0,
|
||
"createtime": perm.createtime,
|
||
"updatetime": perm.updatetime,
|
||
"children": []
|
||
} for perm in user_menu_permissions}
|
||
|
||
# 构建树形结构
|
||
menu_tree = []
|
||
for perm_id, perm_data in perm_dict.items():
|
||
parentid = perm_data["parentid"]
|
||
if parentid == 0:
|
||
# 顶级节点
|
||
menu_tree.append(perm_data)
|
||
elif parentid in perm_dict:
|
||
# 子节点,添加到父节点的 children
|
||
perm_dict[parentid]["children"].append(perm_data)
|
||
|
||
# 递归排序:按 sort 字段升序排列
|
||
def sort_tree(nodes):
|
||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||
for node in nodes:
|
||
if node.get("children"):
|
||
sort_tree(node["children"])
|
||
return nodes
|
||
|
||
menu_tree = sort_tree(menu_tree)
|
||
|
||
# 记录查询日志
|
||
await create_operation_log(
|
||
db, current_user.id, current_user.username, "search", "permission",
|
||
None, None, {"type": "user_permissions_menu"},
|
||
get_client_ip(request), get_user_agent(request),
|
||
)
|
||
await db.commit()
|
||
|
||
# 返回用户权限信息和菜单
|
||
return RolePermissionResponse(
|
||
role_id=role.id,
|
||
role_name=role.name,
|
||
permissions=[PermissionResponse.from_orm(p) for p in role.permissions],
|
||
menu=menu_tree
|
||
)
|