From eb63032c817b0518a1b248c5ecd01ad696e7e3f8 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Sun, 13 Jul 2025 15:53:55 +0800 Subject: [PATCH] perf: User and workspace api adapt new ui --- backend/apps/settings/api/terminology.py | 2 +- backend/apps/system/api/user.py | 30 +++++-- backend/apps/system/api/workspace.py | 86 ++++++++++++++++---- backend/apps/system/crud/user.py | 25 +++++- backend/apps/system/schemas/system_schema.py | 11 ++- backend/common/core/pagination.py | 73 ++++++++++++----- frontend/src/api/auth.ts | 2 + frontend/src/components/layout/Workspace.vue | 46 ++++++++--- frontend/src/i18n/zh-CN.json | 3 +- 9 files changed, 215 insertions(+), 63 deletions(-) diff --git a/backend/apps/settings/api/terminology.py b/backend/apps/settings/api/terminology.py index 62b6a1a3..708e6ee7 100644 --- a/backend/apps/settings/api/terminology.py +++ b/backend/apps/settings/api/terminology.py @@ -18,7 +18,7 @@ async def pager( paginator = Paginator(session) filters = {} return await paginator.get_paginated_response( - model=term_model, + stmt=term_model, pagination=pagination, **filters) diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index 30b5909b..53408c1b 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -1,9 +1,9 @@ from fastapi import APIRouter -from apps.system.crud.user import get_db_user +from apps.system.crud.user import get_db_user, user_ws_options from apps.system.models.user import UserModel from apps.system.schemas.auth import CacheName, CacheNamespace -from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage -from common.core.deps import CurrentUser, SessionDep +from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserWs +from common.core.deps import CurrentUser, SessionDep, Trans from common.core.pagination import Paginator from common.core.schemas import PaginatedResponse, PaginationParams from common.core.security import md5pwd, verify_md5pwd @@ -14,7 +14,6 @@ router = APIRouter(tags=["user"], prefix="/user") async def user_info(current_user: CurrentUser): return current_user - @router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[UserGrid]) async def pager( session: SessionDep, @@ -25,10 +24,25 @@ async def pager( paginator = Paginator(session) filters = {} return await paginator.get_paginated_response( - model=UserModel, + stmt=UserModel, pagination=pagination, **filters) +@router.get("/ws") +async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Trans) -> list[UserWs]: + return await user_ws_options(session, current_user.id, trans) + +@router.put("/ws/{oid}") +@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") +async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int): + ws_list: list[UserWs] = await user_ws_options(session, current_user.id) + if not any(x.id == oid for x in ws_list): + raise RuntimeError(f"oid [{oid}] is invalid!") + user_model: UserModel = get_db_user(session = session, user_id = current_user.id) + user_model.oid = oid + session.add(user_model) + session.commit() + @router.get("/{id}", response_model=UserEditor) async def query(session: SessionDep, id: int) -> UserEditor: db_user: UserModel = get_db_user(session = session, user_id = id) @@ -58,6 +72,11 @@ async def delete(session: SessionDep, id: int): user_model: UserModel = get_db_user(session = session, user_id = id) session.delete(user_model) session.commit() + +@router.delete("") +async def batch_del(session: SessionDep, id_list: list[int]): + for id in id_list: + delete(session, id) @router.put("/language") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") @@ -69,7 +88,6 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U db_user.language = lang session.add(db_user) session.commit() - return {"message": "Language changed successfully", "language": lang} @router.put("/pwd") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") diff --git a/backend/apps/system/api/workspace.py b/backend/apps/system/api/workspace.py index 4afcae22..4f60241c 100644 --- a/backend/apps/system/api/workspace.py +++ b/backend/apps/system/api/workspace.py @@ -1,12 +1,78 @@ -from fastapi import APIRouter -from sqlmodel import select +from typing import Optional +from fastapi import APIRouter, Query +from sqlmodel import or_, select from apps.system.models.system_model import UserWsModel, WorkspaceBase, WorkspaceEditor, WorkspaceModel -from apps.system.schemas.system_schema import UserWsBase, UserWsDTO -from common.core.deps import SessionDep, Trans +from apps.system.models.user import UserModel +from apps.system.schemas.system_schema import UserWsBase, UserWsDTO, WorkspaceUser +from common.core.deps import CurrentUser, SessionDep, Trans +from common.core.pagination import Paginator +from common.core.schemas import PaginatedResponse, PaginationParams from common.utils.time import get_timestamp router = APIRouter(tags=["system/workspace"], prefix="/system/workspace") + +@router.get("/uws/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[WorkspaceUser]) +async def pager( + session: SessionDep, + current_user: CurrentUser, + pageNum: int, + pageSize: int, + keyword: Optional[str] = Query(None, description="搜索关键字(可选)"), + oid: Optional[int] = Query(None, description="空间ID(仅admin用户生效)"), +): + if current_user.isAdmin: + if not oid: + raise RuntimeError('oid miss error') + workspace_id = oid + else: + workspace_id = current_user.oid + pagination = PaginationParams(page=pageNum, size=pageSize) + paginator = Paginator(session) + stmt = select(UserModel.id, UserModel.account, UserModel.name, UserModel.email, UserModel.status, UserModel.create_time, UserModel.oid, UserWsModel.weight).join( + UserWsModel, UserModel.id == UserWsModel.uid + ).where( + UserWsModel.oid == workspace_id, + ).order_by(UserModel.create_time) + + if keyword: + keyword_pattern = f"%{keyword}%" + stmt = stmt.where( + or_( + UserModel.account.ilike(keyword_pattern), + UserModel.name.ilike(keyword_pattern), + UserModel.email.ilike(keyword_pattern) + ) + ) + return await paginator.get_paginated_response( + stmt=stmt, + pagination=pagination, + ) + + +@router.post("/uws") +async def create(session: SessionDep, creator: UserWsDTO): + # 判断uid_list以及oid合法性 + db_model_list = [ + UserWsModel.model_validate({ + "oid": creator.oid, + "uid": uid, + "weight": creator.weight + }) + for uid in creator.uid_list + ] + session.add_all(db_model_list) + session.commit() + +@router.delete("/uws") +async def delete(session: SessionDep, dto: UserWsBase): + db_model_list: list[UserWsModel] = session.exec(select(UserWsModel).where(UserWsModel.uid.in_(dto.uid_list), UserWsModel.oid == dto.oid)).all() + if not db_model_list: + raise ValueError(f"UserWsModel not found") + for db_model in db_model_list: + session.delete(db_model) + session.commit() + @router.get("", response_model=list[WorkspaceModel]) async def query(session: SessionDep, trans: Trans): list_result = session.exec(select(WorkspaceModel).order_by(WorkspaceModel.create_time)).all() @@ -50,16 +116,4 @@ async def delete(session: SessionDep, id: int): session.delete(db_model) session.commit() -@router.post("/uws") -async def create(session: SessionDep, creator: UserWsDTO): - db_model = UserWsModel.model_validate(creator) - session.add(db_model) - session.commit() -@router.delete("/uws") -async def delete(session: SessionDep, dto: UserWsBase): - db_model: UserWsModel = session.exec(select(UserWsModel).where(UserWsModel.uid == dto.uid, UserWsModel.oid == dto.oid)).first() - if not db_model: - raise ValueError(f"UserWsModel not found") - session.delete(db_model) - session.commit() \ No newline at end of file diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py index 5f34daf9..1af1c9a8 100644 --- a/backend/apps/system/crud/user.py +++ b/backend/apps/system/crud/user.py @@ -1,10 +1,12 @@ +from typing import Optional from sqlmodel import Session, select -from apps.system.models.system_model import UserWsModel +from apps.system.models.system_model import UserWsModel, WorkspaceModel from apps.system.schemas.auth import CacheName, CacheNamespace -from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO +from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO, UserWs from common.core.sqlbot_cache import cache +from common.utils.locale import I18n from ..models.user import UserModel from common.core.security import verify_md5pwd @@ -36,4 +38,21 @@ def authenticate(*, session: Session, account: str, password: str) -> BaseUserDT return None if not verify_md5pwd(password, db_user.password): return None - return db_user \ No newline at end of file + return db_user + +async def user_ws_options(session: Session, uid: int, trans: Optional[I18n] = None) -> list[UserWs]: + if uid == 1: + stmt = select(WorkspaceModel.id, WorkspaceModel.name).order_by(WorkspaceModel.create_time) + else: + stmt = select(UserWsModel.oid, WorkspaceModel.name).join( + WorkspaceModel, UserWsModel.oid == WorkspaceModel.id + ).where( + UserWsModel.uid == uid, + ).order_by(WorkspaceModel.create_time) + result = session.exec(stmt) + if not trans: + return result.all() + return [ + UserWs(id = id, name = trans(name) if name.startswith('i18n') else name) + for id, name in result.all() + ] \ No newline at end of file diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index b524e24b..4511b1d2 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -43,7 +43,7 @@ class PwdEditor(BaseModel): new_pwd: str class UserWsBase(BaseModel): - uid: int + uid_list: list[int] oid: int class UserWsDTO(UserWsBase): weight: int = 0 @@ -83,4 +83,11 @@ class AssistantValidator(BaseModel): domain_match=domain_match, token=token, **kwargs - ) \ No newline at end of file + ) + +class WorkspaceUser(UserEditor): + weight: int + create_time: int + +class UserWs(BaseCreatorDTO): + name: str \ No newline at end of file diff --git a/backend/common/core/pagination.py b/backend/common/core/pagination.py index dbd95fa7..783e3198 100644 --- a/backend/common/core/pagination.py +++ b/backend/common/core/pagination.py @@ -1,57 +1,86 @@ -from sqlmodel.ext.asyncio.session import AsyncSession -from sqlmodel import select, func, SQLModel -from typing import Type, TypeVar, Sequence, Optional +from sqlalchemy import Row, Select +from sqlmodel import Session, select, func, SQLModel +from typing import Dict, Type, TypeVar, Sequence, Optional from common.core.schemas import PaginationParams, PaginatedResponse +from sqlmodel.sql.expression import SelectOfScalar +from typing import Union, Any ModelT = TypeVar('ModelT', bound=SQLModel) class Paginator: - def __init__(self, session: AsyncSession): + def __init__(self, session: Session): self.session = session - + def _process_result_row(self, row: Row) -> Dict[str, Any]: + result_dict = {} + for item, key in zip(row, row._fields): + if isinstance(item, SQLModel): + result_dict.update(item.dict()) + else: + result_dict[key] = item + + return result_dict async def paginate( self, - model: Type[ModelT], + stmt: Union[Select, SelectOfScalar, Type[ModelT]], page: int = 1, size: int = 20, order_by: Optional[str] = None, desc: bool = False, **filters - ) -> tuple[Sequence[ModelT], int]: + ) -> tuple[Sequence[Any], int]: offset = (page - 1) * size - stmt = select(model) + single_model: bool = False + if isinstance(stmt, type) and issubclass(stmt, SQLModel): + stmt = select(stmt) + single_model = True + # 应用过滤条件 for field, value in filters.items(): if value is not None: - stmt = stmt.where(getattr(model, field) == value) + # 处理关联模型的字段 (如 user.name) + if '.' in field: + related_model, related_field = field.split('.') + # 这里需要根据实际关联关系调整 + stmt = stmt.where(getattr(getattr(stmt.selected_columns, related_model), related_field) == value) + else: + stmt = stmt.where(getattr(stmt.selected_columns, field) == value) + # 应用排序 if order_by: - column = getattr(model, order_by) + if '.' in order_by: + related_model, related_field = order_by.split('.') + column = getattr(getattr(stmt.selected_columns, related_model), related_field) + else: + column = getattr(stmt.selected_columns, order_by) stmt = stmt.order_by(column.desc() if desc else column.asc()) - count_stmt = select(func.count()).select_from(model) - for field, value in filters.items(): - if value is not None: - count_stmt = count_stmt.where(getattr(model, field) == value) - + # 计算总数 + """ count_stmt = stmt.with_only_columns(func.count(), maintain_column_froms=True) result = self.session.exec(count_stmt) - total = result.first() + total: int = result.first() """ + count_stmt = select(func.count()).select_from(stmt.subquery()) + total_result = self.session.exec(count_stmt) + total: int = total_result.first() + # 应用分页 stmt = stmt.offset(offset).limit(size) + # 执行查询 result = self.session.exec(stmt) - items = result.all() - + if not single_model: + items = [self._process_result_row(row) for row in result] + else: + items = result.all() return items, total async def get_paginated_response( self, - model: Type[ModelT], + stmt: Union[Select, SelectOfScalar, Type[ModelT]], pagination: PaginationParams, **filters - ) -> PaginatedResponse[ModelT]: + ) -> PaginatedResponse[Any]: items, total = await self.paginate( - model=model, + stmt=stmt, page=pagination.page, size=pagination.size, order_by=pagination.order_by, @@ -61,7 +90,7 @@ class Paginator: total_pages = (total + pagination.size - 1) // pagination.size - return PaginatedResponse[ModelT]( + return PaginatedResponse[Any]( items=items, total=total, page=pagination.page, diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 98aa21df..c55a0b89 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -9,4 +9,6 @@ export const userApi = { query: (id: number) => request.get(`/settings/terminology/${id}`), language: (data: any) => request.put('/user/language', data), pwd: (data: any) => request.put('/user/pwd', data), + ws_options: () => request.get('/user/ws'), + ws_change: (oid: number) => request.put(`/user/ws/${oid}`), } diff --git a/frontend/src/components/layout/Workspace.vue b/frontend/src/components/layout/Workspace.vue index 31cc242f..5d3c6982 100644 --- a/frontend/src/components/layout/Workspace.vue +++ b/frontend/src/components/layout/Workspace.vue @@ -1,21 +1,24 @@