Refactor backend MVC (#2)

* docs(requirements.txt):升级fastapi、uvicorn版本

* refactor(user):重构用户router、service

* ref: role list api

* doc: 1

* refactor(backend): mvc ref
This commit is contained in:
zy7y 2022-10-04 18:19:26 +08:00 committed by GitHub
parent 60d07a477a
commit 547a4eeae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 496 additions and 551 deletions

View File

@ -1 +0,0 @@

View File

@ -1,36 +0,0 @@
import asyncio
from fastapi import APIRouter
from starlette.websockets import WebSocket
from websockets.exceptions import WebSocketException
from core.security import generate_token, verify_password
from core.utils import get_system_info
from dbhelper.user import get_user
from schemas import LoginForm, LoginResult, Response
router = APIRouter(tags=["公共"])
@router.post("/login", summary="登录", response_model=Response[LoginResult])
async def login(auth_data: LoginForm):
user_obj = await get_user({"username": auth_data.username, "status__not": 9})
if user_obj:
if verify_password(auth_data.password, user_obj.password):
return Response(
data=LoginResult(
id=user_obj.id, token=generate_token(auth_data.username)
)
)
return Response(code=400, msg="账号或密码错误")
@router.websocket("/ws", name="系统信息")
async def websocket(ws: WebSocket):
await ws.accept()
try:
while True:
await asyncio.sleep(1)
await ws.send_json(get_system_info())
except WebSocketException:
await ws.close()

View File

@ -1,40 +0,0 @@
# router service db router+service db
from fastapi import APIRouter
from core.utils import list_to_tree
from dbhelper.menu import del_menu, get_menu, get_tree_menu, insert_menu, put_menu
from schemas import MenuIn, MenuRead, Response
router = APIRouter(prefix="/menu", tags=["菜单管理"])
@router.post("", summary="菜单新增", response_model=Response[MenuRead])
async def menu_add(data: MenuIn):
return Response(data=await insert_menu(data))
@router.get("", summary="菜单列表", response_model=Response)
async def menu_arr():
menus = await get_tree_menu()
try:
data = list_to_tree(menus)
except KeyError:
return Response(code=400, msg="菜单根节点丢失")
return Response(data=data)
@router.delete("/{pk}", summary="菜单删除", response_model=Response)
async def menu_del(pk: int):
if await get_menu({"pid": pk}) is not None:
return Response(code=400, msg="请先删除子节点")
if await del_menu(pk) == 0:
return Response(code=400, msg="菜单不存在")
return Response()
@router.put("/{pk}", summary="菜单更新", response_model=Response)
async def menu_put(pk: int, data: MenuIn):
"""更新菜单"""
if await put_menu(pk, data) == 0:
return Response(code=400, msg="菜单不存在")
return Response()

View File

@ -1,78 +0,0 @@
from fastapi import APIRouter, Query
from core.utils import list_to_tree
from dbhelper.menu import get_menu
from dbhelper.role import (
del_role,
get_role,
get_role_menus,
get_roles,
new_role,
put_role,
)
from schemas import ListAll, Response, RoleIn, RoleInfo, RoleQuery, RoleRead
router = APIRouter(prefix="/role", tags=["角色管理"])
@router.post("", summary="角色新增", response_model=Response[RoleInfo])
async def role_add(data: RoleIn):
if result := await new_role(data):
return Response(data=result)
return Response(code=400, msg="菜单不存在")
@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response)
async def role_has_menu(rid: int):
"""
rid: 角色ID
"""
menus = await get_role_menus(rid)
try:
result = list_to_tree(menus)
except KeyError:
return Response(code=400, msg="菜单缺少根节点.")
return Response(data=result)
@router.get("", summary="角色列表", response_model=Response[ListAll[list[RoleRead]]])
async def role_arr(
offset: int = Query(default=1, description="偏移量-页码"),
limit: int = Query(default=10, description="数据量"),
):
skip = (offset - 1) * limit
roles, count = await get_roles(skip, limit)
return Response(data=ListAll(total=count, items=roles))
@router.delete("/{pk}", summary="角色删除", response_model=Response)
async def role_del(pk: int):
if await del_role(pk) == 0:
return Response(code=400, msg="角色不存在")
return Response()
@router.put("/{pk}", summary="角色更新", response_model=Response)
async def role_put(pk: int, data: RoleIn):
"""更新角色"""
if await get_role({"id": pk}) is None:
return Response(code=400, msg="角色不存在")
# 如果不为ture -> 有菜单id不存在
if not all([await get_menu({"id": mid}) for mid in data.menus]):
return Response(code=400, msg="菜单不存在")
if await put_role(pk, data) == 0:
return Response(code=400, msg="角色不存在")
return Response()
@router.post("/query", summary="角色查询", response_model=Response[ListAll[list[RoleRead]]])
async def role_query(query: RoleQuery):
"""post条件查询角色表"""
size = query.limit
skip = (query.offset - 1) * size
del query.offset, query.limit
users, count = await get_roles(skip, size, query.dict())
return Response(data=ListAll(total=count, items=users))

