diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py index 13ad73a..76bb7b8 100644 --- a/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py @@ -1,8 +1,9 @@ import re from pydantic import BaseModel, ConfigDict, model_validator from pydantic.alias_generators import to_camel -from typing import Optional +from typing import Optional, List, Union from exceptions.exception import ModelValidatorException +from module_admin.entity.vo.menu_vo import MenuModel class UserLogin(BaseModel): @@ -52,4 +53,31 @@ class SmsCode(BaseModel): is_success: Optional[bool] = None sms_code: str session_id: str - message: Optional[str] = None \ No newline at end of file + message: Optional[str] = None + + +class MenuTreeModel(MenuModel): + children: Optional[Union[List['MenuTreeModel'], None]] = None + + +class MetaModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + title: Optional[str] = None + icon: Optional[str] = None + no_cache: Optional[bool] = None + link: Optional[str] = None + + +class RouterModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + name: Optional[str] = None + path: Optional[str] = None + hidden: Optional[bool] = None + redirect: Optional[str] = None + component: Optional[str] = None + query: Optional[str] = None + always_show: Optional[bool] = None + meta: Optional[MetaModel] = None + children: Optional[Union[List['RouterModel'], None]] = None diff --git a/ruoyi-fastapi-backend/module_admin/service/login_service.py b/ruoyi-fastapi-backend/module_admin/service/login_service.py index 5ae6974..5f9eaaa 100644 --- a/ruoyi-fastapi-backend/module_admin/service/login_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/login_service.py @@ -24,6 +24,7 @@ class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm): """ 自定义OAuth2PasswordRequestForm类,增加验证码及会话编号参数 """ + def __init__( self, grant_type: str = Form(default=None, regex="password"), @@ -47,6 +48,7 @@ class LoginService: """ 登录模块服务层 """ + @classmethod async def authenticate_user(cls, request: Request, query_db: AsyncSession, login_user: UserLogin): """ @@ -231,57 +233,95 @@ class LoginService: """ query_user = await UserDao.get_user_by_id(query_db, user_id=user_id) user_router_menu = sorted([row for row in query_user.get('user_menu_info') if row.menu_type in ['M', 'C']], key=lambda x: x.order_num) - user_router = cls.__generate_user_router_menu(0, user_router_menu) - return user_router + menus = cls.__generate_menus(0, user_router_menu) + user_router = cls.__generate_user_router_menu(menus) + return [router.model_dump(exclude_unset=True, by_alias=True) for router in user_router] @classmethod - def __generate_user_router_menu(cls, pid: int, permission_list): + def __generate_menus(cls, pid: int, permission_list: List[SysMenu]): """ - 工具方法:根据菜单信息生成路由信息树形嵌套数据 + 工具方法:根据菜单信息生成菜单信息树形嵌套数据 :param pid: 菜单id :param permission_list: 菜单列表信息 - :return: 路由信息树形嵌套数据 + :return: 菜单信息树形嵌套数据 """ - router_list = [] + menu_list: List[MenuTreeModel] = [] for permission in permission_list: if permission.parent_id == pid: - children = cls.__generate_user_router_menu(permission.menu_id, permission_list) - router_list_data = {} - if permission.menu_type == 'M': - router_list_data['name'] = permission.path.capitalize() - router_list_data['hidden'] = False if permission.visible == '0' else True - if permission.parent_id == 0: - router_list_data['component'] = 'Layout' - router_list_data['path'] = f'/{permission.path}' - else: - router_list_data['component'] = 'ParentView' - router_list_data['path'] = permission.path - if permission.is_frame == 1: - router_list_data['redirect'] = 'noRedirect' - else: - router_list_data['path'] = permission.path - if children: - router_list_data['alwaysShow'] = True - router_list_data['children'] = children - router_list_data['meta'] = { - 'title': permission.menu_name, - 'icon': permission.icon, - 'noCache': False if permission.is_cache == '0' else True, - 'link': permission.path if permission.is_frame == 0 else None - } - elif permission.menu_type == 'C': - router_list_data['name'] = permission.path.capitalize() - router_list_data['path'] = permission.path - router_list_data['query'] = permission.query - router_list_data['hidden'] = False if permission.visible == '0' else True - router_list_data['component'] = permission.component - router_list_data['meta'] = { - 'title': permission.menu_name, - 'icon': permission.icon, - 'noCache': False if permission.is_cache == '0' else True, - 'link': permission.path if permission.is_frame == 0 else None - } - router_list.append(router_list_data) + children = cls.__generate_menus(permission.menu_id, permission_list) + menu_list_data = MenuTreeModel(**CamelCaseUtil.transform_result(permission)) + if children: + menu_list_data.children = children + menu_list.append(menu_list_data) + + return menu_list + + @classmethod + def __generate_user_router_menu(cls, permission_list: List[MenuTreeModel]): + """ + 工具方法:根据菜单树信息生成路由信息树形嵌套数据 + :param permission_list: 菜单树列表信息 + :return: 路由信息树形嵌套数据 + """ + router_list: List[RouterModel] = [] + for permission in permission_list: + router = RouterModel( + hidden=True if permission.visible == '1' else False, + name=RouterUtil.get_router_name(permission), + path=RouterUtil.get_router_path(permission), + component=RouterUtil.get_component(permission), + query=permission.query, + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + noCache=True if permission.is_cache == 1 else False, + link=permission.path if RouterUtil.is_http(permission.path) else None + ) + ) + c_menus = permission.children + if c_menus and permission.menu_type == 'M': + router.always_show = True + router.redirect = 'noRedirect' + router.children = cls.__generate_user_router_menu(c_menus) + elif RouterUtil.is_menu_frame(permission): + router.meta = None + children_list: List[RouterModel] = [] + children = RouterModel( + path=permission.path, + component=permission.component, + name=permission.path.capitalize(), + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + noCache=True if permission.is_cache == 1 else False, + link=permission.path if RouterUtil.is_http(permission.path) else None + ), + query=permission.query + ) + children_list.append(children) + router.children = children_list + elif permission.parent_id == 0 and RouterUtil.is_inner_link(permission): + router.meta = MetaModel( + title=permission.menu_name, + icon=permission.icon + ) + router.path = '/' + children_list: List[RouterModel] = [] + router_path = RouterUtil.inner_link_replace_each(permission.path) + children = RouterModel( + path=router_path, + component='InnerLink', + name=router_path.capitalize(), + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + link=permission.path if RouterUtil.is_http(permission.path) else None + ) + ) + children_list.append(children) + router.children = children_list + + router_list.append(router) return router_list @@ -386,3 +426,106 @@ class LoginService: # await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id') return True + + +class RouterUtil: + """ + 路由处理工具类 + """ + + @classmethod + def get_router_name(cls, menu: MenuTreeModel): + """ + 获取路由名称 + :param menu: 菜单数对象 + :return: 路由名称 + """ + router_name = menu.path.capitalize() + if cls.is_menu_frame(menu): + router_name = '' + + return router_name + + @classmethod + def get_router_path(cls, menu: MenuTreeModel): + """ + 获取路由地址 + :param menu: 菜单数对象 + :return: 路由地址 + """ + # 内链打开外网方式 + router_path = menu.path + if menu.parent_id != 0 and cls.is_inner_link(menu): + router_path = cls.inner_link_replace_each(router_path) + # 非外链并且是一级目录(类型为目录) + if menu.parent_id == 0 and menu.menu_type == 'M' and menu.is_frame == 1: + router_path = f'/{menu.path}' + # 非外链并且是一级目录(类型为菜单) + elif cls.is_menu_frame(menu): + router_path = '/' + return router_path + + @classmethod + def get_component(cls, menu: MenuTreeModel): + """ + 获取组件信息 + :param menu: 菜单数对象 + :return: 组件信息 + """ + component = 'Layout' + if menu.component and not cls.is_menu_frame(menu): + component = menu.component + elif menu.component and menu.parent_id != 0 and cls.is_inner_link(menu): + component = 'InnerLink' + elif menu.component and cls.is_parent_view(menu): + component = 'ParentView' + return component + + @classmethod + def is_menu_frame(cls, menu: MenuTreeModel): + """ + 判断是否为菜单内部跳转 + :param menu: 菜单数对象 + :return: 是否为菜单内部跳转 + """ + return menu.parent_id == 0 and menu.menu_type == 'C' and menu.is_frame == 1 + + @classmethod + def is_inner_link(cls, menu: MenuTreeModel): + """ + 判断是否为内链组件 + :param menu: 菜单数对象 + :return: 是否为内链组件 + """ + return menu.is_frame == 1 and cls.is_http(menu.path) + + @classmethod + def is_parent_view(cls, menu: MenuTreeModel): + """ + 判断是否为parent_view组件 + :param menu: 菜单数对象 + :return: 是否为parent_view组件 + """ + return menu.parent_id != 0 and menu.menu_type == 'M' + + @classmethod + def is_http(cls, link: str): + """ + 判断是否为http(s)://开头 + :param link: 链接 + :return: 是否为http(s)://开头 + """ + return link.startswith('http://') or link.startswith('https://') + + @classmethod + def inner_link_replace_each(cls, path: str): + """ + 内链域名特殊字符替换 + :param path: 内链域名 + :return: 替换后的内链域名 + """ + old_values = ["http://", "https://", "www.", ".", ":"] + new_values = ["", "", "", "/", "/"] + for old, new in zip(old_values, new_values): + path = path.replace(old, new) + return path