feat: clean user avatar job

This commit is contained in:
xtaodada 2024-11-19 14:33:23 +08:00
parent 8c3875c299
commit 6ecead3b8c
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
10 changed files with 128 additions and 20 deletions

View File

@ -34,7 +34,7 @@
<span title="我的信息" class="tool-fox user-info" style="padding: 0;" v-if="$root.user != null"> <span title="我的信息" class="tool-fox user-info" style="padding: 0;" v-if="$root.user != null">
<el-dropdown @command="handleCommand" trigger="click" size="medium"> <el-dropdown @command="handleCommand" trigger="click" size="medium">
<span class="el-dropdown-link user-name" style="height: 100%; padding: 0 1em; display: inline-block;"> <span class="el-dropdown-link user-name" style="height: 100%; padding: 0 1em; display: inline-block;">
<img :src="sa.cfg.api_url + $root.user.avatar" class="user-avatar"> <img :src="getAvatarUrl($root.user)" class="user-avatar">
<span>{{$root.user.username}}</span> <span>{{$root.user.username}}</span>
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-arrow-down el-icon--right"></i>
</span> </span>
@ -106,6 +106,9 @@
} }
}, },
methods: { methods: {
getAvatarUrl(user) {
return sa.cfg.api_url + '/user/avatar/' + (user.avatar ? user.avatar : 'default');
},
// ------------------------------ ------------------------------ // ------------------------------ ------------------------------
// //
startSearch: function() { startSearch: function() {

View File

@ -113,7 +113,7 @@
}, },
created: function(){ created: function(){
if (sa_admin.user.avatar !== '') { if (sa_admin.user.avatar !== '') {
this.imageUrl = this.getAvatarUrl(sa_admin.user.avatar); this.imageUrl = this.getAvatarUrl("/user/avatar/" + sa_admin.user.avatar);
} }
} }
}) })

View File

