diff --git a/src/frontend/sa-frame/nav/nav-tool-bar.vue b/src/frontend/sa-frame/nav/nav-tool-bar.vue index c3423a4..fc6a228 100644 --- a/src/frontend/sa-frame/nav/nav-tool-bar.vue +++ b/src/frontend/sa-frame/nav/nav-tool-bar.vue @@ -34,7 +34,7 @@ - + {{$root.user.username}} @@ -106,6 +106,9 @@ } }, methods: { + getAvatarUrl(user) { + return sa.cfg.api_url + '/user/avatar/' + (user.avatar ? user.avatar : 'default'); + }, // ------------------------------ 搜索相关 ------------------------------ // 开启搜索 startSearch: function() { diff --git a/src/frontend/sa-view/user/user-avatar.html b/src/frontend/sa-view/user/user-avatar.html index 0c75f9d..8eddc32 100644 --- a/src/frontend/sa-view/user/user-avatar.html +++ b/src/frontend/sa-view/user/user-avatar.html @@ -113,7 +113,7 @@ }, created: function(){ if (sa_admin.user.avatar !== '') { - this.imageUrl = this.getAvatarUrl(sa_admin.user.avatar); + this.imageUrl = this.getAvatarUrl("/user/avatar/" + sa_admin.user.avatar); } } }) diff --git a/src/frontend/sa-view/user/user-list.html b/src/frontend/sa-view/user/user-list.html index 7a16c99..81b2c22 100644 --- a/src/frontend/sa-view/user/user-list.html +++ b/src/frontend/sa-view/user/user-list.html @@ -45,6 +45,15 @@ +
+
+ + + + + + +
- @@ -115,6 +124,7 @@ 详情 人脸信息 重置密码 + 重置头像 禁用 启用 @@ -154,6 +164,7 @@ phone: '', student_id: '', create_type: 0, + avatar_type: 0, sortType: 1, start_time: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-1', // 本月一号 end_time: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + new Date().getDate(), // 本月当日 @@ -169,7 +180,7 @@ sa.ajax('/user/admin/list', this.p, function(res){ this.dataList = res.data.data; // 数据 for (let i = 0; i < this.dataList.length; i++) { - this.dataList[i].avatar = this.dataList[i].avatar ? this.dataList[i].avatar : '/user/avatar/default'; + this.dataList[i].avatar = '/user/avatar/' + (this.dataList[i].avatar ? this.dataList[i].avatar : 'default'); this.dataList[i].status = this.dataList[i].is_active ? 1 : 0; } this.dataCount = res.data.count; // 分页 @@ -231,6 +242,14 @@ }.bind(this), {}) }.bind(this)); }, + // 重置头像 + reset_avatar: function (data) { + sa.confirm('是否重置头像,此操作不可撤销', function() { + sa.ajax('/user/admin/reset_avatar', {user_id: data.id}, function(res) { + sa.ok('重置头像成功'); + }.bind(this), {}) + }.bind(this)); + }, // 批量删除 deleteByIds: function() { // 获取选中元素的id列表 diff --git a/src/route/users_admin.py b/src/route/users_admin.py index fa3e71c..47da49f 100644 --- a/src/route/users_admin.py +++ b/src/route/users_admin.py @@ -1,8 +1,11 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor + from fastapi import HTTPException from fastapi_amis_admin.crud import BaseApiOut from starlette import status -from src.plugin import handler +from src.plugin import handler, job from src.plugin.plugin import Plugin from src.route.users import UserRoutes from src.services.users.schemas import ( @@ -12,6 +15,7 @@ from src.services.users.schemas import ( CreateTypeEnum, ) from src.services.users.services import UserServices +from src.utils.clean_files import clean_files class UserAdminRoutes(Plugin): @@ -30,6 +34,7 @@ class UserAdminRoutes(Plugin): username, nickname, real_name = data.username, data.nickname, data.real_name email, phone, student_id = data.email, data.phone, data.student_id create_type = data.create_type.value + has_avatar = data.has_avatar start_time, end_time = data.start, data.end page_no, page_size = data.pageNo, data.pageSize if page_no < 1: @@ -45,6 +50,7 @@ class UserAdminRoutes(Plugin): phone, student_id, create_type, + has_avatar, start_time, end_time, page_no, @@ -99,3 +105,28 @@ class UserAdminRoutes(Plugin): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error Execute SQL:{e}", ) from e + + @handler.post("/reset_avatar") + async def reset_avatar(self, data: DisableOrEnableUser): + try: + result = await self.user_services.reset_avatar(data.user_id) + if not result: + return BaseApiOut(code=500, msg="操作失败") + return BaseApiOut(code=0, msg="重置成功", data={}) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error Execute SQL:{e}", + ) from e + + @handler.get("/clean_avatar") + @job.cron(name="clean_user_avatar_job", hour=2, minute=0, second=0) + async def clean_user_avatar_job(self): + print("Clean user avatar job start") + data, _ = await self.user_services.get_user_list(has_avatar=True) + paths = [i.avatar for i in data if i.avatar] + + loop = asyncio.get_event_loop() + with ThreadPoolExecutor() as executor: + await loop.run_in_executor(executor, clean_files, paths) + print("Clean user avatar job end") diff --git a/src/route/users_update.py b/src/route/users_update.py index 0bc60c6..5f2c432 100644 --- a/src/route/users_update.py +++ b/src/route/users_update.py @@ -103,10 +103,13 @@ class UserUpdateRoutes(Plugin): avatar = data.avatar if not avatar.startswith(self.avatar_path): return BaseApiOut(status=500, msg="头像地址错误") - if not await check_avatar(user.id, avatar[len(self.avatar_path) :]): + avatar_path = avatar[len(self.avatar_path) :] + if not await check_avatar(user.id, avatar_path): return BaseApiOut(status=500, msg="头像不存在") try: - user = await self.user_services.update_user_avatar(user.username, avatar) + user = await self.user_services.update_user_avatar( + user.username, avatar_path + ) return BaseApiOut(code=0, msg="更新成功", data=user) except Exception as e: raise HTTPException( diff --git a/src/services/users/repositories.py b/src/services/users/repositories.py index 2e9e7b3..cd39724 100644 --- a/src/services/users/repositories.py +++ b/src/services/users/repositories.py @@ -7,7 +7,7 @@ from fastapi_user_auth.auth.models import CasbinRule, LoginHistory from persica.factory.component import AsyncInitializingComponent from pydantic import SecretStr from sqlalchemy import func -from sqlmodel import select, col +from sqlmodel import select, col, or_ from sqlmodel.ext.asyncio.session import AsyncSession from src.core.database import Database @@ -164,6 +164,7 @@ class UserRepo(AsyncInitializingComponent): phone: str, student_id: str, create_type: int, + has_avatar: bool, start_time: Optional[datetime], end_time: Optional[datetime], page_no: int, @@ -197,12 +198,25 @@ class UserRepo(AsyncInitializingComponent): ) if create_type: statement = statement.where(self.user_model.create_type == create_type) + if has_avatar is not None: + if has_avatar: + statement = statement.where( + col(self.user_model.avatar).is_not(None) + ).where(self.user_model.avatar != "") + else: + statement = statement.where( + or_( + col(self.user_model.avatar).is_(None), + self.user_model.avatar == "", + ) + ) if start_time: statement = statement.where(self.user_model.create_time >= start_time) if end_time: statement = statement.where(self.user_model.create_time <= end_time) all_count = await self.get_count(session, statement) - offset = (page_no - 1) * page_size - statement = statement.offset(offset).limit(page_size) + if page_no is not None and page_size is not None: + offset = (page_no - 1) * page_size + statement = statement.offset(offset).limit(page_size) r = await session.exec(statement) return r.all(), all_count diff --git a/src/services/users/schemas.py b/src/services/users/schemas.py index 264fef7..be66dfa 100644 --- a/src/services/users/schemas.py +++ b/src/services/users/schemas.py @@ -127,12 +127,18 @@ class UserList(BaseModel): student_id: str = "" create_type: CreateTypeEnum = CreateTypeEnum.ALL + avatar_type: int = 0 sortType: int = 1 start_time: Optional[str] = "" end_time: Optional[str] = "" pageNo: int = 1 pageSize: int = 10 + @property + def has_avatar(self): + d = {0: None, 1: True, 2: False} + return d.get(self.avatar_type, None) + @property def start(self): if not self.start_time: diff --git a/src/services/users/services.py b/src/services/users/services.py index 0d70871..de91ff0 100644 --- a/src/services/users/services.py +++ b/src/services/users/services.py @@ -104,17 +104,18 @@ class UserServices(AsyncInitializingComponent): async def get_user_list( self, - username: str, - nickname: str, - real_name: str, - email: str, - phone: str, - student_id: str, - create_type: int, - start_time: Optional[datetime], - end_time: Optional[datetime], - page_no: int, - page_size: int, + username: str = None, + nickname: str = None, + real_name: str = None, + email: str = None, + phone: str = None, + student_id: str = None, + create_type: int = None, + has_avatar: bool = None, + start_time: Optional[datetime] = None, + end_time: Optional[datetime] = None, + page_no: int = None, + page_size: int = None, ) -> Tuple[Sequence[UserModel], int]: return await self.repo.get_user_list( username, @@ -124,6 +125,7 @@ class UserServices(AsyncInitializingComponent): phone, student_id, create_type, + has_avatar, start_time, end_time, page_no, @@ -154,6 +156,16 @@ class UserServices(AsyncInitializingComponent): await self.repo.update_user(user) return True + async def reset_avatar(self, user_id: int) -> bool: + user = await self.get_user(user_id) + if not user: + return False + if not user.avatar: + return False + user.avatar = "" + await self.repo.update_user(user) + return True + class UserRoleServices(AsyncInitializingComponent): __order__ = 1 diff --git a/src/utils/clean_files.py b/src/utils/clean_files.py new file mode 100644 index 0000000..45c2a18 --- /dev/null +++ b/src/utils/clean_files.py @@ -0,0 +1,18 @@ +import contextlib +from typing import List + +from ._path import AVATAR_DATA_PATH + + +def clean_files(paths: List[str]): + # 遍历 AVATAR_DATA_PATH 将不存在 paths 中的文件删除 + for d in AVATAR_DATA_PATH.iterdir(): + if not d.is_dir(): + continue + name1 = d.name + for f in d.iterdir(): + name2 = f.name + if f"{name1}/{name2}" not in paths: + with contextlib.suppress(Exception): + f.unlink() + print("clean_files done") diff --git a/src/utils/move_files.py b/src/utils/move_files.py index 7610f73..df388f4 100644 --- a/src/utils/move_files.py +++ b/src/utils/move_files.py @@ -19,6 +19,7 @@ def move_files(paths: List[str]): d.parent.mkdir(exist_ok=True) with contextlib.suppress(Exception): shutil.copy(p, d) + print("Files moved") def move_files_by_uid(data: Tuple[List[str], int]): @@ -34,3 +35,4 @@ def move_files_by_uid(data: Tuple[List[str], int]): d = FACE_IMAGE_DATABASE_PATH / path with contextlib.suppress(Exception): shutil.copy(p, d) + print("Files moved")