View File

@ -1,89 +0,0 @@
from fastapi import APIRouter, Depends, Query
from core.security import check_permissions, get_password_hash
from dbhelper.user import (
del_user,
get_user,
get_user_info,
get_users,
insert_user,
put_user,
select_role,
)
from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead
from schemas.common import ListAll
router = APIRouter(prefix="/user", tags=["用户管理"])
@router.post("", summary="用户新增", response_model=Response[UserRead])
async def user_add(data: UserAdd):
"""新增用户并分配角色 一步到位"""
if await get_user({"username": data.username}) is not None:
return Response(code=400, msg="用户名已存在")
rids = data.roles
del data.roles
data.password = get_password_hash(data.password)
result = await insert_user(data, rids)
if isinstance(result, int):
return Response(code=400, msg=f"角色{result}不存在")
return Response(data=result)
@router.get("/{pk}", summary="用户信息", response_model=Response[UserInfo])
async def user_info(pk: int):
"""获取用户信息"""
obj = await get_user({"id": pk})
if obj is None:
return Response(code=400, msg="用户不存在")
return Response(data=await get_user_info(obj))
@router.get("", summary="用户列表", response_model=Response[ListAll[list[UserRead]]])
async def user_arr(
offset: int = Query(default=1, description="偏移量-页码"),
limit: int = Query(default=10, description="数据量"),
):
"""分页列表数据"""
skip = (offset - 1) * limit
users, count = await get_users(skip, limit)
return Response(data=ListAll(total=count, items=users))
@router.post("/query", summary="用户查询", response_model=Response[ListAll[list[UserRead]]])
async def user_list(query: UserQuery):
"""post查询用户列表"""
size = query.limit
skip = (query.offset - 1) * size
del query.offset, query.limit
users, count = await get_users(skip, size, query.dict())
return Response(data=ListAll(total=count, items=users))
@router.delete("/{pk}", summary="用户删除", response_model=Response)
async def user_del(pk: int):
"""删除用户"""
if await del_user(pk) == 0:
return Response(code=400, msg="用户不存在")
return Response()
@router.put("/{pk}", summary="用户更新", response_model=Response)
async def user_put(pk: int, data: UserPut):
"""更新用户"""
if await get_user({"id": pk}) is None:
return Response(code=400, msg="用户不存在")
result = await put_user(pk, data)
if isinstance(result, int):
return Response(code=400, msg=f"角色不存在{result}")
return Response()
@router.put("/role/{rid}", summary="用户切换角色", response_model=Response)
async def user_select_role(rid: int, user=Depends(check_permissions)):
"""用户切换角色"""
res = await select_role(user.id, rid)
if res == 0:
return Response(code=400, msg=f"角色不存在{res}")
return Response()

View File

