feat: clean user avatar job
This commit is contained in:
parent
8c3875c299
commit
6ecead3b8c
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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列表
|
||||||
|
@ -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")
|
||||||
|
@ -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(
|
||||||
|
@ -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,12 +198,25 @@ 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)
|
||||||
offset = (page_no - 1) * page_size
|
if page_no is not None and page_size is not None:
|
||||||
statement = statement.offset(offset).limit(page_size)
|
offset = (page_no - 1) * page_size
|
||||||
|
statement = statement.offset(offset).limit(page_size)
|
||||||
r = await session.exec(statement)
|
r = await session.exec(statement)
|
||||||
return r.all(), all_count
|
return r.all(), all_count
|
||||||
|
@ -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:
|
||||||
|
@ -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
18
src/utils/clean_files.py
Normal 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")
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user