From 8e154d492abe472035860216329c9eaea56e80a7 Mon Sep 17 00:00:00 2001 From: zy7y <13271962515@163.com> Date: Mon, 12 Sep 2022 23:22:18 +0800 Subject: [PATCH] feat: role & menu api --- backend/.gitignore | 3 +- backend/controller/__init__.py | 16 -- backend/controller/common.py | 9 +- backend/controller/menu.py | 21 +- backend/controller/role.py | 54 ++++- backend/controller/user.py | 61 +++-- backend/core/log.py | 6 +- backend/dbhelper/menu.py | 36 +++ backend/dbhelper/relation.py | 31 +++ backend/dbhelper/role.py | 45 +++- backend/dbhelper/user.py | 95 +++++--- backend/main.py | 2 +- backend/mini.db | Bin 45056 -> 0 bytes backend/router/url.py | 58 ++++- backend/schemas/menu.py | 50 ++-- backend/schemas/role.py | 25 +- backend/schemas/user.py | 48 +++- backend/tests/test_case.py | 411 ++++++++++++++++++++++----------- 18 files changed, 701 insertions(+), 270 deletions(-) create mode 100644 backend/dbhelper/relation.py delete mode 100644 backend/mini.db diff --git a/backend/.gitignore b/backend/.gitignore index adfd4f8..2a852c6 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -141,4 +141,5 @@ cython_debug/ .pdm.toml __pypackages__/ .pytest_cache -.db-* \ No newline at end of file +.db* +*/.pytest_cache \ No newline at end of file diff --git a/backend/controller/__init__.py b/backend/controller/__init__.py index 3921962..8b13789 100644 --- a/backend/controller/__init__.py +++ b/backend/controller/__init__.py @@ -1,17 +1 @@ -from fastapi import Depends, FastAPI -from core.security import check_token - - -def register_routers(app: FastAPI): - from controller.common import common - from controller.menu import menu - from controller.role import role - from controller.user import user - - app.include_router(router=common) - app.include_router( - router=user, - ) - app.include_router(router=menu) - app.include_router(router=role) diff --git a/backend/controller/common.py b/backend/controller/common.py index 71098b5..3635280 100644 --- a/backend/controller/common.py +++ b/backend/controller/common.py @@ -4,7 +4,7 @@ from schemas import LoginForm, LoginResult, Response async def login(auth_data: LoginForm) -> Response[LoginResult]: - user_obj = await get_user({"username": auth_data.username}) + 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( @@ -12,4 +12,9 @@ async def login(auth_data: LoginForm) -> Response[LoginResult]: id=user_obj.id, token=generate_token(auth_data.username) ) ) - return Response(msg="账号或密码错误") + return Response(code=400, msg="账号或密码错误") + + +async def about() -> Response: + """关于""" + pass diff --git a/backend/controller/menu.py b/backend/controller/menu.py index 00cf085..ea9a658 100644 --- a/backend/controller/menu.py +++ b/backend/controller/menu.py @@ -1,6 +1,23 @@ -from dbhelper.menu import insert_menu -from schemas import MenuIn, MenuRead, Response +from fastapi import Query + +from dbhelper.menu import del_menu, get_menus, insert_menu +from schemas import ListAll, MenuIn, MenuRead, Response async def menu_add(data: MenuIn) -> Response[MenuRead]: return Response(data=await insert_menu(data)) + + +async def menu_arr( + offset: int = Query(default=1, description="偏移量"), + limit: int = Query(default=10, description="数量"), +) -> Response[ListAll[list[MenuRead]]]: + skip = (offset - 1) * limit + menus, count = await get_menus(skip, limit) + return Response(data=ListAll(total=count, items=menus)) + + +async def menu_del(pk: int) -> Response: + if await del_menu(pk) == 0: + return Response(code=400, msg="菜单不存在") + return Response() diff --git a/backend/controller/role.py b/backend/controller/role.py index 34693a6..d9e1222 100644 --- a/backend/controller/role.py +++ b/backend/controller/role.py @@ -1,8 +1,18 @@ import json +from fastapi import Query + from core.utils import list_to_tree -from dbhelper.role import get_role_menus, new_role -from schemas import Response, RoleIn, RoleInfo +from dbhelper.relation import role_assigned_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, RoleMenuIn, RoleQuery, RoleRead async def role_add(data: RoleIn) -> Response[RoleInfo]: @@ -17,3 +27,43 @@ async def role_has_menu(rid: int): for obj in menus: obj["meta"] = json.loads(obj["meta"]) if obj["meta"] is not None else None return Response(data=list_to_tree(menus)) + + +async def role_arr( + offset: int = Query(default=1, description="偏移量-页码"), + limit: int = Query(default=10, description="数据量"), +) -> Response[ListAll[list[RoleRead]]]: + skip = (offset - 1) * limit + roles, count = await get_roles(skip, limit) + return Response(data=ListAll(total=count, items=roles)) + + +async def assigned_menu(data: RoleMenuIn) -> Response: + """分配菜单给角色""" + if await get_role({"id": data.rid, "status__not": 9}) is None: + return Response(code=400, msg="角色不存在") + if isinstance(await role_assigned_menu(data), int): + return Response(code=400, msg=f"菜单不存在") + return Response() + + +async def role_del(pk: int) -> Response: + if await del_role(pk) == 0: + return Response(code=400, msg="角色不存在") + return Response() + + +async def role_put(pk: int, data: RoleIn) -> Response: + """更新角色""" + if await put_role(pk, data) == 0: + return Response(code=400, msg="角色不存在") + return Response() + + +async def role_query(query: RoleQuery) -> Response[ListAll[list[RoleRead]]]: + """post条件查询角色表""" + limit = query.size + skip = (query.offset - 1) * limit + del query.offset, query.size + users, count = await get_roles(skip, limit, query.dict()) + return Response(data=ListAll(total=count, items=users)) diff --git a/backend/controller/user.py b/backend/controller/user.py index 7702a91..7a6003b 100644 --- a/backend/controller/user.py +++ b/backend/controller/user.py @@ -1,48 +1,71 @@ from fastapi import Query from core.security import get_password_hash -from dbhelper.user import get_user, get_user_info, get_users, insert_user, new_user -from schemas import Response, UserAdd, UserIn, UserInfo, UserList, UserQuery, UserRead +from dbhelper.user import ( + del_user, + get_user, + get_user_info, + get_users, + insert_user, + put_user, +) +from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead from schemas.common import ListAll -async def user_add(data: UserAdd) -> Response[UserInfo]: +async def user_add(data: UserAdd) -> Response[UserRead]: """新增用户并分配角色 一步到位""" + if await get_user({"username": data.username}) is not None: + return Response(code=400, msg="用户名已存在") roles = data.rids del data.rids data.password = get_password_hash(data.password) - return await insert_user(data, roles) - - -async def create_user(data: UserIn) -> Response[UserRead]: - """新增用户""" - result = await get_user({"username": data.username}) - if result is None: - data.password = get_password_hash(data.password) - return Response(data=await new_user(data)) - return Response(msg="用户名已存在") + result = await insert_user(data, roles) + if isinstance(result, int): + return Response(code=400, msg=f"角色{result}不存在") + return Response(data=result) async def user_info(pk: int) -> Response[UserInfo]: - try: - return Response(data=await get_user_info(pk)) - except Exception as e: - return Response(msg=f"用户不存在 {e}") + """获取用户信息""" + obj = await get_user({"id": pk}) + if obj is None: + return Response(code=400, msg="用户不存在") + return Response(data=await get_user_info(obj)) async def user_arr( offset: int = Query(default=1, description="偏移量-页码"), limit: int = Query(default=10, description="数据量"), -) -> Response[ListAll[UserList]]: +) -> Response[ListAll[list[UserRead]]]: + """分页列表数据""" skip = (offset - 1) * limit users, count = await get_users(skip, limit) return Response(data=ListAll(total=count, items=users)) -async def user_list(query: UserQuery) -> Response[ListAll[UserList]]: +async def user_list(query: UserQuery) -> Response[ListAll[list[UserRead]]]: """post查询用户列表""" limit = query.size skip = (query.offset - 1) * limit del query.offset, query.size users, count = await get_users(skip, limit, query.dict()) return Response(data=ListAll(total=count, items=users)) + + +async def user_del(pk: int) -> Response: + """删除用户""" + if await del_user(pk) == 0: + return Response(code=400, msg="用户不存在") + return Response() + + +async def user_put(pk: int, data: UserPut) -> Response: + """更新用户""" + 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() diff --git a/backend/core/log.py b/backend/core/log.py index 6d7cab1..e46aa57 100644 --- a/backend/core/log.py +++ b/backend/core/log.py @@ -10,6 +10,6 @@ sh.setLevel(logging.DEBUG) sh.setFormatter(fmt) # will print debug sql -logger_db_client = logging.getLogger("mini-rbac") -logger_db_client.setLevel(logging.DEBUG) -logger_db_client.addHandler(sh) +logger = logging.getLogger("mini-rbac") +logger.setLevel(logging.DEBUG) +logger.addHandler(sh) diff --git a/backend/dbhelper/menu.py b/backend/dbhelper/menu.py index dd90c43..1695c61 100644 --- a/backend/dbhelper/menu.py +++ b/backend/dbhelper/menu.py @@ -5,3 +5,39 @@ 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), await result.count() + + +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) diff --git a/backend/dbhelper/relation.py b/backend/dbhelper/relation.py new file mode 100644 index 0000000..24d9927 --- /dev/null +++ b/backend/dbhelper/relation.py @@ -0,0 +1,31 @@ +from tortoise import connections + +from dbhelper.menu import get_menu +from models import RoleMenuModel, UserRoleModel +from schemas import UserRole + + +async def user_assigned_role(data: UserRole): + """给用户分配角色""" + return await UserRoleModel.create(**data.dict()) + + +async def role_assigned_menu(data): + """给角色分配菜单""" + for mid in data.menus: + if await get_menu({"id": mid}) is None: + return mid + + # todo 性能优化 + db = connections.get("default") + # 1. 先把所有数据做删除 + await db.execute_query_dict( + """ + update sys_role_menu set status = 9 where rid = (?) + """, + [data.rid], + ) + # 2. 新增数据 + await RoleMenuModel.bulk_create( + [RoleMenuModel(rid=data.rid, mid=mid) for mid in data.menus] + ) diff --git a/backend/dbhelper/role.py b/backend/dbhelper/role.py index 9e2d8a0..5fab1fe 100644 --- a/backend/dbhelper/role.py +++ b/backend/dbhelper/role.py @@ -11,9 +11,9 @@ async def get_role_menus(rid: int): db = connections.get("default") return await db.execute_query_dict( """ - select m.id, m.name, m.meta, m.path, m.type, m.component, m.pid, m.identifier, m.api_regx,m.api, m.method, m.sort + select m.id, m.name, m.meta, m.path, m.type, m.component, m.pid, m.identifier, m.regx,m.api, m.method FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid - AND sys_role_menu.rid = (%s) AND m.`status` = 1 ORDER BY m.sort""", + AND sys_role_menu.rid = (?) AND m.`status` = 1""", [rid], ) @@ -21,3 +21,44 @@ async def get_role_menus(rid: int): async def new_role(role: RoleIn): """新增角色""" return await RoleModel.create(**role.dict()) + + +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(status__not=9, **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: int, data): + """更新角色""" + return await RoleModel.filter(id=pk).update(**data.dict()) diff --git a/backend/dbhelper/user.py b/backend/dbhelper/user.py index b397a24..708adff 100644 --- a/backend/dbhelper/user.py +++ b/backend/dbhelper/user.py @@ -1,7 +1,9 @@ -from tortoise.transactions import atomic +from fastapi.encoders import jsonable_encoder +from tortoise import connections -from models import RoleModel, UserModel, UserRoleModel -from schemas.user import UserIn, UserRole +from dbhelper.role import get_role +from models import UserModel, UserRoleModel +from schemas import UserPut async def get_user(kwargs): @@ -16,25 +18,21 @@ async def get_user(kwargs): return await UserModel.filter(**kwargs).first() -async def get_user_info(pk: int): +async def get_user_info(user: UserModel): """ - 根据id查用户角色列表,当前激活角色 + 根据id查用户角色列表 按激活角色倒序显示 """ - user = await UserModel.get(pk=pk).values( - "id", "username", "nickname", "identity", "created", "modified" + db = connections.get("default") + sql_result = await db.execute_query_dict( + """ + select r.id, r.name, ur.status from sys_role as r + left join sys_user_role as ur on r.id = ur.rid where + ur.uid = (?) and ur.status != 9 and r.status != 9 order by ur.status desc + """, + [user.id], ) - role = ( - await UserRoleModel.filter(uid=pk, status__not_in=[9, 5]) - .all() - .values("rid", "status") - ) - active_rid = role[0].get("rid") - rids = [] - for obj in role: - if obj.get("status") == 5: - active_rid = obj.get("rid") - rids.append(obj.get("rid")) - return {**user, "active_rid": active_rid, "rids": rids} + + return {**jsonable_encoder(user), "roles": sql_result} async def get_users(skip: int, limit: int, kwargs: dict = None): @@ -58,22 +56,51 @@ async def get_users(skip: int, limit: int, kwargs: dict = None): return await result.offset(skip).limit(limit), await result.count() -@atomic() async def insert_user(user, roles): - for index, rid in enumerate(roles): - # 1. 查角色表是否有该角色 - await RoleModel.get(pk=rid) - # 创建用户 - obj = await UserModel.create(**user.dict()) + """新增用户,选择角色""" + for role in roles: + if await get_role({"id": role.rid, "status__not": 9}) is None: + return role.rid - user_role = UserRole(rid=rid, uid=obj.id) - if index == 0: - user_role.status = 5 - # 第一个角色默认, 添加到关系表 - await UserRoleModel.create(**user_role.dict()) - return user + # 创建用户 + obj = await UserModel.create(**user.dict()) + + await UserRoleModel.bulk_create( + [UserRoleModel(rid=role.rid, uid=obj.id, status=role.status) for role in roles] + ) + return obj -async def new_user(user: UserIn): - """新增用户""" - return await UserModel.create(**user.dict()) +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 + + roles = data.rids + del data.rids + for role in roles: + if await get_role({"id": role.rid, "status__not": 9}) is None: + return role.rid + + # 更新用户 + data.password = get_password_hash(data.password) + await UserModel.filter(id=uid).update(**data.dict()) + + # todo 1. 先前有的角色,这次更新成没有 2. 先前没有的角色 这次更新成有, 3. 只更新了状态 + + db = connections.get("default") + # 1. 先把所有数据做删除 + await db.execute_query_dict( + """ + update sys_user_role set status = 9 where uid = (?) + """, + [uid], + ) + # 2. 新增次此更新的数据 + await UserRoleModel.bulk_create( + [UserRoleModel(uid=uid, **role.dict()) for role in roles] + ) diff --git a/backend/main.py b/backend/main.py index 21ad460..42fd74d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from core.events import close_orm, init_orm -from core.log import logger_db_client as logger +from core.log import logger from core.middleware import middlewares from router.url import routes diff --git a/backend/mini.db b/backend/mini.db deleted file mode 100644 index 89b135b159d1338f82cf193924569c52aa4878c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 zcmeI5-ESLJ7Qj8@B#zy*189+ns%U42M5#$*&x}7}M?zXRb(&=3I5&RR z4r7}(R24O$AzfNhSOQzMKnn}05F`akTF6rVfEBO%vf8JO$Bu-wt9{wmy?4g;c>K|V zw9+coxk~Ii_s%`{%T68YMyi0=taB%O-=_ z+e_r0j3;Bl>3HmPIK7Y#XVZch7E|GDJQf}hT%5pFrOpX^hJrqC$VZP(9QGZfndYS! zIxs;qnz9UifI(uJgES+eFU#Vf(O~ZFC6?@&$n2b;*;lh+DpeZt9vb)63pLRwAXONo zRmgY57o;bHqkeC2hW?FjhW1W}0;3a1g5Nh0q9+3Ae|mhJwmn5}-`(1Id!2sDc2JR! z&O|cVG(%7Mz2oCZl)5v0*f;E*9uLtRE?4;drgSbxbJDd7(&{qpq4Uevcdq=2cJ7?J zuzmM)RbW&UA{hbc6&m&Xrb1r-BvQ02YG^tbL>h(FJR~~5d^NxIt@Q5Z&bzl&If+y( zemwrO1W8wKOYhvO6@;;4NL5FMDlM+PRlM+JJf>ENtyI5X;q9B!@*R0UljT*X!C>y{ zB9@=l7_V%%_QobdE&$f5H+y4i;ZrdgNeB#m)EgWc@dgiYE{kfq>50*2rhV8Ui?=_? zuiR11myAcB*B0bi3vxZ|U~7NKU%sU7Iv+`=Po+d;?n)iJyQ&ho%*ImTdahDtC@?V< z3VN{{;nP`dHA?!1c~3Ml8)4{@g6O0#ABe?dv9-6Ehq?%z+~}lVm_yxmIFikzRQAka0oyNFz;X54};63#5lqt=AwBEl>-BTj2+>^8N#t4>ogErVrYhmlVT%Ou%m z?&~9Fl(t7{w$j22Ss@t}O08IVQ?K!vn#==O+(ERfWoe9%+p~)&D(xArx~w;;ks=SI zN}53j`cpg5`u0re18Z8agpfhK)z}mouqyfI7p2u((xvt7?{7%C4YjG8k7Q1&4IPI& zO1XgIo>_5D-BtFUMk7hwUCZ8F_+l&n{_8YbxVoPIZk6T=Td!_!ZPL7S@xsnqRin~q zDlwl*3du~Pnw3KNYq#?6ol`5HFP}uRc7^55>akrshGgO4q^JtCmxl+^dy6|)E=$*6 zD{d^&xZ56##Hz-Hk@>hY{Msv|5EzTSTcr`uwB(@|EI^o5g$gl(HtvUNk^3nts$#7x(>ZUuf^$z21(`I+#7L zp(F@E00;m9AOHk_01yBIKwwuB$UR}CbVk3wr?1D@Z`7Ihck(RH_p_dUj;GmyLC!PC zIjnZx;db(UEIY`u56V{-fD7w43YVA8F!5+A$qX{nX!OsVwW0ZFMo8H3@@r8QN{#M?|J<+bHWZ$gLF z<|{fC*Y6d+Td(LerPPewN{Qt?e!N^)@nyV@ zQubvWmwg#WD!z<2x^tcS2kORKS=Rt-cQ#|9&HtaBXGk~lfx59)mUp}D?q<4a^A$h7 zZyO+=O{Mk#$7OCfQsIU-xOM*W@ohWZQj9Q6v7+BF9YiUI*300e*l5C8%|00;m9 zAOHk_01){95_q)5*h5s`yI_2*h3Fy3>U(qAOG#Dt#26lJY40Jb?@ei|N>T0%X;ow^ zKZuYo@VC^}rk7Ky?@u5fL6VKtw*}zq|L8~m5P$#>00KY&2mk>f00e*l5C8%|00;nq zhnN7q{txs2huFYyXg~l600AHX1b_e#00KY&2mk>f00a;LeEpxK{5t9r^Z@|~00AHX z1b_e#00KY&2mk>f00e*l5O^?wXGsHozUbpUHP3)V59V`PJq`|qW)JSu=Hs8{EBEay z?-e*k$b!uc1lz41o^`rh%>--n)7g1J%%8uxvv^aHEYJV<{h*^_=mP=}00KY&2mk>f z00e*l5C8%|00;nq-9RAs3(|-u;GMtBwboC;Ijh_4;Q7X{hCowvZGJ{TukR40uU{{2 ztmfbRV>dtB&GFsVV`IVCvAMvZ(BbGPH*E9Tecp(`9p#37rzQdmBhe@~IcptG2|m|= z;C22kVSgzR$aNC3TIRk6YB@Ysm&3*Ljo*ae9<0fa#L&wqiks&P->xaI98h)h*+%@Q zcwc&KAUUUp+{E1jmM$^!rZ diff --git a/backend/router/url.py b/backend/router/url.py index 18d6154..ef6784b 100644 --- a/backend/router/url.py +++ b/backend/router/url.py @@ -1,11 +1,20 @@ from typing import Any, Callable, get_type_hints -from fastapi import routing +from fastapi import Depends, routing -from controller.common import login -from controller.menu import menu_add -from controller.role import role_add, role_has_menu -from controller.user import create_user, user_arr, user_info, user_list +from controller.common import about, login +from controller.menu import menu_add, menu_arr, menu_del +from controller.role import ( + assigned_menu, + role_add, + role_arr, + role_del, + role_has_menu, + role_put, + role_query, +) +from controller.user import user_add, user_arr, user_del, user_info, user_list, user_put +from core.security import check_token class Route(routing.APIRoute): @@ -106,18 +115,47 @@ class Route(routing.APIRoute): routes = [ Route.post("/login", endpoint=login, tags=["公共"], summary="登录"), - # 用户管理 - Route.post("/user", endpoint=create_user, tags=["用户管理"], summary="用户新增"), - Route.get("/user/{pk}", endpoint=user_info, tags=["用户管理"], summary="用户信息"), + Route.get("/about", endpoint=about, tags=["公共"], summary="关于"), + # 用户管理 Route.get("/user", endpoint=user_arr, tags=["用户管理"], summary="用户列表"), + Route.post("/user", endpoint=user_add, tags=["用户管理"], summary="用户新增"), + Route.delete( + "/user/{pk}", + endpoint=user_del, + tags=["用户管理"], + summary="用户删除", + ), + Route.put("/user/{pk}", endpoint=user_put, tags=["用户管理"], summary="用户更新"), + Route.get("/user/{pk}", endpoint=user_info, tags=["用户管理"], summary="用户信息"), Route.post("/user/query", endpoint=user_list, tags=["用户管理"], summary="用户列表查询"), - # 角色管理 + # 角色管理, + Route.get("/role", endpoint=role_arr, tags=["角色管理"], summary="角色列表"), Route.post("/role", endpoint=role_add, tags=["角色管理"], summary="角色新增"), + Route.delete( + "/role/{pk}", + endpoint=role_del, + tags=["角色管理"], + summary="角色删除", + dependencies=[Depends(check_token)], + ), Route.get( - "role/{rid}/menu", endpoint=role_has_menu, tags=["角色管理"], summary="查询角色拥有权限" + "/role/{rid}/menu", endpoint=role_has_menu, tags=["角色管理"], summary="查询角色拥有权限" + ), + Route.put("/role", endpoint=role_put, tags=["角色管理"], summary="角色更新"), + Route.post("/role/query", endpoint=role_query, tags=["角色管理"], summary="角色条件查询"), + Route.post( + "/role/assigned/menu", endpoint=assigned_menu, tags=["角色管理"], summary="角色分配菜单" ), # 菜单新增 + Route.get("/menu", endpoint=menu_arr, tags=["菜单管理"], summary="菜单列表"), Route.post("/menu", endpoint=menu_add, tags=["菜单管理"], summary="菜单新增"), + Route.delete( + "/menu/{pk}", + endpoint=menu_del, + tags=["菜单管理"], + summary="菜单删除", + dependencies=[Depends(check_token)], + ), ] __all__ = [routes] diff --git a/backend/schemas/menu.py b/backend/schemas/menu.py index 0d6b58d..707670e 100644 --- a/backend/schemas/menu.py +++ b/backend/schemas/menu.py @@ -1,32 +1,26 @@ -from tortoise.contrib.pydantic import pydantic_model_creator +from typing import Optional -from models import MenuModel +from pydantic import BaseModel, Field -MenuRead = pydantic_model_creator(MenuModel, name="MenuOut") -MenuIn = pydantic_model_creator(MenuModel, name="MenuIn", exclude_readonly=True) +from schemas.common import ReadBase -# from pydantic import BaseModel, Field -# from typing import Optional -# from core import ReadBase -# -# -# class MenuBasic(BaseModel): -# name: str -# meta: Optional[str] = Field(default=None, description="元信息") -# path: Optional[str] = Field(default=None, description="前端路由地址") -# type: int = Field(description="0 目录 1 组件 2 按钮") -# component: Optional[str] = Field(default=None, description="前端组件地址") -# pid: int = Field(default=0, description="0 表示没有根节点") -# identifier: Optional[str] = Field(default=None, description="权限标识符 -> 按钮显示") -# api: Optional[str] = Field(default=None, description="后端接口地址") -# method: Optional[str] = Field(default=None, description="接口请求方法") -# regx: Optional[str] = Field(default=None, description="正则匹配") -# -# -# class MenuIn(MenuBasic): -# pass -# -# -# class MenuRead(MenuBasic, ReadBase): -# pass +class MenuBasic(BaseModel): + name: str + meta: dict = Field(default=None, description="元信息") + path: Optional[str] = Field(default=None, description="前端路由地址") + type: int = Field(description="0 目录 1 组件 2 按钮") + component: Optional[str] = Field(default=None, description="前端组件地址") + pid: int = Field(default=0, description="0 表示没有根节点") + identifier: Optional[str] = Field(default=None, description="权限标识符 -> 按钮显示") + api: Optional[str] = Field(default=None, description="后端接口地址") + method: Optional[str] = Field(default=None, description="接口请求方法") + regx: Optional[str] = Field(default=None, description="正则匹配") + + +class MenuIn(MenuBasic): + pass + + +class MenuRead(MenuBasic, ReadBase): + pass diff --git a/backend/schemas/role.py b/backend/schemas/role.py index 5f9ac52..fa11675 100644 --- a/backend/schemas/role.py +++ b/backend/schemas/role.py @@ -1,10 +1,17 @@ from pydantic import BaseModel, Field -from tortoise.contrib.pydantic import pydantic_model_creator -from models import RoleModel -from schemas.common import ReadBase +from schemas.common import QueryData, ReadBase -RoleRed = pydantic_model_creator(RoleModel, name="RoleOut") + +class RoleMenuIn(BaseModel): + """角色 -分配菜单id""" + + rid: int = Field(description="角色ID") + menus: list[int] = Field(description="菜单ID 列表") + + +class RoleMenuRead(RoleMenuIn, ReadBase): + pass class RoleBasic(BaseModel): @@ -16,9 +23,15 @@ class RoleIn(RoleBasic): pass -class RoleRed(RoleBasic, ReadBase): +class RoleRead(RoleBasic, ReadBase): pass -class RoleInfo(RoleRed): +class RoleInfo(RoleRead): pass + + +class RoleQuery(QueryData): + """查询模型""" + + name: str = Field("", description="角色名") diff --git a/backend/schemas/user.py b/backend/schemas/user.py index 67d94df..427f509 100644 --- a/backend/schemas/user.py +++ b/backend/schemas/user.py @@ -1,14 +1,19 @@ -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field -from tortoise.contrib.pydantic import pydantic_model_creator -from models import UserRoleModel -from schemas.common import QueryData +from schemas.common import QueryData, ReadBase -UserRole = pydantic_model_creator(UserRoleModel, name="UserRole", exclude_readonly=True) -from schemas.common import ReadBase +class UserRole(BaseModel): + uid: int = Field(description="用户id") + rid: int = Field(description="角色id") + + +class UserRoleRead(UserRole, ReadBase): + """用户 角色 读取模型""" + + pass class UserBasic(BaseModel): @@ -24,18 +29,41 @@ class UserRead(UserBasic, ReadBase): pass +class UserHasRole(BaseModel): + """用户拥有角色""" + + id: int + name: str + status: int = Field(default=1, description="激活角色 5 正常 1 删除 9") + + class UserInfo(UserRead): - active_rid: int = Field(..., description="用户当前激活角色") - rids: List[int] = Field(..., description="用户拥有角色") + """用户信息模型""" + + roles: list[UserHasRole] = Field(..., description="用户拥有角色") + + +class RoleActive(BaseModel): + rid: int = Field(description="角色id") + status: int = Field(default=1, description="激活角色 5 正常 1 删除 9") class UserAdd(UserIn): - rids: List[int] = Field(..., description="用户角色列表") + """新增用户模型""" + + rids: list[RoleActive] = Field(..., description="选择角色列表") class UserQuery(QueryData): + """查询模型""" + username: Optional[str] = Field("", description="用户名") nickname: Optional[str] = Field("", description="姓名") -UserList = List[UserRead] +class UserPut(BaseModel): + """用户更新模型""" + + nickname: str = Field(..., description="用户昵称") + password: str = Field(..., description="密码") + rids: list[RoleActive] = Field(..., description="选择角色列表") diff --git a/backend/tests/test_case.py b/backend/tests/test_case.py index 7edda83..2ea916d 100644 --- a/backend/tests/test_case.py +++ b/backend/tests/test_case.py @@ -1,51 +1,49 @@ -import json - +import pytest import requests as client +from core.log import logger from schemas.menu import MenuIn -from schemas.role import RoleIn -from schemas.user import UserIn +from schemas.role import RoleIn, RoleMenuIn +from schemas.user import RoleActive, UserAdd base = "http://localhost:8000" -def test_user_add(): - url = base + "/user" - res = client.request( - method="post", - url=url, - json=UserIn(username="admin", nickname="超级管理员", password="123456").dict(), - ) - assert res.status_code == 200 - res = client.request( - method="post", - url=url, - json=UserIn(username="tester", nickname="测试员", password="123456").dict(), - ) - assert res.status_code == 200 - - -def test_role_add(): - url = base + "/role" - res = client.request( - method="post", url=url, json=RoleIn(name="super", remark="全部权限").dict() - ) - assert res.status_code == 200 - res = client.request( - method="post", url=url, json=RoleIn(name="user", remark="用户权限").dict() - ) - assert res.status_code == 200 - - -def test_menu_add(): - url = base + "/menu" - # id 1 - res = client.request( - method="post", - url=url, - json=MenuIn( +params = [ + # 创建角色 + ("/role", RoleIn(name="super", remark="全部权限").dict()), + ("/role", RoleIn(name="user", remark="用户权限").dict()), + # 创建用户 + ( + "/user", + UserAdd( + username="admin", + nickname="管理员", + password="123456", + rids=[ + RoleActive(rid=1, status=5), + RoleActive(rid=2), + ], + ).dict(), + ), + ( + "/user", + UserAdd( + username="tester", + nickname="测试员", + password="123456", + rids=[ + RoleActive(rid=2, status=5), + ], + ).dict(), + ), + # 创建菜单 + # 目录 + ( + "/menu", + MenuIn( # id 1 name="系统管理", - meta=json.dumps({"icon": "Group"}), + meta={"icon": "Group"}, path="/system", type=0, component=None, @@ -55,16 +53,28 @@ def test_menu_add(): method=None, regx=None, ).dict(), - ) - - assert res.status_code == 200 - # id 2 - res = client.request( - method="post", - url=url, - json=MenuIn( + ), + ( + "/menu", + MenuIn( # id 2 + name="系统设置", + meta={"icon": "setting"}, + path="/system", + type=0, + component=None, + pid=0, + identifier=None, + api=None, + method=None, + regx=None, + ).dict(), + ), + # 组件 + ( + "/menu", + MenuIn( # id 3 name="用户管理", - meta=json.dumps({"icon": "User"}), + meta={"icon": "User"}, path="/system/user", type=1, component="/system/user.vue", @@ -74,15 +84,12 @@ def test_menu_add(): method="{'GET'}", regx="^/user$", ).dict(), - ) - assert res.status_code == 200 - # id 3 - res = client.request( - method="post", - url=url, - json=MenuIn( + ), + ( + "/menu", + MenuIn( # id 4 name="角色管理", - meta=json.dumps({"icon": "User"}), + meta={"icon": "Role"}, path="/system/role", type=1, component="/system/role.vue", @@ -92,15 +99,12 @@ def test_menu_add(): method="{'GET'}", regx="^/role$", ).dict(), - ) - - # id 4 - res = client.request( - method="post", - url=url, - json=MenuIn( + ), + ( + "/menu", + MenuIn( # id 5 name="菜单管理", - meta=json.dumps({"icon": "Menu"}), + meta={"icon": "Menu"}, path="/system/menu", type=1, component="/system/menu.vue", @@ -110,92 +114,231 @@ def test_menu_add(): method="{'GET'}", regx="^/menu$", ).dict(), - ) - - # id 5 - res = client.request( - method="post", - url=url, - json=MenuIn( - name="系统设置", - meta=json.dumps({"icon": "Setting"}), - path="/setting", - type=0, - component=None, - pid=0, + ), + ( + "/menu", + MenuIn( # id 6 + name="关于", + meta={"icon": "Menu"}, + path="/setting/about", + type=1, + component="/setting/about.vue", + pid=2, identifier=None, - api=None, - method=None, - regx=None, + api="/about", + method="{'GET'}", + regx="^/about", ).dict(), - ) - - res = client.request( - method="post", - url=url, - json=MenuIn( - name="系统监控", - meta=json.dumps({"icon": "minitor"}), - path="/setting/minitor", - type=0, - component="/setting/minitor.vue", - pid=5, - identifier=None, - api=None, - method=None, - regx=None, - ).dict(), - ) - - res = client.request( - method="post", - url=url, - json=MenuIn( - name="新增用户", - meta=json.dumps({"icon": "Add"}), + ), + # 按钮 + ( + "/menu", + MenuIn( + name="用户新增", + meta={"icon": "Add"}, path=None, type=2, component=None, - pid=2, - identifier="user:add", + pid=3, + identifier="user:create", api="/user", method="{'POST'}", regx="^/user$", ).dict(), - ) - assert res.status_code == 200 - - res = client.request( - method="post", - url=url, - json=MenuIn( - name="查询用户", - meta=json.dumps({"icon": "Select"}), + ), + ( + "/menu", + MenuIn( + name="用户删除", + meta={"icon": "Delete"}, path=None, type=2, component=None, - pid=2, + pid=3, + identifier="user:delete", + api="/user/{pk}", + method="{'DELETE'}", + regx="^/user/(?P[^/]+)$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="用户更新", + meta={"icon": "Update"}, + path=None, + type=2, + component=None, + pid=3, + identifier="user:update", + api="/user/{pk}", + method="{'PUT'}", + regx="^/user/(?P[^/]+)$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="用户详情", + meta={"icon": "Info"}, + path=None, + type=2, + component=None, + pid=3, + identifier="user:get", + api="/user/{pk}", + method="{'GET'}", + regx="^/user/(?P[^/]+)$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="用户查询", + meta={"icon": "Search"}, + path=None, + type=2, + component=None, + pid=3, identifier="user:query", api="/user/query", method="{'POST'}", regx="^/user/query$", ).dict(), - ) - - res = client.request( - method="post", - url=url, - json=MenuIn( - name="角色管理", - meta=json.dumps({"icon": "User"}), - path="/system/role", - type=1, - component="/system/role.vue", - pid=1, - identifier=None, + ), + # 角色管理 + ( + "/menu", + MenuIn( + name="角色新增", + meta={"icon": "Add"}, + path=None, + type=2, + component=None, + pid=4, + identifier="role:create", api="/role", - method="{'GET'}", - regx="^/role", + method="{'POST'}", + regx="^/role$", ).dict(), - ) + ), + ( + "/menu", + MenuIn( + name="角色删除", + meta={"icon": "Delete"}, + path=None, + type=2, + component=None, + pid=4, + identifier="role:delete", + api="/role/{pk}", + method="{'DELETE'}", + regx="^/role/(?P[^/]+)$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="查询角色拥有权限", + meta={"icon": "Delete"}, + path=None, + type=2, + component=None, + pid=4, + identifier=None, + api="/role/{rid}/menu", + method="{'GET'}", + regx="^/role/(?P[^/]+)/menu$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="查询角色", + meta={"icon": "Search"}, + path=None, + type=2, + component=None, + pid=4, + identifier="", + api="/role/query", + method="{'POST'}", + regx="^/role/query$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="分配权限", + meta={"icon": "Delete"}, + path=None, + type=2, + component=None, + pid=4, + identifier="role:assign", + api="/role/assigned/menu", + method="{'POST'}", + regx="^/role/assigned/menu$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="更新角色", + meta={"icon": "Update"}, + path=None, + type=2, + component=None, + pid=4, + identifier="role:update", + api="/role", + method="{'PUT'}", + regx="^/role$", + ).dict(), + ), + # 菜单管理的权限 + ( + "/menu", + MenuIn( + name="新增菜单", + meta={"icon": "Update"}, + path=None, + type=2, + component=None, + pid=5, + identifier="menu:create", + api="/menu", + method="{'POST'}", + regx="^/menu$", + ).dict(), + ), + ( + "/menu", + MenuIn( + name="删除菜单", + meta={"icon": "Delete"}, + path=None, + type=2, + component=None, + pid=5, + identifier="menu:delete", + api="/menu/{pk}", + method="{'DELETE'}", + regx="/menu/(?P[^/]+)$", + ).dict(), + ), + # 分配权限 + ( + "/role/assigned/menu", + RoleMenuIn(rid=1, menus=[num for num in range(1, 20)]).dict(), + ), + ("/role/assigned/menu", RoleMenuIn(rid=2, menus=[3, 7, 8, 9, 10, 11]).dict()), +] + + +@pytest.mark.parametrize("path, data", params) +def test_add_data(path, data): + res = client.post(url=base + path, json=data) + logger.info(res.json()) assert res.status_code == 200