@ -1,14 +1,14 @@
"""数据库通用查询方法""" """数据库通用查询方法"""
from typing import Optional from tortoise import connections
from tortoise import connections, models from models import MenuModel, RoleMenuModel, RoleModel, UserModel, UserRoleModel
class DbHelper: class DbHelper:
def __init__(self, model: models.Model): def __init__(self, model):
""" """
初始化 初始化
:param model: 模型类 :param model: 模型类 orm model
""" """
self.model = model self.model = model
@ -20,7 +20,7 @@ class DbHelper:
""" """
return self.model.filter(**kwargs) return self.model.filter(**kwargs)
async def select(self, kwargs: dict = None) -> Optional[models.Model]: async def select(self, kwargs: dict = None):
""" """
查询符合条件的第一个对象, 查无结果时返回None 查询符合条件的第一个对象, 查无结果时返回None
:param kwargs: kwargs: {"name:"7y", "id": 1} :param kwargs: kwargs: {"name:"7y", "id": 1}
@ -58,7 +58,7 @@ class DbHelper:
return await self.model.create(**data) return await self.model.create(**data)
async def selects( async def selects(
self, offset: int, limit: int, kwargs: dict = None, order_by: str = None self, offset: int, limit: int, kwargs: dict = None, order_by: str = "-created"
) -> dict: ) -> dict:
""" """
条件分页查询数据列表, 支持排序 条件分页查询数据列表, 支持排序
@ -81,13 +81,13 @@ class DbHelper:
items=await objs.offset(offset).limit(limit), total=await objs.count() items=await objs.offset(offset).limit(limit), total=await objs.count()
) )
async def inserts(self, objs: list[models.Model]): async def inserts(self, objs: list):
""" """
批量新增数据 批量新增数据
:param objs: 模型列表 :param objs: 模型列表
:return: :return:
""" """
await self.model.bulk_create(objs) await self.model.bulk_create([self.model(**obj) for obj in objs])
@classmethod @classmethod
async def raw_sql(cls, sql: str, args: list = None): async def raw_sql(cls, sql: str, args: list = None):
@ -101,3 +101,48 @@ class DbHelper:
if args is None: if args is None:
args = [] args = []
return await db.execute_query_dict(sql, args) return await db.execute_query_dict(sql, args)
UserDao = DbHelper(UserModel)
RoleDao = DbHelper(RoleModel)
UserRoleDao = DbHelper(UserRoleModel)
MenuDao = DbHelper(MenuModel)
RoleMenuDao = DbHelper(RoleMenuModel)
async def has_roles(uid):
"""
获取用户角色信息,激活的角色升序
:param uid: 用户id
:return:
"""
sql = """select r.id, r.name, ur.status from sys_role as r , sys_user_role as ur where r.id = ur.rid and
ur.uid = (?) and r.status = 1 and ur.status !=9 order by ur.status desc
"""
return await UserRoleDao.raw_sql(sql, [uid])
async def has_user(username):
"""
通过用户名检索数据是否存在
:param username:
:return:
"""
return await UserDao.select({"username": username, "status__not": 9})
async def has_permissions(rid, is_menu=False):
"""
根据角色ID查到当前拥有的接口权限
:param rid: 角色ID
:param is_menu: 是否是菜单默认不是 -接口
:return:
"""
filters = "m.api, m.method"
if is_menu:
filters = "m.id, m.name, m.icon, m.path, m.type, m.component, m.pid, m.identifier, m.api, m.method"
sql = f"""
select {filters}
FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid
AND srm.rid = (?) and m.status != 9 order by m.id asc"""
return await RoleMenuDao.raw_sql(sql, [rid])

View File

@ -1,15 +1 @@
import logging from loguru import logger
import sys
fmt = logging.Formatter(
fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
sh = logging.StreamHandler(sys.stdout)
sh.setLevel(logging.DEBUG)
sh.setFormatter(fmt)
# will print debug sql
logger = logging.getLogger("mini-rbac")
logger.setLevel(logging.DEBUG)
logger.addHandler(sh)

View File

@ -1,12 +1,28 @@
from fastapi.middleware import Middleware from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from core.log import logger
class CustomRequestLogMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
logger.info(
f"Client: {request.client} Method: {request.method} "
f"Path: {request.url} Headers: {request.headers}"
)
# python-multipart == await request.form()
response = await call_next(request)
return response
middlewares = [ middlewares = [
Middleware(CustomRequestLogMiddleware),
Middleware( Middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) ),
] ]

View File

