You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iTi-Flask/iti/applications/routes/sys/menu.py

193 lines
5.9 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import copy
from apiflask import APIBlueprint
from flask_jwt_extended import jwt_required, current_user
from iti.applications.common.exceptions.biz_exp import BizException
from iti.applications.extensions import db
from iti.applications.models import SysMenu, sys_role_menu
from iti.applications.common.utils import success
from iti.applications.routes.sys.schemas.menu import (
MenuCreateRequest,
MenuUpdateRequest,
MenuExistsRequest,
)
from iti.applications.service.sys.sys_menu import get_menu_tree, get_user_menu_ids
from iti.applications.models import SysMenuSchema
from iti.applications.common.enums import MenuTypeEnum
from sqlalchemy import select, delete, func, exists
from iti.applications.common.events import MenuEvents
from iti.applications.extensions import eventbus
from iti.applications.common import permission
bp = APIBlueprint("sys_menu", __name__, url_prefix="/menu", tag="系统.菜单管理")
@bp.get("/list")
@jwt_required()
@bp.doc(security="JWT")
@bp.output(SysMenuSchema(many=True))
def get_menu_list():
"""
获取菜单树列表
"""
return success(get_menu_tree())
@bp.get("/tree")
@jwt_required()
@bp.doc(security="JWT")
@bp.output(SysMenuSchema(many=True))
def get_menu_tree_api():
"""
获取菜单树列表接口
过滤条件:
- 状态为启用
- 类型为非按钮
- 基于当前用户实际拥有的菜单
"""
# 获取当前用户拥有的菜单ID
user_menu_ids = get_user_menu_ids(current_user.id)
return success(
get_menu_tree(
type_filter=[
MenuTypeEnum.MENU,
MenuTypeEnum.CATALOG,
MenuTypeEnum.EMBEDDED,
MenuTypeEnum.LINK,
],
user_menu_ids=user_menu_ids,
)
)
@bp.post("")
@jwt_required()
@bp.doc(security="JWT")
@permission("system:menu:create")
@bp.input(MenuCreateRequest, location="json")
def create_menu(json_data: dict):
"""
创建菜单
"""
menu = SysMenu(**json_data)
# 基于同级菜单的最大排序计算出新的排序并更新到menu的sort字段和meta的order字段
max_sort = db.session.scalar(
select(func.max(SysMenu.sort)).filter_by(parent_id=menu.parent_id)
)
if max_sort is not None:
menu.sort = max_sort + 1
else:
menu.sort = 0
menu.meta["order"] = menu.sort
db.session.add(menu)
db.session.commit()
return success()
@bp.put("/<string:id>")
@jwt_required()
@bp.doc(security="JWT")
@permission("system:menu:edit")
@bp.input(MenuUpdateRequest(partial=True), location="json")
def update_menu(id: str, json_data: dict):
"""
更新菜单
"""
menu = db.session.scalar(select(SysMenu).filter_by(id=id))
if not menu:
raise BizException("菜单不存在")
old_menu = copy.deepcopy(menu)
for key, value in json_data.items():
if value is not None:
setattr(menu, key, value)
# 提交事务
db.session.commit()
# 触发菜单事件
eventbus.emit(MenuEvents.MENU_UPDATED.value, menu, old_menu)
return success()
@bp.delete("/<string:id>")
@jwt_required()
@bp.doc(security="JWT")
@permission("system:menu:delete")
def delete_menu(id: str):
"""
删除菜单
基本规则:
1. 删除菜单需同时删除其绑定关系
2. 若菜单包含子菜单,则需要删除所有子孙菜单(包括其绑定关系)
"""
menu = db.session.scalar(select(SysMenu).filter_by(id=id))
if not menu:
raise BizException("菜单不存在")
try:
# 获取当前菜单及其所有子孙菜单
from iti.applications.service.sys.sys_menu import build_descendants_cte
descendants_query = build_descendants_cte(SysMenu.id == id)
descendant_menus = db.session.scalars(descendants_query).all()
descendant_ids = [m.id for m in descendant_menus]
# 批量删除所有菜单的角色绑定关系
db.session.execute(delete(sys_role_menu).where(sys_role_menu.c.menu_id.in_(descendant_ids)))
# 批量删除所有子孙菜单
db.session.execute(delete(SysMenu).where(SysMenu.id.in_(descendant_ids)))
# 提交事务
db.session.commit()
# 触发菜单删除事件(为每个被删除的菜单触发)
for descendant_menu in descendant_menus:
eventbus.emit(MenuEvents.MENU_DELETED.value, descendant_menu)
except Exception as e:
db.session.rollback()
raise BizException(f"删除菜单失败: {str(e)}")
return success()
@bp.get("/exists")
@jwt_required()
@bp.doc(security="JWT")
@bp.input(MenuExistsRequest, location="query")
def menu_exists(query_data: dict):
"""
检查菜单的 path 或 name 是否已被其他记录使用(重复检查)
return: bool
- True: 存在重复(冲突)
- False: 不存在重复(可用)
使用场景:
- 创建菜单:检查 path/name 是否已存在
- 更新菜单:检查其他记录是否使用了相同的 path/name排除自己
"""
path = query_data.get("path")
name = query_data.get("name")
exclude_id = query_data.get("id")
# 优先检查 path
if path:
# 构建查询条件path 相同,但排除自己(如果提供了 id
conditions = [SysMenu.path == path]
if exclude_id:
conditions.append(SysMenu.id != exclude_id)
ret = db.session.scalar(select(exists().where(*conditions)))
return success(ret)
# 检查 name
if name:
# 构建查询条件name 相同,但排除自己(如果提供了 id
conditions = [SysMenu.name == name]
if exclude_id:
conditions.append(SysMenu.id != exclude_id)
ret = db.session.scalar(select(exists().where(*conditions)))
return success(ret)
# 没有提供检查字段,返回 False
return success(False)