mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Add menu-related interfaces (#118)
* Adding menu-related interfaces * fix schema typo * add menu query interface validation
This commit is contained in:
@@ -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,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}%'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('菜单类型只能是 0,1,2')
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user