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
29 changed files with 496 additions and 551 deletions

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:
def __init__(self, model: models.Model):
def __init__(self, model):
"""
初始化
:param model: 模型类
:param model: 模型类 orm model
"""
self.model = model
@@ -20,7 +20,7 @@ class DbHelper:
"""
return self.model.filter(**kwargs)
async def select(self, kwargs: dict = None) -> Optional[models.Model]:
async def select(self, kwargs: dict = None):
"""
查询符合条件的第一个对象, 查无结果时返回None
:param kwargs: kwargs: {"name:"7y", "id": 1}
@@ -58,7 +58,7 @@ class DbHelper:
return await self.model.create(**data)
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:
"""
条件分页查询数据列表, 支持排序
@@ -81,13 +81,13 @@ class DbHelper:
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: 模型列表
:return:
"""
await self.model.bulk_create(objs)
await self.model.bulk_create([self.model(**obj) for obj in objs])
@classmethod
async def raw_sql(cls, sql: str, args: list = None):
@@ -101,3 +101,48 @@ class DbHelper:
if args is None:
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
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)
from loguru import logger

View File

@@ -1,12 +1,28 @@
from fastapi.middleware import Middleware
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 = [
Middleware(CustomRequestLogMiddleware),
Middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
),
]

View File

@@ -6,10 +6,8 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from core.dbhelper import has_permissions, has_roles, has_user
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
SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU"
@@ -59,20 +57,20 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
return await get_user({"username": username})
return await has_user(username)
except JWTError:
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)
active_rid = result["roles"][0]["id"]
roles = await has_roles(user.id)
active_rid = roles[0]["id"]
# 白名单 登录用户信息, 登录用户菜单信息
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:
@@ -82,11 +80,11 @@ async def check_permissions(request: Request, user: UserModel = Depends(check_to
for k, v in request.path_params.items():
api = api.replace(v, "{%s}" % k)
# 2. 登录之后查一次 后面去结果查 todo 更新权限时需要更新 , 最好结果放redis
# todo 结果放redis
cache_key = f"{user.username}_{active_rid}"
# 缓存到fastapi 应用实例中
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(
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())