|
|
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_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.output(SysMenuSchema(many=True))
|
|
|
def get_menu_list():
|
|
|
"""
|
|
|
获取菜单树列表
|
|
|
"""
|
|
|
return success(get_menu_tree())
|
|
|
|
|
|
|
|
|
@bp.get("/tree")
|
|
|
@jwt_required()
|
|
|
@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()
|
|
|
@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()
|
|
|
@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()
|
|
|
@permission("system:menu:delete")
|
|
|
def delete_menu(id: str):
|
|
|
"""
|
|
|
删除菜单
|
|
|
"""
|
|
|
menu = db.session.scalar(select(SysMenu).filter_by(id=id))
|
|
|
if not menu:
|
|
|
raise BizException("菜单不存在")
|
|
|
|
|
|
try:
|
|
|
# 删除菜单,需要同时删除 菜单-角色 关联关系
|
|
|
db.session.execute(delete(sys_role_menu).filter_by(menu_id=id))
|
|
|
# 删除菜单
|
|
|
db.session.execute(menu)
|
|
|
# 提交事务
|
|
|
db.session.commit()
|
|
|
# 触发菜单删除事件
|
|
|
eventbus.emit(MenuEvents.MENU_DELETED.value, menu)
|
|
|
except Exception as e:
|
|
|
db.session.rollback()
|
|
|
raise BizException(f"删除菜单失败: {str(e)}")
|
|
|
return success()
|
|
|
|
|
|
|
|
|
@bp.get("/exists")
|
|
|
@jwt_required()
|
|
|
@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)
|