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_system.models import SysMenu, sys_role_menu from iti.applications.common.utils import success from iti_system.routes.schemas.menu import ( MenuCreateRequest, MenuUpdateRequest, MenuExistsRequest, ) from iti_system.service.sys.sys_menu import get_menu_tree, get_user_menu_ids from iti_system.models import SysMenuSchema from iti.applications.common.enums import MenuTypeEnum from sqlalchemy import select, delete, func, exists from iti_system.events.names import MenuEvents from iti.applications.extensions import eventbus from iti_system.permission 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("/") @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("/") @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_system.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)