perf: User and workspace api adapt new ui

This commit is contained in:
fit2cloud-chenyw
2025-07-13 15:53:55 +08:00
parent 6d39a77451
commit eb63032c81
9 changed files with 215 additions and 63 deletions
+1 -1
View File
@@ -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)
+24 -6
View File
@@ -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")
+70 -16
View File
@@ -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()
+22 -3
View File
@@ -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
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()
]
+9 -2
View File
@@ -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
)
)
class WorkspaceUser(UserEditor):
weight: int
create_time: int
class UserWs(BaseCreatorDTO):
name: str
+51 -22
View File
@@ -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,
+2
View File
@@ -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}`),
}
+34 -12
View File
@@ -1,21 +1,24 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import icon_expand_down_filled from '@/assets/svg/icon_expand-down_filled.svg'
import icon_moments_categories_outlined from '@/assets/svg/icon_moments-categories_outlined.svg'
import icon_done_outlined from '@/assets/svg/icon_done_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { userApi } from '@/api/auth'
import { ElMessage } from 'element-plus-secondary'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { t } = useI18n()
defineProps({
collapse: { type: [Boolean], required: true },
})
const name = ref('默认工作空间')
const currentWorkspace = ref('默认工作空间')
const defaultDatasourceList = ref([
{
name: '默认工作空间',
},
] as any[])
const currentWorkspace = ref({
id: '',
name: '',
})
const defaultDatasourceList = ref([] as any[])
const workspaceKeywords = ref('')
const defaultWorkspaceListWithSearch = computed(() => {
if (!workspaceKeywords.value) return defaultDatasourceList.value
@@ -34,9 +37,28 @@ const formatKeywords = (item: string) => {
const emit = defineEmits(['selectWorkspace'])
const handleDefaultWorkspaceChange = (item: any) => {
currentWorkspace.value = item.name
currentWorkspace.value = { id: item.id, name: item.name }
userApi.ws_change(item.id).then(() => {
ElMessage.success(t('common.switch_success'))
})
emit('selectWorkspace', item)
}
const init_ws_data = async () => {
const res = await userApi.ws_options()
defaultDatasourceList.value = res
}
const init_current_ws = () => {
const oid = userStore.getOid
currentWorkspace.value = defaultDatasourceList.value.find(
(item) => item.id.toString() === oid.toString()
)
}
onMounted(async () => {
await init_ws_data()
init_current_ws()
})
</script>
<template>
@@ -46,7 +68,7 @@ const handleDefaultWorkspaceChange = (item: any) => {
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
<span v-if="!collapse" class="name">{{ name }}</span>
<span v-if="!collapse" class="name">{{ currentWorkspace.name }}</span>
<el-icon v-if="!collapse" style="transform: scale(0.5)" class="expand" size="24">
<icon_expand_down_filled></icon_expand_down_filled>
</el-icon></button
@@ -69,7 +91,7 @@ const handleDefaultWorkspaceChange = (item: any) => {
v-for="ele in defaultWorkspaceListWithSearch"
:key="ele.name"
class="popover-item"
:class="currentWorkspace === ele.name && 'isActive'"
:class="currentWorkspace.id === ele.id && 'isActive'"
@click="handleDefaultWorkspaceChange(ele)"
>
<el-icon size="16">
+2 -1
View File
@@ -18,7 +18,8 @@
"next": "下一步",
"save": "保存",
"logout": "退出登录",
"please_input": "请输入{msg}"
"please_input": "请输入{msg}",
"switch_success": "切换成功"
},
"dashboard": {
"select_dashboard": "请选择仪表板",