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:
parent
60d07a477a
commit
547a4eeae6
@ -1 +0,0 @@
|
||||
|
@ -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()
|
@ -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()
|
@ -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))
|
@ -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()
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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=["*"],
|
||||
)
|
||||
),
|
||||
]
|
||||
|
@ -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
63
backend/core/service.py
Normal 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())
|
@ -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())
|
@ -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]
|
||||
)
|
@ -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)
|
@ -13,9 +13,7 @@ app = FastAPI(
|
||||
exception_handlers=exception_handlers,
|
||||
)
|
||||
|
||||
load_routers(
|
||||
app, "controller", no_depends="common", depends=[Depends(check_permissions)]
|
||||
)
|
||||
load_routers(app, "router", no_depends="auth", depends=[Depends(check_permissions)])
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
BIN
backend/mini.db
BIN
backend/mini.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -7,3 +7,4 @@ requests==2.28.1
|
||||
tortoise-orm==0.19.2
|
||||
uvicorn==0.18.3
|
||||
websockets==10.3
|
||||
loguru==0.6.0
|
||||
|
19
backend/router/auth.py
Normal file
19
backend/router/auth.py
Normal 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
30
backend/router/menu.py
Normal 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
46
backend/router/role.py
Normal 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
53
backend/router/user.py
Normal 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)
|
@ -1,4 +1 @@
|
||||
from schemas.common import *
|
||||
from schemas.menu import *
|
||||
from schemas.role import *
|
||||
from schemas.user import *
|
||||
|
||||
|
0
backend/service/__init__.py
Normal file
0
backend/service/__init__.py
Normal file
26
backend/service/auth.py
Normal file
26
backend/service/auth.py
Normal 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
26
backend/service/menu.py
Normal 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
61
backend/service/role.py
Normal 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
90
backend/service/user.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user