@ -6,10 +6,8 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from core.dbhelper import has_permissions, has_roles, has_user
from core.exceptions import PermissionsError, TokenAuthFailure from core.exceptions import PermissionsError, TokenAuthFailure
from dbhelper.menu import get_apis
from dbhelper.user import get_user, get_user_info
from models import UserModel
# JWT # JWT
SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU" SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU"
@ -59,20 +57,20 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)):
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub") username: str = payload.get("sub")
return await get_user({"username": username}) return await has_user(username)
except JWTError: except JWTError:
raise TokenAuthFailure(403, "认证失败") raise TokenAuthFailure(403, "认证失败")
async def check_permissions(request: Request, user: UserModel = Depends(check_token)): async def check_permissions(request: Request, user=Depends(check_token)):
"""检查接口权限""" """检查接口权限"""
# 查询当前激活角色 # 查询当前激活角色
result = await get_user_info(user) roles = await has_roles(user.id)
active_rid = result["roles"][0]["id"] active_rid = roles[0]["id"]
# 白名单 登录用户信息, 登录用户菜单信息 # 白名单 登录用户信息, 登录用户菜单信息
whitelist = [(f"/user/{user.id}", "GET"), (f"/role/{active_rid}/menu", "GET")] + [ whitelist = [(f"/user/{user.id}", "GET"), (f"/role/{active_rid}/menu", "GET")] + [
(f"/user/role/{rid['id']}", "PUT") for rid in result["roles"] (f"/user/role/{rid['id']}", "PUT") for rid in roles
] ]
if (request.url.path, request.method) in whitelist: if (request.url.path, request.method) in whitelist:
@ -82,11 +80,11 @@ async def check_permissions(request: Request, user: UserModel = Depends(check_to
for k, v in request.path_params.items(): for k, v in request.path_params.items():
api = api.replace(v, "{%s}" % k) api = api.replace(v, "{%s}" % k)
# 2. 登录之后查一次 后面去结果查 todo 更新权限时需要更新 , 最好结果放redis # todo 结果放redis
cache_key = f"{user.username}_{active_rid}" cache_key = f"{user.username}_{active_rid}"
# 缓存到fastapi 应用实例中 # 缓存到fastapi 应用实例中
if not hasattr(request.app.state, cache_key): if not hasattr(request.app.state, cache_key):
setattr(request.app.state, cache_key, await get_apis(active_rid)) setattr(request.app.state, cache_key, await has_permissions(active_rid))
if {"api": api, "method": request.method} not in getattr( if {"api": api, "method": request.method} not in getattr(
request.app.state, cache_key request.app.state, cache_key
): ):

63
backend/core/service.py Normal file
View File

@ -0,0 +1,63 @@
from core.dbhelper import DbHelper
class Service:
filter_del = {"status__not": 9}
def __init__(self, dao: DbHelper):
self.dao = dao
async def get_items(self, offset, limit):
"""
分页获取数据, 过滤掉删除
:param offset: 起始值
:param limit: 偏移量
:return:
"""
skip = (offset - 1) * limit
return dict(data=await self.dao.selects(skip, limit, Service.filter_del))
async def query_items(self, query):
"""
根据条件查询结果
:param query:
:return:
"""
size = query.limit
skip = (query.offset - 1) * size
del query.offset, query.limit
filters = {f"{k}__contains": v for k, v in query.dict().items()}
filters.update(Service.filter_del)
return dict(data=await self.dao.selects(skip, size, filters))
async def delete_item(self, pk):
"""
逻辑删除数据
:param pk:主键
:return:
"""
filters = {"id": pk}
filters.update(Service.filter_del)
if await self.dao.update(filters, {"status": 9}) == 0:
return dict(code=400, msg="数据不存在")
return dict()
async def update_item(self, pk, data):
"""
更新数据,不通用可重写
:param pk: 主键
:param data: pydantic model
:return:
"""
if await self.dao.update({"id": pk}, data.dict()) == 0:
return dict(code=400, msg="数据不存在")
return dict()
async def create_item(self, data):
"""
创建数据不通用可重写
:param data: pydantic model
:return:
"""
return await self.dao.insert(data.dict())

View File

@ -1,66 +0,0 @@
from tortoise import connections
from models import MenuModel
from schemas.menu import MenuIn
async def insert_menu(menu: MenuIn):
"""新增菜单"""
return await MenuModel.create(**menu.dict())
async def get_menus(skip: int, limit: int, kwargs: dict = None):
"""
分页获取用户并且支持字段模糊查询
Args:
skip: 偏移量
limit: 数量
kwargs: 查询字典
Returns:
"""
if kwargs is not None:
kwargs = {f"{k}__contains": v for k, v in kwargs.items()}
else:
kwargs = {}
result = MenuModel.filter(status__not=9, **kwargs).all().order_by("-created")
return await result.offset(skip).limit(limit)
async def get_tree_menu():
return await MenuModel.filter(status__not=9).all().values()
async def get_menu(kwargs):
"""
根据条件查询到第一条符合结果的数据
Args:
kwargs:
Returns:
"""
return await MenuModel.filter(**kwargs).first()
async def del_menu(mid: int):
"""删除菜单"""
return await MenuModel.filter(id=mid).update(status=9)
async def get_apis(pk: int):
"""返回当前角色拥有的接口权限列表"""
db = connections.get("default")
return await db.execute_query_dict(
"""
select m.api, m.method
FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid
AND srm.rid = (?) and m.status != 9""",
[pk],
)
async def put_menu(pk: int, data):
"""更新菜单"""
return await MenuModel.filter(id=pk).update(**data.dict())

View File

@ -1,79 +0,0 @@
from tortoise import connections
from models import MenuModel, RoleMenuModel, RoleModel
from schemas.role import RoleIn
async def get_role_menus(rid: int):
"""
根据角色id 获取菜单
"""
db = connections.get("default")
# asc 降序
return await db.execute_query_dict(
"""
select m.id, m.name, m.icon, m.path, m.type, m.component, m.pid, m.identifier, m.api, m.method
FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid
AND sys_role_menu.rid = (?) AND sys_role_menu.`status` = 1 order by m.id asc""",
[rid],
)
async def new_role(role: RoleIn):
"""新增角色"""
# 校验菜单是否存在
if not all([await MenuModel.filter(id=mid).first() for mid in role.menus]):
return False
obj = await RoleModel.create(name=role.name, remark=role.remark)
# 写入菜单
await RoleMenuModel.bulk_create(
[RoleMenuModel(rid=obj.id, mid=mid) for mid in role.menus]
)
return obj
async def get_roles(skip: int, limit: int, kwargs: dict = None):
"""
分页获取用户并且支持字段模糊查询
Args:
skip: 偏移量
limit: 数量
kwargs: 查询字典
Returns:
"""
if kwargs is not None:
kwargs = {f"{k}__contains": v for k, v in kwargs.items()}
else:
kwargs = {}
result = RoleModel.filter(**kwargs).all().order_by("-created")
return await result.offset(skip).limit(limit), await result.count()
async def get_role(kwargs):
"""
根据条件查询到第一条符合结果的数据
Args:
kwargs:
Returns:
"""
return await RoleModel.filter(**kwargs).first()
async def del_role(rid: int):
"""删除用户"""
return await RoleModel.filter(id=rid).update(status=9)
async def put_role(pk, data):
"""更新角色 菜单"""
await RoleModel.filter(id=pk).update(name=data.name, remark=data.remark)
await RoleMenuModel.filter(rid=pk).update(status=9)
await RoleMenuModel.bulk_create(
[RoleMenuModel(rid=pk, mid=mid) for mid in data.menus]
)

View File

@ -1,121 +0,0 @@
from fastapi.encoders import jsonable_encoder
from tortoise import connections
from dbhelper.role import get_role
from models import UserModel, UserRoleModel
from schemas import UserPut
async def get_user(kwargs):
"""
根据条件查询到第一条符合结果的数据
Args:
kwargs:
Returns:
"""
return await UserModel.filter(**kwargs).first()
async def get_user_info(user: UserModel):
"""
根据id查用户角色列表 按激活角色倒序显示
"""
db = connections.get("default")
# 查角色表 用户角色表中 角色状态 = 1 关联表中 状态 != 9 为有效角色
sql_result = await db.execute_query_dict(
"""
select r.id, r.name, ur.status from sys_role as r , sys_user_role as ur where r.id = ur.rid and
ur.uid = (?) and r.status = 1 and ur.status !=9 order by ur.status desc
""",
[user.id],
)
return {
**jsonable_encoder(user),
"roles": sql_result,
}
async def get_users(skip: int, limit: int, kwargs: dict = None):
"""
分页获取用户并且支持字段模糊查询
Args:
skip: 偏移量
limit: 数量
kwargs: 查询字典
Returns:
"""
if kwargs is not None:
kwargs = {f"{k}__contains": v for k, v in kwargs.items()}
else:
kwargs = {}
result = UserModel.filter(**kwargs).all().order_by("-created")
return await result.offset(skip).limit(limit), await result.count()
async def insert_user(user, roles):
"""新增用户,选择角色"""
for role in roles:
if await get_role({"id": role.rid, "status__not": 9}) is None:
return role.rid
# 创建用户
obj = await UserModel.create(**user.dict())
# 已有角色 关联 角色id 和是否选中状态
await UserRoleModel.bulk_create(
[UserRoleModel(rid=role.rid, uid=obj.id, status=role.status) for role in roles]
)
return obj
async def del_user(uid: int):
"""删除用户"""
return await UserModel.filter(id=uid).update(status=9)
async def put_user(uid: int, data: UserPut):
"""更新用户"""
from core.security import get_password_hash
rids = data.roles
del data.roles
for role in rids:
if await get_role({"id": role.rid, "status__not": 9}) is None:
return role.rid
# 更新用户
if data.password != "加密之后的密码":
data.password = get_password_hash(data.password)
else:
del data.password
await UserModel.filter(id=uid).update(**data.dict())
# todo 1. 先前有的角色,这次更新成没有 2. 先前没有的角色 这次更新成有, 3. 只更新了状态
db = connections.get("default")
# 1. 先把用户有的角色做删除
has_roles = await db.execute_query_dict(
"""
select r.id from sys_role as r , sys_user_role as ur where r.id = ur.rid and
ur.uid = (?) and r.status = 1 and ur.status !=9
""",
[uid],
)
# 2. 将先有的数据标记 删除
[await UserRoleModel.filter(rid=role["id"]).update(status=9) for role in has_roles]
# 2. 新增次此更新的数据
await UserRoleModel.bulk_create(
[UserRoleModel(uid=uid, **role.dict()) for role in rids]
)
async def select_role(uid: int, rid: int):
"""用户切换角色"""
# 1.将用户id 未删除角色状态置为正常 1 除切换角色id
await UserRoleModel.filter(uid=uid, rid__not=rid, status__not=9).update(status=1)
# 2.将用户id 角色id 和当前角色匹配的数据置为选中
return await UserRoleModel.filter(uid=uid, rid=rid, status__not=9).update(status=5)

View File

@ -13,9 +13,7 @@ app = FastAPI(
exception_handlers=exception_handlers, exception_handlers=exception_handlers,
) )
load_routers( load_routers(app, "router", no_depends="auth", depends=[Depends(check_permissions)])
app, "controller", no_depends="common", depends=[Depends(check_permissions)]
)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7,3 +7,4 @@ requests==2.28.1
tortoise-orm==0.19.2 tortoise-orm==0.19.2
uvicorn==0.18.3 uvicorn==0.18.3
websockets==10.3 websockets==10.3
loguru==0.6.0

19
backend/router/auth.py Normal file
View File

@ -0,0 +1,19 @@
from fastapi import APIRouter, WebSocket
from schemas import common as BaseSchema
from service import auth as AuthService
router = APIRouter(tags=["公共"])
LoginResult = BaseSchema.Response[BaseSchema.LoginResult]
@router.post("/login", summary="登录", response_model=LoginResult)
async def login(data: BaseSchema.LoginForm):
return await AuthService.user_login(data)
@router.websocket("/ws", name="系统信息")
async def get_system_info(ws: WebSocket):
await AuthService.system_info(ws)

30
backend/router/menu.py Normal file
View File

@ -0,0 +1,30 @@
from fastapi import APIRouter
from schemas import common as BaseSchema
from schemas import menu as MenuSchema
from service.menu import service as MenuService
router = APIRouter(prefix="/menu", tags=["菜单管理"])
Response = BaseSchema.Response
@router.post("", summary="菜单新增", response_model=Response[MenuSchema.MenuRead])
async def menu_add(data: MenuSchema.MenuIn):
return await MenuService.create_item(data)
@router.get("", summary="菜单列表", response_model=Response)
async def menu_arr():
return await MenuService.get_items()
@router.delete("/{pk}", summary="菜单删除", response_model=Response)
async def menu_del(pk: int):
return await MenuService.delete_item(pk)
@router.put("/{pk}", summary="菜单更新", response_model=Response)
async def menu_put(pk: int, data: MenuSchema.MenuIn):
"""更新菜单"""
return await MenuService.update_item(pk, data)

46
backend/router/role.py Normal file
View File

@ -0,0 +1,46 @@
from fastapi import APIRouter, Query
from schemas import common as BaseSchema
from schemas import role as RoleSchema
from service.role import service as RoleService
router = APIRouter(prefix="/role", tags=["角色管理"])
Response = BaseSchema.Response
ListAll = BaseSchema.ListAll
role_list_schema = ListAll[list[RoleSchema.RoleRead]]
@router.get("", summary="角色列表", response_model=Response[role_list_schema])
async def role_list(
offset: int = Query(default=1, description="偏移量-页码"),
limit: int = Query(default=10, description="数据量"),
):
return await RoleService.get_items(offset, limit)
@router.post("/query", summary="角色查询", response_model=Response[role_list_schema])
async def role_query(query: RoleSchema.RoleQuery):
return await RoleService.query_items(query)
@router.post("", summary="角色新增", response_model=Response[RoleSchema.RoleInfo])
async def role_create(data: RoleSchema.RoleIn):
return await RoleService.create_item(data)
@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response)
async def role_has_menu(rid: int):
return await RoleService.has_tree_menus(rid)
@router.delete("/{pk}", summary="角色删除", response_model=Response)
async def role_del(pk: int):
return await RoleService.delete_item(pk)
@router.put("/{pk}", summary="角色更新", response_model=Response)
async def role_put(pk: int, data: RoleSchema.RoleIn):
"""更新角色"""
return await RoleService.update_item(pk, data)

