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">
<el-dropdown @command="handleCommand" trigger="click" size="medium">
<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>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
@ -106,6 +106,9 @@
}
},
methods: {
getAvatarUrl(user) {
return sa.cfg.api_url + '/user/avatar/' + (user.avatar ? user.avatar : 'default');
},
// ------------------------------ ------------------------------
//
startSearch: function() {

View File

@ -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);
}
}
})

View File

@ -45,6 +45,15 @@
<label class="c-label">学号:</label>
<el-input v-model="p.student_id" placeholder="模糊查询"></el-input>
</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">
<label class="c-label">注册日期:</label>
<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="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 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 === 0" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">启用</el-button>
</template>
@ -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列表

View File

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

View File

@ -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(

View File

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

View File

@ -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:

View File

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

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