Add menu-related interfaces (#118)

* Adding menu-related interfaces

* fix schema typo

* add menu query interface validation
This commit is contained in:
Wu Clan
2023-06-13 16:37:39 +08:00
committed by GitHub
parent ceb8bdc607
commit 5c64d99451
8 changed files with 163 additions and 21 deletions
+49 -2
View File
@@ -1,7 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from typing import Annotated
from fastapi import APIRouter, Query, Request
from backend.app.common.casbin_rbac import DependsRBAC
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.response.response_schema import response_base
from backend.app.schemas.menu import GetAllMenu, CreateMenu, UpdateMenu
from backend.app.services.menu_service import MenuService
from backend.app.utils.serializers import select_to_json
router = APIRouter()
# TODO: 添加 menu 相关接口
@router.get('/{pk}', summary='获取目录详情', dependencies=[DependsJwtAuth])
async def get_menu(pk: int):
menu = await MenuService.get(pk=pk)
data = GetAllMenu(**select_to_json(menu))
return await response_base.success(data=data)
@router.get('', summary='获取所有目录展示树', dependencies=[DependsJwtAuth])
async def get_all_menus(
name: Annotated[str | None, Query()] = None,
status: Annotated[bool | None, Query()] = None,
):
menu = await MenuService.get_select(name=name, status=status)
return await response_base.success(data=menu)
@router.post('', summary='创建目录', dependencies=[DependsRBAC])
async def create_menu(request: Request, obj: CreateMenu):
await MenuService.create(obj=obj, user_id=request.user.id)
return await response_base.success()
@router.put('/{pk}', summary='更新目录', dependencies=[DependsRBAC])
async def update_menu(request: Request, pk: int, obj: UpdateMenu):
count = await MenuService.update(pk=pk, obj=obj, user_id=request.user.id)
if count > 0:
return await response_base.success()
return await response_base.fail()
@router.delete('{pk}', summary='删除目录', dependencies=[DependsRBAC])
async def delete_menu(pk: int):
count = await MenuService.delete(pk=pk)
if count > 0:
return await response_base.success()
return await response_base.fail()
+2 -2
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from typing import Any
from sqlalchemy import select, desc, and_
from sqlalchemy import select, desc, and_, asc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -21,7 +21,7 @@ class CRUDDept(CRUDBase[Dept, CreateDept, UpdateDept]):
async def get_all(
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: bool = None
) -> Any:
se = select(self.model).order_by(desc(self.model.sort))
se = select(self.model).order_by(asc(self.model.sort))
where_list = [self.model.del_flag == 0]
if name:
where_list.append(self.model.name.like(f'%{name}%'))
+32
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import select, asc, and_
from backend.app.crud.base import CRUDBase
from backend.app.models import Menu
from backend.app.schemas.menu import CreateMenu, UpdateMenu
@@ -9,5 +11,35 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
async def get(self, db, menu_id: int) -> Menu | None:
return await self.get_(db, pk=menu_id)
async def get_by_name(self, db, name: str) -> Menu | None:
return await self.get_(db, name=name)
async def get_all(self, db, name: str | None = None, status: bool | None = None) -> list[Menu]:
se = select(self.model).order_by(asc(self.model.sort))
where_list = []
if name:
where_list.append(self.model.name.like(f'%{name}%'))
if status is not None:
where_list.append(self.model.status == status)
if where_list:
se = se.where(and_(*where_list))
menu = await db.execute(se)
return menu.scalars().all()
async def create(self, db, obj_in: dict) -> None:
obj = self.model(**obj_in)
db.add(obj)
async def update(self, db, menu_id: int, obj_in: dict) -> int:
return await self.update_(db, menu_id, obj_in)
async def delete(self, db, menu_id: int) -> int:
return await self.delete_(db, menu_id)
async def get_children(self, db, menu_id: int) -> list[Menu]:
result = await db.execute(select(self.model).where(self.model.id == menu_id))
menu = result.scalars().first()
return menu.children
MenuDao: CRUDMenu = CRUDMenu(Menu)
+15 -6
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from typing import Union
from sqlalchemy import String, ForeignKey
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -15,14 +17,21 @@ class Menu(Base):
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='菜单名称')
parent_id: Mapped[int] = mapped_column(default=0, comment='父菜单ID')
level: Mapped[int] = mapped_column(default=0, comment='菜单层级')
sort: Mapped[int] = mapped_column(default=0, comment='显示顺')
path: Mapped[str] = mapped_column(String(200), default='', comment='路由地址')
sort: Mapped[int] = mapped_column(default=0, comment='')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址')
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
icon: Mapped[str | None] = mapped_column(String(100), default='#', comment='菜单图标')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
status: Mapped[bool] = mapped_column(default=True, comment='菜单状态(0停用 1正常)')
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)')
parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
)
# 父级菜单一对多
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
# 菜单角色多对多
roles: Mapped[list['Role']] = relationship( # noqa: F821
init=False, secondary=sys_role_menu, back_populates='menus'
+3 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.database.base_class import Base, id_key
@@ -16,7 +17,8 @@ class Role(Base):
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
data_scope: Mapped[int | None] = mapped_column(default=2, comment='数据范围(1:全部数据权限 2:自定数据权限)')
del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在')
status: Mapped[bool] = mapped_column(default=True, comment='角色状态(0停用 1正常')
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
# 角色用户多对多
users: Mapped[list['User']] = relationship( # noqa: F821
init=False, secondary=sys_user_role, back_populates='roles'
+7 -6
View File
@@ -9,19 +9,20 @@ from backend.app.common.enums import MenuType
class MenuBase(BaseModel):
name: str
parent_id: int = Field(default=0, ge=0, description='菜单父级ID')
level: int = Field(default=0, ge=0, description='菜单层级')
parent_id: int = Field(default=None, ge=1, description='菜单父级ID')
sort: int = Field(default=0, ge=0, description='排序')
path: str = Field(..., description='路由地址')
menu_type: int = Field(default=MenuType.directory, ge=0, description='菜单类型(0目录 1菜单 2按钮)')
icon: str | None = None
path: str | None = None
menu_type: int = Field(default=MenuType.directory, ge=0, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = None
perms: str | None = None
status: bool
remark: str | None = None
del_flag: bool
@validator('menu_type')
def menu_type_validator(cls, v):
if v not in MenuType.get_member_values():
raise ValueError('菜单类型只能是0、1、2')
raise ValueError('菜单类型只能是 012')
return v
+3 -2
View File
@@ -10,8 +10,9 @@ from backend.app.schemas.menu import GetAllMenu
class RoleBase(BaseModel):
name: str
data_scope: int | None = Field(default=RoleDataScope.custom, description='数据范围(1:全部数据权限 2:自定数据权限)') # noqa: E501
del_flag: bool
data_scope: int | None = Field(default=RoleDataScope.custom, description='数据范围(1:全部数据权限 2:自定数据权限)')
status: bool
remark: str | None = None
@validator('data_scope')
def data_scope_validator(cls, v):
+52 -2
View File
@@ -1,7 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.common.exception import errors
from backend.app.crud.crud_menu import MenuDao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.menu import CreateMenu, UpdateMenu
from backend.app.utils.build_tree import get_tree_data
class MenuService:
# TODO: 添加 menu 相关服务
pass
@staticmethod
async def get(pk: int):
async with async_db_session() as db:
menu = await MenuDao.get(db, menu_id=pk)
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
return menu
@staticmethod
async def get_select(name: str | None = None, status: bool | None = None):
async with async_db_session() as db:
menu_select = await MenuDao.get_all(db, name=name, status=status)
menu_tree = await get_tree_data(menu_select)
return menu_tree
@staticmethod
async def create(obj: CreateMenu, user_id: int):
async with async_db_session.begin() as db:
menu = await MenuDao.get_by_name(db, obj.name)
if menu:
raise errors.ForbiddenError(msg='菜单名称已存在')
new_obj = obj.dict()
new_obj.update({'level': obj.parent_id + 1 if obj.parent_id else 1, 'create_user': user_id})
await MenuDao.create(db, new_obj)
@staticmethod
async def update(pk: int, obj: UpdateMenu, user_id: int):
async with async_db_session.begin() as db:
menu = await MenuDao.get(db, pk)
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
if menu.name != obj.name:
if await MenuDao.get_by_name(db, obj.name):
raise errors.ForbiddenError(msg='菜单名称已存在')
new_obj = obj.dict()
new_obj.update({'level': obj.parent_id + 1 if obj.parent_id else 1, 'update_user': user_id})
count = await MenuDao.update(db, pk, new_obj)
return count
@staticmethod
async def delete(pk: int):
async with async_db_session.begin() as db:
children = await MenuDao.get_children(db, pk)
if children:
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
count = await MenuDao.delete(db, pk)
return count