From 6b2402f21291c9edba091209e31de2e66a9aef3b Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 31 Jul 2025 16:46:02 +0800 Subject: [PATCH] Add some interfaces for user profiles (#745) --- backend/app/admin/api/v1/sys/user.py | 40 +++- backend/app/admin/crud/crud_user.py | 17 +- backend/app/admin/service/user_service.py | 225 ++++++++++++---------- 3 files changed, 167 insertions(+), 115 deletions(-) diff --git a/backend/app/admin/api/v1/sys/user.py b/backend/app/admin/api/v1/sys/user.py index edab3556..f292fe91 100644 --- a/backend/app/admin/api/v1/sys/user.py +++ b/backend/app/admin/api/v1/sys/user.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Depends, Path, Query, Request +from fastapi import APIRouter, Body, Depends, Path, Query, Request from backend.app.admin.schema.role import GetRoleDetail from backend.app.admin.schema.user import ( @@ -71,7 +71,7 @@ async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaMode return response_base.success(data=data) -@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsJwtAuth]) +@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC]) async def update_user( request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam ) -> ResponseModel: @@ -93,11 +93,41 @@ async def update_user_permission( return response_base.fail() -@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsJwtAuth]) +@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth]) +async def update_user_password(request: Request, obj: ResetPasswordParam) -> ResponseModel: + count = await user_service.update_password(request=request, obj=obj) + if count > 0: + return response_base.success() + return response_base.fail() + + +@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsRBAC]) async def reset_user_password( - pk: Annotated[int, Path(description='用户 ID')], obj: ResetPasswordParam + request: Request, + pk: Annotated[int, Path(description='用户 ID')], + password: Annotated[str, Body(embed=True, description='新密码')], ) -> ResponseModel: - count = await user_service.reset_pwd(pk=pk, obj=obj) + count = await user_service.reset_password(request=request, pk=pk, password=password) + if count > 0: + return response_base.success() + return response_base.fail() + + +@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth]) +async def update_user_nickname( + request: Request, nickname: Annotated[str, Body(embed=True, description='用户昵称')] +) -> ResponseModel: + count = await user_service.update_nickname(request=request, nickname=nickname) + if count > 0: + return response_base.success() + return response_base.fail() + + +@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth]) +async def update_user_avatar( + request: Request, avatar: Annotated[str, Body(embed=True, description='用户头像地址')] +) -> ResponseModel: + count = await user_service.update_avatar(request=request, avatar=avatar) if count > 0: 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 152ded74..ff49222f 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -117,6 +117,17 @@ class CRUDUser(CRUDPlus[User]): input_user.roles = roles.scalars().all() return count + async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int: + """ + 更新用户昵称 + + :param db: 数据库会话 + :param user_id: 用户 ID + :param nickname: 用户昵称 + :return: + """ + return await self.update_model(db, user_id, {'nickname': nickname}) + async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int: """ 更新用户头像 @@ -148,17 +159,17 @@ class CRUDUser(CRUDPlus[User]): """ return await self.select_model_by_column(db, email=email) - async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int: + async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int: """ 重置用户密码 :param db: 数据库会话 :param pk: 用户 ID - :param new_pwd: 新密码 + :param password: 新密码 :return: """ salt = bcrypt.gensalt() - new_pwd = get_hash_password(new_pwd, salt) + new_pwd = get_hash_password(password, salt) return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt}) async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select: diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index acc8ed0a..60521415 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -103,11 +103,10 @@ class UserService: :return: """ async with async_db_session.begin() as db: + superuser_verify(request) user = await user_dao.get_with_relation(db, user_id=pk) if not user: raise errors.NotFoundError(msg='用户不存在') - if request.user.username != user.username: - raise errors.ForbiddenError(msg='只能修改自己的信息') if obj.username != user.username: if await user_dao.get_by_username(db, obj.username): raise errors.ConflictError(msg='用户名已注册') @@ -119,98 +118,7 @@ class UserService: return count @staticmethod - async def update_superuser(*, request: Request, pk: int) -> int: - """ - 更新用户管理员状态 - - :param request: FastAPI 请求对象 - :param pk: 用户 ID - :return: - """ - async with async_db_session.begin() as db: - superuser_verify(request) - user = await user_dao.get(db, pk) - if not user: - raise errors.NotFoundError(msg='用户不存在') - if pk == request.user.id: - raise errors.ForbiddenError(msg='禁止修改自身权限') - count = await user_dao.set_super(db, pk, not user.status) - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') - return count - - @staticmethod - async def update_staff(*, request: Request, pk: int) -> int: - """ - 更新用户职员状态 - - :param request: FastAPI 请求对象 - :param pk: 用户 ID - :return: - """ - async with async_db_session.begin() as db: - superuser_verify(request) - user = await user_dao.get(db, pk) - if not user: - raise errors.NotFoundError(msg='用户不存在') - if pk == request.user.id: - raise errors.ForbiddenError(msg='禁止修改自身权限') - count = await user_dao.set_staff(db, pk, not user.is_staff) - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') - return count - - @staticmethod - async def update_status(*, request: Request, pk: int) -> int: - """ - 更新用户状态 - - :param request: FastAPI 请求对象 - :param pk: 用户 ID - :return: - """ - async with async_db_session.begin() as db: - superuser_verify(request) - user = await user_dao.get(db, pk) - if not user: - raise errors.NotFoundError(msg='用户不存在') - if pk == request.user.id: - raise errors.ForbiddenError(msg='禁止修改自身权限') - count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1) - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') - return count - - @staticmethod - async def update_multi_login(*, request: Request, pk: int) -> int: - """ - 更新用户多端登录状态 - - :param request: FastAPI 请求对象 - :param pk: 用户 ID - :return: - """ - async with async_db_session.begin() as db: - superuser_verify(request) - user = await user_dao.get(db, pk) - if not user: - raise errors.NotFoundError(msg='用户不存在') - multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login - new_multi_login = not multi_login - count = await user_dao.set_multi_login(db, pk, new_multi_login) - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') - token = get_token(request) - token_payload = jwt_decode(token) - if pk == user.id: - # 系统管理员修改自身时,除当前 token 外,其他 token 失效 - if not new_multi_login: - key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}' - await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}') - else: - # 系统管理员修改他人时,他人 token 全部失效 - if not new_multi_login: - key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}' - await redis_client.delete_prefix(key_prefix) - return count - - async def update_permission(self, *, request: Request, pk: int, type: UserPermissionType) -> int: + async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int: """ 更新用户权限 @@ -219,30 +127,133 @@ class UserService: :param type: 权限类型 :return: """ - match type: - case UserPermissionType.superuser: - count = await self.update_superuser(request=request, pk=pk) - case UserPermissionType.staff: - count = await self.update_staff(request=request, pk=pk) - case UserPermissionType.status: - count = await self.update_status(request=request, pk=pk) - case UserPermissionType.multi_login: - count = await self.update_multi_login(request=request, pk=pk) - case _: - raise errors.RequestError(msg='权限类型不存在') + async with async_db_session.begin() as db: + superuser_verify(request) + match type: + case UserPermissionType.superuser: + user = await user_dao.get(db, pk) + if not user: + raise errors.NotFoundError(msg='用户不存在') + if pk == request.user.id: + raise errors.ForbiddenError(msg='禁止修改自身权限') + count = await user_dao.set_super(db, pk, not user.status) + case UserPermissionType.staff: + user = await user_dao.get(db, pk) + if not user: + raise errors.NotFoundError(msg='用户不存在') + if pk == request.user.id: + raise errors.ForbiddenError(msg='禁止修改自身权限') + count = await user_dao.set_staff(db, pk, not user.is_staff) + case UserPermissionType.status: + user = await user_dao.get(db, pk) + if not user: + raise errors.NotFoundError(msg='用户不存在') + if pk == request.user.id: + raise errors.ForbiddenError(msg='禁止修改自身权限') + count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1) + case UserPermissionType.multi_login: + user = await user_dao.get(db, pk) + if not user: + raise errors.NotFoundError(msg='用户不存在') + multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login + new_multi_login = not multi_login + count = await user_dao.set_multi_login(db, pk, new_multi_login) + token = get_token(request) + token_payload = jwt_decode(token) + if pk == user.id: + # 系统管理员修改自身时,除当前 token 外,其他 token 失效 + if not new_multi_login: + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}' + await redis_client.delete_prefix( + key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}' + ) + else: + # 系统管理员修改他人时,他人 token 全部失效 + if not new_multi_login: + key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}' + await redis_client.delete_prefix(key_prefix) + case _: + raise errors.RequestError(msg='权限类型不存在') + + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') return count @staticmethod - async def reset_pwd(*, pk: int, obj: ResetPasswordParam) -> int: + async def reset_password(*, request: Request, pk: int, password: str) -> int: """ 重置用户密码 + :param request: FastAPI 请求对象 :param pk: 用户 ID + :param password: 新密码 + :return: + """ + async with async_db_session.begin() as db: + superuser_verify(request) + user = await user_dao.get(db, pk) + if not user: + raise errors.NotFoundError(msg='用户不存在') + count = await user_dao.reset_password(db, user.id, password) + key_prefix = [ + f'{settings.TOKEN_REDIS_PREFIX}:{user.id}', + f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}', + f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}', + ] + for prefix in key_prefix: + await redis_client.delete(prefix) + return count + + @staticmethod + async def update_nickname(*, request: Request, nickname: str) -> int: + """ + 更新用户昵称 + + :param request: FastAPI 请求对象 + :param nickname: 用户昵称 + :return: + """ + async with async_db_session.begin() as db: + token = get_token(request) + token_payload = jwt_decode(token) + user = await user_dao.get(db, token_payload.id) + if not user: + raise errors.NotFoundError(msg='用户不存在') + count = await user_dao.update_nickname(db, token_payload.id, nickname) + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + return count + + @staticmethod + async def update_avatar(*, request: Request, avatar: str) -> int: + """ + 更新用户头像 + + :param request: FastAPI 请求对象 + :param avatar: 头像地址 + :return: + """ + async with async_db_session.begin() as db: + token = get_token(request) + token_payload = jwt_decode(token) + user = await user_dao.get(db, token_payload.id) + if not user: + raise errors.NotFoundError(msg='用户不存在') + count = await user_dao.update_avatar(db, token_payload.id, avatar) + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + return count + + @staticmethod + async def update_password(*, request: Request, obj: ResetPasswordParam) -> int: + """ + 更新用户密码 + + :param request: FastAPI 请求对象 :param obj: 密码重置参数 :return: """ async with async_db_session.begin() as db: - user = await user_dao.get(db, pk) + token = get_token(request) + token_payload = jwt_decode(token) + user = await user_dao.get(db, token_payload.id) if not user: raise errors.NotFoundError(msg='用户不存在') if not password_verify(obj.old_password, user.password):