mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
开源准备
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Generator, List
|
||||
from fastapi import BackgroundTasks, Request, UploadFile
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.schemas.common.common_schema import UploadResponseSchema, FileListResponseSchema
|
||||
from app.utils.upload_util import UploadUtil
|
||||
|
||||
|
||||
class CommonService:
|
||||
"""通用模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_file_list_services(cls, request: Request) -> List[Dict]:
|
||||
"""获取文件列表service"""
|
||||
file_list_tree = UploadUtil.get_file_tree(settings.STATIC_ROOT)
|
||||
file_list = []
|
||||
for item in file_list_tree:
|
||||
file_list.append(FileListResponseSchema(
|
||||
name=item.name,
|
||||
type='directory' if item.is_dir() else 'file',
|
||||
size=item.stat().st_size if item.is_file() else None,
|
||||
modified_time=datetime.fromtimestamp(item.stat().st_mtime).isoformat()
|
||||
).model_dump())
|
||||
return file_list
|
||||
|
||||
@classmethod
|
||||
async def upload_service(cls, request: Request, file: UploadFile) -> Dict:
|
||||
"""
|
||||
通用上传service
|
||||
:param request: Request对象
|
||||
:param file: 上传文件对象
|
||||
:return: 上传结果字典
|
||||
"""
|
||||
filename, filepath = await UploadUtil.upload_file(file)
|
||||
|
||||
return UploadResponseSchema(
|
||||
file_path=f'{filepath}',
|
||||
file_name=filename,
|
||||
origin_name=file.filename,
|
||||
file_url=f'{request.base_url}{filepath}',
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def download_services(cls, file_name: str) -> Generator:
|
||||
"""
|
||||
下载下载目录文件service
|
||||
|
||||
:param background_tasks: 后台任务对象
|
||||
:param file_name: 下载的文件名称
|
||||
:param delete: 是否在下载完成后删除文件
|
||||
:return: 文件二进制流
|
||||
"""
|
||||
if '..' in file_name:
|
||||
raise CustomException(msg='文件名称不合法')
|
||||
|
||||
filepath = settings.DOWNLOAD_FILE_PATH.joinpath(file_name)
|
||||
if not UploadUtil.check_file_exists(filepath):
|
||||
raise CustomException(msg='文件不存在')
|
||||
|
||||
return UploadUtil.generate_file(filepath)
|
||||
|
||||
@classmethod
|
||||
async def download_resource_services(cls, resource: str) -> Generator:
|
||||
"""
|
||||
下载上传目录文件service
|
||||
:param resource: 下载的文件路径
|
||||
:return: 文件二进制流
|
||||
"""
|
||||
filepath = Path(resource).joinpath(settings.UPLOAD_FILE_PATH)
|
||||
filename = filepath.name
|
||||
|
||||
if '..' in filename:
|
||||
raise CustomException(msg='文件名称不合法')
|
||||
|
||||
if not all([
|
||||
UploadUtil.check_file_timestamp(filename),
|
||||
UploadUtil.check_file_machine(filename),
|
||||
UploadUtil.check_file_random_code(filename)
|
||||
]):
|
||||
raise CustomException(msg='文件名称不合法')
|
||||
|
||||
if not UploadUtil.check_file_exists(filepath):
|
||||
raise CustomException(msg='文件不存在')
|
||||
|
||||
return UploadUtil.generate_file(filepath)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.api.v1.schemas.monitor.cache_schema import CacheMonitorSchema, CacheInfoSchema
|
||||
from app.core.cache_crud import Cache
|
||||
|
||||
class CacheService:
|
||||
"""
|
||||
缓存监控模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_statistical_info_services(cls, request: Request)->dict:
|
||||
"""
|
||||
获取缓存监控信息service
|
||||
|
||||
:param request: Request对象
|
||||
:return: 缓存监控信息
|
||||
"""
|
||||
info = await Cache(request).info()
|
||||
db_size = await Cache(request).db_size()
|
||||
command_stats_dict = await Cache(request).commandstats()
|
||||
|
||||
command_stats = [
|
||||
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
|
||||
]
|
||||
result = CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info)
|
||||
|
||||
return result.model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_name_services(cls)->list:
|
||||
"""
|
||||
获取缓存名称列表信息service
|
||||
|
||||
:return: 缓存名称列表信息
|
||||
"""
|
||||
name_list = []
|
||||
for key_config in RedisInitKeyConfig:
|
||||
name_list.append(
|
||||
CacheInfoSchema(
|
||||
cache_key='',
|
||||
cache_name=key_config.key,
|
||||
cache_value='',
|
||||
remark=key_config.remark,
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
return name_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_key_services(cls, request: Request, cache_name: str)->list:
|
||||
"""
|
||||
获取缓存键名列表信息service
|
||||
|
||||
:param request: Request对象
|
||||
:param cache_name: 缓存名称
|
||||
:return: 缓存键名列表信息
|
||||
"""
|
||||
cache_keys = await Cache(request).get_keys(f'{cache_name}*')
|
||||
cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')]
|
||||
|
||||
return cache_key_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_value_services(cls, request: Request, cache_name: str, cache_key: str)->dict:
|
||||
"""
|
||||
获取缓存内容信息service
|
||||
|
||||
:param request: Request对象
|
||||
:param cache_name: 缓存名称
|
||||
:param cache_key: 缓存键名
|
||||
:return: 缓存内容信息
|
||||
"""
|
||||
cache_value = await Cache(request).get(f'{cache_name}:{cache_key}')
|
||||
|
||||
return CacheInfoSchema(cache_key=cache_key, cache_name=cache_name, cache_value=cache_value, remark='').model_dump()
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_name_services(cls, request: Request, cache_name: str)->bool:
|
||||
"""
|
||||
清除缓存名称对应所有键值service
|
||||
|
||||
:param request: Request对象
|
||||
:param cache_name: 缓存名称
|
||||
:return: 操作缓存响应信息
|
||||
"""
|
||||
cache_keys = await Cache(request).get_keys(f'{cache_name}*')
|
||||
if cache_keys:
|
||||
await Cache(request).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_key_services(cls, request: Request, cache_key: str)->bool:
|
||||
"""
|
||||
清除缓存名称对应所有键值service
|
||||
|
||||
:param request: Request对象
|
||||
:param cache_key: 缓存键名
|
||||
:return: 操作缓存响应信息
|
||||
"""
|
||||
cache_keys = await Cache(request).get_keys(f'*{cache_key}')
|
||||
if cache_keys:
|
||||
await Cache(request).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_all_services(cls, request: Request)->bool:
|
||||
"""
|
||||
清除所有缓存service
|
||||
|
||||
:param request: Request对象
|
||||
:return: 操作缓存响应信息
|
||||
"""
|
||||
cache_keys = await Cache(request).get_keys
|
||||
if cache_keys:
|
||||
await Cache(request).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from typing import Dict, List
|
||||
from fastapi import Request
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.params.monitor.online_param import OnlineQueryParams
|
||||
from app.api.v1.schemas.monitor.online_schema import OnlineOutSchema
|
||||
from app.core.cache_crud import Cache
|
||||
|
||||
class OnlineService:
|
||||
"""在线用户管理模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_online_list(cls, request: Request, search: OnlineQueryParams) -> List[Dict]:
|
||||
"""获取在线用户列表信息"""
|
||||
# 获取所有在线用户信息
|
||||
token_keys = await Cache(request).get_keys(f'{RedisInitKeyConfig.ONLINE_USER.key}*')
|
||||
if not token_keys:
|
||||
return []
|
||||
|
||||
# 批量获取在线用户信息
|
||||
online_values = await Cache(request).mget(*token_keys)
|
||||
online_list = []
|
||||
|
||||
for online_value in online_values:
|
||||
# 将字符串解析为字典
|
||||
online_data = json.loads(online_value)
|
||||
online_info = OnlineOutSchema(
|
||||
session_id=online_data['session_id'],
|
||||
user_id=online_data['user_id'],
|
||||
user_name=online_data['user_name'],
|
||||
ipaddr=online_data['ipaddr'],
|
||||
login_location=online_data['login_location'],
|
||||
os=online_data['os'],
|
||||
browser=online_data['browser'],
|
||||
login_time=online_data['login_time']
|
||||
).model_dump(mode='json') # 添加mode='json'参数以序列化datetime
|
||||
|
||||
if cls._match_search_conditions(online_info, search):
|
||||
online_list.append(online_info)
|
||||
|
||||
return online_list
|
||||
|
||||
@classmethod
|
||||
async def delete_online(cls, request: Request, ids: str) -> bool:
|
||||
"""强制下线在线用户"""
|
||||
if not ids:
|
||||
raise CustomException(msg='传入ids不能为空')
|
||||
|
||||
# 批量删除token
|
||||
token_ids = ids.split(',')
|
||||
for token_id in token_ids:
|
||||
await Cache(request).delete(f"{RedisInitKeyConfig.ONLINE_USER.key}:{token_id}")
|
||||
await Cache(request).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _match_search_conditions(online_info: Dict, search: OnlineQueryParams) -> bool:
|
||||
"""检查是否匹配搜索条件"""
|
||||
# 根据params中的定义,需要进行模糊匹配
|
||||
if search.user_name:
|
||||
search_name = search.user_name[1].strip('%') # 去掉like和%
|
||||
if search_name not in online_info['user_name']:
|
||||
return False
|
||||
|
||||
if search.login_location:
|
||||
search_location = search.login_location[1].strip('%')
|
||||
if search_location not in online_info['login_location']:
|
||||
return False
|
||||
|
||||
# ipaddr是精确匹配
|
||||
if search.ipaddr:
|
||||
if online_info['ipaddr'] != search.ipaddr[1]: # 取eq后面的值
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import psutil
|
||||
import socket
|
||||
import time
|
||||
from typing import List, Dict
|
||||
|
||||
from app.api.v1.schemas.monitor.server_schema import (
|
||||
CpuInfoSchema,
|
||||
MemoryInfoSchema,
|
||||
PyInfoSchema,
|
||||
ServerMonitorSchema,
|
||||
DiskInfoSchema,
|
||||
SysInfoSchema
|
||||
)
|
||||
from app.utils.common_util import bytes2human
|
||||
|
||||
|
||||
class ServerService:
|
||||
"""服务监控模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_server_monitor_info(cls) -> Dict:
|
||||
"""获取服务器监控信息"""
|
||||
return ServerMonitorSchema(
|
||||
cpu=cls._get_cpu_info().model_dump(),
|
||||
mem=cls._get_memory_info().model_dump(),
|
||||
sys=cls._get_system_info().model_dump(),
|
||||
py=cls._get_python_info().model_dump(),
|
||||
disks=cls._get_disk_info()
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
def _get_cpu_info(cls) -> CpuInfoSchema:
|
||||
"""获取CPU信息"""
|
||||
cpu_times = psutil.cpu_times_percent()
|
||||
return CpuInfoSchema(
|
||||
cpu_num=psutil.cpu_count(logical=True),
|
||||
used=cpu_times.user,
|
||||
sys=cpu_times.system,
|
||||
free=cpu_times.idle
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_memory_info(cls) -> MemoryInfoSchema:
|
||||
"""获取内存信息"""
|
||||
memory = psutil.virtual_memory()
|
||||
return MemoryInfoSchema(
|
||||
total=bytes2human(memory.total),
|
||||
used=bytes2human(memory.used),
|
||||
free=bytes2human(memory.free),
|
||||
usage=memory.percent
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_system_info(cls) -> SysInfoSchema:
|
||||
"""获取系统信息"""
|
||||
hostname = socket.gethostname()
|
||||
return SysInfoSchema(
|
||||
computer_ip=socket.gethostbyname(hostname),
|
||||
computer_name=platform.node(),
|
||||
os_arch=platform.machine(),
|
||||
os_name=platform.platform(),
|
||||
user_dir=str(Path.cwd())
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_python_info(cls) -> PyInfoSchema:
|
||||
"""获取Python解释器信息"""
|
||||
current_process = psutil.Process()
|
||||
memory = psutil.virtual_memory()
|
||||
process_memory = current_process.memory_info()
|
||||
|
||||
start_time = current_process.create_time()
|
||||
run_time = ServerService._calculate_run_time(start_time)
|
||||
|
||||
return PyInfoSchema(
|
||||
name=current_process.name(),
|
||||
version=platform.python_version(),
|
||||
start_time=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time)),
|
||||
run_time=run_time,
|
||||
home=str(Path(current_process.exe())),
|
||||
memory_total=bytes2human(memory.available),
|
||||
memory_used=bytes2human(process_memory.rss),
|
||||
memory_free=bytes2human(memory.available - process_memory.rss),
|
||||
memory_usage=round((process_memory.rss / memory.available) * 100, 2)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_disk_info(cls) -> List[Dict]:
|
||||
"""获取磁盘信息"""
|
||||
disk_info = []
|
||||
for partition in psutil.disk_partitions():
|
||||
try:
|
||||
# 使用mountpoint而不是device来获取磁盘使用情况
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
mount_point = str(Path(partition.mountpoint))
|
||||
disk_info.append(
|
||||
DiskInfoSchema(
|
||||
dir_name=mount_point, # 使用mountpoint替代device
|
||||
sys_type_name=partition.fstype,
|
||||
type_name=f'本地固定磁盘({mount_point})',
|
||||
total=bytes2human(usage.total),
|
||||
used=bytes2human(usage.used),
|
||||
free=bytes2human(usage.free),
|
||||
usage=usage.percent # 直接使用数字而不是字符串
|
||||
).model_dump()
|
||||
)
|
||||
except (PermissionError, FileNotFoundError):
|
||||
# 明确指定可能的异常
|
||||
continue
|
||||
return disk_info
|
||||
|
||||
@classmethod
|
||||
def _calculate_run_time(cls,start_time: float) -> str:
|
||||
"""计算运行时间"""
|
||||
difference = time.time() - start_time
|
||||
days = int(difference // (24 * 60 * 60))
|
||||
hours = int((difference % (24 * 60 * 60)) // (60 * 60))
|
||||
minutes = int((difference % (60 * 60)) // 60)
|
||||
return f'{days}天{hours}小时{minutes}分钟'
|
||||
@@ -0,0 +1,273 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, Union, NewType
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timedelta
|
||||
from user_agents import parse
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.logger import logger
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.schemas.monitor.online_schema import OnlineOutSchema
|
||||
from app.utils.common_util import get_random_character
|
||||
from app.utils.captcha_util import CaptchaUtil
|
||||
from app.api.v1.cruds.system.user_crud import UserCRUD
|
||||
from app.api.v1.models.system.user_model import UserModel
|
||||
from app.api.v1.schemas.system.auth_schema import (
|
||||
JWTPayloadSchema,
|
||||
JWTOutSchema,
|
||||
AuthSchema,
|
||||
CaptchaOutSchema,
|
||||
LogoutPayloadSchema,
|
||||
RefreshTokenPayloadSchema
|
||||
)
|
||||
from app.core.hash_bcrpy import PwdUtil
|
||||
from app.core.security import (
|
||||
CustomOAuth2PasswordRequestForm,
|
||||
create_access_token,
|
||||
decode_access_token
|
||||
)
|
||||
from app.utils.ip_local_util import IpLocalUtil
|
||||
from app.core.cache_crud import Cache
|
||||
|
||||
CaptchaKey = NewType('CaptchaKey', str)
|
||||
CaptchaBase64 = NewType('CaptchaBase64', str)
|
||||
|
||||
|
||||
class LoginService:
|
||||
"""登录认证服务"""
|
||||
|
||||
@classmethod
|
||||
async def authenticate_user(cls, request: Request, login_form: CustomOAuth2PasswordRequestForm, db: AsyncSession) -> UserModel:
|
||||
"""
|
||||
用户认证
|
||||
|
||||
Args:
|
||||
request: 请求对象
|
||||
login_form: 登录表单
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
UserModel: 认证通过的用户对象
|
||||
|
||||
Raises:
|
||||
CustomException: 认证失败时抛出异常
|
||||
"""
|
||||
# 判断是否来自API文档
|
||||
referer = request.headers.get('referer', '')
|
||||
request_from_docs = referer.endswith(('docs', 'redoc'))
|
||||
|
||||
# 验证码校验
|
||||
if settings.CAPTCHA_ENABLE and not request_from_docs:
|
||||
await CaptchaService.check_captcha(request=request, key=login_form.captcha_key, captcha=login_form.captcha)
|
||||
|
||||
# 用户认证
|
||||
auth = AuthSchema(db=db)
|
||||
user = await UserCRUD(auth).get_user_by_username(username=login_form.username)
|
||||
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
if not PwdUtil.verify_password(plain_password=login_form.password, password_hash=user.password):
|
||||
logger.warning(f'用户 {login_form.username} 密码错误')
|
||||
raise CustomException(msg="密码错误")
|
||||
|
||||
if not user.available:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
# 更新最后登录时间
|
||||
user = await UserCRUD(auth).update_user_last_login(id=user.id)
|
||||
|
||||
# 创建token
|
||||
token = await cls.create_token(request=request, username=user.username)
|
||||
user_agent = parse(request.headers.get("user-agent"))
|
||||
|
||||
# 缓存中构建在线用户信息
|
||||
await Cache(request).set(
|
||||
key=f"{RedisInitKeyConfig.ONLINE_USER.key}:{user.username}",
|
||||
value=OnlineOutSchema(
|
||||
session_id=token.access_token,
|
||||
user_id=user.id,
|
||||
user_name=user.name,
|
||||
ipaddr=request.client.host,
|
||||
login_location=IpLocalUtil.get_ip_location(request.client.host),
|
||||
os=user_agent.os.family,
|
||||
browser = user_agent.browser.family,
|
||||
login_time=user.last_login
|
||||
).model_dump_json(),
|
||||
expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
async def create_token(cls, request: Request, username: str) -> JWTOutSchema:
|
||||
"""
|
||||
创建访问令牌和刷新令牌
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
request: 请求对象
|
||||
|
||||
Returns:
|
||||
JWTOutSchema: 包含访问令牌和刷新令牌的响应对象
|
||||
"""
|
||||
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
refresh_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
now = datetime.now()
|
||||
access_token = create_access_token(payload=JWTPayloadSchema(
|
||||
sub=username,
|
||||
is_refresh=False,
|
||||
exp=now + access_expires,
|
||||
))
|
||||
refresh_token = create_access_token(payload=JWTPayloadSchema(
|
||||
sub=username,
|
||||
is_refresh=True,
|
||||
exp=now + refresh_expires,
|
||||
))
|
||||
|
||||
# 清除该用户之前的token
|
||||
await Cache(request).delete(key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}')
|
||||
await Cache(request).delete(key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}')
|
||||
|
||||
# 设置新的token
|
||||
await Cache(request).set(
|
||||
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}',
|
||||
value=access_token,
|
||||
expire=int(access_expires.total_seconds())
|
||||
)
|
||||
|
||||
await Cache(request).set(
|
||||
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}',
|
||||
value=refresh_token,
|
||||
expire=int(refresh_expires.total_seconds())
|
||||
)
|
||||
|
||||
return JWTOutSchema(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=access_expires.total_seconds(),
|
||||
token_type=settings.TOKEN_TYPE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def refresh_token(cls, request: Request, refresh_token: RefreshTokenPayloadSchema) -> JWTOutSchema:
|
||||
"""
|
||||
刷新访问令牌
|
||||
|
||||
Args:
|
||||
refresh_token: 刷新令牌
|
||||
|
||||
Returns:
|
||||
JWTOutSchema: 新的令牌对象
|
||||
|
||||
Raises:
|
||||
CustomException: 刷新令牌无效时抛出异常
|
||||
"""
|
||||
token_payload: JWTPayloadSchema = decode_access_token(refresh_token.refresh_token)
|
||||
if not token_payload.is_refresh:
|
||||
raise CustomException(msg="非法凭证")
|
||||
|
||||
return await cls.create_token(request=request, username=token_payload.sub)
|
||||
|
||||
@classmethod
|
||||
async def logout_services(cls, request: Request, token: LogoutPayloadSchema) -> bool:
|
||||
"""
|
||||
退出登录
|
||||
|
||||
Args:
|
||||
request: 请求对象
|
||||
token: 令牌
|
||||
|
||||
Returns:
|
||||
bool: 退出成功返回True
|
||||
"""
|
||||
payload: JWTPayloadSchema = decode_access_token(token.token)
|
||||
username: str = payload.sub
|
||||
|
||||
# 删除Redis中的token
|
||||
await Cache(request).clear()
|
||||
|
||||
logger.info(f"用户退出登录成功,会话编号:{username}")
|
||||
return True
|
||||
|
||||
|
||||
class CaptchaService:
|
||||
"""验证码服务"""
|
||||
|
||||
@classmethod
|
||||
async def get_captcha(cls, request: Request) -> Dict[str, Union[CaptchaKey, CaptchaBase64]]:
|
||||
"""
|
||||
获取验证码
|
||||
|
||||
Args:
|
||||
request: 请求对象
|
||||
|
||||
Returns:
|
||||
Dict: 包含验证码key和base64图片的字典
|
||||
|
||||
Raises:
|
||||
CustomException: 验证码服务未启用时抛出异常
|
||||
"""
|
||||
if not settings.CAPTCHA_ENABLE:
|
||||
raise CustomException(msg="未开启验证码服务")
|
||||
|
||||
# 生成验证码图片和值
|
||||
captcha_base64, captcha_value = CaptchaUtil.captcha_arithmetic()
|
||||
captcha_key = get_random_character()
|
||||
|
||||
# 保存到Redis并设置过期时间
|
||||
redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{captcha_key}"
|
||||
await Cache(request).set(
|
||||
key=redis_key,
|
||||
value=captcha_value,
|
||||
expire=settings.CAPTCHA_EXPIRE_SECONDS
|
||||
)
|
||||
|
||||
logger.info(f"生成验证码成功,验证码:{captcha_value}")
|
||||
|
||||
# 返回验证码信息
|
||||
return CaptchaOutSchema(
|
||||
key=CaptchaKey(captcha_key),
|
||||
img_base=CaptchaBase64(f"data:image/png;base64,{captcha_base64}")
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def check_captcha(cls, request: Request, key: str, captcha: str) -> bool:
|
||||
"""
|
||||
校验验证码
|
||||
|
||||
Args:
|
||||
request: 请求对象
|
||||
key: 验证码key
|
||||
captcha: 用户输入的验证码
|
||||
|
||||
Returns:
|
||||
bool: 验证通过返回True
|
||||
|
||||
Raises:
|
||||
CustomException: 验证码无效或错误时抛出异常
|
||||
"""
|
||||
if not captcha:
|
||||
raise CustomException(msg="验证码不能为空")
|
||||
|
||||
# 获取Redis中存储的验证码
|
||||
redis_key = f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}'
|
||||
|
||||
captcha_value = await Cache(request).get(redis_key)
|
||||
if not captcha_value:
|
||||
logger.warning('验证码已过期或不存在')
|
||||
raise CustomException(msg="验证码已过期")
|
||||
|
||||
# 验证码不区分大小写比对
|
||||
if captcha.lower() != captcha_value.lower():
|
||||
logger.warning(f'验证码错误,用户输入:{captcha},正确值:{captcha_value}')
|
||||
raise CustomException(msg="验证码错误")
|
||||
|
||||
# 验证成功后删除验证码,避免重复使用
|
||||
await Cache(request).delete(key=redis_key)
|
||||
logger.info(f'验证码校验成功,key:{key}')
|
||||
return True
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
from app.api.v1.cruds.system.dept_crud import DeptCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.dept_schema import (
|
||||
DeptCreateSchema,
|
||||
DeptUpdateSchema,
|
||||
DeptOutSchema
|
||||
)
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.utils.common_util import (
|
||||
get_parent_id_map,
|
||||
get_parent_recursion,
|
||||
get_child_id_map,
|
||||
get_child_recursion
|
||||
)
|
||||
from app.api.v1.params.system.dept_param import DeptQueryParams
|
||||
|
||||
|
||||
class DeptService:
|
||||
"""
|
||||
部门管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_dept_detail_services(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""
|
||||
获取部门详情service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param id: 部门ID
|
||||
:return: 部门详情对象
|
||||
"""
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=id)
|
||||
return DeptOutSchema.model_validate(dept).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_dept_list_services(cls, auth: AuthSchema, search: DeptQueryParams, order_by: List[Dict] = None) -> List[Dict]:
|
||||
"""
|
||||
获取部门列表service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param search: 查询参数对象
|
||||
:param order_by: 排序参数
|
||||
:return: 部门列表对象
|
||||
"""
|
||||
order_by = order_by if order_by else [{"order": "asc"}]
|
||||
dept_list = await DeptCRUD(auth).get_dept_list(search=search.__dict__, order_by=order_by)
|
||||
return [DeptOutSchema.model_validate(dept).model_dump() for dept in dept_list]
|
||||
|
||||
@classmethod
|
||||
async def create_dept_services(cls, auth: AuthSchema, data: DeptCreateSchema) -> Dict:
|
||||
"""
|
||||
创建部门service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param data: 部门创建对象
|
||||
:return: 新创建的部门对象
|
||||
"""
|
||||
dept = await DeptCRUD(auth).create(data=data)
|
||||
return DeptOutSchema.model_validate(dept).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_dept_services(cls, auth: AuthSchema, data: DeptUpdateSchema) -> Dict:
|
||||
"""
|
||||
更新部门service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param data: 部门更新对象
|
||||
:return: 更新后的部门对象
|
||||
"""
|
||||
dept = await DeptCRUD(auth).update(id=data.id, data=data)
|
||||
if data.available:
|
||||
await cls.batch_set_available_services(auth=auth, data=BatchSetAvailable(ids=[data.id], available=True))
|
||||
else:
|
||||
await cls.batch_set_available_services(auth=auth, data=BatchSetAvailable(ids=[data.id], available=False))
|
||||
return DeptOutSchema.model_validate(dept).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_dept_services(cls, auth: AuthSchema, id: int) -> None:
|
||||
"""
|
||||
删除部门service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param id: 部门ID
|
||||
"""
|
||||
await DeptCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def batch_set_available_services(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量设置部门可用状态service
|
||||
|
||||
:param auth: 认证对象
|
||||
:param data: 批量设置可用状态对象
|
||||
"""
|
||||
dept_list = await DeptCRUD(auth).get_dept_list()
|
||||
total_ids = []
|
||||
|
||||
if data.available:
|
||||
id_map = get_parent_id_map(model_list=dept_list)
|
||||
for dept_id in data.ids:
|
||||
enable_ids = get_parent_recursion(id=dept_id, id_map=id_map)
|
||||
total_ids.extend(enable_ids)
|
||||
else:
|
||||
id_map = get_child_id_map(model_list=dept_list)
|
||||
for dept_id in data.ids:
|
||||
disable_ids = get_child_recursion(id=dept_id, id_map=id_map)
|
||||
total_ids.extend(disable_ids)
|
||||
|
||||
await DeptCRUD(auth).set_dept_available(ids=total_ids, available=data.available)
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
from app.api.v1.cruds.system.menu_crud import MenuCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.menu_schema import (
|
||||
MenuCreateSchema,
|
||||
MenuUpdateSchema,
|
||||
MenuOutSchema
|
||||
)
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.utils.common_util import (
|
||||
get_parent_id_map,
|
||||
get_parent_recursion,
|
||||
get_child_id_map,
|
||||
get_child_recursion
|
||||
)
|
||||
from app.api.v1.params.system.menu_param import MenuQueryParams
|
||||
|
||||
|
||||
class MenuService:
|
||||
"""
|
||||
菜单模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_menu_detail(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
menu = await MenuCRUD(auth).get_menu_by_id(id=id)
|
||||
menu_dict = MenuOutSchema.model_validate(menu).model_dump()
|
||||
return menu_dict
|
||||
|
||||
@classmethod
|
||||
async def get_menu_list(cls, auth: AuthSchema, search: MenuQueryParams, order_by: List[Dict] = None) -> List[Dict]:
|
||||
order_by = order_by if order_by else [{"order": "asc"}]
|
||||
menu_list = await MenuCRUD(auth).get_menu_list(search=search.__dict__, order_by=order_by)
|
||||
menu_dict_list = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_list]
|
||||
return menu_dict_list
|
||||
|
||||
@classmethod
|
||||
async def create_menu(cls, auth: AuthSchema, data: MenuCreateSchema) -> Dict:
|
||||
if data.parent_id:
|
||||
parent_menu = await MenuCRUD(auth).get_menu_by_id(id=data.parent_id)
|
||||
data.parent_name = parent_menu.name
|
||||
new_menu = await MenuCRUD(auth).create(data=data)
|
||||
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
|
||||
return new_menu_dict
|
||||
|
||||
@classmethod
|
||||
async def update_menu(cls, auth: AuthSchema, data: MenuUpdateSchema) -> Dict:
|
||||
if data.parent_id:
|
||||
parent_menu = await MenuCRUD(auth).get_menu_by_id(id=data.parent_id)
|
||||
data.parent_name = parent_menu.name
|
||||
new_menu = await MenuCRUD(auth).update(id=data.id, data=data)
|
||||
|
||||
await cls.set_menu_available(auth=auth, data=BatchSetAvailable(ids=[data.id], available=data.available))
|
||||
|
||||
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
|
||||
return new_menu_dict
|
||||
|
||||
@classmethod
|
||||
async def delete_menu(cls, auth: AuthSchema, id: int) -> None:
|
||||
await MenuCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def set_menu_available(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
递归获取所有父、子级菜单,然后批量修改菜单可用状态
|
||||
"""
|
||||
menu_list = await MenuCRUD(auth).get_menu_list()
|
||||
total_ids = []
|
||||
|
||||
if data.available:
|
||||
# 激活,则需要把所有父级菜单都激活
|
||||
id_map = get_parent_id_map(model_list=menu_list)
|
||||
for menu_id in data.ids:
|
||||
enable_ids = get_parent_recursion(id=menu_id, id_map=id_map)
|
||||
total_ids.extend(enable_ids)
|
||||
else:
|
||||
# 禁止,则需要把所有子级菜单都禁止
|
||||
id_map = get_child_id_map(model_list=menu_list)
|
||||
for menu_id in data.ids:
|
||||
disable_ids = get_child_recursion(id=menu_id, id_map=id_map)
|
||||
total_ids.extend(disable_ids)
|
||||
|
||||
await MenuCRUD(auth).set_menu_available(ids=total_ids, available=data.available)
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, List, Dict
|
||||
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.notice_schema import NoticeCreateSchema, NoticeUpdateSchema, NoticeOutSchema
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.api.v1.params.system.notice_param import NoticeQueryParams
|
||||
from app.api.v1.cruds.system.notice_crud import NoticeCRUD
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
|
||||
class NoticeService:
|
||||
"""
|
||||
公告管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_notice_detail_services(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
config_obj = await NoticeCRUD(auth).get_notice_by_id(id=id)
|
||||
return NoticeOutSchema.model_validate(config_obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_notice_list_services(cls, auth: AuthSchema, search: NoticeQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
|
||||
config_obj_list = await NoticeCRUD(auth).get_notice_list(search=search.__dict__, order_by=order_by)
|
||||
return [NoticeOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_notice_services(cls, auth: AuthSchema, data: NoticeCreateSchema) -> Dict:
|
||||
config_obj = await NoticeCRUD(auth).create_notice(data=data)
|
||||
return NoticeOutSchema.model_validate(config_obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_notice_services(cls, auth: AuthSchema, data: NoticeUpdateSchema) -> Dict:
|
||||
config_obj = await NoticeCRUD(auth).update_notice(id=data.id, data=data)
|
||||
return NoticeOutSchema.model_validate(config_obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_notice_services(cls, auth: AuthSchema, id: int) -> None:
|
||||
await NoticeCRUD(auth).delete_notice(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def set_notice_available_services(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
await NoticeCRUD(auth).set_notice_available(ids=data.ids, available=data.available)
|
||||
|
||||
@classmethod
|
||||
async def export_notice_services(cls, notice_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""导出公告列表"""
|
||||
mapping_dict = {
|
||||
'id': '公告编号',
|
||||
'notice_title': '公告标题',
|
||||
'notice_type': '公告类型(1通知 2公告)',
|
||||
'notice_content': '公告内容',
|
||||
'available': '状态',
|
||||
'description': '备注',
|
||||
'created_at': '创建时间',
|
||||
'updated_at': '更新时间',
|
||||
'creator_id': '创建者ID',
|
||||
'creator': '创建者',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = notice_list.copy()
|
||||
for item in data:
|
||||
item['available'] = '正常' if item.get('available') else '停用'
|
||||
|
||||
# 转换为中文键
|
||||
new_data = [
|
||||
{mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)}
|
||||
for item in data
|
||||
]
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=new_data)
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
from app.api.v1.cruds.system.operation_log_crud import OperationLogCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.operation_log_schema import (
|
||||
OperationLogCreateSchema,
|
||||
OperationLogOutSchema
|
||||
)
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.api.v1.params.system.operation_log_param import OperationLogQueryParams
|
||||
|
||||
|
||||
class OperationLogService:
|
||||
"""
|
||||
日志模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_log_detail(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""获取日志详情"""
|
||||
log = await OperationLogCRUD(auth).get_operation_log_by_id(id=id)
|
||||
log_dict = OperationLogOutSchema.model_validate(log).model_dump()
|
||||
return log_dict
|
||||
|
||||
@classmethod
|
||||
async def get_log_list(cls, auth: AuthSchema, search: OperationLogQueryParams, order_by: List[Dict] = None) -> List[Dict]:
|
||||
"""获取日志列表"""
|
||||
order_by = order_by if order_by else [{"created_at": "desc"}]
|
||||
log_list = await OperationLogCRUD(auth).get_operation_log_list(search=search.__dict__, order_by=order_by)
|
||||
log_dict_list = [OperationLogOutSchema.model_validate(log).model_dump() for log in log_list]
|
||||
return log_dict_list
|
||||
|
||||
@classmethod
|
||||
async def create_log(cls, auth: AuthSchema, data: OperationLogCreateSchema) -> Dict:
|
||||
"""创建日志"""
|
||||
new_log = await OperationLogCRUD(auth).create(data=data)
|
||||
new_log_dict = OperationLogOutSchema.model_validate(new_log).model_dump()
|
||||
return new_log_dict
|
||||
|
||||
@classmethod
|
||||
async def delete_log(cls, auth: AuthSchema, id: int) -> None:
|
||||
"""删除日志"""
|
||||
await OperationLogCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def export_log_list(cls, operation_log_list: List) -> bytes:
|
||||
"""
|
||||
导出日志信息
|
||||
|
||||
Args:
|
||||
operation_log_list: 操作日志信息列表
|
||||
|
||||
Returns:
|
||||
bytes: 操作日志信息excel的二进制数据
|
||||
"""
|
||||
# 操作日志字段映射
|
||||
mapping_dict = {
|
||||
'id': '日志编号',
|
||||
'description': '系统模块',
|
||||
'request_method': '请求方式',
|
||||
'request_path': '请求URL',
|
||||
'request_ip': '操作地址',
|
||||
'request_location': '操作地点',
|
||||
'request_payload': '请求参数',
|
||||
'response_json': '返回参数',
|
||||
'response_code': '操作状态',
|
||||
'created_at': '操作日期',
|
||||
'const_time': '消耗时间',
|
||||
'request_os': '操作系统',
|
||||
'request_browser': '浏览器'
|
||||
}
|
||||
|
||||
# 处理数据
|
||||
data = operation_log_list.copy()
|
||||
for item in data:
|
||||
# 处理状态
|
||||
item['response_code'] = '成功' if item.get('response_code') == 200 else '失败'
|
||||
|
||||
# 转换为中文键
|
||||
new_data = [
|
||||
{mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)}
|
||||
for item in data
|
||||
]
|
||||
|
||||
return ExcelUtil.export_list2excel(new_data)
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from app.api.v1.cruds.system.position_crud import PositionCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.position_schema import (
|
||||
PositionCreateSchema,
|
||||
PositionUpdateSchema,
|
||||
PositionOutSchema,
|
||||
)
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.api.v1.params.system.position_param import PositionQueryParams
|
||||
|
||||
class PositionService:
|
||||
"""岗位模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_position_detail(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""获取岗位详情"""
|
||||
position = await PositionCRUD(auth).get_position_by_id(id=id)
|
||||
return PositionOutSchema.model_validate(position).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_position_list(cls, auth: AuthSchema, search: PositionQueryParams, order_by: List[Dict] = None) -> List[Dict]:
|
||||
"""获取岗位列表"""
|
||||
order_by = order_by if order_by else [{"order": "asc"}]
|
||||
position_list = await PositionCRUD(auth).get_position_list(search=search.__dict__, order_by=order_by)
|
||||
return [PositionOutSchema.model_validate(position).model_dump() for position in position_list]
|
||||
|
||||
@classmethod
|
||||
async def create_position(cls, auth: AuthSchema, data: PositionCreateSchema) -> Dict:
|
||||
"""创建岗位"""
|
||||
new_position = await PositionCRUD(auth).create(data=data)
|
||||
return PositionOutSchema.model_validate(new_position).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_position(cls, auth: AuthSchema, data: PositionUpdateSchema) -> Dict:
|
||||
"""更新岗位"""
|
||||
updated_position = await PositionCRUD(auth).update(id=data.id, data=data)
|
||||
return PositionOutSchema.model_validate(updated_position).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_position(cls, auth: AuthSchema, id: int) -> None:
|
||||
"""删除岗位"""
|
||||
await PositionCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def set_position_available(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""设置岗位状态"""
|
||||
await PositionCRUD(auth).set_position_available(ids=data.ids, available=data.available)
|
||||
|
||||
@classmethod
|
||||
async def export_post_list(cls, post_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""导出岗位列表"""
|
||||
mapping_dict = {
|
||||
'id': '岗位编号',
|
||||
'name': '岗位名称',
|
||||
'order': '显示顺序',
|
||||
'available': '状态',
|
||||
'creator': '创建者',
|
||||
'create_datetime': '创建时间',
|
||||
'modifier': '更新者',
|
||||
'update_datetime': '更新时间',
|
||||
'description': '备注',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = post_list.copy()
|
||||
for item in data:
|
||||
item['available'] = '正常' if item.get('available') else '停用'
|
||||
|
||||
# 转换为中文键
|
||||
new_data = [
|
||||
{mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)}
|
||||
for item in data
|
||||
]
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=new_data)
|
||||
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from app.api.v1.cruds.system.dept_crud import DeptCRUD
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.utils.common_util import get_child_id_map, get_child_recursion, get_parent_id_map, get_parent_recursion
|
||||
from app.api.v1.cruds.system.role_crud import RoleCRUD, MenuCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.role_schema import (
|
||||
RoleCreateSchema,
|
||||
RoleUpdateSchema,
|
||||
RolePermissionSettingSchema,
|
||||
RoleOutSchema
|
||||
)
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.api.v1.params.system.role_param import RoleQueryParams
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""角色模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_role_detail(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""获取角色详情"""
|
||||
role = await RoleCRUD(auth).get_role_by_id(id=id)
|
||||
return RoleOutSchema.model_validate(role).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_role_list(cls, auth: AuthSchema, search: RoleQueryParams, order_by: List[Dict[str, str]] = None) -> List[Dict]:
|
||||
"""获取角色列表"""
|
||||
order_by = order_by if order_by else [{"order": "asc"}]
|
||||
role_list = await RoleCRUD(auth).get_role_list(search=search.__dict__, order_by=order_by)
|
||||
return [RoleOutSchema.model_validate(role).model_dump() for role in role_list]
|
||||
|
||||
@classmethod
|
||||
async def create_role(cls, auth: AuthSchema, data: RoleCreateSchema) -> Dict:
|
||||
"""创建角色"""
|
||||
new_role = await RoleCRUD(auth).create(data=data)
|
||||
return RoleOutSchema.model_validate(new_role).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_role(cls, auth: AuthSchema, data: RoleUpdateSchema) -> Dict:
|
||||
"""更新角色"""
|
||||
updated_role = await RoleCRUD(auth).update(id=data.id, data=data)
|
||||
return RoleOutSchema.model_validate(updated_role).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_role(cls, auth: AuthSchema, id: int) -> None:
|
||||
"""删除角色"""
|
||||
await RoleCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def set_role_permission(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None:
|
||||
"""设置角色权限"""
|
||||
# 设置角色菜单权限
|
||||
await RoleCRUD(auth).set_role_menus(role_ids=data.role_ids, menu_ids=data.menu_ids)
|
||||
|
||||
# 设置数据权限范围
|
||||
await RoleCRUD(auth).set_role_data_scope(role_ids=data.role_ids, data_scope=data.data_scope)
|
||||
|
||||
# 设置自定义数据权限部门
|
||||
if data.data_scope == 5 and data.dept_ids:
|
||||
await RoleCRUD(auth).set_role_depts(role_ids=data.role_ids, dept_ids=data.dept_ids)
|
||||
else:
|
||||
await RoleCRUD(auth).set_role_depts(role_ids=data.role_ids, dept_ids=[])
|
||||
|
||||
@classmethod
|
||||
async def set_role_available(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""设置角色可用状态"""
|
||||
await RoleCRUD(auth).set_role_available(ids=data.ids, available=data.available)
|
||||
|
||||
@classmethod
|
||||
async def export_role_list(cls, role_list: List) -> bytes:
|
||||
"""导出角色列表"""
|
||||
# 字段映射配置
|
||||
mapping_dict = {
|
||||
'id': '角色编号',
|
||||
'name': '角色名称',
|
||||
'order': '显示顺序',
|
||||
'data_scope': '数据权限',
|
||||
'available': '状态',
|
||||
'creator': '创建者',
|
||||
'create_datetime': '创建时间',
|
||||
'modifier': '更新者',
|
||||
'update_datetime': '更新时间',
|
||||
'description': '备注'
|
||||
}
|
||||
|
||||
# 数据权限映射
|
||||
data_scope_map = {
|
||||
1: '仅本人数据权限',
|
||||
2: '本部门数据权限',
|
||||
3: '本部门及以下数据权限',
|
||||
4: '全部数据权限',
|
||||
5: '自定义数据权限'
|
||||
}
|
||||
|
||||
# 处理数据
|
||||
data = role_list.copy()
|
||||
for item in data:
|
||||
item['available'] = '正常' if item.get('available') else '停用'
|
||||
item['data_scope'] = data_scope_map.get(item.get('data_scope'))
|
||||
|
||||
# 转换为中文键
|
||||
new_data = [
|
||||
{mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)}
|
||||
for item in data
|
||||
]
|
||||
|
||||
return ExcelUtil.export_list2excel(new_data)
|
||||
@@ -0,0 +1,394 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import Request, UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.hash_bcrpy import PwdUtil
|
||||
from app.api.v1.cruds.system.position_crud import PositionCRUD
|
||||
from app.api.v1.cruds.system.role_crud import RoleCRUD
|
||||
from app.api.v1.schemas.common.common_schema import UploadResponseSchema
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.utils.upload_util import UploadUtil
|
||||
from app.api.v1.cruds.system.user_crud import UserCRUD
|
||||
from app.api.v1.cruds.system.menu_crud import MenuCRUD
|
||||
from app.api.v1.cruds.system.dept_crud import DeptCRUD
|
||||
from app.api.v1.schemas.system.auth_schema import AuthSchema
|
||||
from app.api.v1.schemas.system.menu_schema import MenuOutSchema
|
||||
from app.api.v1.schemas.system.user_schema import (
|
||||
CurrentUserUpdateSchema,
|
||||
UserOutSchema,
|
||||
UserCreateSchema,
|
||||
UserUpdateSchema,
|
||||
UserChangePasswordSchema,
|
||||
UserRegisterSchema,
|
||||
UserForgetPasswordSchema
|
||||
)
|
||||
from app.api.v1.params.system.user_param import UserQueryParams
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_detail_by_id(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""获取用户详情"""
|
||||
user = await UserCRUD(auth).get_user_by_id(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
# 如果用户绑定了部门,则获取部门名称
|
||||
if user.dept_id:
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=user.dept_id)
|
||||
user.dept_name = dept.name if dept else None
|
||||
else:
|
||||
user.dept_name = None
|
||||
|
||||
return UserOutSchema.model_validate(user).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_user_list(cls, search: UserQueryParams, order_by: List[Dict], auth: AuthSchema) -> List[Dict]:
|
||||
user_list = await UserCRUD(auth).get_user_list(search=search.__dict__, order_by=order_by)
|
||||
user_dict_list = []
|
||||
for user in user_list:
|
||||
if user.dept_id:
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=user.dept_id)
|
||||
user.dept_name = dept.name if dept else None
|
||||
else:
|
||||
user.dept_name = None
|
||||
user_dict = UserOutSchema.model_validate(user).model_dump()
|
||||
user_dict_list.append(user_dict)
|
||||
|
||||
return user_dict_list
|
||||
|
||||
@classmethod
|
||||
async def create_user(cls, data: UserCreateSchema, auth: AuthSchema) -> Dict:
|
||||
# 检查用户名是否存在
|
||||
user = await UserCRUD(auth).get_user_by_username(username=data.username)
|
||||
if user:
|
||||
raise CustomException(msg='已存在相同用户名称的账号')
|
||||
|
||||
# 检查部门是否存在
|
||||
if data.dept_id:
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=data.dept_id)
|
||||
if not dept:
|
||||
raise CustomException(msg='部门不存在')
|
||||
|
||||
# 创建用户
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
|
||||
new_user = await UserCRUD(auth).create(data=user_dict)
|
||||
|
||||
# 设置角色和岗位
|
||||
if data.role_ids and len(data.role_ids) > 0:
|
||||
await UserCRUD(auth).set_user_roles(user_ids=[new_user.id], role_ids=data.role_ids)
|
||||
if data.position_ids and len(data.position_ids) > 0:
|
||||
await UserCRUD(auth).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids)
|
||||
|
||||
new_user_dict = UserOutSchema.model_validate(new_user).model_dump()
|
||||
return new_user_dict
|
||||
|
||||
@classmethod
|
||||
async def update_user(cls, data: UserUpdateSchema, auth: AuthSchema) -> Dict:
|
||||
# 检查用户是否存在
|
||||
user = await UserCRUD(auth).get_user_by_id(id=data.id)
|
||||
if not user:
|
||||
raise CustomException(msg='用户不存在')
|
||||
|
||||
# 检查用户名是否重复
|
||||
exist_user = await UserCRUD(auth).get_user_by_username(username=data.username)
|
||||
if exist_user and exist_user.id != data.id:
|
||||
raise CustomException(msg='已存在相同的用户名')
|
||||
|
||||
# 检查部门是否存在且可用
|
||||
if data.dept_id:
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=data.dept_id)
|
||||
if not dept:
|
||||
raise CustomException(msg='部门不存在')
|
||||
if not dept.available:
|
||||
raise CustomException(msg='部门已被禁用')
|
||||
|
||||
# 更新密码
|
||||
if data.password:
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
|
||||
# 更新用户
|
||||
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
|
||||
new_user = await UserCRUD(auth).update(id=data.id, data=user_dict)
|
||||
|
||||
# 更新角色和岗位
|
||||
if data.role_ids and len(data.role_ids) > 0:
|
||||
# 检查角色是否都存在且可用
|
||||
roles = await RoleCRUD(auth).get_role_list(search={"id": ("in", data.role_ids)})
|
||||
if len(roles) != len(data.role_ids):
|
||||
raise CustomException(msg='部分角色不存在')
|
||||
if not all(role.available for role in roles):
|
||||
raise CustomException(msg='部分角色已被禁用')
|
||||
await UserCRUD(auth).set_user_roles(user_ids=[data.id], role_ids=data.role_ids)
|
||||
|
||||
if data.position_ids and len(data.position_ids) > 0:
|
||||
# 检查岗位是否都存在且可用
|
||||
positions = await PositionCRUD(auth).get_position_list(search={"id": ("in", data.position_ids)})
|
||||
if len(positions) != len(data.position_ids):
|
||||
raise CustomException(msg='部分岗位不存在')
|
||||
if not all(position.available for position in positions):
|
||||
raise CustomException(msg='部分岗位已被禁用')
|
||||
await UserCRUD(auth).set_user_positions(user_ids=[data.id], position_ids=data.position_ids)
|
||||
|
||||
user_dict = UserOutSchema.model_validate(new_user).model_dump()
|
||||
return user_dict
|
||||
|
||||
@classmethod
|
||||
async def delete_user(cls, auth: AuthSchema, id: int) -> None:
|
||||
"""删除用户"""
|
||||
user = await UserCRUD(auth).get_user_by_id(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员不能删除")
|
||||
|
||||
# 删除用户角色关联数据
|
||||
await UserCRUD(auth).set_user_roles(user_ids=[id], role_ids=[])
|
||||
|
||||
# 删除用户岗位关联数据
|
||||
await UserCRUD(auth).set_user_positions(user_ids=[id], position_ids=[])
|
||||
|
||||
# 删除用户
|
||||
await UserCRUD(auth).delete(ids=[id])
|
||||
|
||||
@classmethod
|
||||
async def get_current_user_info(cls, auth: AuthSchema) -> Dict:
|
||||
"""获取当前用户信息"""
|
||||
# 获取用户基本信息
|
||||
user = await UserCRUD(auth).get_user_by_id(id=auth.user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=user.dept_id)
|
||||
user.dept_name = dept.name if dept else None
|
||||
user_dict = UserOutSchema.model_validate(user).model_dump()
|
||||
|
||||
# 获取菜单权限
|
||||
if auth.user.is_superuser:
|
||||
menu_all = await MenuCRUD(auth).get_menu_list(search={'type': ('in', [1, 2]), 'available': True})
|
||||
menus = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_all]
|
||||
else:
|
||||
menus = [
|
||||
MenuOutSchema.model_validate(menu).model_dump()
|
||||
for role in auth.user.roles
|
||||
for menu in role.menus
|
||||
if menu.available and menu.type in [1, 2]
|
||||
]
|
||||
user_dict["menus"] = menus
|
||||
return user_dict
|
||||
|
||||
@classmethod
|
||||
async def update_current_user_info(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> Dict:
|
||||
"""更新当前用户信息"""
|
||||
user = await UserCRUD(auth).get_user_by_id(id=auth.user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
new_user = await UserCRUD(auth).update(id=auth.user.id, data=data)
|
||||
return UserOutSchema.model_validate(new_user).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def set_user_available(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""设置用户状态"""
|
||||
for id in data.ids:
|
||||
user = await UserCRUD(auth).get_user_by_id(id=id)
|
||||
if not user:
|
||||
raise CustomException(msg=f"用户ID {id} 不存在")
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员状态不能修改")
|
||||
await UserCRUD(auth).set_user_available(ids=data.ids, available=data.available)
|
||||
|
||||
@classmethod
|
||||
async def upload_avatar(cls, request: Request, file: UploadFile) -> Dict:
|
||||
"""上传头像"""
|
||||
if not file:
|
||||
raise CustomException(msg="请选择要上传的文件")
|
||||
filename, filepath = await UploadUtil.upload_file(file=file)
|
||||
|
||||
return UploadResponseSchema(
|
||||
file_path=f'{filepath}',
|
||||
file_name=filename,
|
||||
origin_name=file.filename,
|
||||
file_url=f'{request.base_url}{filepath}',
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def change_user_password(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> Dict:
|
||||
"""修改用户密码"""
|
||||
if not data.old_password or not data.new_password:
|
||||
raise CustomException(msg='密码不能为空')
|
||||
|
||||
# 验证原密码
|
||||
user = await UserCRUD(auth).get_user_by_id(id=auth.user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
if not PwdUtil.verify_password(plain_password=data.old_password, password_hash=user.password):
|
||||
raise CustomException(msg='原密码输入错误')
|
||||
|
||||
# 更新密码
|
||||
new_password_hash = PwdUtil.set_password_hash(password=data.new_password)
|
||||
new_user = await UserCRUD(auth).change_password(id=user.id, password_hash=new_password_hash)
|
||||
return UserOutSchema.model_validate(new_user).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def register_user(cls, auth: AuthSchema, data: UserRegisterSchema) -> Dict:
|
||||
"""用户注册"""
|
||||
# 检查用户名是否存在
|
||||
user = await UserCRUD(auth).get_user_by_username(username=data.username)
|
||||
if user:
|
||||
raise CustomException(msg='用户名已存在')
|
||||
|
||||
data.password = PwdUtil.set_password_hash(password=data.password)
|
||||
dict_data = data.model_dump(exclude_unset=True)
|
||||
result = await UserCRUD(auth).create(data=dict_data)
|
||||
return UserOutSchema.model_validate(result).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def forget_password(cls, auth: AuthSchema, data: UserForgetPasswordSchema) -> Dict:
|
||||
"""用户忘记密码"""
|
||||
user = await UserCRUD(auth).get_user_by_username(username=data.username)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
if not user.available:
|
||||
raise CustomException(msg="用户已停用")
|
||||
if user.mobile != data.mobile:
|
||||
raise CustomException(msg="手机号不匹配")
|
||||
new_password_hash = PwdUtil.set_password_hash(password=data.new_password)
|
||||
new_user = await UserCRUD(auth).forget_password(id=user.id, password_hash=new_password_hash)
|
||||
return UserOutSchema.model_validate(new_user).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def batch_import_user(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> Dict:
|
||||
"""批量导入用户"""
|
||||
|
||||
header_dict = {
|
||||
'部门编号': 'dept_id',
|
||||
'用户名': 'username',
|
||||
'名称': 'name',
|
||||
'邮箱': 'email',
|
||||
'手机号': 'mobile',
|
||||
'性别': 'gender',
|
||||
'状态': 'available'
|
||||
}
|
||||
|
||||
try:
|
||||
# 读取Excel文件
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
# 重命名列名
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
required_fields = ['username', 'name', 'dept_id']
|
||||
for field in required_fields:
|
||||
if df[field].isnull().any():
|
||||
raise CustomException(msg=f"{header_dict[field]}不能为空")
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
|
||||
# 处理每一行数据
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# 数据转换
|
||||
row['gender'] = 1 if row['gender'] == '男' else (2 if row['gender'] == '女' else 1)
|
||||
row['available'] = True if row['available'] == '正常' else False
|
||||
|
||||
# 检查部门是否存在
|
||||
dept = await DeptCRUD(auth).get_dept_by_id(id=int(row['dept_id']))
|
||||
if not dept:
|
||||
raise CustomException(msg=f"部门ID {row['dept_id']} 不存在")
|
||||
|
||||
# 构建用户数据
|
||||
user_data = UserCreateSchema(
|
||||
username=str(row['username']).strip(),
|
||||
name=str(row['name']).strip(),
|
||||
email=str(row['email']).strip() if not pd.isna(row['email']) else None,
|
||||
mobile=str(row['mobile']).strip() if not pd.isna(row['mobile']) else None,
|
||||
gender=row['gender'],
|
||||
available=row['available'],
|
||||
dept_id=int(row['dept_id']),
|
||||
password="123456" # 设置默认密码
|
||||
)
|
||||
|
||||
# 处理用户导入
|
||||
exists_user = await UserCRUD(auth).get_user_by_username(username=user_data.username)
|
||||
if exists_user:
|
||||
if update_support:
|
||||
await UserCRUD(auth).update(id=exists_user.id, data=user_data)
|
||||
success_count += 1
|
||||
else:
|
||||
error_msgs.append(f"第{index+1}行: 用户 {user_data.username} 已存在")
|
||||
else:
|
||||
await UserCRUD(auth).create(data=user_data)
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{index+1}行: {str(e)}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"成功导入 {success_count} 条数据" + ("\n" + "\n".join(error_msgs) if error_msgs else "")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_import_template_user(cls) -> bytes:
|
||||
"""获取用户导入模板"""
|
||||
header_list = ['部门编号', '用户名', '名称', '邮箱', '手机号', '性别', '状态']
|
||||
selector_header_list = ['性别', '状态']
|
||||
option_list = [{'性别': ['男', '女', '未知']}, {'状态': ['正常', '停用']}]
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def export_user_list(cls, user_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""导出用户列表"""
|
||||
if not user_list:
|
||||
raise CustomException(msg="没有数据可导出")
|
||||
|
||||
# 定义字段映射
|
||||
mapping_dict = {
|
||||
'id': '用户编号',
|
||||
'username': '用户名称',
|
||||
'name': '用户昵称',
|
||||
'dept_name': '部门',
|
||||
'email': '邮箱地址',
|
||||
'mobile': '手机号码',
|
||||
'gender': '性别',
|
||||
'available': '状态',
|
||||
'creator': '创建者',
|
||||
'create_datetime': '创建时间',
|
||||
'modifier': '更新者',
|
||||
'update_datetime': '更新时间',
|
||||
'description': '备注'
|
||||
}
|
||||
|
||||
# 复制数据并转换
|
||||
data = user_list.copy()
|
||||
for item in data:
|
||||
item['available'] = '正常' if item.get('available') else '停用'
|
||||
gender = item.get('gender')
|
||||
item['gender'] = '男' if gender == 1 else ('女' if gender == 2 else '未知')
|
||||
|
||||
# 转换为中文键
|
||||
new_data = [
|
||||
{mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)}
|
||||
for item in data
|
||||
]
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=new_data)
|
||||
Reference in New Issue
Block a user