53
backend/router/user.py Normal file
View File

@ -0,0 +1,53 @@
from fastapi import APIRouter, Depends, Query
from core.security import check_permissions
from schemas import common as BaseSchema
from schemas import user as UserSchema
from service.user import service as UserService
router = APIRouter(prefix="/user", tags=["用户管理"])
Response = BaseSchema.Response
ListAll = BaseSchema.ListAll
user_list_schema = ListAll[list[UserSchema.UserRead]]
@router.get("", summary="用户列表", response_model=Response[user_list_schema])
async def user_list(
offset: int = Query(default=1, description="偏移量-页码"),
limit: int = Query(default=10, description="数据量"),
):
return await UserService.get_items(offset, limit)
@router.post("/query", summary="用户查询", response_model=Response[user_list_schema])
async def user_query(query: UserSchema.UserQuery):
return await UserService.query_items(query)
@router.post("", summary="用户新增", response_model=Response[UserSchema.UserRead])
async def user_create(data: UserSchema.UserAdd):
return await UserService.create_item(data)
@router.delete("/{pk}", summary="用户删除", response_model=Response)
async def user_delete(pk: int):
return await UserService.delete_item(pk)
@router.get("/{pk}", summary="用户信息", response_model=Response[UserSchema.UserInfo])
async def user_info(pk: int):
return await UserService.get_item(pk)
@router.put("/{pk}", summary="用户更新", response_model=Response)
async def user_update(pk: int, data: UserSchema.UserPut):
return await UserService.update_item(pk, data)
@router.put("/role/{rid}", summary="用户切换角色", response_model=Response)
async def user_change_role(
rid: int, user: UserSchema.UserRead = Depends(check_permissions)
):
return await UserService.change_current_role(user.id, rid)

