Skip to content

Commit 6c5e7a6

Browse files
authored
Add department-related interfaces and others (#101)
* Add tool to build tree structure data * Update to keyword parameter style * Add department-related interfaces * Update departmental section interface permissions * Add TODO and minor fixes * Fix department relationships * Fix user foreign key relationship deletion setting * Add path parameters to the operation log * Complete todo items * Update department deletion logic * Update operation log entry records * Add AES encryption algorithm * Add operation log request entry to the secret * Fix naming prefixes * Add easy encryption tools * FIX CASBIN_EXCLUDE typing * Update user password reset interface * Add confirm_password to the operation log encryption
1 parent abcc9d2 commit 6c5e7a6

35 files changed

+540
-120
lines changed

backend/app/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ APS_REDIS_PASSWORD=''
1717
APS_REDIS_DATABASE=1
1818
# Token
1919
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
20+
# Opera Log
21+
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'

backend/app/api/v1/auth/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
2020
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> GetSwaggerToken:
21-
token, user = await AuthService().swagger_login(form_data)
21+
token, user = await AuthService().swagger_login(form_data=form_data)
2222
return GetSwaggerToken(access_token=token, user=user)
2323

2424

@@ -44,12 +44,12 @@ async def user_login(request: Request, obj: Auth, background_tasks: BackgroundTa
4444

4545
@router.post('/new_token', summary='创建新 token', dependencies=[DependsJwtAuth])
4646
async def create_new_token(refresh_token: Annotated[str, Query(...)]):
47-
access_token, access_expire = await AuthService.new_token(refresh_token)
47+
access_token, access_expire = await AuthService.new_token(refresh_token=refresh_token)
4848
data = GetNewToken(access_token=access_token, access_token_expire_time=access_expire)
4949
return await response_base.success(data=data)
5050

5151

5252
@router.post('/logout', summary='用户登出', dependencies=[DependsJwtAuth])
5353
async def user_logout(request: Request):
54-
await AuthService.logout(request)
54+
await AuthService.logout(request=request)
5555
return await response_base.success()

backend/app/api/v1/dept.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,54 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3-
from fastapi import APIRouter
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Query, Request
6+
7+
from backend.app.common.casbin_rbac import DependsRBAC
8+
from backend.app.common.jwt import DependsJwtAuth
9+
from backend.app.common.response.response_schema import response_base
10+
from backend.app.schemas.dept import CreateDept, GetAllDept, UpdateDept
11+
from backend.app.services.dept_service import DeptService
12+
from backend.app.utils.serializers import select_to_json
413

514
router = APIRouter()
615

7-
# TODO: 添加 dept 相关接口
16+
17+
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
18+
async def get_dept(pk: int):
19+
dept = await DeptService.get(pk=pk)
20+
data = GetAllDept(**select_to_json(dept))
21+
return await response_base.success(data=data)
22+
23+
24+
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
25+
async def get_all_depts(
26+
name: Annotated[str | None, Query()] = None,
27+
leader: Annotated[str | None, Query()] = None,
28+
phone: Annotated[str | None, Query()] = None,
29+
status: Annotated[bool | None, Query()] = None,
30+
):
31+
dept = await DeptService.get_select(name=name, leader=leader, phone=phone, status=status)
32+
return await response_base.success(data=dept)
33+
34+
35+
@router.post('', summary='创建部门', dependencies=[DependsRBAC])
36+
async def create_dept(request: Request, obj: CreateDept):
37+
await DeptService.create(obj=obj, user_id=request.user.id)
38+
return await response_base.success()
39+
40+
41+
@router.put('/{pk}', summary='更新部门', dependencies=[DependsRBAC])
42+
async def update_dept(request: Request, pk: int, obj: UpdateDept):
43+
count = await DeptService.update(pk=pk, obj=obj, user_id=request.user.id)
44+
if count > 0:
45+
return await response_base.success()
46+
return await response_base.fail()
47+
48+
49+
@router.delete('{pk}', summary='删除部门', dependencies=[DependsRBAC])
50+
async def delete_dept(pk: int):
51+
count = await DeptService.delete(pk=pk)
52+
if count > 0:
53+
return await response_base.success()
54+
return await response_base.fail()

backend/app/api/v1/login_log.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def get_all_login_logs(
2929

3030
@router.delete('', summary='(批量)删除登录日志', dependencies=[DependsRBAC])
3131
async def delete_login_log(pk: Annotated[list[int], Query(...)]):
32-
count = await LoginLogService.delete(pk)
32+
count = await LoginLogService.delete(pk=pk)
3333
if count > 0:
3434
return await response_base.success()
3535
return await response_base.fail()

backend/app/api/v1/opera_log.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def get_all_opera_logs(
2929

3030
@router.delete('', summary='(批量)删除操作日志', dependencies=[DependsRBAC])
3131
async def delete_opera_log(pk: Annotated[list[int], Query(...)]):
32-
count = await OperaLogService.delete(pk)
32+
count = await OperaLogService.delete(pk=pk)
3333
if count > 0:
3434
return await response_base.success()
3535
return await response_base.fail()

backend/app/api/v1/user.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@
1717

1818
@router.post('/register', summary='用户注册')
1919
async def user_register(obj: CreateUser):
20-
await UserService.register(obj)
20+
await UserService.register(obj=obj)
2121
return await response_base.success()
2222

2323

24-
@router.post('/password/reset', summary='密码重置')
25-
async def password_reset(obj: ResetPassword):
26-
count = await UserService.pwd_reset(obj)
24+
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
25+
async def password_reset(request: Request, obj: ResetPassword):
26+
count = await UserService.pwd_reset(request=request, obj=obj)
2727
if count > 0:
2828
return await response_base.success()
2929
return await response_base.fail()
3030

3131

3232
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
3333
async def get_user(username: str):
34-
current_user = await UserService.get_userinfo(username)
34+
current_user = await UserService.get_userinfo(username=username)
3535
data = GetAllUserInfo(**select_to_json(current_user))
3636
return await response_base.success(data=data)
3737

backend/app/common/enums.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,18 @@ class LoginLogStatus(IntEnum):
5555

5656
fail = 0
5757
success = 1
58+
59+
60+
class BuildTreeType(StrEnum):
61+
"""构建树形结构类型"""
62+
63+
traversal = 'traversal'
64+
recursive = 'recursive'
65+
66+
67+
class OperaLogCipherType(IntEnum):
68+
"""操作日志加密类型"""
69+
70+
aes = 0
71+
md5 = 1
72+
plan = 2

backend/app/core/conf.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class Settings(BaseSettings):
2929
APS_REDIS_DATABASE: int
3030

3131
# Env Token
32-
TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32))
32+
TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32)
33+
34+
# Env Opera Log
35+
OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str
3336

3437
# FastAPI
3538
API_V1_STR: str = '/v1'
@@ -95,7 +98,7 @@ def validator_api_url(cls, values):
9598

9699
# Casbin
97100
CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf'
98-
CASBIN_EXCLUDE: list[dict[str, str], dict[str, str]] = [
101+
CASBIN_EXCLUDE: list[dict[str, str]] = [
99102
{'method': 'POST', 'path': '/v1/auth/swagger_login'},
100103
{'method': 'POST', 'path': '/v1/auth/login'},
101104
{'method': 'POST', 'path': '/v1/auth/register'},
@@ -109,6 +112,8 @@ def validator_api_url(cls, values):
109112
OPENAPI_URL,
110113
'/v1/auth/swagger_login',
111114
]
115+
OPERA_LOG_ENCRYPT: int = 1 # 请求入参加密, 0: AES (高性能损耗), 1: md5, 2: 不加密, other: 替换为 ******
116+
OPERA_LOG_ENCRYPT_INCLUDE: list[str] = ['password', 'old_password', 'new_password', 'confirm_password']
112117

113118
class Config:
114119
# https://docs.pydantic.dev/usage/settings/#dotenv-env-support

backend/app/crud/base.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Any, Dict, Generic, Type, TypeVar, NoReturn
44

55
from pydantic import BaseModel
6-
from sqlalchemy import select, update, delete
6+
from sqlalchemy import select, update, delete, and_
77
from sqlalchemy.ext.asyncio import AsyncSession
88

99
from backend.app.database.base_class import MappedBase
@@ -17,16 +17,36 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
1717
def __init__(self, model: Type[ModelType]):
1818
self.model = model
1919

20-
async def get_(self, db: AsyncSession, pk: int) -> ModelType | None:
20+
async def get_(
21+
self,
22+
db: AsyncSession,
23+
*,
24+
pk: int | None = None,
25+
name: str | None = None,
26+
status: int | None = None,
27+
del_flag: int | None = None,
28+
) -> ModelType | None:
2129
"""
22-
通过主键 id 获取一条数据
30+
通过主键 id 或者 name 获取一条数据
2331
2432
:param db:
2533
:param pk:
34+
:param name:
35+
:param status:
36+
:param del_flag:
2637
:return:
2738
"""
28-
model = await db.execute(select(self.model).where(self.model.id == pk))
29-
return model.scalars().first()
39+
assert pk is not None or name is not None, '查询错误, pk 和 name 参数不能同时为空'
40+
where_list = [self.model.id == pk] if pk is not None else [self.model.name == name]
41+
if status is not None:
42+
assert status in (0, 1), '查询错误, status 参数只能为 0 或 1'
43+
where_list.append(self.model.status == status)
44+
if del_flag is not None:
45+
assert del_flag in (0, 1), '查询错误, del_flag 参数只能为 0 或 1'
46+
where_list.append(self.model.del_flag == del_flag)
47+
48+
result = await db.execute(select(self.model).where(and_(*where_list)))
49+
return result.scalars().first()
3050

3151
async def create_(self, db: AsyncSession, obj_in: CreateSchemaType, user_id: int | None = None) -> NoReturn:
3252
"""
@@ -38,10 +58,10 @@ async def create_(self, db: AsyncSession, obj_in: CreateSchemaType, user_id: int
3858
:return:
3959
"""
4060
if user_id:
41-
db_obj = self.model(**obj_in.dict(), create_user=user_id)
61+
create_data = self.model(**obj_in.dict(), create_user=user_id)
4262
else:
43-
db_obj = self.model(**obj_in.dict())
44-
db.add(db_obj)
63+
create_data = self.model(**obj_in.dict())
64+
db.add(create_data)
4565

4666
async def update_(
4767
self, db: AsyncSession, pk: int, obj_in: UpdateSchemaType | Dict[str, Any], user_id: int | None = None
@@ -61,16 +81,21 @@ async def update_(
6181
update_data = obj_in.dict(exclude_unset=True)
6282
if user_id:
6383
update_data.update({'update_user': user_id})
64-
model = await db.execute(update(self.model).where(self.model.id == pk).values(**update_data))
65-
return model.rowcount
84+
result = await db.execute(update(self.model).where(self.model.id == pk).values(**update_data))
85+
return result.rowcount
6686

67-
async def delete_(self, db: AsyncSession, pk: int) -> int:
87+
async def delete_(self, db: AsyncSession, pk: int, *, del_flag: int | None = None) -> int:
6888
"""
6989
通过主键 id 删除一条数据
7090
7191
:param db:
7292
:param pk:
93+
:param del_flag:
7394
:return:
7495
"""
75-
model = await db.execute(delete(self.model).where(self.model.id == pk))
76-
return model.rowcount
96+
if del_flag is None:
97+
result = await db.execute(delete(self.model).where(self.model.id == pk))
98+
else:
99+
assert del_flag == 1, '删除错误, del_flag 参数只能为 1'
100+
result = await db.execute(update(self.model).where(self.model.id == pk).values(del_flag=del_flag))
101+
return result.rowcount

backend/app/crud/crud_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
class CRUDApi(CRUDBase[Api, CreateApi, UpdateApi]):
1414
async def get(self, db: AsyncSession, pk: int) -> Api | None:
15-
return await self.get_(db, pk)
15+
return await self.get_(db, pk=pk)
1616

1717
async def get_all(self, name: str = None, method: str = None, path: str = None) -> Select:
1818
se = select(self.model).order_by(desc(self.model.created_time))

backend/app/crud/crud_dept.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,62 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from typing import Any
4+
5+
from sqlalchemy import select, desc, and_
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
from sqlalchemy.orm import selectinload
8+
39
from backend.app.crud.base import CRUDBase
410
from backend.app.models import Dept
511
from backend.app.schemas.dept import CreateDept, UpdateDept
612

713

814
class CRUDDept(CRUDBase[Dept, CreateDept, UpdateDept]):
9-
async def get(self, db, dept_id: int):
10-
return await self.get_(db, dept_id)
15+
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
16+
return await self.get_(db, pk=dept_id, del_flag=0)
17+
18+
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
19+
return await self.get_(db, name=name)
20+
21+
async def get_all(
22+
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: bool = None
23+
) -> Any:
24+
se = select(self.model).order_by(desc(self.model.sort))
25+
where_list = [self.model.del_flag == 0]
26+
if name:
27+
where_list.append(self.model.name.like(f'%{name}%'))
28+
if leader:
29+
where_list.append(self.model.leader.like(f'%{leader}%'))
30+
if phone:
31+
where_list.append(self.model.phone.startswith(phone))
32+
if status is not None:
33+
where_list.append(self.model.status == status)
34+
if where_list:
35+
se = se.where(and_(*where_list))
36+
dept = await db.execute(se)
37+
return dept.scalars().all()
38+
39+
async def create(self, db: AsyncSession, obj_in: dict) -> None:
40+
obj = self.model(**obj_in)
41+
db.add(obj)
42+
43+
async def update(self, db: AsyncSession, dept_id: int, obj_in: dict) -> int:
44+
return await self.update_(db, dept_id, obj_in)
45+
46+
async def delete(self, db: AsyncSession, dept_id: int) -> int:
47+
return await self.delete_(db, dept_id, del_flag=1)
48+
49+
async def get_user_relation(self, db: AsyncSession, dept_id: int) -> Any:
50+
result = await db.execute(
51+
select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
52+
)
53+
user_relation = result.scalars().first()
54+
return user_relation.users
55+
56+
async def get_children(self, db: AsyncSession, dept_id: int) -> Any:
57+
result = await db.execute(select(self.model).where(self.model.id == dept_id))
58+
dept = result.scalars().first()
59+
return dept.children
1160

1261

1362
DeptDao: CRUDDept = CRUDDept(Dept)

backend/app/crud/crud_menu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
99
async def get(self, db, menu_id: int) -> Menu | None:
10-
return await self.get_(db, menu_id)
10+
return await self.get_(db, pk=menu_id)
1111

1212

1313
MenuDao: CRUDMenu = CRUDMenu(Menu)

backend/app/crud/crud_role.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
class CRUDRole(CRUDBase[Role, CreateRole, UpdateRole]):
1414
async def get(self, db, role_id: int) -> Role | None:
15-
return await self.get_(db, role_id)
15+
return await self.get_(db, pk=role_id)
1616

1717
async def get_with_relation(self, db, role_id: int) -> Role | None:
1818
role = await db.execute(

backend/app/crud/crud_user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
class CRUDUser(CRUDBase[User, CreateUser, UpdateUser]):
1818
async def get(self, db: AsyncSession, user_id: int) -> User | None:
19-
return await self.get_(db, user_id)
19+
return await self.get_(db, pk=user_id)
2020

2121
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
2222
user = await db.execute(select(self.model).where(self.model.username == username))

0 commit comments

Comments
 (0)