# +====================================================================== # | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] # +---------------------------------------------------------------------- # | Copyright (c) 2021-2026 一米阳光团队 版权所有 # +---------------------------------------------------------------------- # | 开源协议: Apache-2.0 # | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 # +---------------------------------------------------------------------- # | 官方网站: https://www.djangoadmin.cn # +---------------------------------------------------------------------- # | 研发团队: 一米阳光团队 荣誉出品 # +====================================================================== # | 版权和免责声明: # | 一、知识产权声明 # | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 # | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, # | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 # | 二、使用限制与禁止条款 # | 您理解并同意,禁止将本产品用于以下任何行为: # | 1. 任何违反中华人民共和国法律法规的用途; # | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; # | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 # | 三、责任豁免 # | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 # | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 # | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 # | 四、违约与法律责任 # | 如发现任何违反本声明的行为,本团队有权: # | 1. 无条件配合公安机关及其他有权机关的调查取证工作; # | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; # | 3. 保留进一步采取刑事控告等法律措施的权利。 # | 五、许可范围说明 # | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 # | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 # | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 # | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 # | 成的一切经济损失及商誉损害。 # | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 # | 许可协议》附件为准。 # +====================================================================== import asyncio from datetime import datetime, timezone from fastapi.logger import logger from sqlalchemy import and_, or_ from modules.system.menu.schemas import MenuForm from modules.system.menu.models import Menu from modules.system.role.models import RoleMenu from modules.system.user.models import UserRole from core.database import get_db_session from core import R from core.security import get_user_id from utils.request import like_contains, parse_id_list from utils.perm_cache import get_cached_permissions, set_cached_permissions, get_cached_menus, set_cached_menus, invalidate_menu_change # 查询菜单列表 def get_menu_page(request): db = get_db_session() # 实例化查询对象 query = db.query(Menu).filter(Menu.is_delete == 0) # 菜单名称 name = request.query_params.get('name') if name: # 菜单名称模糊查询 query = query.filter(Menu.name.like(like_contains(name), escape='\\')) # 查询数据 menu_list = query.order_by(Menu.sort.asc()).all() # 实例化数组对象 result = [] # 遍历数据源 if menu_list: for item in menu_list: # 对象转字典 data = item.to_dict() # 加入列表 result.append(data) # 返回结果 return R.ok(data=result) # 根据ID查询菜单详情 def get_menu_detail(menu_id): db = get_db_session() # 根据ID查询菜单 menu = db.query(Menu).filter(and_(Menu.id == menu_id, Menu.is_delete == 0)).first() # 查询结果判空 if not menu: return None # 对象转字典 data = menu.to_dict() # 查询菜单权限节点 permission_list = db.query(Menu).filter( and_(Menu.parent_id == menu_id, Menu.type == 1, Menu.is_delete == 0)).all() # 选中的权限节点 checked_list = [] if permission_list: for v in permission_list: checked_list.append(v.sort) # 权限节点 data['checked_list'] = checked_list # 返回结果 return data # 添加菜单 async def add_menu(request, data: MenuForm): def _add(): db = get_db_session() # 权限节点 checkedList = data.checked_list or [] # 表单数据赋值给对象 menu = Menu( title=data.title, icon=data.icon, path=data.path, parent_id=data.parent_id, type=data.type, permission=data.permission, component=data.component, status=data.status, hide=data.hide, target=data.target, sort=data.sort ) menu.create_user = get_user_id(request) # 插入数据 menu.save() # 保存节点数据 set_permission(data.type, checkedList, data.title, data.path, menu.id, get_user_id(request)) # 同步DB操作放入线程池,避免阻塞事件循环 await asyncio.to_thread(_add) # 清理权限缓存 await invalidate_menu_change() # 返回结果 return R.ok(msg="添加成功") # 更新菜单 async def update_menu(request, data: MenuForm): def _update(): db = get_db_session() # 记录ID判空 if not data.id or data.id <= 0: return R.failed("记录ID不能为空") # 根据ID查询记录 menu = db.query(Menu).filter(and_(Menu.id == data.id, Menu.is_delete == 0)).first() # 查询结果判空 if not menu: return R.failed("记录不存在") # 权限节点 checkedList = data.checked_list or [] # 更新数据源(排除 id 和 checked_list) updData = data.model_dump(exclude={'id', 'checked_list'}) updData['update_user'] = get_user_id(request) updData['update_time'] = datetime.now(tz=timezone.utc) # 更新数据源 result = db.query(Menu).filter_by(id=data.id).update(updData) # 提交数据 if not result: return R.failed("更新失败") # 保存节点数据 set_permission(data.type, checkedList, data.title, data.path, menu.id, get_user_id(request)) return None # 同步DB操作放入线程池,避免阻塞事件循环 err = await asyncio.to_thread(_update) if err: return err # 清理权限缓存 await invalidate_menu_change() # 返回结果 return R.ok(msg="更新成功") # 删除菜单 async def delete_menu(menu_id): def _delete(): db = get_db_session() # 记录ID为空判断 if not menu_id: return R.failed("记录ID不存在"), None # 分裂字符串 id_list = parse_id_list(menu_id) if not id_list: return R.failed("记录ID不存在"), None # 批量查询并校验 records = db.query(Menu).filter(and_(Menu.id.in_(id_list), Menu.is_delete == 0)).all() if len(records) != len(id_list): return R.failed("记录不存在"), None # 批量设置删除标识 for record in records: record.is_delete = 1 return None, records # 同步DB操作放入线程池,避免阻塞事件循环 err, records = await asyncio.to_thread(_delete) if err: return err # 清理权限缓存 await invalidate_menu_change() # 返回结果 return R.ok(msg="本次共删除{0}条数据".format(len(records))) # 保存节点数据 def set_permission(menu_type, checkedList, title, path, parent_id, user_id): db = get_db_session() # 参数判空 if menu_type != 0 or not checkedList or path == "": return # 删除现有节点 db.query(Menu).filter(and_(Menu.parent_id == parent_id, Menu.is_delete == 0)).delete() # 模块名称 moduleTitle = title.replace("管理", "") # 请求path处理 pathArr = path.split('/') if len(pathArr) < 3: return # 模块名 moduleName = pathArr[len(pathArr) - 1] # 遍历数据源 for v in checkedList: # 实例化菜单对象 entity = Menu() # 节点值 value = int(v) if value == 1: # 查询 entity.title = "查询" + moduleTitle entity.path = "/" + moduleName + "/list" entity.permission = "sys:" + moduleName + ":list" elif value == 5: # 添加 entity.title = "添加" + moduleTitle entity.path = "/" + moduleName + "/add" entity.permission = "sys:" + moduleName + ":add" elif value == 10: # 修改 entity.title = "修改" + moduleTitle entity.path = "/" + moduleName + "/update" entity.permission = "sys:" + moduleName + ":update" elif value == 15: # 删除 entity.title = "删除" + moduleTitle entity.path = "/" + moduleName + "/delete" entity.permission = "sys:" + moduleName + ":delete" elif value == 20: # 详情 entity.title = moduleTitle + "详情" entity.path = "/" + moduleName + "/detail" entity.permission = "sys:" + moduleName + ":detail" elif value == 25: # 设置状态 entity.title = "设置状态" entity.path = "/" + moduleName + "/status" entity.permission = "sys:" + moduleName + ":status" elif value == 30: # 批量删除 entity.title = "批量删除" entity.path = "/" + moduleName + "/dall" entity.permission = "sys:" + moduleName + ":dall" elif value == 35: # 添加子级 entity.title = "添加子级" entity.path = "/" + moduleName + "/addz" entity.permission = "sys:" + moduleName + ":addz" elif value == 40: # 全部展开 entity.title = "全部展开" entity.path = "/" + moduleName + "/expand" entity.permission = "sys:" + moduleName + ":expand" elif value == 45: # 全部折叠 entity.title = "全部折叠" entity.path = "/" + moduleName + "/collapse" entity.permission = "sys:" + moduleName + ":collapse" elif value == 50: # 导出数据 entity.title = "导出" + moduleTitle entity.path = "/" + moduleName + "/export" entity.permission = "sys:" + moduleName + ":export" elif value == 55: # 导入数据 entity.title = "导入" + moduleTitle entity.path = "/" + moduleName + "/import" entity.permission = "sys:" + moduleName + ":import" elif value == 60: # 分配权限 entity.title = "分配权限" entity.path = "/" + moduleName + "/permission" entity.permission = "sys:" + moduleName + ":permission" elif value == 65: # 重置密码 entity.title = "重置密码" entity.path = "/" + moduleName + "/resetPwd" entity.permission = "sys:" + moduleName + ":resetPwd" # 设置默认值 entity.parent_id = parent_id entity.type = 1 entity.status = 1 entity.target = "_self" entity.sort = value entity.create_user = user_id # 插入数据 entity.save() # 获取用户权限节点 def get_permissions_list_sync(user_id): """同步查询用户权限节点(直查DB,不读缓存),供同步鉴权路径使用""" db = get_db_session() if user_id == 1: # 超级管理员 menu_list = db.query(Menu).filter(and_(Menu.is_delete == 0, Menu.type == 1)).all() else: # 其他用户:通过 ORM join 查询权限节点 menu_list = ( db.query(Menu) .join(RoleMenu, RoleMenu.menu_id == Menu.id) .join(UserRole, UserRole.role_id == RoleMenu.role_id) .filter( and_( UserRole.user_id == user_id, or_(Menu.type == 1, and_(Menu.type == 0, Menu.permission != "")), Menu.status == 1, Menu.is_delete == 0, ) ) .all() ) return [item.permission for item in (menu_list or [])] async def get_permissions_list(user_id): # 尝试读取缓存 cached = await get_cached_permissions(user_id) if cached is not None: return cached permission_list = await asyncio.to_thread(get_permissions_list_sync, user_id) # 写入缓存 await set_cached_permissions(user_id, permission_list) return permission_list # 根据用户ID查询菜单列表 def get_permission_menu_list_sync(user_id): """同步查询用户菜单树(直查DB,不读缓存),供线程池内使用""" db = get_db_session() if user_id == 1: # 超级管理员 # 查询全部菜单列表 menu_list = db.query(Menu).filter(and_(Menu.is_delete == 0, Menu.status == 1, Menu.type == 0)).order_by( Menu.sort.asc()) return get_tree_list(menu_list) # 其他用户:通过 ORM join 查询菜单列表 menu_list = ( db.query(Menu) .join(RoleMenu, RoleMenu.menu_id == Menu.id) .join(UserRole, UserRole.role_id == RoleMenu.role_id) .filter( and_( UserRole.user_id == user_id, Menu.type == 0, Menu.status == 1, Menu.is_delete == 0, ) ) .order_by(Menu.sort.asc()) .all() ) return get_tree_list(menu_list) async def get_permission_menu_list(user_id): # 尝试读取缓存 cached = await get_cached_menus(user_id) if cached is not None: return cached menu_list = await asyncio.to_thread(get_permission_menu_list_sync, user_id) # 写入缓存 await set_cached_menus(user_id, menu_list) return menu_list # 根据数据源获取树状结构 def get_tree_list(source_list): # 实例化数组 menu_list = [] if source_list: for item in source_list: data = { 'id': item.id, 'title': item.title, 'icon': item.icon, 'path': item.path, 'parent_id': item.parent_id, 'type': item.type, 'component': item.component, 'permission': item.permission, 'target': item.target, 'hide': item.hide, } menu_list.append(data) # 处理数据源为树状结构 result = get_tree(menu_list, 0) # 返回结果 return result # 获取树状结构 def get_tree(data, parent_id): # 按 parent_id 建立索引,O(n) 构建 + O(n) 遍历 children_map = {} for item in data: pid = item["parent_id"] if pid not in children_map: children_map[pid] = [] children_map[pid].append(item) def build(pid): result = [] for item in children_map.get(pid, []): item["children"] = build(item["id"]) result.append(item) return result return build(parent_id)