@ -45,6 +45,15 @@
<label class="c-label">学号:</label> <label class="c-label">学号:</label>
<el-input v-model="p.student_id" placeholder="模糊查询"></el-input> <el-input v-model="p.student_id" placeholder="模糊查询"></el-input>
</div> </div>
<br/>
<div class="c-item">
<label class="c-label">头像状态:</label>
<el-select v-model="p.avatar_type">
<el-option label="所有" :value="0"></el-option>
<el-option label="已设置头像" :value="1"></el-option>
<el-option label="未设置头像" :value="2"></el-option>
</el-select>
</div>
<div class="c-item"> <div class="c-item">
<label class="c-label">注册日期:</label> <label class="c-label">注册日期:</label>
<el-date-picker v-model="p.start_time" type="date" value-format="yyyy-MM-dd" placeholder="开始日期"></el-date-picker> - <el-date-picker v-model="p.start_time" type="date" value-format="yyyy-MM-dd" placeholder="开始日期"></el-date-picker> -
@ -115,6 +124,7 @@
<el-button class="c-btn" type="success" icon="el-icon-view" @click="get(s.row)">详情</el-button> <el-button class="c-btn" type="success" icon="el-icon-view" @click="get(s.row)">详情</el-button>
<el-button class="c-btn" type="success" icon="el-icon-view" @click="getFace(s.row)">人脸信息</el-button> <el-button class="c-btn" type="success" icon="el-icon-view" @click="getFace(s.row)">人脸信息</el-button>
<el-button class="c-btn" type="danger" icon="el-icon-delete" @click="reset_pwd(s.row)">重置密码</el-button> <el-button class="c-btn" type="danger" icon="el-icon-delete" @click="reset_pwd(s.row)">重置密码</el-button>
<el-button v-if="s.row.avatar !== '/user/avatar/default'" class="c-btn" type="danger" icon="el-icon-delete" @click="reset_avatar(s.row)">重置头像</el-button>
<el-button v-if="s.row.status === 1" class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">禁用</el-button> <el-button v-if="s.row.status === 1" class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">禁用</el-button>
<el-button v-if="s.row.status === 0" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">启用</el-button> <el-button v-if="s.row.status === 0" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">启用</el-button>
</template> </template>
@ -154,6 +164,7 @@
phone: '', phone: '',
student_id: '', student_id: '',
create_type: 0, create_type: 0,
avatar_type: 0,
sortType: 1, sortType: 1,
start_time: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-1', // 本月一号 start_time: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-1', // 本月一号
end_time: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + new Date().getDate(), // 本月当日 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){ sa.ajax('/user/admin/list', this.p, function(res){
this.dataList = res.data.data; // 数据 this.dataList = res.data.data; // 数据
for (let i = 0; i < this.dataList.length; i++) { 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.dataList[i].status = this.dataList[i].is_active ? 1 : 0;
} }
this.dataCount = res.data.count; // 分页 this.dataCount = res.data.count; // 分页
@ -231,6 +242,14 @@
}.bind(this), {}) }.bind(this), {})
}.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() { deleteByIds: function() {
// 获取选中元素的id列表 // 获取选中元素的id列表

View File

@ -1,8 +1,11 @@
import asyncio
from concurrent.futures import ThreadPoolExecutor
from fastapi import HTTPException from fastapi import HTTPException
from fastapi_amis_admin.crud import BaseApiOut from fastapi_amis_admin.crud import BaseApiOut
from starlette import status from starlette import status
from src.plugin import handler from src.plugin import handler, job
from src.plugin.plugin import Plugin from src.plugin.plugin import Plugin
from src.route.users import UserRoutes from src.route.users import UserRoutes
from src.services.users.schemas import ( from src.services.users.schemas import (
@ -12,6 +15,7 @@ from src.services.users.schemas import (
CreateTypeEnum, CreateTypeEnum,
) )
from src.services.users.services import UserServices from src.services.users.services import UserServices
from src.utils.clean_files import clean_files
class UserAdminRoutes(Plugin): class UserAdminRoutes(Plugin):
@ -30,6 +34,7 @@ class UserAdminRoutes(Plugin):
username, nickname, real_name = data.username, data.nickname, data.real_name username, nickname, real_name = data.username, data.nickname, data.real_name
email, phone, student_id = data.email, data.phone, data.student_id email, phone, student_id = data.email, data.phone, data.student_id
create_type = data.create_type.value create_type = data.create_type.value
has_avatar = data.has_avatar
start_time, end_time = data.start, data.end start_time, end_time = data.start, data.end
page_no, page_size = data.pageNo, data.pageSize page_no, page_size = data.pageNo, data.pageSize
if page_no < 1: if page_no < 1:
@ -45,6 +50,7 @@ class UserAdminRoutes(Plugin):
phone, phone,
student_id, student_id,
create_type, create_type,
has_avatar,
start_time, start_time,
end_time, end_time,
page_no, page_no,
@ -99,3 +105,28 @@ class UserAdminRoutes(Plugin):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error Execute SQL{e}", detail=f"Error Execute SQL{e}",
) from 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")

View File

@ -103,10 +103,13 @@ class UserUpdateRoutes(Plugin):
avatar = data.avatar avatar = data.avatar
if not avatar.startswith(self.avatar_path): if not avatar.startswith(self.avatar_path):
return BaseApiOut(status=500, msg="头像地址错误") 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="头像不存在") return BaseApiOut(status=500, msg="头像不存在")
try: 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) return BaseApiOut(code=0, msg="更新成功", data=user)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(

View File

@ -7,7 +7,7 @@ from fastapi_user_auth.auth.models import CasbinRule, LoginHistory
from persica.factory.component import AsyncInitializingComponent from persica.factory.component import AsyncInitializingComponent
from pydantic import SecretStr from pydantic import SecretStr
from sqlalchemy import func from sqlalchemy import func
from sqlmodel import select, col from sqlmodel import select, col, or_
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from src.core.database import Database from src.core.database import Database
@ -164,6 +164,7 @@ class UserRepo(AsyncInitializingComponent):
phone: str, phone: str,
student_id: str, student_id: str,
create_type: int, create_type: int,
has_avatar: bool,
start_time: Optional[datetime], start_time: Optional[datetime],
end_time: Optional[datetime], end_time: Optional[datetime],
page_no: int, page_no: int,
@ -197,11 +198,24 @@ class UserRepo(AsyncInitializingComponent):
) )
if create_type: if create_type:
statement = statement.where(self.user_model.create_type == 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: if start_time:
statement = statement.where(self.user_model.create_time >= start_time) statement = statement.where(self.user_model.create_time >= start_time)
if end_time: if end_time:
statement = statement.where(self.user_model.create_time <= end_time) statement = statement.where(self.user_model.create_time <= end_time)
all_count = await self.get_count(session, statement) all_count = await self.get_count(session, statement)
if page_no is not None and page_size is not None:
offset = (page_no - 1) * page_size offset = (page_no - 1) * page_size
statement = statement.offset(offset).limit(page_size) statement = statement.offset(offset).limit(page_size)
r = await session.exec(statement) r = await session.exec(statement)

View File

@ -127,12 +127,18 @@ class UserList(BaseModel):
student_id: str = "" student_id: str = ""
create_type: CreateTypeEnum = CreateTypeEnum.ALL create_type: CreateTypeEnum = CreateTypeEnum.ALL
avatar_type: int = 0
sortType: int = 1 sortType: int = 1
start_time: Optional[str] = "" start_time: Optional[str] = ""
end_time: Optional[str] = "" end_time: Optional[str] = ""
pageNo: int = 1 pageNo: int = 1
pageSize: int = 10 pageSize: int = 10
@property
def has_avatar(self):
d = {0: None, 1: True, 2: False}
return d.get(self.avatar_type, None)
@property @property
def start(self): def start(self):
if not self.start_time: if not self.start_time:

View File

@ -104,17 +104,18 @@ class UserServices(AsyncInitializingComponent):
async def get_user_list( async def get_user_list(
self, self,
username: str, username: str = None,
nickname: str, nickname: str = None,
real_name: str, real_name: str = None,
email: str, email: str = None,
phone: str, phone: str = None,
student_id: str, student_id: str = None,
create_type: int, create_type: int = None,
start_time: Optional[datetime], has_avatar: bool = None,
end_time: Optional[datetime], start_time: Optional[datetime] = None,
page_no: int, end_time: Optional[datetime] = None,
page_size: int, page_no: int = None,
page_size: int = None,
) -> Tuple[Sequence[UserModel], int]: ) -> Tuple[Sequence[UserModel], int]:
return await self.repo.get_user_list( return await self.repo.get_user_list(
username, username,
@ -124,6 +125,7 @@ class UserServices(AsyncInitializingComponent):
phone, phone,
student_id, student_id,
create_type, create_type,
has_avatar,
start_time, start_time,
end_time, end_time,
page_no, page_no,
@ -154,6 +156,16 @@ class UserServices(AsyncInitializingComponent):
await self.repo.update_user(user) await self.repo.update_user(user)
return True 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): class UserRoleServices(AsyncInitializingComponent):
__order__ = 1 __order__ = 1

18
src/utils/clean_files.py Normal file
View File

@ -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")

View File

@ -19,6 +19,7 @@ def move_files(paths: List[str]):
d.parent.mkdir(exist_ok=True) d.parent.mkdir(exist_ok=True)
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
shutil.copy(p, d) shutil.copy(p, d)
print("Files moved")
def move_files_by_uid(data: Tuple[List[str], int]): 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 d = FACE_IMAGE_DATABASE_PATH / path
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
shutil.copy(p, d) shutil.copy(p, d)
print("Files moved")