From c7431822117fc75fd93a58701957bdf926eb4543 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 17 Aug 2024 09:26:16 +0800 Subject: [PATCH] Optimize serialization and jwt performance (#382) * Optimize serialization and algorithm performance * Fix get tree data to async * Removed CPU-intensive multi-threaded execution * Update pdm pre commit to 2.18.0 * Add jwt user info cache * Update ci * Fix ci * Fix redis delete prefix * Adapt to JWT user instance * Update jwt middleware token parse --- .github/workflows/ci.yml | 17 +++---- .pre-commit-config.yaml | 4 +- backend/app/admin/api/v1/auth/auth.py | 6 +-- backend/app/admin/api/v1/auth/captcha.py | 2 +- backend/app/admin/api/v1/log/login_log.py | 10 ++-- backend/app/admin/api/v1/log/opera_log.py | 10 ++-- backend/app/admin/api/v1/monitor/redis.py | 2 +- backend/app/admin/api/v1/monitor/server.py | 14 +++--- backend/app/admin/api/v1/oauth2/github.py | 2 +- backend/app/admin/api/v1/oauth2/linux_do.py | 2 +- backend/app/admin/api/v1/sys/api.py | 16 +++--- backend/app/admin/api/v1/sys/casbin.py | 34 ++++++------- backend/app/admin/api/v1/sys/config.py | 12 ++--- backend/app/admin/api/v1/sys/dept.py | 16 +++--- backend/app/admin/api/v1/sys/dict_data.py | 16 +++--- backend/app/admin/api/v1/sys/dict_type.py | 12 ++--- backend/app/admin/api/v1/sys/menu.py | 18 +++---- backend/app/admin/api/v1/sys/role.py | 30 +++++------ backend/app/admin/api/v1/sys/user.py | 50 +++++++++---------- backend/app/admin/crud/crud_user.py | 6 +-- backend/app/admin/schema/user.py | 4 ++ backend/app/admin/service/auth_service.py | 10 ++-- backend/app/admin/service/config_service.py | 2 +- backend/app/admin/service/dept_service.py | 2 +- backend/app/admin/service/menu_service.py | 6 +-- backend/app/admin/service/role_service.py | 4 +- backend/app/admin/service/user_service.py | 41 +++++++++------ backend/app/generator/api/v1/gen.py | 46 ++++++++--------- backend/app/task/api/v1/task.py | 10 ++-- backend/common/exception/exception_handler.py | 14 +++--- backend/common/response/response_schema.py | 11 ++-- backend/common/security/jwt.py | 24 ++++----- backend/core/conf.py | 4 ++ backend/database/db_redis.py | 4 +- backend/middleware/jwt_auth_middleware.py | 30 ++++++++--- backend/pdm.lock | 43 ++++++++++++++-- backend/requirements.txt | 3 ++ backend/utils/build_tree.py | 21 ++++---- backend/utils/serializers.py | 7 +-- deploy/backend/gunicorn.conf.py | 17 ++++--- 40 files changed, 319 insertions(+), 263 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c70b66..d4eef333 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,18 +17,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 + - name: Setup pdm + uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} - cache: pip - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r ./backend/requirements.txt + cd backend + pdm install - - name: pre-commit - uses: pre-commit/action@v3.0.0 - with: - extra_args: --all-files --verbose + - name: Run lint + run: | + cd backend + pdm lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b6acf80..72c25511 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: check-toml - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.5.1 + rev: v0.5.7 hooks: - id: ruff args: @@ -20,7 +20,7 @@ repos: - id: ruff-format - repo: https://github.com/pdm-project/pdm - rev: 2.16.1 + rev: 2.18.0 hooks: - id: pdm-export args: diff --git a/backend/app/admin/api/v1/auth/auth.py b/backend/app/admin/api/v1/auth/auth.py index 59f2940b..e9c2a48d 100644 --- a/backend/app/admin/api/v1/auth/auth.py +++ b/backend/app/admin/api/v1/auth/auth.py @@ -30,16 +30,16 @@ async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetS ) async def user_login(request: Request, obj: AuthLoginParam, background_tasks: BackgroundTasks) -> ResponseModel: data = await auth_service.login(request=request, obj=obj, background_tasks=background_tasks) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post('/token/new', summary='创建新 token', dependencies=[DependsJwtAuth]) async def create_new_token(request: Request, refresh_token: Annotated[str, Query(...)]) -> ResponseModel: data = await auth_service.new_token(request=request, refresh_token=refresh_token) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post('/logout', summary='用户登出', dependencies=[DependsJwtAuth]) async def user_logout(request: Request) -> ResponseModel: await auth_service.logout(request=request) - return await response_base.success() + return response_base.success() diff --git a/backend/app/admin/api/v1/auth/captcha.py b/backend/app/admin/api/v1/auth/captcha.py index 9197a372..3ae7e9fd 100644 --- a/backend/app/admin/api/v1/auth/captcha.py +++ b/backend/app/admin/api/v1/auth/captcha.py @@ -27,4 +27,4 @@ async def get_captcha(request: Request) -> ResponseModel: await redis_client.set( f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=admin_settings.CAPTCHA_LOGIN_EXPIRE_SECONDS ) - return await response_base.success(data={'image_type': img_type, 'image': img}) + return response_base.success(data={'image_type': img_type, 'image': img}) diff --git a/backend/app/admin/api/v1/log/login_log.py b/backend/app/admin/api/v1/log/login_log.py index db5393d1..101dc41f 100644 --- a/backend/app/admin/api/v1/log/login_log.py +++ b/backend/app/admin/api/v1/log/login_log.py @@ -32,7 +32,7 @@ async def get_pagination_login_logs( ) -> ResponseModel: log_select = await login_log_service.get_select(username=username, status=status, ip=ip) page_data = await paging_data(db, log_select, GetLoginLogListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.delete( @@ -46,8 +46,8 @@ async def get_pagination_login_logs( async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await login_log_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -61,5 +61,5 @@ async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseMode async def delete_all_login_logs() -> ResponseModel: count = await login_log_service.delete_all() if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/log/opera_log.py b/backend/app/admin/api/v1/log/opera_log.py index 42b88058..db6edad5 100644 --- a/backend/app/admin/api/v1/log/opera_log.py +++ b/backend/app/admin/api/v1/log/opera_log.py @@ -32,7 +32,7 @@ async def get_pagination_opera_logs( ) -> ResponseModel: log_select = await opera_log_service.get_select(username=username, status=status, ip=ip) page_data = await paging_data(db, log_select, GetOperaLogListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.delete( @@ -46,8 +46,8 @@ async def get_pagination_opera_logs( async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await opera_log_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -61,5 +61,5 @@ async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseMode async def delete_all_opera_logs() -> ResponseModel: count = await opera_log_service.delete_all() if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/monitor/redis.py b/backend/app/admin/api/v1/monitor/redis.py index 5fb861e2..23a88588 100644 --- a/backend/app/admin/api/v1/monitor/redis.py +++ b/backend/app/admin/api/v1/monitor/redis.py @@ -20,4 +20,4 @@ router = APIRouter() ) async def get_redis_info() -> ResponseModel: data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()} - return await response_base.success(data=data) + return response_base.success(data=data) diff --git a/backend/app/admin/api/v1/monitor/server.py b/backend/app/admin/api/v1/monitor/server.py index e9f95345..334cb1f6 100644 --- a/backend/app/admin/api/v1/monitor/server.py +++ b/backend/app/admin/api/v1/monitor/server.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from fastapi import APIRouter, Depends -from starlette.concurrency import run_in_threadpool from backend.common.response.response_schema import ResponseModel, response_base from backend.common.security.jwt import DependsJwtAuth @@ -20,12 +19,11 @@ router = APIRouter() ], ) async def get_server_info() -> ResponseModel: - """IO密集型任务,使用线程池尽量减少性能损耗""" data = { - 'cpu': await run_in_threadpool(server_info.get_cpu_info), - 'mem': await run_in_threadpool(server_info.get_mem_info), - 'sys': await run_in_threadpool(server_info.get_sys_info), - 'disk': await run_in_threadpool(server_info.get_disk_info), - 'service': await run_in_threadpool(server_info.get_service_info), + 'cpu': server_info.get_cpu_info(), + 'mem': server_info.get_mem_info(), + 'sys': server_info.get_sys_info(), + 'disk': server_info.get_disk_info(), + 'service': server_info.get_service_info(), } - return await response_base.success(data=data) + return response_base.success(data=data) diff --git a/backend/app/admin/api/v1/oauth2/github.py b/backend/app/admin/api/v1/oauth2/github.py index 8b839375..96ef89a1 100644 --- a/backend/app/admin/api/v1/oauth2/github.py +++ b/backend/app/admin/api/v1/oauth2/github.py @@ -19,7 +19,7 @@ _github_oauth2 = FastAPIOAuth20(_github_client, admin_settings.OAUTH2_GITHUB_RED @router.get('', summary='获取 Github 授权链接') async def github_auth2() -> ResponseModel: auth_url = await _github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) - return await response_base.success(data=auth_url) + return response_base.success(data=auth_url) @router.get( diff --git a/backend/app/admin/api/v1/oauth2/linux_do.py b/backend/app/admin/api/v1/oauth2/linux_do.py index 105afdaf..8c72a00c 100644 --- a/backend/app/admin/api/v1/oauth2/linux_do.py +++ b/backend/app/admin/api/v1/oauth2/linux_do.py @@ -22,7 +22,7 @@ _linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_ @router.get('', summary='获取 Linux Do 授权链接') async def linux_do_auth2() -> ResponseModel: auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) - return await response_base.success(data=auth_url) + return response_base.success(data=auth_url) @router.get( diff --git a/backend/app/admin/api/v1/sys/api.py b/backend/app/admin/api/v1/sys/api.py index 556f49a1..a37faffc 100644 --- a/backend/app/admin/api/v1/sys/api.py +++ b/backend/app/admin/api/v1/sys/api.py @@ -19,13 +19,13 @@ router = APIRouter() @router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth]) async def get_all_apis() -> ResponseModel: data = await api_service.get_all() - return await response_base.success(data=data) + return response_base.success(data=data) @router.get('/{pk}', summary='获取接口详情', dependencies=[DependsJwtAuth]) async def get_api(pk: Annotated[int, Path(...)]) -> ResponseModel: api = await api_service.get(pk=pk) - return await response_base.success(data=api) + return response_base.success(data=api) @router.get( @@ -44,7 +44,7 @@ async def get_pagination_apis( ) -> ResponseModel: api_select = await api_service.get_select(name=name, method=method, path=path) page_data = await paging_data(db, api_select, GetApiListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.post( @@ -57,7 +57,7 @@ async def get_pagination_apis( ) async def create_api(obj: CreateApiParam) -> ResponseModel: await api_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -71,8 +71,8 @@ async def create_api(obj: CreateApiParam) -> ResponseModel: async def update_api(pk: Annotated[int, Path(...)], obj: UpdateApiParam) -> ResponseModel: count = await api_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -86,5 +86,5 @@ async def update_api(pk: Annotated[int, Path(...)], obj: UpdateApiParam) -> Resp async def delete_api(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await api_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/casbin.py b/backend/app/admin/api/v1/sys/casbin.py index 691335a7..cbbaa7a4 100644 --- a/backend/app/admin/api/v1/sys/casbin.py +++ b/backend/app/admin/api/v1/sys/casbin.py @@ -40,13 +40,13 @@ async def get_pagination_casbin( ) -> ResponseModel: casbin_select = await casbin_service.get_casbin_list(ptype=ptype, sub=sub) page_data = await paging_data(db, casbin_select, GetPolicyListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.get('/policies', summary='获取所有P权限策略', dependencies=[DependsJwtAuth]) async def get_all_policies(role: Annotated[int | None, Query(description='角色ID')] = None) -> ResponseModel: policies = await casbin_service.get_policy_list(role=role) - return await response_base.success(data=policies) + return response_base.success(data=policies) @router.post( @@ -68,7 +68,7 @@ async def create_policy(p: CreatePolicyParam) -> ResponseModel: **格式**: 用户 uuid + 访问路径 path + 访问方法 method """ data = await casbin_service.create_policy(p=p) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post( @@ -81,7 +81,7 @@ async def create_policy(p: CreatePolicyParam) -> ResponseModel: ) async def create_policies(ps: list[CreatePolicyParam]) -> ResponseModel: data = await casbin_service.create_policies(ps=ps) - return await response_base.success(data=data) + return response_base.success(data=data) @router.put( @@ -94,7 +94,7 @@ async def create_policies(ps: list[CreatePolicyParam]) -> ResponseModel: ) async def update_policy(old: UpdatePolicyParam, new: UpdatePolicyParam) -> ResponseModel: data = await casbin_service.update_policy(old=old, new=new) - return await response_base.success(data=data) + return response_base.success(data=data) @router.put( @@ -107,7 +107,7 @@ async def update_policy(old: UpdatePolicyParam, new: UpdatePolicyParam) -> Respo ) async def update_policies(old: list[UpdatePolicyParam], new: list[UpdatePolicyParam]) -> ResponseModel: data = await casbin_service.update_policies(old=old, new=new) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -120,7 +120,7 @@ async def update_policies(old: list[UpdatePolicyParam], new: list[UpdatePolicyPa ) async def delete_policy(p: DeletePolicyParam) -> ResponseModel: data = await casbin_service.delete_policy(p=p) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -133,7 +133,7 @@ async def delete_policy(p: DeletePolicyParam) -> ResponseModel: ) async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseModel: data = await casbin_service.delete_policies(ps=ps) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -147,14 +147,14 @@ async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseModel: async def delete_all_policies(sub: DeleteAllPoliciesParam) -> ResponseModel: count = await casbin_service.delete_all_policies(sub=sub) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.get('/groups', summary='获取所有G权限策略', dependencies=[DependsJwtAuth]) async def get_all_groups() -> ResponseModel: data = await casbin_service.get_group_list() - return await response_base.success(data=data) + return response_base.success(data=data) @router.post( @@ -176,7 +176,7 @@ async def create_group(g: CreateUserRoleParam) -> ResponseModel: 但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 策略所添加的访问权限 """ data = await casbin_service.create_group(g=g) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post( @@ -189,7 +189,7 @@ async def create_group(g: CreateUserRoleParam) -> ResponseModel: ) async def create_groups(gs: list[CreateUserRoleParam]) -> ResponseModel: data = await casbin_service.create_groups(gs=gs) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -202,7 +202,7 @@ async def create_groups(gs: list[CreateUserRoleParam]) -> ResponseModel: ) async def delete_group(g: DeleteUserRoleParam) -> ResponseModel: data = await casbin_service.delete_group(g=g) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -215,7 +215,7 @@ async def delete_group(g: DeleteUserRoleParam) -> ResponseModel: ) async def delete_groups(gs: list[DeleteUserRoleParam]) -> ResponseModel: data = await casbin_service.delete_groups(gs=gs) - return await response_base.success(data=data) + return response_base.success(data=data) @router.delete( @@ -229,5 +229,5 @@ async def delete_groups(gs: list[DeleteUserRoleParam]) -> ResponseModel: async def delete_all_groups(uuid: Annotated[UUID, Query(...)]) -> ResponseModel: count = await casbin_service.delete_all_groups(uuid=uuid) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/config.py b/backend/app/admin/api/v1/sys/config.py index cf3cf95f..a6c01c02 100644 --- a/backend/app/admin/api/v1/sys/config.py +++ b/backend/app/admin/api/v1/sys/config.py @@ -17,7 +17,7 @@ router = APIRouter() @router.get('', summary='获取系统配置详情', dependencies=[DependsJwtAuth]) async def get_config() -> ResponseModel: config = await config_service.get() - return await response_base.success(data=config) + return response_base.success(data=config) @router.post( @@ -30,7 +30,7 @@ async def get_config() -> ResponseModel: ) async def create_config(obj: CreateConfigParam) -> ResponseModel: await config_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -44,8 +44,8 @@ async def create_config(obj: CreateConfigParam) -> ResponseModel: async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel: count = await config_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -59,5 +59,5 @@ async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) - async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await config_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/dept.py b/backend/app/admin/api/v1/sys/dept.py index 614f6152..da65e981 100644 --- a/backend/app/admin/api/v1/sys/dept.py +++ b/backend/app/admin/api/v1/sys/dept.py @@ -18,8 +18,8 @@ router = APIRouter() @router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth]) async def get_dept(pk: Annotated[int, Path(...)]) -> ResponseModel: dept = await dept_service.get(pk=pk) - data = GetDeptListDetails(**await select_as_dict(dept)) - return await response_base.success(data=data) + data = GetDeptListDetails(**select_as_dict(dept)) + return response_base.success(data=data) @router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth]) @@ -30,7 +30,7 @@ async def get_all_depts_tree( status: Annotated[int | None, Query()] = None, ) -> ResponseModel: dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status) - return await response_base.success(data=dept) + return response_base.success(data=dept) @router.post( @@ -43,7 +43,7 @@ async def get_all_depts_tree( ) async def create_dept(obj: CreateDeptParam) -> ResponseModel: await dept_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -57,8 +57,8 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel: async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> ResponseModel: count = await dept_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -72,5 +72,5 @@ async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> Re async def delete_dept(pk: Annotated[int, Path(...)]) -> ResponseModel: count = await dept_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/dict_data.py b/backend/app/admin/api/v1/sys/dict_data.py index 0e00e48a..0de8b037 100644 --- a/backend/app/admin/api/v1/sys/dict_data.py +++ b/backend/app/admin/api/v1/sys/dict_data.py @@ -20,8 +20,8 @@ router = APIRouter() @router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth]) async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseModel: dict_data = await dict_data_service.get(pk=pk) - data = GetDictDataListDetails(**await select_as_dict(dict_data)) - return await response_base.success(data=data) + data = GetDictDataListDetails(**select_as_dict(dict_data)) + return response_base.success(data=data) @router.get( @@ -40,7 +40,7 @@ async def get_pagination_dict_datas( ) -> ResponseModel: dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status) page_data = await paging_data(db, dict_data_select, GetDictDataListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.post( @@ -53,7 +53,7 @@ async def get_pagination_dict_datas( ) async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel: await dict_data_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -67,8 +67,8 @@ async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel: async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataParam) -> ResponseModel: count = await dict_data_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -82,5 +82,5 @@ async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataPar async def delete_dict_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await dict_data_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/dict_type.py b/backend/app/admin/api/v1/sys/dict_type.py index 5ddc6662..171a9a8c 100644 --- a/backend/app/admin/api/v1/sys/dict_type.py +++ b/backend/app/admin/api/v1/sys/dict_type.py @@ -32,7 +32,7 @@ async def get_pagination_dict_types( ) -> ResponseModel: dict_type_select = await dict_type_service.get_select(name=name, code=code, status=status) page_data = await paging_data(db, dict_type_select, GetDictTypeListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.post( @@ -45,7 +45,7 @@ async def get_pagination_dict_types( ) async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel: await dict_type_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -59,8 +59,8 @@ async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel: async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypeParam) -> ResponseModel: count = await dict_type_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -74,5 +74,5 @@ async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypePar async def delete_dict_type(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await dict_type_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/menu.py b/backend/app/admin/api/v1/sys/menu.py index dbb7a91b..cb6d2716 100644 --- a/backend/app/admin/api/v1/sys/menu.py +++ b/backend/app/admin/api/v1/sys/menu.py @@ -18,14 +18,14 @@ router = APIRouter() @router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth]) async def get_user_sidebar_tree(request: Request) -> ResponseModel: menu = await menu_service.get_user_menu_tree(request=request) - return await response_base.success(data=menu) + return response_base.success(data=menu) @router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth]) async def get_menu(pk: Annotated[int, Path(...)]) -> ResponseModel: menu = await menu_service.get(pk=pk) - data = GetMenuListDetails(**await select_as_dict(menu)) - return await response_base.success(data=data) + data = GetMenuListDetails(**select_as_dict(menu)) + return response_base.success(data=data) @router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth]) @@ -33,7 +33,7 @@ async def get_all_menus( title: Annotated[str | None, Query()] = None, status: Annotated[int | None, Query()] = None ) -> ResponseModel: menu = await menu_service.get_menu_tree(title=title, status=status) - return await response_base.success(data=menu) + return response_base.success(data=menu) @router.post( @@ -46,7 +46,7 @@ async def get_all_menus( ) async def create_menu(obj: CreateMenuParam) -> ResponseModel: await menu_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -60,8 +60,8 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel: async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> ResponseModel: count = await menu_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -75,5 +75,5 @@ async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> Re async def delete_menu(pk: Annotated[int, Path(...)]) -> ResponseModel: count = await menu_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/role.py b/backend/app/admin/api/v1/sys/role.py index f7c73668..e08d4edf 100644 --- a/backend/app/admin/api/v1/sys/role.py +++ b/backend/app/admin/api/v1/sys/role.py @@ -21,28 +21,28 @@ router = APIRouter() @router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth]) async def get_all_roles() -> ResponseModel: roles = await role_service.get_all() - data = await select_list_serialize(roles) - return await response_base.success(data=data) + data = select_list_serialize(roles) + return response_base.success(data=data) @router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth]) async def get_user_all_roles(pk: Annotated[int, Path(...)]) -> ResponseModel: roles = await role_service.get_user_roles(pk=pk) - data = await select_list_serialize(roles) - return await response_base.success(data=data) + data = select_list_serialize(roles) + return response_base.success(data=data) @router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth]) async def get_role_all_menus(pk: Annotated[int, Path(...)]) -> ResponseModel: menu = await menu_service.get_role_menu_tree(pk=pk) - return await response_base.success(data=menu) + return response_base.success(data=menu) @router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth]) async def get_role(pk: Annotated[int, Path(...)]) -> ResponseModel: role = await role_service.get(pk=pk) - data = GetRoleListDetails(**await select_as_dict(role)) - return await response_base.success(data=data) + data = GetRoleListDetails(**select_as_dict(role)) + return response_base.success(data=data) @router.get( @@ -61,7 +61,7 @@ async def get_pagination_roles( ) -> ResponseModel: role_select = await role_service.get_select(name=name, data_scope=data_scope, status=status) page_data = await paging_data(db, role_select, GetRoleListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.post( @@ -74,7 +74,7 @@ async def get_pagination_roles( ) async def create_role(obj: CreateRoleParam) -> ResponseModel: await role_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -88,8 +88,8 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel: async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> ResponseModel: count = await role_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.put( @@ -105,8 +105,8 @@ async def update_role_menus( ) -> ResponseModel: count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -120,5 +120,5 @@ async def update_role_menus( async def delete_role(pk: Annotated[list[int], Query(...)]) -> ResponseModel: count = await role_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/api/v1/sys/user.py b/backend/app/admin/api/v1/sys/user.py index a8a6d56d..f755ae75 100644 --- a/backend/app/admin/api/v1/sys/user.py +++ b/backend/app/admin/api/v1/sys/user.py @@ -29,44 +29,44 @@ router = APIRouter() @router.post('/register', summary='注册用户') async def register_user(obj: RegisterUserParam) -> ResponseModel: await user_service.register(obj=obj) - return await response_base.success() + return response_base.success() @router.post('/add', summary='添加用户', dependencies=[DependsRBAC]) async def add_user(request: Request, obj: AddUserParam) -> ResponseModel: await user_service.add(request=request, obj=obj) current_user = await user_service.get_userinfo(username=obj.username) - data = GetUserInfoListDetails(**await select_as_dict(current_user)) - return await response_base.success(data=data) + data = GetUserInfoListDetails(**select_as_dict(current_user)) + return response_base.success(data=data) @router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth]) async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel: count = await user_service.pwd_reset(request=request, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'}) async def get_current_user(request: Request) -> ResponseModel: - data = GetCurrentUserInfoDetail(**await select_as_dict(request.user)) - return await response_base.success(data=data) + data = GetCurrentUserInfoDetail(**request.user.model_dump()) + return response_base.success(data=data) @router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth]) async def get_user(username: Annotated[str, Path(...)]) -> ResponseModel: current_user = await user_service.get_userinfo(username=username) - data = GetUserInfoListDetails(**await select_as_dict(current_user)) - return await response_base.success(data=data) + data = GetUserInfoListDetails(**select_as_dict(current_user)) + return response_base.success(data=data) @router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth]) async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel: count = await user_service.update(request=request, username=username, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.put( @@ -81,15 +81,15 @@ async def update_user_role( request: Request, username: Annotated[str, Path(...)], obj: UpdateUserRoleParam ) -> ResponseModel: await user_service.update_roles(request=request, username=username, obj=obj) - return await response_base.success() + return response_base.success() @router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth]) async def update_avatar(request: Request, username: Annotated[str, Path(...)], avatar: AvatarParam) -> ResponseModel: count = await user_service.update_avatar(request=request, username=username, avatar=avatar) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.get( @@ -109,39 +109,39 @@ async def get_pagination_users( ): user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status) page_data = await paging_data(db, user_select, GetUserInfoListDetails) - return await response_base.success(data=page_data) + return response_base.success(data=page_data) @router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC]) async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel: count = await user_service.update_permission(request=request, pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC]) async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel: count = await user_service.update_staff(request=request, pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC]) async def status_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel: count = await user_service.update_status(request=request, pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.put('/{pk}/multi', summary='修改用户多点登录状态', dependencies=[DependsRBAC]) async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel: count = await user_service.update_multi_login(request=request, pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -156,5 +156,5 @@ async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> Response async def delete_user(username: Annotated[str, Path(...)]) -> ResponseModel: count = await user_service.delete(username=username) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index b53961a0..2b6eb95d 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -74,7 +74,7 @@ class CRUDUser(CRUDPlus[User]): """ if not social: salt = text_captcha(5) - obj.password = await get_hash_password(f'{obj.password}{salt}') + obj.password = get_hash_password(f'{obj.password}{salt}') dict_obj = obj.model_dump() dict_obj.update({'is_staff': True, 'salt': salt}) else: @@ -92,7 +92,7 @@ class CRUDUser(CRUDPlus[User]): :return: """ salt = text_captcha(5) - obj.password = await get_hash_password(f'{obj.password}{salt}') + obj.password = get_hash_password(f'{obj.password}{salt}') dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) @@ -225,7 +225,7 @@ class CRUDUser(CRUDPlus[User]): user = await self.get(db, user_id) return user.is_staff - async def get_status(self, db: AsyncSession, user_id: int) -> bool: + async def get_status(self, db: AsyncSession, user_id: int) -> int: """ 获取用户状态 diff --git a/backend/app/admin/schema/user.py b/backend/app/admin/schema/user.py index 81f45200..2b3b84a6 100644 --- a/backend/app/admin/schema/user.py +++ b/backend/app/admin/schema/user.py @@ -91,6 +91,10 @@ class GetCurrentUserInfoDetail(GetUserInfoListDetails): return values +class CurrentUserIns(GetUserInfoListDetails): + model_config = ConfigDict(from_attributes=True) + + class ResetPasswordParam(SchemaBase): old_password: str new_password: str diff --git a/backend/app/admin/service/auth_service.py b/backend/app/admin/service/auth_service.py index 4e1870fa..4a430978 100644 --- a/backend/app/admin/service/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -34,7 +34,7 @@ class AuthService: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: raise errors.NotFoundError(msg='用户名或密码有误') - elif not await password_verify(f'{obj.password}{current_user.salt}', current_user.password): + elif not password_verify(f'{obj.password}{current_user.salt}', current_user.password): raise errors.AuthorizationError(msg='用户名或密码有误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') @@ -49,7 +49,7 @@ class AuthService: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: raise errors.NotFoundError(msg='用户名或密码有误') - elif not await password_verify(obj.password + current_user.salt, current_user.password): + elif not password_verify(obj.password + current_user.salt, current_user.password): raise errors.AuthorizationError(msg='用户名或密码有误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') @@ -105,7 +105,7 @@ class AuthService: @staticmethod async def new_token(*, request: Request, refresh_token: str) -> GetNewToken: - user_id = await jwt_decode(refresh_token) + user_id = jwt_decode(refresh_token) if request.user.id != user_id: raise errors.TokenError(msg='Refresh Token 无效') async with async_db_session() as db: @@ -136,8 +136,8 @@ class AuthService: key = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token}' await redis_client.delete(key) else: - prefix = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:' - await redis_client.delete_prefix(prefix) + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:' + await redis_client.delete_prefix(key_prefix) auth_service = AuthService() diff --git a/backend/app/admin/service/config_service.py b/backend/app/admin/service/config_service.py index 070483c8..192247d5 100644 --- a/backend/app/admin/service/config_service.py +++ b/backend/app/admin/service/config_service.py @@ -19,7 +19,7 @@ class ConfigService: config = await config_dao.get_one(db) if not config: raise errors.NotFoundError(msg='系统配置不存在') - data_map = await select_as_dict(config) + data_map = select_as_dict(config) del data_map['created_time'] del data_map['updated_time'] await redis_client.hset(admin_settings.CONFIG_REDIS_KEY, mapping=data_map) diff --git a/backend/app/admin/service/dept_service.py b/backend/app/admin/service/dept_service.py index 97bb1506..8ba644f3 100644 --- a/backend/app/admin/service/dept_service.py +++ b/backend/app/admin/service/dept_service.py @@ -25,7 +25,7 @@ class DeptService: ) -> list[dict[str, Any]]: async with async_db_session() as db: dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status) - tree_data = await get_tree_data(dept_select) + tree_data = get_tree_data(dept_select) return tree_data @staticmethod diff --git a/backend/app/admin/service/menu_service.py b/backend/app/admin/service/menu_service.py index 01f583d3..b3b54675 100644 --- a/backend/app/admin/service/menu_service.py +++ b/backend/app/admin/service/menu_service.py @@ -28,7 +28,7 @@ class MenuService: async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]: async with async_db_session() as db: menu_select = await menu_dao.get_all(db, title=title, status=status) - menu_tree = await get_tree_data(menu_select) + menu_tree = get_tree_data(menu_select) return menu_tree @staticmethod @@ -39,7 +39,7 @@ class MenuService: raise errors.NotFoundError(msg='角色不存在') menu_ids = [menu.id for menu in role.menus] menu_select = await menu_dao.get_role_menus(db, False, menu_ids) - menu_tree = await get_tree_data(menu_select) + menu_tree = get_tree_data(menu_select) return menu_tree @staticmethod @@ -52,7 +52,7 @@ class MenuService: for role in roles: menu_ids.extend([menu.id for menu in role.menus]) menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids) - menu_tree = await get_tree_data(menu_select) + menu_tree = get_tree_data(menu_select) return menu_tree @staticmethod diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index d95e10b8..45eefeb0 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -72,7 +72,9 @@ class RoleService: if not menu: raise errors.NotFoundError(msg='菜单不存在') count = await role_dao.update_menus(db, pk, menu_ids) - await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') + if pk in [role.id for role in request.user.roles]: + await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index 05ed7b7b..afec92c1 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -70,20 +70,22 @@ class UserService: @staticmethod async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int: async with async_db_session.begin() as db: - if not await password_verify(f'{obj.old_password}{request.user.salt}', request.user.password): + user = await user_dao.get(db, request.user.id) + if not password_verify(f'{obj.old_password}{user.salt}', user.password): raise errors.ForbiddenError(msg='原密码错误') np1 = obj.new_password np2 = obj.confirm_password if np1 != np2: raise errors.ForbiddenError(msg='密码输入不一致') - new_pwd = await get_hash_password(f'{obj.new_password}{request.user.salt}') + new_pwd = get_hash_password(f'{obj.new_password}{user.salt}') count = await user_dao.reset_password(db, request.user.id, new_pwd) - prefix = [ - f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:', - f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:', + key_prefix = [ + f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}', + f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}', + f'{settings.USER_REDIS_PREFIX}:{request.user.id}', ] - for i in prefix: - await redis_client.delete_prefix(i) + for key in key_prefix: + await redis_client.delete_prefix(key) return count @staticmethod @@ -116,6 +118,7 @@ class UserService: if email: raise errors.ForbiddenError(msg='邮箱已注册') count = await user_dao.update_userinfo(db, input_user, obj) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -133,6 +136,7 @@ class UserService: raise errors.NotFoundError(msg='角色不存在') await user_dao.update_role(db, input_user, obj) await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') @staticmethod async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int: @@ -144,6 +148,7 @@ class UserService: if not input_user: raise errors.NotFoundError(msg='用户不存在') count = await user_dao.update_avatar(db, input_user, avatar) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -162,6 +167,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') super_status = await user_dao.get_super(db, user_id) count = await user_dao.set_super(db, pk, False if super_status else True) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -176,6 +182,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') staff_status = await user_dao.get_staff(db, user_id) count = await user_dao.set_staff(db, pk, False if staff_status else True) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -190,6 +197,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') status = await user_dao.get_status(db, user_id) count = await user_dao.set_status(db, pk, False if status else True) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -200,6 +208,7 @@ class UserService: raise errors.NotFoundError(msg='用户不存在') else: count = await user_dao.set_multi_login(db, pk) + await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') token = await get_token(request) user_id = request.user.id latest_multi_login = await user_dao.get_multi_login(db, pk) @@ -207,13 +216,13 @@ class UserService: # 当前用户修改自身时(普通/超级),除当前token外,其他token失效 if pk == user_id: if not latest_multi_login: - prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}:' - await redis_client.delete_prefix(prefix, exclude=prefix + token) + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}' + await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token}') # 超级用户修改他人时,他人token将全部失效 else: if not latest_multi_login: - prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}:' - await redis_client.delete_prefix(prefix) + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}' + await redis_client.delete_prefix(key_prefix) return count @staticmethod @@ -223,12 +232,12 @@ class UserService: if not input_user: raise errors.NotFoundError(msg='用户不存在') count = await user_dao.delete(db, input_user.id) - prefix = [ - f'{settings.TOKEN_REDIS_PREFIX}:{input_user.id}:', - f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{input_user.id}:', + key_prefix = [ + f'{settings.TOKEN_REDIS_PREFIX}:{input_user.id}', + f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{input_user.id}', ] - for i in prefix: - await redis_client.delete_prefix(i) + for key in key_prefix: + await redis_client.delete_prefix(key) return count diff --git a/backend/app/generator/api/v1/gen.py b/backend/app/generator/api/v1/gen.py index ebad100e..e43212a9 100644 --- a/backend/app/generator/api/v1/gen.py +++ b/backend/app/generator/api/v1/gen.py @@ -27,22 +27,22 @@ router = APIRouter() @router.get('/businesses/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth]) async def get_all_businesses() -> ResponseModel: businesses = await gen_business_service.get_all() - data = await select_list_serialize(businesses) - return await response_base.success(data=data) + data = select_list_serialize(businesses) + return response_base.success(data=data) @router.get('/businesses/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth]) async def get_business(pk: Annotated[int, Path(...)]) -> ResponseModel: business = await gen_service.get_business_with_model(pk=pk) - data = GetGenBusinessListDetails(**await select_as_dict(business)) - return await response_base.success(data=data) + data = GetGenBusinessListDetails(**select_as_dict(business)) + return response_base.success(data=data) @router.get('/businesses/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth]) async def get_business_models(pk: Annotated[int, Path(...)]) -> ResponseModel: models = await gen_model_service.get_by_business(business_id=pk) - data = await select_list_serialize(models) - return await response_base.success(data=data) + data = select_list_serialize(models) + return response_base.success(data=data) @router.post( @@ -56,7 +56,7 @@ async def get_business_models(pk: Annotated[int, Path(...)]) -> ResponseModel: ) async def create_business(obj: CreateGenBusinessParam) -> ResponseModel: await gen_business_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -70,8 +70,8 @@ async def create_business(obj: CreateGenBusinessParam) -> ResponseModel: async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessParam) -> ResponseModel: count = await gen_business_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -85,15 +85,15 @@ async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessP async def delete_business(pk: Annotated[int, Path(...)]) -> ResponseModel: count = await gen_business_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.get('/models/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth]) async def get_model(pk: Annotated[int, Path(...)]) -> ResponseModel: model = await gen_model_service.get(pk=pk) - data = GetGenModelListDetails(**await select_as_dict(model)) - return await response_base.success(data=data) + data = GetGenModelListDetails(**select_as_dict(model)) + return response_base.success(data=data) @router.post( @@ -106,7 +106,7 @@ async def get_model(pk: Annotated[int, Path(...)]) -> ResponseModel: ) async def create_model(obj: CreateGenModelParam) -> ResponseModel: await gen_model_service.create(obj=obj) - return await response_base.success() + return response_base.success() @router.put( @@ -120,8 +120,8 @@ async def create_model(obj: CreateGenModelParam) -> ResponseModel: async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) -> ResponseModel: count = await gen_model_service.update(pk=pk, obj=obj) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.delete( @@ -135,14 +135,14 @@ async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel: count = await gen_model_service.delete(pk=pk) if count > 0: - return await response_base.success() - return await response_base.fail() + return response_base.success() + return response_base.fail() @router.get('/tables', summary='获取数据库表', dependencies=[DependsRBAC]) async def get_all_tables(table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba') -> ResponseModel: data = await gen_service.get_tables(table_schema=table_schema) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post( @@ -159,19 +159,19 @@ async def import_table( table_schema: Annotated[str, Body(..., description='数据库名')] = 'fba', ) -> ResponseModel: await gen_service.import_business_and_model(app=app, table_schema=table_schema, table_name=table_name) - return await response_base.success() + return response_base.success() @router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth]) async def preview_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel: data = await gen_service.preview(pk=pk) - return await response_base.success(data=data) + return response_base.success(data=data) @router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth]) async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]): data = await gen_service.get_generate_path(pk=pk) - return await response_base.success(data=data) + return response_base.success(data=data) @router.post( @@ -185,7 +185,7 @@ async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]): ) async def generate_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel: await gen_service.generate(pk=pk) - return await response_base.success() + return response_base.success() @router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth]) diff --git a/backend/app/task/api/v1/task.py b/backend/app/task/api/v1/task.py index 91873336..8eceed67 100644 --- a/backend/app/task/api/v1/task.py +++ b/backend/app/task/api/v1/task.py @@ -16,25 +16,25 @@ router = APIRouter() @router.get('', summary='获取所有可执行任务模块', dependencies=[DependsJwtAuth]) async def get_all_tasks() -> ResponseModel: tasks = task_service.get_list() - return await response_base.success(data=tasks) + return response_base.success(data=tasks) @router.get('/current', summary='获取当前正在执行的任务', dependencies=[DependsJwtAuth]) async def get_current_task() -> ResponseModel: task = task_service.get() - return await response_base.success(data=task) + return response_base.success(data=task) @router.get('/{uid}/status', summary='获取任务状态', dependencies=[DependsJwtAuth]) async def get_task_status(uid: Annotated[str, Path(description='任务ID')]) -> ResponseModel: status = task_service.get_status(uid) - return await response_base.success(data=status) + return response_base.success(data=status) @router.get('/{uid}', summary='获取任务结果', dependencies=[DependsJwtAuth]) async def get_task_result(uid: Annotated[str, Path(description='任务ID')]) -> ResponseModel: task = task_service.get_result(uid) - return await response_base.success(data=task) + return response_base.success(data=task) @router.post( @@ -51,4 +51,4 @@ async def run_task( kwargs: Annotated[dict | None, Body(description='任务函数关键字参数')] = None, ) -> ResponseModel: task = task_service.run(name=name, args=args, kwargs=kwargs) - return await response_base.success(data=task) + return response_base.success(data=task) diff --git a/backend/common/exception/exception_handler.py b/backend/common/exception/exception_handler.py index 2dd2ef55..97134431 100644 --- a/backend/common/exception/exception_handler.py +++ b/backend/common/exception/exception_handler.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from asgiref.sync import sync_to_async from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from pydantic import ValidationError @@ -21,7 +20,6 @@ from backend.core.conf import settings from backend.utils.serializers import MsgSpecJSONResponse -@sync_to_async def _get_exception_code(status_code: int): """ 获取返回状态码, OpenAPI, Uvicorn... 可用状态码基于 RFC 定义, 详细代码见下方链接 @@ -101,11 +99,11 @@ def register_exception(app: FastAPI): 'data': None, } else: - res = await response_base.fail(res=CustomResponseCode.HTTP_400) + res = response_base.fail(res=CustomResponseCode.HTTP_400) content = res.model_dump() request.state.__request_http_exception__ = content # 用于在中间件中获取异常信息 return MsgSpecJSONResponse( - status_code=await _get_exception_code(exc.status_code), + status_code=_get_exception_code(exc.status_code), content=content, headers=exc.headers, ) @@ -166,7 +164,7 @@ def register_exception(app: FastAPI): 'data': None, } else: - res = await response_base.fail(res=CustomResponseCode.HTTP_500) + res = response_base.fail(res=CustomResponseCode.HTTP_500) content = res.model_dump() return MsgSpecJSONResponse( status_code=StandardResponseCode.HTTP_500, @@ -184,7 +182,7 @@ def register_exception(app: FastAPI): """ if isinstance(exc, BaseExceptionMixin): return MsgSpecJSONResponse( - status_code=await _get_exception_code(exc.code), + status_code=_get_exception_code(exc.code), content={ 'code': exc.code, 'msg': str(exc.msg), @@ -204,7 +202,7 @@ def register_exception(app: FastAPI): 'data': None, } else: - res = await response_base.fail(res=CustomResponseCode.HTTP_500) + res = response_base.fail(res=CustomResponseCode.HTTP_500) content = res.model_dump() return MsgSpecJSONResponse(status_code=StandardResponseCode.HTTP_500, content=content) @@ -235,7 +233,7 @@ def register_exception(app: FastAPI): 'data': None, } else: - res = await response_base.fail(res=CustomResponseCode.HTTP_500) + res = response_base.fail(res=CustomResponseCode.HTTP_500) content = res.model_dump() response = MsgSpecJSONResponse( status_code=exc.code if isinstance(exc, BaseExceptionMixin) else StandardResponseCode.HTTP_500, diff --git a/backend/common/response/response_schema.py b/backend/common/response/response_schema.py index 683f0efb..8346be2a 100644 --- a/backend/common/response/response_schema.py +++ b/backend/common/response/response_schema.py @@ -3,7 +3,6 @@ from datetime import datetime from typing import Any -from asgiref.sync import sync_to_async from fastapi import Response from pydantic import BaseModel, ConfigDict @@ -62,7 +61,6 @@ class ResponseBase: """ @staticmethod - @sync_to_async def __response(*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None) -> ResponseModel: """ 请求成功返回通用方法 @@ -73,24 +71,23 @@ class ResponseBase: """ return ResponseModel(code=res.code, msg=res.msg, data=data) - async def success( + def success( self, *, res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200, data: Any | None = None, ) -> ResponseModel: - return await self.__response(res=res, data=data) + return self.__response(res=res, data=data) - async def fail( + def fail( self, *, res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_400, data: Any = None, ) -> ResponseModel: - return await self.__response(res=res, data=data) + return self.__response(res=res, data=data) @staticmethod - @sync_to_async def fast_success( *, res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200, diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index ea1a457f..9c2075b3 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -24,7 +24,6 @@ pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') DependsJwtAuth = Depends(HTTPBearer()) -@sync_to_async def get_hash_password(password: str) -> str: """ Encrypt passwords using the hash algorithm @@ -35,7 +34,6 @@ def get_hash_password(password: str) -> str: return pwd_context.hash(password) -@sync_to_async def password_verify(plain_password: str, hashed_password: str) -> bool: """ Password verification @@ -65,8 +63,8 @@ async def create_access_token(sub: str, expires_delta: timedelta | None = None, to_encode = {'exp': expire, 'sub': sub, **kwargs} token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM) if multi_login is False: - prefix = f'{settings.TOKEN_REDIS_PREFIX}:{sub}:' - await redis_client.delete_prefix(prefix) + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{sub}' + await redis_client.delete_prefix(key_prefix) key = f'{settings.TOKEN_REDIS_PREFIX}:{sub}:{token}' await redis_client.setex(key, expire_seconds, token) return token, expire @@ -94,8 +92,8 @@ async def create_refresh_token(sub: str, expire_time: datetime | None = None, ** to_encode = {'exp': expire, 'sub': sub, **kwargs} refresh_token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM) if multi_login is False: - prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}:' - await redis_client.delete_prefix(prefix) + key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}' + await redis_client.delete_prefix(key_prefix) key = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}:{refresh_token}' await redis_client.setex(key, expire_seconds, refresh_token) return refresh_token, expire @@ -141,7 +139,6 @@ def get_token(request: Request) -> str: return token -@sync_to_async def jwt_decode(token: str) -> int: """ Decode token @@ -161,33 +158,32 @@ def jwt_decode(token: str) -> int: return user_id -async def jwt_authentication(token: str) -> dict[str, int]: +async def jwt_authentication(token: str) -> int: """ JWT authentication :param token: :return: """ - user_id = await jwt_decode(token) + user_id = jwt_decode(token) key = f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token}' token_verify = await redis_client.get(key) if not token_verify: raise TokenError(msg='Token 已过期') - return {'sub': user_id} + return user_id -async def get_current_user(db: AsyncSession, data: dict) -> User: +async def get_current_user(db: AsyncSession, pk: int) -> User: """ Get the current user through token :param db: - :param data: + :param pk: :return: """ - user_id = data.get('sub') from backend.app.admin.crud.crud_user import user_dao - user = await user_dao.get_with_relation(db, user_id=user_id) + user = await user_dao.get_with_relation(db, user_id=pk) if not user: raise TokenError(msg='Token 无效') if not user.status: diff --git a/backend/core/conf.py b/backend/core/conf.py index 038ff2a1..fa44fb0b 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -91,6 +91,10 @@ class Settings(BaseSettings): f'{API_V1_STR}/auth/login', ] + # Sys User + USER_REDIS_PREFIX: str = 'fba_user' + USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 + # Log LOG_LEVEL: str = 'INFO' LOG_FORMAT: str = '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}' diff --git a/backend/database/db_redis.py b/backend/database/db_redis.py index ac134aee..584aabc5 100644 --- a/backend/database/db_redis.py +++ b/backend/database/db_redis.py @@ -56,8 +56,8 @@ class RedisCli(Redis): keys.append(key) else: keys.append(key) - for key in keys: - await self.delete(key) + if keys: + await self.delete(*keys) # 创建 redis 客户端实例 diff --git a/backend/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py index 849452f4..d05d5a16 100644 --- a/backend/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -3,15 +3,19 @@ from typing import Any from fastapi import Request, Response +from fastapi.security.utils import get_authorization_scheme_param +from pydantic_core import from_json from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError from starlette.requests import HTTPConnection +from backend.app.admin.schema.user import CurrentUserIns from backend.common.exception.errors import TokenError from backend.common.log import log from backend.common.security import jwt from backend.core.conf import settings from backend.database.db_mysql import async_db_session -from backend.utils.serializers import MsgSpecJSONResponse +from backend.database.db_redis import redis_client +from backend.utils.serializers import MsgSpecJSONResponse, select_as_dict class _AuthenticationError(AuthenticationError): @@ -31,22 +35,34 @@ class JwtAuthMiddleware(AuthenticationBackend): """覆盖内部认证错误处理""" return MsgSpecJSONResponse(content={'code': exc.code, 'msg': exc.msg, 'data': None}, status_code=exc.code) - async def authenticate(self, request: Request): - auth = request.headers.get('Authorization') - if not auth: + async def authenticate(self, request: Request) -> tuple[AuthCredentials, CurrentUserIns] | None: + token = request.headers.get('Authorization') + if not token: return if request.url.path in settings.TOKEN_EXCLUDE: return - scheme, token = auth.split() + scheme, token = get_authorization_scheme_param(token) if scheme.lower() != 'bearer': return try: sub = await jwt.jwt_authentication(token) - async with async_db_session() as db: - user = await jwt.get_current_user(db, data=sub) + cache_user = await redis_client.get(f'{settings.USER_REDIS_PREFIX}:{sub}') + if not cache_user: + async with async_db_session() as db: + current_user = await jwt.get_current_user(db, sub) + user = CurrentUserIns(**select_as_dict(current_user)) + await redis_client.setex( + f'{settings.USER_REDIS_PREFIX}:{sub}', + settings.USER_REDIS_EXPIRE_SECONDS, + user.model_dump_json(), + ) + else: + # TODO: 在恰当的时机,应替换为使用 model_validate_json + # https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing + user = CurrentUserIns.model_validate(from_json(cache_user, allow_partial=True)) except TokenError as exc: raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers) except Exception as e: diff --git a/backend/pdm.lock b/backend/pdm.lock index e4d6a686..0eaf16d8 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -2,10 +2,13 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "lint", "deploy"] -strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.1" -content_hash = "sha256:f47f1c2393a99ade8265506d6df9f4ff8acac1ecd6a19a05afd4755e346dde98" +groups = ["default", "deploy", "lint"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:0686efb72a0c8cb2c95ab137a86b5d7cf1bc9d34d562eb01797b049ca466d352" + +[[metadata.targets]] +requires_python = ">=3.10,<3.13" [[package]] name = "aiofiles" @@ -27,6 +30,8 @@ groups = ["default"] dependencies = [ "Mako", "SQLAlchemy>=1.3.0", + "importlib-metadata; python_version < \"3.9\"", + "importlib-resources; python_version < \"3.9\"", "typing-extensions>=4", ] files = [ @@ -54,6 +59,9 @@ version = "0.6.0" requires_python = ">=3.8" summary = "Reusable constraint types to use with typing.Annotated" groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] files = [ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, @@ -97,6 +105,9 @@ requires_python = ">=3.7" summary = "Timeout context manager for asyncio programs" groups = ["default"] marker = "python_full_version <= \"3.11.2\"" +dependencies = [ + "typing-extensions>=3.6.5; python_version < \"3.8\"", +] files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -148,6 +159,9 @@ version = "23.2.0" requires_python = ">=3.7" summary = "Classes Without Boilerplate" groups = ["default"] +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, @@ -230,11 +244,13 @@ requires_python = ">=3.8" summary = "Distributed Task Queue." groups = ["default"] dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", "billiard<5.0,>=4.2.0", "click-didyoumean>=0.3.0", "click-plugins>=1.1.1", "click-repl>=0.2.0", "click<9.0,>=8.1.2", + "importlib-metadata>=3.6; python_version < \"3.8\"", "kombu<6.0,>=5.3.4", "python-dateutil>=2.8.2", "tzdata>=2022.7", @@ -334,6 +350,7 @@ summary = "Composable command line interface toolkit" groups = ["default", "deploy"] dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, @@ -691,6 +708,7 @@ requires_python = ">=3.5" summary = "WSGI HTTP Server for UNIX" groups = ["default"] dependencies = [ + "importlib-metadata; python_version < \"3.8\"", "packaging", ] files = [ @@ -704,6 +722,9 @@ version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["default"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -925,6 +946,8 @@ summary = "Messaging library for Python." groups = ["default"] dependencies = [ "amqp<6.0.0,>=5.1.1", + "backports-zoneinfo[tzdata]>=0.2.1; python_version < \"3.9\"", + "typing-extensions; python_version < \"3.10\"", "vine", ] files = [ @@ -939,6 +962,7 @@ requires_python = ">=3.5" summary = "Python logging made (stupidly) simple" groups = ["default"] dependencies = [ + "aiocontextvars>=0.2.0; python_version < \"3.7\"", "colorama>=0.3.4; sys_platform == \"win32\"", "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] @@ -1330,6 +1354,7 @@ groups = ["default"] dependencies = [ "annotated-types>=0.4.0", "pydantic-core==2.20.1", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", "typing-extensions>=4.6.1; python_version < \"3.13\"", ] files = [ @@ -1464,6 +1489,7 @@ dependencies = [ "attrs>=19.2.0", "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", "iniconfig", "packaging", "pluggy<2.0,>=0.12", @@ -1591,6 +1617,8 @@ summary = "Python client for Redis database and key-value store" groups = ["default"] dependencies = [ "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", + "importlib-metadata>=1.0; python_version < \"3.8\"", + "typing-extensions; python_version < \"3.8\"", ] files = [ {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, @@ -1622,6 +1650,7 @@ groups = ["default"] dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", ] files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, @@ -1731,6 +1760,7 @@ summary = "Database Abstraction Library" groups = ["default"] dependencies = [ "greenlet!=0.4.17; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", + "importlib-metadata; python_version < \"3.8\"", "typing-extensions>=4.6.0", ] files = [ @@ -1785,6 +1815,7 @@ summary = "The little ASGI library that shines." groups = ["default"] dependencies = [ "anyio<5,>=3.4.0", + "typing-extensions>=3.10.0; python_version < \"3.10\"", ] files = [ {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, @@ -2039,6 +2070,7 @@ groups = ["default"] dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", "platformdirs<5,>=3.9.1", ] files = [ @@ -2128,6 +2160,9 @@ name = "wcwidth" version = "0.2.13" summary = "Measures the displayed width of unicode strings in a terminal" groups = ["default"] +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, diff --git a/backend/requirements.txt b/backend/requirements.txt index 68fd532c..00bec2fc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -31,6 +31,7 @@ ecdsa==0.18.0 email-validator==2.0.0 exceptiongroup==1.2.0; python_version < "3.11" fast-captcha==0.3.2 +fastapi==0.111.0 fastapi-cli==0.0.2 fastapi-limiter==0.1.6 fastapi-oauth20==0.0.1a2 @@ -86,6 +87,7 @@ python-jose==3.3.0 python-multipart==0.0.9 pytz==2023.3 pyyaml==6.0.1 +redis==5.0.1 redis[hiredis]==5.0.1 rich==13.7.1 rsa==4.9 @@ -107,6 +109,7 @@ tzdata==2024.1 ua-parser==0.18.0 ujson==5.9.0 user-agents==2.2.0 +uvicorn==0.29.0 uvicorn[standard]==0.29.0 uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy" vine==5.1.0 diff --git a/backend/utils/build_tree.py b/backend/utils/build_tree.py index 8fb2291a..4a948ad1 100644 --- a/backend/utils/build_tree.py +++ b/backend/utils/build_tree.py @@ -2,20 +2,17 @@ # -*- coding: utf-8 -*- from typing import Any, Sequence -from asgiref.sync import sync_to_async - from backend.common.enums import BuildTreeType from backend.utils.serializers import RowData, select_list_serialize -async def get_tree_nodes(row: Sequence[RowData]) -> list[dict[str, Any]]: +def get_tree_nodes(row: Sequence[RowData]) -> list[dict[str, Any]]: """获取所有树形结构节点""" - tree_nodes = await select_list_serialize(row) + tree_nodes = select_list_serialize(row) tree_nodes.sort(key=lambda x: x['sort']) return tree_nodes -@sync_to_async def traversal_to_tree(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]: """ 通过遍历算法构造树形结构 @@ -44,9 +41,9 @@ def traversal_to_tree(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]: return tree -async def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | None = None) -> list[dict[str, Any]]: +def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | None = None) -> list[dict[str, Any]]: """ - 通过递归算法构造树形结构 + 通过递归算法构造树形结构(性能影响较大) :param nodes: :param parent_id: @@ -55,14 +52,14 @@ async def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | Non tree = [] for node in nodes: if node['parent_id'] == parent_id: - child_node = await recursive_to_tree(nodes, parent_id=node['id']) + child_node = recursive_to_tree(nodes, parent_id=node['id']) if child_node: node['children'] = child_node tree.append(node) return tree -async def get_tree_data( +def get_tree_data( row: Sequence[RowData], build_type: BuildTreeType = BuildTreeType.traversal, *, parent_id: int | None = None ) -> list[dict[str, Any]]: """ @@ -73,12 +70,12 @@ async def get_tree_data( :param parent_id: :return: """ - nodes = await get_tree_nodes(row) + nodes = get_tree_nodes(row) match build_type: case BuildTreeType.traversal: - tree = await traversal_to_tree(nodes) + tree = traversal_to_tree(nodes) case BuildTreeType.recursive: - tree = await recursive_to_tree(nodes, parent_id=parent_id) + tree = recursive_to_tree(nodes, parent_id=parent_id) case _: raise ValueError(f'无效的算法类型:{build_type}') return tree diff --git a/backend/utils/serializers.py b/backend/utils/serializers.py index 0f4b928c..8a5d805c 100644 --- a/backend/utils/serializers.py +++ b/backend/utils/serializers.py @@ -5,7 +5,6 @@ from typing import Any, Sequence, TypeVar import msgspec -from asgiref.sync import sync_to_async from sqlalchemy import Row, RowMapping from starlette.responses import JSONResponse @@ -14,7 +13,6 @@ RowData = Row | RowMapping | Any R = TypeVar('R', bound=RowData) -@sync_to_async def select_columns_serialize(row: R) -> dict: """ Serialize SQLAlchemy select table columns, does not contain relational columns @@ -33,18 +31,17 @@ def select_columns_serialize(row: R) -> dict: return obj_dict -async def select_list_serialize(row: Sequence[R]) -> list: +def select_list_serialize(row: Sequence[R]) -> list: """ Serialize SQLAlchemy select list :param row: :return: """ - ret_list = [await select_columns_serialize(_) for _ in row] + ret_list = [select_columns_serialize(_) for _ in row] return ret_list -@sync_to_async def select_as_dict(row: R) -> dict: """ Converting SQLAlchemy select to dict, which can contain relational data, diff --git a/deploy/backend/gunicorn.conf.py b/deploy/backend/gunicorn.conf.py index db921353..8473a762 100644 --- a/deploy/backend/gunicorn.conf.py +++ b/deploy/backend/gunicorn.conf.py @@ -1,8 +1,9 @@ +# fmt: off # 监听内网端口 -bind = "0.0.0.0:8001" +bind = '0.0.0.0:8001' # 工作目录 -chdir = "/fba/backend/" +chdir = '/fba/backend/' # 并行工作进程数 workers = 1 @@ -22,25 +23,25 @@ timeout = 120 daemon = False # 工作模式协程 -worker_class = "uvicorn.workers.UvicornWorker" +worker_class = 'uvicorn.workers.UvicornWorker' # 设置最大并发量 worker_connections = 2000 # 设置进程文件目录 -pidfile = "/fba/gunicorn.pid" +pidfile = '/fba/gunicorn.pid' # 设置访问日志和错误信息日志路径 -accesslog = "/var/log/fastapi_server/gunicorn_access.log" -errorlog = "/var/log/fastapi_server/gunicorn_error.log" +accesslog = '/var/log/fastapi_server/gunicorn_access.log' +errorlog = '/var/log/fastapi_server/gunicorn_error.log' # 设置这个值为true 才会把打印信息记录到错误日志里 capture_output = True # 设置日志记录水平 -loglevel = "debug" +loglevel = 'debug' # python程序 -pythonpath = "/usr/local/lib/python3.10/site-packages" +pythonpath = '/usr/local/lib/python3.10/site-packages' # 启动 gunicorn -c gunicorn.conf.py main:app