View File

@ -1,4 +1 @@
from schemas.common import *
from schemas.menu import *
from schemas.role import *
from schemas.user import *

View File

26
backend/service/auth.py Normal file
View File

@ -0,0 +1,26 @@
import asyncio
from websockets.exceptions import WebSocketException
from core.dbhelper import has_user
from core.security import generate_token, verify_password
from core.utils import get_system_info
async def user_login(data):
"""用户登录"""
user_obj = await has_user(data.username)
if user_obj:
if verify_password(data.password, user_obj.password):
return dict(data=dict(id=user_obj.id, token=generate_token(data.username)))
return dict(code=400, msg="账号或密码错误")
async def system_info(ws):
await ws.accept()
try:
while True:
await asyncio.sleep(1)
await ws.send_json(get_system_info())
except WebSocketException:
await ws.close()

26
backend/service/menu.py Normal file
View File

@ -0,0 +1,26 @@
from core.dbhelper import MenuDao
from core.service import Service
from core.utils import list_to_tree
class MenuService(Service):
def __init__(self):
super(MenuService, self).__init__(MenuDao)
async def get_items(self):
sql = "select * from sys_menu where status != 9 ;"
menus = await self.dao.raw_sql(sql)
try:
return dict(data=list_to_tree(menus))
except KeyError:
return dict(code=400, msg="菜单根节点丢失")
async def delete_item(self, pk):
if await MenuDao.select({"pid": pk, "status__not": 9}) is not None:
return dict(code=400, msg="请先删除子节点")
if await MenuDao.delete(pk) == 0:
return dict(code=400, msg="菜单不存在")
return dict()
service = MenuService()

