基于FastApi框架测试平台(12)-角色管理功能实现(一)

630 阅读4分钟

前言

之前我们简单规划了平台基本功能,我们先从角色列表的增删改查开始,这里涉及到角色绑定权限。

Models

这里我们用三张表来表示角色、权限、以及角色与权限之间的关系。

role

class Role(BaseModel):
    __tablename__ = 'role'
    
    id = Column(INT, primary_key=True, index=True, autoincrement=True)
    created_at = Column(DATETIME, default=datetime.now, comment="创建时间")
    updated_at = Column(DATETIME, default=datetime.now, onupdate=datetime.now, comment="更新时间")
    name = Column(String(100), nullable=False)
    status = Column(INT, default=1, comment="1:可用 0:不可用")
    comment = Column(String(100), comment="备注")

这个表主要记录角色本身的信息,比如名字、状态

ability

class Ability(BaseModel):
    __tablename__ = "ability"
    
    id = Column(INT, primary_key=True, index=True, autoincrement=True)
    created_at = Column(DATETIME, default=datetime.now, comment="创建时间")
    updated_at = Column(DATETIME, default=datetime.now, onupdate=datetime.now, comment="更新时间")
    name = Column(String(100))
    handler = Column(String(100))
    status = Column(INT, default=1, comment="1:可用 0:不可用")

这个表主要记录权限数据

role_bind_ability

class RoleBindAbility(BaseModel):
    __tablename__ = 'role_bind_ability'
​
    role_id = Column(INT)
    ability_id = Column(INT)

这个表来记录角色与权限之间的关系,删除角色时,要将此表相关的数据也删除。

定义接口

无非就是增删改查。我们先定义接口,再逐个进行实现。

@router.post("/create")
def role_create(data: role_schema.Role):
    pass

@router.post("/update/{role_id}")
def role_update(role_id: int, data: role_schema.Role):
    pass

@router.delete("/delete/{role_id}")
def role_delete(role_id: int):
    pass

@router.get("/list")
def role_list():
    pass

实现接口

我们先想想逻辑:

  • 为避免角色重复,先查询角色名称是否存在,如果存在,直接返回。
  • 如果不存在,我们就提交数据库,保存到role表中
  • 如果未勾选相关权限,返回添加的数据。这样就完成了创建一个角色
  • 如果勾选的相关权限,此时我们还需要将关联数据保存到role_bind_ability表中

逻辑清晰了,就变为代码:

class RoleService(object):  
    
    @classmethod
    def create(cls, name: str, comment: str, ability_ids: list = None):
        try:
            with SessionLocal() as session:
                role = session.query(Role).filter(Role.name == name, Role.status != 0).first()
                if role:
                    return None, '角色已存在'
                r = Role(name=name, comment=comment)
                session.add(r)
                session.commit()
                if ability_ids:
                    cls.bind_abilities(r.id, ability_ids)
                return r, None
        except Exception as e:
            cls.log.error(f"创建角色失败:{str(e)}")
            return None, str(r)
     
    @classmethod
    def bind_abilities(cls, _id, ability_ids: list):
        try:
            with SessionLocal() as session:
                [session.delete(item) for item in session.query(RoleBindAbility).filter(RoleBindAbility.role_id == _id).all()]
                if ability_ids:
                    [session.add(RoleBindAbility(role_id=_id, ability_id=abl_id)) for abl_id in ability_ids]
                session.commit()
                return 1, None
        except Exception as e:
            cls.log.error(f"角色绑定权限失败:{str(e)}")
            return None, str(e)

可以看到与设想逻辑差不多,绑定权限时,先删除了之前已经绑定的权限,然后重新进行关联。

增加实现了,更改就简单了,无非就是多了role_id

  • 根据role_id查询到角色
  • 更改名称、别称、权限即可
class RoleService(object):  
  
    @classmethod
    def update(cls, _id, name, comment, ability_ids):
        try:
            with SessionLocal() as session:
                r = session.query(Role).filter(Role.id == _id).first()
                r.name = name
                r.comment = comment
                cls.bind_abilities(_id, ability_ids)
                session.add(r)
                session.commit()
                return 1, None
        except Exception as e:
            session.rollback()
            cls.log.error(f"更新角色失败:{str(e)}")
            return None, str(e)

可以看到,把绑定权限的方法单独抽离出来还是有好处的,更改时直接调用该方法即可。这里注意,在发生异常时,我们回滚之前所有的操作,取消对数据库的修改。

这里我们对role表做软删除,更改状态(和上面更新操作一样),对role_bind_ability做物理删除,直接删除数据

class RoleService(object):
    
    @classmethod
    def delete(cls, role_id):
        try:
            with SessionLocal() as session:
                role = session.query(Role).filter(Role.id == role_id).first()
                role.status = 0
                session.add(role)
                role_bind_ability = session.query(RoleBindAbility).filter(RoleBindAbility.role_id == role_id).all()
                [session.delete(item) for item in role_bind_ability]
                session.commit()
                return 1, None
        except Exception as e:
            cls.log.error(f"删除角色失败:{str(e)}")
            return None, "删除角色失败:{str(e)}"

查比较复杂一些,需要联表查询,role、ability、role_bind_ability联表查询才能得到想要的结果。

我们先手写sql:

select Role.id, Role.name, Role.comment, GROUP_CONCAT(Ability.id) AS ability_ids FROM role LEFT OUTER JOIN role_bind_ability ON role.id = role_bind_ability.role_id LEFT OUTER JOIN ability ON ability.id = role_bind_ability.ability_id
WHERE role.status = 1 GROUP BY Role.id, Role.name, Role.comment;

在这个查询中,我们使用了 GROUP_CONCAT 函数来将每个角色对应的多个权限拼接成一个字符串。通过使用 GROUP BY 子句,我们可以确保每个角色只出现一次。

sqlalchemy提供了select来拼接sql,当然直接用上面的sql也是可以执行的

class RoleService(object):
    @classmethod
    def _query(cls):
        return select(Role.id, Role.name, Role.comment, func.group_concat(Ability.id).label('abilitys')
                      ,func.group_concat(Ability.name).label('ability_names')).outerjoin(
            RoleBindAbility, Role.id == RoleBindAbility.role_id).outerjoin(
            Ability, Ability.id == RoleBindAbility.ability_id).group_by(Role.id, Role.name, Role.comment)
​
    @classmethod
    def query_all(cls):
        conditions = [Role.status == 1]
        sql = cls._query().where(*conditions)
        try:
            with SessionLocal() as session:
                data = session.execute(sql).fetchall()
                return data, None
        except Exception as e:
            return None, str(e)

到这里。就实现的角色增删改查的核心功能。

最后,在定义好的接口中调用这些方法,返回数据即可,举个例子

@router.post("/create")
def role_create(data: role_schema.Role):
    data, err = RoleService.create(data.name, data.comment, data.ability_ids)
    if err:
        return resp_error(USER_ERROR_A0500, msg=err)
    return resp_success()

其他接口也一样。

后续

1、这里实现每个方法都重复写with SessionLocal() as session:,有点懒了,之后想想怎么优化

2、查询时,未实现分页查询