开源准备

This commit is contained in:
zhangtao
2024-12-10 17:39:26 +08:00
commit cbaf324e19
315 changed files with 39314 additions and 0 deletions
+2
View File
@@ -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)