61
backend/service/role.py Normal file
View File

@ -0,0 +1,61 @@
from core.dbhelper import MenuDao, RoleDao, RoleMenuDao, has_permissions
from core.service import Service
from core.utils import list_to_tree
class RoleService(Service):
def __init__(self):
super(RoleService, self).__init__(RoleDao)
async def create_item(self, role):
"""
创建角色
:param role: pydantic model
:return:
"""
if not all(
[await MenuDao.select({"id": mid, "status__not": 9}) for mid in role.menus]
):
return dict(code=400, msg="菜单不存在")
obj = await RoleDao.insert(dict(name=role.name, remark=role.remark))
# 写入菜单
await RoleMenuDao.inserts([dict(rid=obj.id, mid=mid) for mid in role.menus])
return dict(data=obj)
async def update_item(self, pk, data):
"""
更新角色
:param pk:
:param data:
:return:
"""
if await RoleDao.select({"id": pk}) is None:
return dict(code=400, msg="角色不存在")
# 如果不为ture -> 有菜单id不存在
if not all([await MenuDao.select({"id": mid}) for mid in data.menus]):
return dict(code=400, msg="菜单不存在")
await RoleDao.update(dict(id=pk), dict(name=data.name, remark=data.remark))
await RoleMenuDao.update(dict(rid=pk), dict(status=9))
await RoleMenuDao.inserts([dict(rid=pk, mid=mid) for mid in data.menus])
return dict()
@staticmethod
async def has_tree_menus(pk):
"""
查询角色拥有菜单
:param pk:
:return:
"""
menus = await has_permissions(pk, is_menu=True)
try:
return dict(data=list_to_tree(menus))
except KeyError:
return dict(code=400, msg="菜单缺少根节点.")
service = RoleService()

90
backend/service/user.py Normal file
View File

@ -0,0 +1,90 @@
from fastapi.encoders import jsonable_encoder
from core.dbhelper import RoleDao, UserDao, UserRoleDao, has_roles
from core.security import get_password_hash
from core.service import Service
class UserService(Service):
def __init__(self):
super(UserService, self).__init__(UserDao)
async def create_item(self, data):
"""创建用户"""
# 检查用户是否存在
if await self.dao.select({"username": data.username}) is not None:
return dict(code=400, msg="用户名已存在")
rids = data.roles
del data.roles
data.password = get_password_hash(data.password)
# 检查选中的角色是否存在
for role in rids:
if await RoleDao.select(dict(id=role.rid, status__not=9)) is None:
return dict(code=400, msg=f"角色{role.rid}不存在")
# 创建用户- 用户表写入数据
user_obj = await UserDao.insert(data.dict())
# 关联表写入数据
await UserRoleDao.inserts(
[dict(rid=role.rid, uid=user_obj.id, status=role.status) for role in rids]
)
return dict(data=user_obj)
async def get_item(self, pk):
"""获取用户信息"""
user_obj = await self.dao.select({"id": pk})
if user_obj is None:
return dict(code=400, msg="用户不存在")
roles = await has_roles(user_obj.id)
return dict(data=dict(**jsonable_encoder(user_obj), roles=roles))
async def update_item(self, pk, data):
"""用户编辑修改"""
if await self.dao.select({"id": pk}) is None:
return dict(code=400, msg="用户不存在")
rids = data.roles
del data.roles
for role in rids:
if await RoleDao.select({"id": role.rid, "status__not": 9}) is None:
return role.rid
# 更新用户
if data.password != "加密之后的密码":
data.password = get_password_hash(data.password)
else:
del data.password
await UserDao.update(dict(id=pk), data.dict())
# todo 1. 先前有的角色,这次更新成没有 2. 先前没有的角色 这次更新成有, 3. 只更新了状态
roles = await has_roles(pk)
# 2. 将先有的数据标记 删除
[
await UserRoleDao.update(dict(rid=role["id"], uid=pk), dict(status=9))
for role in roles
]
# 2. 新增次此更新的数据
await UserRoleDao.inserts(
[dict(role.dict(), uid=pk, status=role.status) for role in rids]
)
return dict()
@staticmethod
async def change_current_role(uid, rid):
"""用户切换角色"""
# 1.将用户id 未删除角色状态置为正常 1 除切换角色id
await UserRoleDao.update(
dict(uid=uid, rid__not=rid, status__not=9), dict(status=1)
)
# 2.将用户id 角色id 和当前角色匹配的数据置为选中
res = await UserRoleDao.update(
dict(uid=uid, rid=rid, status__not=9), dict(status=5)
)
if res == 0:
return dict(code=400, msg=f"角色不存在{res}")
return dict()
service = UserService()