初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
@@ -0,0 +1,126 @@
from fastapi import Request
from common.enums import RedisInitKeyConfig
from common.vo import CrudResponseModel
from config.get_redis import RedisUtil
from module_admin.entity.vo.cache_vo import CacheInfoModel, CacheMonitorModel
class CacheService:
"""
缓存监控模块服务层
"""
@classmethod
async def get_cache_monitor_statistical_info_services(cls, request: Request) -> CacheMonitorModel:
"""
获取缓存监控信息service
:param request: Request对象
:return: 缓存监控信息
"""
info = await request.app.state.redis.info()
db_size = await request.app.state.redis.dbsize()
command_stats_dict = await request.app.state.redis.info('commandstats')
command_stats = [
{'name': key.split('_')[1], 'value': str(value.get('calls'))} for key, value in command_stats_dict.items()
]
result = CacheMonitorModel(commandStats=command_stats, dbSize=db_size, info=info)
return result
@classmethod
async def get_cache_monitor_cache_name_services(cls) -> list[CacheInfoModel]:
"""
获取缓存名称列表信息service
:return: 缓存名称列表信息
"""
name_list = [
CacheInfoModel(
cacheKey='',
cacheName=key_config.key,
cacheValue='',
remark=key_config.remark,
)
for key_config in RedisInitKeyConfig
]
return name_list
@classmethod
async def get_cache_monitor_cache_key_services(cls, request: Request, cache_name: str) -> list[str]:
"""
获取缓存键名列表信息service
:param request: Request对象
:param cache_name: 缓存名称
:return: 缓存键名列表信息
"""
cache_keys: list[str] = await request.app.state.redis.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
) -> CacheInfoModel:
"""
获取缓存内容信息service
:param request: Request对象
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 缓存内容信息
"""
cache_value = await request.app.state.redis.get(f'{cache_name}:{cache_key}')
return CacheInfoModel(cacheKey=cache_key, cacheName=cache_name, cacheValue=cache_value, remark='')
@classmethod
async def clear_cache_monitor_cache_name_services(cls, request: Request, cache_name: str) -> CrudResponseModel:
"""
清除缓存名称对应所有键值service
:param request: Request对象
:param cache_name: 缓存名称
:return: 操作缓存响应信息
"""
cache_keys = await request.app.state.redis.keys(f'{cache_name}*')
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
return CrudResponseModel(is_success=True, message=f'{cache_name}对应键值清除成功')
@classmethod
async def clear_cache_monitor_cache_key_services(cls, request: Request, cache_key: str) -> CrudResponseModel:
"""
清除缓存名称对应所有键值service
:param request: Request对象
:param cache_key: 缓存键名
:return: 操作缓存响应信息
"""
cache_keys = await request.app.state.redis.keys(f'*{cache_key}')
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
return CrudResponseModel(is_success=True, message=f'{cache_key}清除成功')
@classmethod
async def clear_cache_monitor_all_services(cls, request: Request) -> CrudResponseModel:
"""
清除所有缓存service
:param request: Request对象
:return: 操作缓存响应信息
"""
cache_keys = await request.app.state.redis.keys()
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
await RedisUtil.init_sys_dict(request.app.state.redis)
await RedisUtil.init_sys_config(request.app.state.redis)
return CrudResponseModel(is_success=True, message='所有缓存清除成功')
@@ -0,0 +1,54 @@
import base64
import io
import random
import anyio
from PIL import Image, ImageDraw, ImageFont
class CaptchaService:
"""
验证码模块服务层
"""
@classmethod
async def create_captcha_image_service(cls) -> list[str, int]:
# 创建空白图像
image = Image.new('RGB', (160, 60), color='#EAEAEA')
# 创建绘图对象
draw = ImageDraw.Draw(image)
# 设置字体
font_path = (await anyio.Path.cwd()) / 'assets' / 'font' / 'Arial.ttf'
font = ImageFont.truetype(font_path, size=30)
# 生成两个0-9之间的随机整数
num1 = random.randint(0, 9)
num2 = random.randint(0, 9)
# 从运算符列表中随机选择一个
operational_character_list = ['+', '-', '*']
operational_character = random.choice(operational_character_list)
# 根据选择的运算符进行计算
if operational_character == '+':
result = num1 + num2
elif operational_character == '-':
result = num1 - num2
# 非负数结果的验证码更友好一些
if result < 0:
num1, num2 = num2, num1
result = num1 - num2
else:
result = num1 * num2
# 绘制文本
text = f'{num1} {operational_character} {num2} = ?'
draw.text((25, 15), text, fill='blue', font=font)
# 将图像数据保存到内存中
buffer = io.BytesIO()
image.save(buffer, format='PNG')
# 将图像数据转换为base64字符串
base64_string = base64.b64encode(buffer.getvalue()).decode()
return [base64_string, result]
@@ -0,0 +1,99 @@
import os
from datetime import datetime
import aiofiles
from fastapi import BackgroundTasks, Request, UploadFile
from common.vo import CrudResponseModel
from config.env import UploadConfig
from exceptions.exception import ServiceException
from module_admin.entity.vo.common_vo import UploadResponseModel
from utils.upload_util import UploadUtil
class CommonService:
"""
通用模块服务层
"""
@classmethod
async def upload_service(cls, request: Request, file: UploadFile) -> CrudResponseModel:
"""
通用上传service
:param request: Request对象
:param file: 上传文件对象
:return: 上传结果
"""
if not UploadUtil.check_file_extension(file):
raise ServiceException(message='文件类型不合法')
relative_path = (
f'upload/{datetime.now().strftime("%Y")}/{datetime.now().strftime("%m")}/{datetime.now().strftime("%d")}'
)
dir_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path)
try:
os.makedirs(dir_path)
except FileExistsError:
pass
filename = f'{file.filename.rsplit(".", 1)[0]}_{datetime.now().strftime("%Y%m%d%H%M%S")}{UploadConfig.UPLOAD_MACHINE}{UploadUtil.generate_random_number()}.{file.filename.rsplit(".")[-1]}'
filepath = os.path.join(dir_path, filename)
async with aiofiles.open(filepath, 'wb') as f:
# 流式写出大型文件,这里的10代表10MB
while True:
chunk = await file.read(1024 * 1024 * 10)
if not chunk:
break
await f.write(chunk)
return CrudResponseModel(
is_success=True,
result=UploadResponseModel(
fileName=f'{UploadConfig.UPLOAD_PREFIX}/{relative_path}/{filename}',
newFileName=filename,
originalFilename=file.filename,
url=f'{request.base_url}{UploadConfig.UPLOAD_PREFIX[1:]}/{relative_path}/{filename}',
),
message='上传成功',
)
@classmethod
async def download_services(
cls, background_tasks: BackgroundTasks, file_name: str, delete: bool
) -> CrudResponseModel:
"""
下载下载目录文件service
:param background_tasks: 后台任务对象
:param file_name: 下载的文件名称
:param delete: 是否在下载完成后删除文件
:return: 上传结果
"""
filepath = os.path.join(UploadConfig.DOWNLOAD_PATH, file_name)
if '..' in file_name:
raise ServiceException(message='文件名称不合法')
if not UploadUtil.check_file_exists(filepath):
raise ServiceException(message='文件不存在')
if delete:
background_tasks.add_task(UploadUtil.delete_file, filepath)
return CrudResponseModel(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功')
@classmethod
async def download_resource_services(cls, resource: str) -> CrudResponseModel:
"""
下载上传目录文件service
:param resource: 下载的文件名称
:return: 上传结果
"""
filepath = os.path.join(resource.replace(UploadConfig.UPLOAD_PREFIX, UploadConfig.UPLOAD_PATH))
filename = resource.rsplit('/', 1)[-1]
if (
'..' in filename
or not UploadUtil.check_file_timestamp(filename)
or not UploadUtil.check_file_machine(filename)
or not UploadUtil.check_file_random_code(filename)
):
raise ServiceException(message='文件名称不合法')
if not UploadUtil.check_file_exists(filepath):
raise ServiceException(message='文件不存在')
return CrudResponseModel(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功')
@@ -0,0 +1,234 @@
from typing import Any
from fastapi import Request
from redis import asyncio as aioredis
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.enums import RedisInitKeyConfig
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.config_dao import ConfigDao
from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel, DeleteConfigModel
from utils.common_util import CamelCaseUtil
from utils.excel_util import ExcelUtil
class ConfigService:
"""
参数配置管理模块服务层
"""
@classmethod
async def get_config_list_services(
cls, query_db: AsyncSession, query_object: ConfigPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取参数配置列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 参数配置列表信息对象
"""
config_list_result = await ConfigDao.get_config_list(query_db, query_object, is_page)
return config_list_result
@classmethod
async def init_cache_sys_config_services(cls, query_db: AsyncSession, redis: aioredis.Redis) -> None:
"""
应用初始化:获取所有参数配置对应的键值对信息并缓存service
:param query_db: orm对象
:param redis: redis对象
:return:
"""
# 获取以sys_config:开头的键列表
keys = await redis.keys(f'{RedisInitKeyConfig.SYS_CONFIG.key}:*')
# 删除匹配的键
if keys:
await redis.delete(*keys)
config_all = await ConfigDao.get_config_list(query_db, ConfigPageQueryModel(), is_page=False)
for config_obj in config_all:
await redis.set(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_obj.get("configKey")}',
config_obj.get('configValue'),
)
@classmethod
async def query_config_list_from_cache_services(cls, redis: aioredis.Redis, config_key: str) -> Any:
"""
从缓存获取参数键名对应值service
:param redis: redis对象
:param config_key: 参数键名
:return: 参数键名对应值
"""
result = await redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_key}')
return result
@classmethod
async def check_config_key_unique_services(cls, query_db: AsyncSession, page_object: ConfigModel) -> bool:
"""
校验参数键名是否唯一service
:param query_db: orm对象
:param page_object: 参数配置对象
:return: 校验结果
"""
config_id = -1 if page_object.config_id is None else page_object.config_id
config = await ConfigDao.get_config_detail_by_info(query_db, ConfigModel(configKey=page_object.config_key))
if config and config.config_id != config_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_config_services(
cls, request: Request, query_db: AsyncSession, page_object: ConfigModel
) -> CrudResponseModel:
"""
新增参数配置信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 新增参数配置对象
:return: 新增参数配置校验结果
"""
if not await cls.check_config_key_unique_services(query_db, page_object):
raise ServiceException(message=f'新增参数{page_object.config_name}失败,参数键名已存在')
try:
await ConfigDao.add_config_dao(query_db, page_object)
await query_db.commit()
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:{page_object.config_key}', page_object.config_value
)
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_config_services(
cls, request: Request, query_db: AsyncSession, page_object: ConfigModel
) -> CrudResponseModel:
"""
编辑参数配置信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 编辑参数配置对象
:return: 编辑参数配置校验结果
"""
edit_config = page_object.model_dump(exclude_unset=True)
config_info = await cls.config_detail_services(query_db, page_object.config_id)
if config_info.config_id:
if not await cls.check_config_key_unique_services(query_db, page_object):
raise ServiceException(message=f'修改参数{page_object.config_name}失败,参数键名已存在')
try:
await ConfigDao.edit_config_dao(query_db, edit_config)
await query_db.commit()
if config_info.config_key != page_object.config_key:
await request.app.state.redis.delete(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_info.config_key}'
)
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:{page_object.config_key}', page_object.config_value
)
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='参数配置不存在')
@classmethod
async def delete_config_services(
cls, request: Request, query_db: AsyncSession, page_object: DeleteConfigModel
) -> CrudResponseModel:
"""
删除参数配置信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 删除参数配置对象
:return: 删除参数配置校验结果
"""
if page_object.config_ids:
config_id_list = page_object.config_ids.split(',')
try:
delete_config_key_list = []
for config_id in config_id_list:
config_info = await cls.config_detail_services(query_db, int(config_id))
if config_info.config_type == CommonConstant.YES:
raise ServiceException(message=f'内置参数{config_info.config_key}不能删除')
await ConfigDao.delete_config_dao(query_db, ConfigModel(configId=int(config_id)))
delete_config_key_list.append(f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_info.config_key}')
await query_db.commit()
if delete_config_key_list:
await request.app.state.redis.delete(*delete_config_key_list)
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入参数配置id为空')
@classmethod
async def config_detail_services(cls, query_db: AsyncSession, config_id: int) -> ConfigModel:
"""
获取参数配置详细信息service
:param query_db: orm对象
:param config_id: 参数配置id
:return: 参数配置id对应的信息
"""
config = await ConfigDao.get_config_detail_by_id(query_db, config_id=config_id)
result = ConfigModel(**CamelCaseUtil.transform_result(config)) if config else ConfigModel()
return result
@staticmethod
async def export_config_list_services(config_list: list) -> bytes:
"""
导出参数配置信息service
:param config_list: 参数配置信息列表
:return: 参数配置信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'configId': '参数主键',
'configName': '参数名称',
'configKey': '参数键名',
'configValue': '参数键值',
'configType': '系统内置',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in config_list:
if item.get('configType') == 'Y':
item['configType'] = '是'
else:
item['configType'] = '否'
binary_data = ExcelUtil.export_list2excel(config_list, mapping_dict)
return binary_data
@classmethod
async def refresh_sys_config_services(cls, request: Request, query_db: AsyncSession) -> CrudResponseModel:
"""
刷新字典缓存信息service
:param request: Request对象
:param query_db: orm对象
:return: 刷新字典缓存校验结果
"""
await cls.init_cache_sys_config_services(query_db, request.app.state.redis)
return CrudResponseModel(is_success=True, message='刷新成功')
@@ -0,0 +1,285 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.vo import CrudResponseModel
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.dao.dept_dao import DeptDao
from module_admin.entity.do.dept_do import SysDept
from module_admin.entity.vo.dept_vo import DeleteDeptModel, DeptModel, DeptTreeModel
from utils.common_util import CamelCaseUtil
class DeptService:
"""
部门管理模块服务层
"""
@classmethod
async def get_dept_tree_services(
cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: ColumnElement
) -> list[dict[str, Any]]:
"""
获取部门树信息service
:param query_db: orm对象
:param page_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 部门树信息对象
"""
dept_list_result = await DeptDao.get_dept_list_for_tree(query_db, page_object, data_scope_sql)
dept_tree_model_result = cls.list_to_tree(dept_list_result)
dept_tree_result = [dept.model_dump(exclude_unset=True, by_alias=True) for dept in dept_tree_model_result]
return dept_tree_result
@classmethod
async def get_dept_for_edit_option_services(
cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: ColumnElement
) -> list[dict[str, Any]]:
"""
获取部门编辑部门树信息service
:param query_db: orm对象
:param page_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 部门树信息对象
"""
dept_list_result = await DeptDao.get_dept_info_for_edit_option(query_db, page_object, data_scope_sql)
return CamelCaseUtil.transform_result(dept_list_result)
@classmethod
async def get_dept_list_services(
cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: ColumnElement
) -> list[dict[str, Any]]:
"""
获取部门列表信息service
:param query_db: orm对象
:param page_object: 分页查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 部门列表信息对象
"""
dept_list_result = await DeptDao.get_dept_list(query_db, page_object, data_scope_sql)
return CamelCaseUtil.transform_result(dept_list_result)
@classmethod
async def check_dept_data_scope_services(
cls, query_db: AsyncSession, dept_id: int, data_scope_sql: ColumnElement
) -> CrudResponseModel:
"""
校验部门是否有数据权限service
:param query_db: orm对象
:param dept_id: 部门id
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 校验结果
"""
depts = await DeptDao.get_dept_list(query_db, DeptModel(deptId=dept_id), data_scope_sql)
if depts:
return CrudResponseModel(is_success=True, message='校验通过')
raise ServiceException(message='没有权限访问部门数据')
@classmethod
async def check_dept_name_unique_services(cls, query_db: AsyncSession, page_object: DeptModel) -> bool:
"""
校验部门名称是否唯一service
:param query_db: orm对象
:param page_object: 部门对象
:return: 校验结果
"""
dept_id = -1 if page_object.dept_id is None else page_object.dept_id
dept = await DeptDao.get_dept_detail_by_info(
query_db, DeptModel(deptName=page_object.dept_name, parentId=page_object.parent_id)
)
if dept and dept.dept_id != dept_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_dept_services(cls, query_db: AsyncSession, page_object: DeptModel) -> CrudResponseModel:
"""
新增部门信息service
:param query_db: orm对象
:param page_object: 新增部门对象
:return: 新增部门校验结果
"""
if not await cls.check_dept_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增部门{page_object.dept_name}失败,部门名称已存在')
parent_info = await DeptDao.get_dept_by_id(query_db, page_object.parent_id)
if parent_info.status != CommonConstant.DEPT_NORMAL:
raise ServiceException(message=f'部门{parent_info.dept_name}停用,不允许新增')
page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}'
try:
await DeptDao.add_dept_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_dept_services(cls, query_db: AsyncSession, page_object: DeptModel) -> CrudResponseModel:
"""
编辑部门信息service
:param query_db: orm对象
:param page_object: 编辑部门对象
:return: 编辑部门校验结果
"""
if not await cls.check_dept_name_unique_services(query_db, page_object):
raise ServiceException(message=f'修改部门{page_object.dept_name}失败,部门名称已存在')
if page_object.dept_id == page_object.parent_id:
raise ServiceException(message=f'修改部门{page_object.dept_name}失败,上级部门不能是自己')
if (
page_object.status == CommonConstant.DEPT_DISABLE
and (await DeptDao.count_normal_children_dept_dao(query_db, page_object.dept_id)) > 0
):
raise ServiceException(message=f'修改部门{page_object.dept_name}失败,该部门包含未停用的子部门')
new_parent_dept = await DeptDao.get_dept_by_id(query_db, page_object.parent_id)
old_dept = await DeptDao.get_dept_by_id(query_db, page_object.dept_id)
try:
if new_parent_dept and old_dept:
new_ancestors = f'{new_parent_dept.ancestors},{new_parent_dept.dept_id}'
old_ancestors = old_dept.ancestors
page_object.ancestors = new_ancestors
await cls.update_dept_children(query_db, page_object.dept_id, new_ancestors, old_ancestors)
edit_dept = page_object.model_dump(exclude_unset=True)
await DeptDao.edit_dept_dao(query_db, edit_dept)
if (
page_object.status == CommonConstant.DEPT_NORMAL
and page_object.ancestors
and page_object.ancestors != 0
):
await cls.update_parent_dept_status_normal(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def delete_dept_services(cls, query_db: AsyncSession, page_object: DeleteDeptModel) -> CrudResponseModel:
"""
删除部门信息service
:param query_db: orm对象
:param page_object: 删除部门对象
:return: 删除部门校验结果
"""
if page_object.dept_ids:
dept_id_list = page_object.dept_ids.split(',')
try:
for dept_id in dept_id_list:
if (await DeptDao.count_children_dept_dao(query_db, int(dept_id))) > 0:
raise ServiceWarning(message='存在下级部门,不允许删除')
if (await DeptDao.count_dept_user_dao(query_db, int(dept_id))) > 0:
raise ServiceWarning(message='部门存在用户,不允许删除')
await DeptDao.delete_dept_dao(query_db, DeptModel(deptId=dept_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入部门id为空')
@classmethod
async def dept_detail_services(cls, query_db: AsyncSession, dept_id: int) -> DeptModel:
"""
获取部门详细信息service
:param query_db: orm对象
:param dept_id: 部门id
:return: 部门id对应的信息
"""
dept = await DeptDao.get_dept_detail_by_id(query_db, dept_id=dept_id)
result = DeptModel(**CamelCaseUtil.transform_result(dept)) if dept else DeptModel()
return result
@classmethod
def list_to_tree(cls, permission_list: Sequence[SysDept]) -> list[DeptTreeModel]:
"""
工具方法:根据部门列表信息生成树形嵌套数据
:param permission_list: 部门列表信息
:return: 部门树形嵌套数据
"""
_permission_list = [
DeptTreeModel(id=item.dept_id, label=item.dept_name, parentId=item.parent_id) for item in permission_list
]
# 转成id为key的字典
mapping: dict[int, DeptTreeModel] = dict(zip([i.id for i in _permission_list], _permission_list, strict=False))
# 树容器
container: list[DeptTreeModel] = []
for d in _permission_list:
# 如果找不到父级项,则是根节点
parent = mapping.get(d.parent_id)
if parent is None:
container.append(d)
else:
children: list[DeptTreeModel] = parent.children
if not children:
children = []
children.append(d)
parent.children = children
return container
@classmethod
async def replace_first(cls, original_str: str, old_str: str, new_str: str) -> str:
"""
工具方法:替换字符串
:param original_str: 需要替换的原始字符串
:param old_str: 用于匹配的字符串
:param new_str: 替换的字符串
:return: 替换后的字符串
"""
if original_str.startswith(old_str):
return original_str.replace(old_str, new_str, 1)
return original_str
@classmethod
async def update_parent_dept_status_normal(cls, query_db: AsyncSession, dept: DeptModel) -> None:
"""
更新父部门状态为正常
:param query_db: orm对象
:param dept: 部门对象
:return:
"""
dept_id_list = dept.ancestors.split(',')
await DeptDao.update_dept_status_normal_dao(query_db, list(map(int, dept_id_list)))
@classmethod
async def update_dept_children(
cls, query_db: AsyncSession, dept_id: int, new_ancestors: str, old_ancestors: str
) -> None:
"""
更新子部门信息
:param query_db: orm对象
:param dept_id: 部门id
:param new_ancestors: 新的祖先
:param old_ancestors: 旧的祖先
:return:
"""
children = await DeptDao.get_children_dept_dao(query_db, dept_id)
update_children = []
for child in children:
child_ancestors = await cls.replace_first(child.ancestors, old_ancestors, new_ancestors)
update_children.append({'dept_id': child.dept_id, 'ancestors': child_ancestors})
if children:
await DeptDao.update_dept_children_dao(query_db, update_children)
@@ -0,0 +1,464 @@
import json
from collections.abc import Sequence
from typing import Any
from fastapi import Request
from redis import asyncio as aioredis
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.enums import RedisInitKeyConfig
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.dict_dao import DictDataDao, DictTypeDao
from module_admin.entity.do.dict_do import SysDictData
from module_admin.entity.vo.dict_vo import (
DeleteDictDataModel,
DeleteDictTypeModel,
DictDataModel,
DictDataPageQueryModel,
DictTypeModel,
DictTypePageQueryModel,
)
from utils.common_util import CamelCaseUtil
from utils.excel_util import ExcelUtil
class DictTypeService:
"""
字典类型管理模块服务层
"""
@classmethod
async def get_dict_type_list_services(
cls, query_db: AsyncSession, query_object: DictTypePageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取字典类型列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 字典类型列表信息对象
"""
dict_type_list_result = await DictTypeDao.get_dict_type_list(query_db, query_object, is_page)
return dict_type_list_result
@classmethod
async def check_dict_type_unique_services(cls, query_db: AsyncSession, page_object: DictTypeModel) -> bool:
"""
校验字典类型称是否唯一service
:param query_db: orm对象
:param page_object: 字典类型对象
:return: 校验结果
"""
dict_id = -1 if page_object.dict_id is None else page_object.dict_id
dict_type = await DictTypeDao.get_dict_type_detail_by_info(
query_db, DictTypeModel(dictType=page_object.dict_type)
)
if dict_type and dict_type.dict_id != dict_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_dict_type_services(
cls, request: Request, query_db: AsyncSession, page_object: DictTypeModel
) -> CrudResponseModel:
"""
新增字典类型信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 新增岗位对象
:return: 新增字典类型校验结果
"""
if not await cls.check_dict_type_unique_services(query_db, page_object):
raise ServiceException(message=f'新增字典{page_object.dict_name}失败,字典类型已存在')
try:
await DictTypeDao.add_dict_type_dao(query_db, page_object)
await query_db.commit()
await request.app.state.redis.set(f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}', '')
result = {'is_success': True, 'message': '新增成功'}
except Exception as e:
await query_db.rollback()
raise e
return CrudResponseModel(**result)
@classmethod
async def edit_dict_type_services(
cls, request: Request, query_db: AsyncSession, page_object: DictTypeModel
) -> CrudResponseModel:
"""
编辑字典类型信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 编辑字典类型对象
:return: 编辑字典类型校验结果
"""
edit_dict_type = page_object.model_dump(exclude_unset=True)
dict_type_info = await cls.dict_type_detail_services(query_db, page_object.dict_id)
if dict_type_info.dict_id:
if not await cls.check_dict_type_unique_services(query_db, page_object):
raise ServiceException(message=f'修改字典{page_object.dict_name}失败,字典类型已存在')
try:
query_dict_data = DictDataPageQueryModel(dictType=dict_type_info.dict_type)
dict_data_list = await DictDataDao.get_dict_data_list(query_db, query_dict_data, is_page=False)
if dict_type_info.dict_type != page_object.dict_type:
for dict_data in dict_data_list:
edit_dict_data = DictDataModel(
dictCode=dict_data.get('dict_code'),
dictType=page_object.dict_type,
updateBy=page_object.update_by,
updateTime=page_object.update_time,
).model_dump(exclude_unset=True)
await DictDataDao.edit_dict_data_dao(query_db, edit_dict_data)
await DictTypeDao.edit_dict_type_dao(query_db, edit_dict_type)
await query_db.commit()
if dict_type_info.dict_type != page_object.dict_type:
dict_data = [CamelCaseUtil.transform_result(row) for row in dict_data_list if row]
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}',
json.dumps(dict_data, ensure_ascii=False, default=str),
)
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='字典类型不存在')
@classmethod
async def delete_dict_type_services(
cls, request: Request, query_db: AsyncSession, page_object: DeleteDictTypeModel
) -> CrudResponseModel:
"""
删除字典类型信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 删除字典类型对象
:return: 删除字典类型校验结果
"""
if page_object.dict_ids:
dict_id_list = page_object.dict_ids.split(',')
try:
delete_dict_type_list = []
for dict_id in dict_id_list:
dict_type_into = await cls.dict_type_detail_services(query_db, int(dict_id))
if (await DictDataDao.count_dict_data_dao(query_db, dict_type_into.dict_type)) > 0:
raise ServiceException(message=f'{dict_type_into.dict_name}已分配,不能删除')
await DictTypeDao.delete_dict_type_dao(query_db, DictTypeModel(dictId=int(dict_id)))
delete_dict_type_list.append(f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type_into.dict_type}')
await query_db.commit()
if delete_dict_type_list:
await request.app.state.redis.delete(*delete_dict_type_list)
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入字典类型id为空')
@classmethod
async def dict_type_detail_services(cls, query_db: AsyncSession, dict_id: int) -> DictTypeModel:
"""
获取字典类型详细信息service
:param query_db: orm对象
:param dict_id: 字典类型id
:return: 字典类型id对应的信息
"""
dict_type = await DictTypeDao.get_dict_type_detail_by_id(query_db, dict_id=dict_id)
result = DictTypeModel(**CamelCaseUtil.transform_result(dict_type)) if dict_type else DictTypeModel()
return result
@staticmethod
async def export_dict_type_list_services(dict_type_list: list) -> bytes:
"""
导出字典类型信息service
:param dict_type_list: 字典信息列表
:return: 字典信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'dictId': '字典编号',
'dictName': '字典名称',
'dictType': '字典类型',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in dict_type_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '停用'
binary_data = ExcelUtil.export_list2excel(dict_type_list, mapping_dict)
return binary_data
@classmethod
async def refresh_sys_dict_services(cls, request: Request, query_db: AsyncSession) -> CrudResponseModel:
"""
刷新字典缓存信息service
:param request: Request对象
:param query_db: orm对象
:return: 刷新字典缓存校验结果
"""
await DictDataService.init_cache_sys_dict_services(query_db, request.app.state.redis)
result = {'is_success': True, 'message': '刷新成功'}
return CrudResponseModel(**result)
class DictDataService:
"""
字典数据管理模块服务层
"""
@classmethod
async def get_dict_data_list_services(
cls, query_db: AsyncSession, query_object: DictDataPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取字典数据列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 字典数据列表信息对象
"""
dict_data_list_result = await DictDataDao.get_dict_data_list(query_db, query_object, is_page)
return dict_data_list_result
@classmethod
async def query_dict_data_list_services(cls, query_db: AsyncSession, dict_type: str) -> Sequence[SysDictData]:
"""
获取字典数据列表信息service
:param query_db: orm对象
:param dict_type: 字典类型
:return: 字典数据列表信息对象
"""
dict_data_list_result = await DictDataDao.query_dict_data_list(query_db, dict_type)
return dict_data_list_result
@classmethod
async def init_cache_sys_dict_services(cls, query_db: AsyncSession, redis: aioredis.Redis) -> None:
"""
应用初始化:获取所有字典类型对应的字典数据信息并缓存service
:param query_db: orm对象
:param redis: redis对象
:return:
"""
# 获取以sys_dict:开头的键列表
keys = await redis.keys(f'{RedisInitKeyConfig.SYS_DICT.key}:*')
# 删除匹配的键
if keys:
await redis.delete(*keys)
dict_type_all = await DictTypeDao.get_all_dict_type(query_db)
for dict_type_obj in [item for item in dict_type_all if item.status == '0']:
dict_type = dict_type_obj.dict_type
dict_data_list = await DictDataDao.query_dict_data_list(query_db, dict_type)
dict_data = [CamelCaseUtil.transform_result(row) for row in dict_data_list if row]
await redis.set(
f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}',
json.dumps(dict_data, ensure_ascii=False, default=str),
)
@classmethod
async def query_dict_data_list_from_cache_services(
cls, redis: aioredis.Redis, dict_type: str
) -> list[dict[str, Any]]:
"""
从缓存获取字典数据列表信息service
:param redis: redis对象
:param dict_type: 字典类型
:return: 字典数据列表信息对象
"""
result = []
dict_data_list_result = await redis.get(f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}')
if dict_data_list_result:
result = json.loads(dict_data_list_result)
return CamelCaseUtil.transform_result(result)
@classmethod
async def check_dict_data_unique_services(cls, query_db: AsyncSession, page_object: DictDataModel) -> bool:
"""
校验字典数据是否唯一service
:param query_db: orm对象
:param page_object: 字典数据对象
:return: 校验结果
"""
dict_code = -1 if page_object.dict_code is None else page_object.dict_code
dict_data = await DictDataDao.get_dict_data_detail_by_info(query_db, page_object)
if dict_data and dict_data.dict_code != dict_code:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_dict_data_services(
cls, request: Request, query_db: AsyncSession, page_object: DictDataModel
) -> CrudResponseModel:
"""
新增字典数据信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 新增岗位对象
:return: 新增字典数据校验结果
"""
if not await cls.check_dict_data_unique_services(query_db, page_object):
raise ServiceException(
message=f'新增字典数据{page_object.dict_label}失败,{page_object.dict_type}下已存在该字典数据'
)
try:
await DictDataDao.add_dict_data_dao(query_db, page_object)
await query_db.commit()
dict_data_list = await cls.query_dict_data_list_services(query_db, page_object.dict_type)
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}',
json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str),
)
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_dict_data_services(
cls, request: Request, query_db: AsyncSession, page_object: DictDataModel
) -> CrudResponseModel:
"""
编辑字典数据信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 编辑字典数据对象
:return: 编辑字典数据校验结果
"""
edit_data_type = page_object.model_dump(exclude_unset=True)
dict_data_info = await cls.dict_data_detail_services(query_db, page_object.dict_code)
if dict_data_info.dict_code:
if not await cls.check_dict_data_unique_services(query_db, page_object):
raise ServiceException(
message=f'新增字典数据{page_object.dict_label}失败,{page_object.dict_type}下已存在该字典数据'
)
try:
await DictDataDao.edit_dict_data_dao(query_db, edit_data_type)
await query_db.commit()
dict_data_list = await cls.query_dict_data_list_services(query_db, page_object.dict_type)
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}',
json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str),
)
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='字典数据不存在')
@classmethod
async def delete_dict_data_services(
cls, request: Request, query_db: AsyncSession, page_object: DeleteDictDataModel
) -> CrudResponseModel:
"""
删除字典数据信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 删除字典数据对象
:return: 删除字典数据校验结果
"""
if page_object.dict_codes:
dict_code_list = page_object.dict_codes.split(',')
try:
delete_dict_type_list = []
for dict_code in dict_code_list:
dict_data = await cls.dict_data_detail_services(query_db, int(dict_code))
await DictDataDao.delete_dict_data_dao(query_db, DictDataModel(dictCode=dict_code))
delete_dict_type_list.append(dict_data.dict_type)
await query_db.commit()
for dict_type in list(set(delete_dict_type_list)):
dict_data_list = await cls.query_dict_data_list_services(query_db, dict_type)
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}',
json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str),
)
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入字典数据id为空')
@classmethod
async def dict_data_detail_services(cls, query_db: AsyncSession, dict_code: int) -> DictDataModel:
"""
获取字典数据详细信息service
:param query_db: orm对象
:param dict_code: 字典数据id
:return: 字典数据id对应的信息
"""
dict_data = await DictDataDao.get_dict_data_detail_by_id(query_db, dict_code=dict_code)
result = DictDataModel(**CamelCaseUtil.transform_result(dict_data)) if dict_data else DictDataModel()
return result
@staticmethod
async def export_dict_data_list_services(dict_data_list: list) -> bytes:
"""
导出字典数据信息service
:param dict_data_list: 字典数据信息列表
:return: 字典数据信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'dictCode': '字典编码',
'dictSort': '字典标签',
'dictLabel': '字典键值',
'dictValue': '字典排序',
'dictType': '字典类型',
'cssClass': '样式属性',
'listClass': '表格回显样式',
'isDefault': '是否默认',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in dict_data_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '停用'
if item.get('isDefault') == 'Y':
item['isDefault'] = '是'
else:
item['isDefault'] = '否'
binary_data = ExcelUtil.export_list2excel(dict_data_list, mapping_dict)
return binary_data
@@ -0,0 +1,144 @@
from typing import Any
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from common.vo import CrudResponseModel, PageModel
from module_admin.dao.job_log_dao import JobLogDao
from module_admin.entity.vo.job_vo import DeleteJobLogModel, JobLogModel, JobLogPageQueryModel
from module_admin.service.dict_service import DictDataService
from utils.excel_util import ExcelUtil
class JobLogService:
"""
定时任务日志管理模块服务层
"""
@classmethod
async def get_job_log_list_services(
cls, query_db: AsyncSession, query_object: JobLogPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取定时任务日志列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 定时任务日志列表信息对象
"""
job_log_list_result = await JobLogDao.get_job_log_list(query_db, query_object, is_page)
return job_log_list_result
@classmethod
def add_job_log_services(cls, query_db: Session, page_object: JobLogModel) -> CrudResponseModel:
"""
新增定时任务日志信息service
:param query_db: orm对象
:param page_object: 新增定时任务日志对象
:return: 新增定时任务日志校验结果
"""
try:
JobLogDao.add_job_log_dao(query_db, page_object)
query_db.commit()
result = {'is_success': True, 'message': '新增成功'}
except Exception as e:
query_db.rollback()
result = {'is_success': False, 'message': str(e)}
return CrudResponseModel(**result)
@classmethod
async def delete_job_log_services(cls, query_db: AsyncSession, page_object: DeleteJobLogModel) -> CrudResponseModel:
"""
删除定时任务日志信息service
:param query_db: orm对象
:param page_object: 删除定时任务日志对象
:return: 删除定时任务日志校验结果
"""
if page_object.job_log_ids:
job_log_id_list = page_object.job_log_ids.split(',')
try:
for job_log_id in job_log_id_list:
await JobLogDao.delete_job_log_dao(query_db, JobLogModel(jobLogId=job_log_id))
await query_db.commit()
result = {'is_success': True, 'message': '删除成功'}
except Exception as e:
await query_db.rollback()
raise e
else:
result = {'is_success': False, 'message': '传入定时任务日志id为空'}
return CrudResponseModel(**result)
@classmethod
async def clear_job_log_services(cls, query_db: AsyncSession) -> CrudResponseModel:
"""
清除定时任务日志信息service
:param query_db: orm对象
:return: 清除定时任务日志校验结果
"""
try:
await JobLogDao.clear_job_log_dao(query_db)
await query_db.commit()
result = {'is_success': True, 'message': '清除成功'}
except Exception as e:
await query_db.rollback()
raise e
return CrudResponseModel(**result)
@staticmethod
async def export_job_log_list_services(request: Request, job_log_list: list) -> bytes:
"""
导出定时任务日志信息service
:param request: Request对象
:param job_log_list: 定时任务日志信息列表
:return: 定时任务日志信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'jobLogId': '任务日志编码',
'jobName': '任务名称',
'jobGroup': '任务组名',
'jobExecutor': '任务执行器',
'invokeTarget': '调用目标字符串',
'jobArgs': '位置参数',
'jobKwargs': '关键字参数',
'jobTrigger': '任务触发器',
'jobMessage': '日志信息',
'status': '执行状态',
'exceptionInfo': '异常信息',
'createTime': '创建时间',
}
job_group_list = await DictDataService.query_dict_data_list_from_cache_services(
request.app.state.redis, dict_type='sys_job_group'
)
job_group_option = [{'label': item.get('dictLabel'), 'value': item.get('dictValue')} for item in job_group_list]
job_group_option_dict = {item.get('value'): item for item in job_group_option}
job_executor_list = await DictDataService.query_dict_data_list_from_cache_services(
request.app.state.redis, dict_type='sys_job_executor'
)
job_executor_option = [
{'label': item.get('dictLabel'), 'value': item.get('dictValue')} for item in job_executor_list
]
job_executor_option_dict = {item.get('value'): item for item in job_executor_option}
for item in job_log_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '暂停'
if str(item.get('jobGroup')) in job_group_option_dict:
item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label')
if str(item.get('jobExecutor')) in job_executor_option_dict:
item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label')
binary_data = ExcelUtil.export_list2excel(job_log_list, mapping_dict)
return binary_data
@@ -0,0 +1,273 @@
from typing import Any
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant, JobConstant
from common.vo import CrudResponseModel, PageModel
from config.get_scheduler import SchedulerUtil
from exceptions.exception import ServiceException
from module_admin.dao.job_dao import JobDao
from module_admin.entity.vo.job_vo import DeleteJobModel, EditJobModel, JobModel, JobPageQueryModel
from module_admin.service.dict_service import DictDataService
from utils.common_util import CamelCaseUtil
from utils.cron_util import CronUtil
from utils.excel_util import ExcelUtil
from utils.string_util import StringUtil
class JobService:
"""
定时任务管理模块服务层
"""
@classmethod
async def get_job_list_services(
cls, query_db: AsyncSession, query_object: JobPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取定时任务列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 定时任务列表信息对象
"""
job_list_result = await JobDao.get_job_list(query_db, query_object, is_page)
return job_list_result
@classmethod
async def check_job_unique_services(cls, query_db: AsyncSession, page_object: JobModel) -> bool:
"""
校验定时任务是否存在service
:param query_db: orm对象
:param page_object: 定时任务对象
:return: 校验结果
"""
job_id = -1 if page_object.job_id is None else page_object.job_id
job = await JobDao.get_job_detail_by_info(query_db, page_object)
if job and job.job_id != job_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_job_services(cls, query_db: AsyncSession, page_object: JobModel) -> CrudResponseModel:
"""
新增定时任务信息service
:param query_db: orm对象
:param page_object: 新增定时任务对象
:return: 新增定时任务校验结果
"""
if not CronUtil.validate_cron_expression(page_object.cron_expression):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,Cron表达式不正确')
if StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许rmi调用')
if StringUtil.contains_any_ignore_case(
page_object.invoke_target, [CommonConstant.LOOKUP_LDAP, CommonConstant.LOOKUP_LDAPS]
):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许ldap(s)调用')
if StringUtil.contains_any_ignore_case(page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS]):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用')
if StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串存在违规')
if not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不在白名单内')
if not await cls.check_job_unique_services(query_db, page_object):
raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,定时任务已存在')
try:
add_job = await JobDao.add_job_dao(query_db, page_object)
job_info = await cls.job_detail_services(query_db, add_job.job_id)
if job_info.status == '0':
SchedulerUtil.add_scheduler_job(job_info=job_info)
await query_db.commit()
await SchedulerUtil.request_scheduler_sync()
result = {'is_success': True, 'message': '新增成功'}
except Exception as e:
await query_db.rollback()
raise e
return CrudResponseModel(**result)
@classmethod
def _deal_edit_job(cls, page_object: EditJobModel, edit_job: dict[str, Any]) -> None:
"""
处理编辑定时任务字典
:param page_object: 编辑定时任务对象
:param edit_job: 编辑定时任务字典
"""
if page_object.type == 'status':
del edit_job['type']
@classmethod
async def edit_job_services(cls, query_db: AsyncSession, page_object: EditJobModel) -> CrudResponseModel:
"""
编辑定时任务信息service
:param query_db: orm对象
:param page_object: 编辑定时任务对象
:return: 编辑定时任务校验结果
"""
edit_job = page_object.model_dump(exclude_unset=True)
cls._deal_edit_job(page_object, edit_job)
job_info = await cls.job_detail_services(query_db, page_object.job_id)
if job_info:
if page_object.type != 'status':
if not CronUtil.validate_cron_expression(page_object.cron_expression):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,Cron表达式不正确')
if StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许rmi调用')
if StringUtil.contains_any_ignore_case(
page_object.invoke_target, [CommonConstant.LOOKUP_LDAP, CommonConstant.LOOKUP_LDAPS]
):
raise ServiceException(
message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许ldap(s)调用'
)
if StringUtil.contains_any_ignore_case(
page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS]
):
raise ServiceException(
message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用'
)
if StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串存在违规')
if not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串不在白名单内')
if not await cls.check_job_unique_services(query_db, page_object):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,定时任务已存在')
try:
await JobDao.edit_job_dao(query_db, edit_job, job_info)
SchedulerUtil.remove_scheduler_job(job_id=edit_job.get('job_id'))
if edit_job.get('status') == '0':
job_info = await cls.job_detail_services(query_db, edit_job.get('job_id'))
SchedulerUtil.add_scheduler_job(job_info=job_info)
await query_db.commit()
await SchedulerUtil.request_scheduler_sync()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='定时任务不存在')
@classmethod
async def execute_job_once_services(cls, query_db: AsyncSession, page_object: JobModel) -> CrudResponseModel:
"""
执行一次定时任务service
:param query_db: orm对象
:param page_object: 定时任务对象
:return: 执行一次定时任务结果
"""
SchedulerUtil.remove_scheduler_job(job_id=page_object.job_id)
job_info = await cls.job_detail_services(query_db, page_object.job_id)
if job_info:
SchedulerUtil.execute_scheduler_job_once(job_info=job_info)
return CrudResponseModel(is_success=True, message='执行成功')
raise ServiceException(message='定时任务不存在')
@classmethod
async def delete_job_services(cls, query_db: AsyncSession, page_object: DeleteJobModel) -> CrudResponseModel:
"""
删除定时任务信息service
:param query_db: orm对象
:param page_object: 删除定时任务对象
:return: 删除定时任务校验结果
"""
if page_object.job_ids:
job_id_list = page_object.job_ids.split(',')
try:
for job_id in job_id_list:
await JobDao.delete_job_dao(query_db, JobModel(jobId=job_id))
SchedulerUtil.remove_scheduler_job(job_id=job_id)
await query_db.commit()
await SchedulerUtil.request_scheduler_sync()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入定时任务id为空')
@classmethod
async def job_detail_services(cls, query_db: AsyncSession, job_id: int) -> JobModel:
"""
获取定时任务详细信息service
:param query_db: orm对象
:param job_id: 定时任务id
:return: 定时任务id对应的信息
"""
job = await JobDao.get_job_detail_by_id(query_db, job_id=job_id)
result = JobModel(**CamelCaseUtil.transform_result(job)) if job else JobModel()
return result
@staticmethod
async def export_job_list_services(request: Request, job_list: list) -> bytes:
"""
导出定时任务信息service
:param request: Request对象
:param job_list: 定时任务信息列表
:return: 定时任务信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'jobId': '任务编码',
'jobName': '任务名称',
'jobGroup': '任务组名',
'jobExecutor': '任务执行器',
'invokeTarget': '调用目标字符串',
'jobArgs': '位置参数',
'jobKwargs': '关键字参数',
'cronExpression': 'cron执行表达式',
'misfirePolicy': '计划执行错误策略',
'concurrent': '是否并发执行',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
job_group_list = await DictDataService.query_dict_data_list_from_cache_services(
request.app.state.redis, dict_type='sys_job_group'
)
job_group_option = [{'label': item.get('dictLabel'), 'value': item.get('dictValue')} for item in job_group_list]
job_group_option_dict = {item.get('value'): item for item in job_group_option}
job_executor_list = await DictDataService.query_dict_data_list_from_cache_services(
request.app.state.redis, dict_type='sys_job_executor'
)
job_executor_option = [
{'label': item.get('dictLabel'), 'value': item.get('dictValue')} for item in job_executor_list
]
job_executor_option_dict = {item.get('value'): item for item in job_executor_option}
for item in job_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '暂停'
if str(item.get('jobGroup')) in job_group_option_dict:
item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label')
if str(item.get('jobExecutor')) in job_executor_option_dict:
item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label')
if item.get('misfirePolicy') == '1':
item['misfirePolicy'] = '立即执行'
elif item.get('misfirePolicy') == '2':
item['misfirePolicy'] = '执行一次'
else:
item['misfirePolicy'] = '放弃执行'
if item.get('concurrent') == '0':
item['concurrent'] = '允许'
else:
item['concurrent'] = '禁止'
binary_data = ExcelUtil.export_list2excel(job_list, mapping_dict)
return binary_data
@@ -0,0 +1,508 @@
import asyncio
import hashlib
import json
import os
import uuid
from typing import Any
from fastapi import Request
from redis import asyncio as aioredis
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from config.database import AsyncSessionLocal
from config.env import LogConfig
from exceptions.exception import ServiceException
from middlewares.trace_middleware.ctx import TraceCtx
from module_admin.dao.log_dao import LoginLogDao, OperationLogDao
from module_admin.entity.vo.log_vo import (
DeleteLoginLogModel,
DeleteOperLogModel,
LogininforModel,
LoginLogPageQueryModel,
OperLogModel,
OperLogPageQueryModel,
UnlockUser,
)
from module_admin.service.dict_service import DictDataService
from utils.excel_util import ExcelUtil
from utils.log_util import LogSanitizer, logger
class OperationLogService:
"""
操作日志管理模块服务层
"""
@classmethod
async def get_operation_log_list_services(
cls, query_db: AsyncSession, query_object: OperLogPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取操作日志列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 操作日志列表信息对象
"""
operation_log_list_result = await OperationLogDao.get_operation_log_list(query_db, query_object, is_page)
return operation_log_list_result
@classmethod
async def add_operation_log_services(cls, query_db: AsyncSession, page_object: OperLogModel) -> CrudResponseModel:
"""
新增操作日志service
:param query_db: orm对象
:param page_object: 新增操作日志对象
:return: 新增操作日志校验结果
"""
try:
await OperationLogDao.add_operation_log_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def delete_operation_log_services(
cls, query_db: AsyncSession, page_object: DeleteOperLogModel
) -> CrudResponseModel:
"""
删除操作日志信息service
:param query_db: orm对象
:param page_object: 删除操作日志对象
:return: 删除操作日志校验结果
"""
if page_object.oper_ids:
oper_id_list = page_object.oper_ids.split(',')
try:
for oper_id in oper_id_list:
await OperationLogDao.delete_operation_log_dao(query_db, OperLogModel(operId=oper_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入操作日志id为空')
@classmethod
async def clear_operation_log_services(cls, query_db: AsyncSession) -> CrudResponseModel:
"""
清除操作日志信息service
:param query_db: orm对象
:return: 清除操作日志校验结果
"""
try:
await OperationLogDao.clear_operation_log_dao(query_db)
await query_db.commit()
return CrudResponseModel(is_success=True, message='清除成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def export_operation_log_list_services(cls, request: Request, operation_log_list: list) -> bytes:
"""
导出操作日志信息service
:param request: Request对象
:param operation_log_list: 操作日志信息列表
:return: 操作日志信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'operId': '日志编号',
'title': '系统模块',
'businessType': '操作类型',
'method': '方法名称',
'requestMethod': '请求方式',
'operName': '操作人员',
'deptName': '部门名称',
'operUrl': '请求URL',
'operIp': '操作地址',
'operLocation': '操作地点',
'operParam': '请求参数',
'jsonResult': '返回参数',
'status': '操作状态',
'error_msg': '错误消息',
'operTime': '操作日期',
'costTime': '消耗时间(毫秒)',
}
operation_type_list = await DictDataService.query_dict_data_list_from_cache_services(
request.app.state.redis, dict_type='sys_oper_type'
)
operation_type_option = [
{'label': item.get('dictLabel'), 'value': item.get('dictValue')} for item in operation_type_list
]
operation_type_option_dict = {item.get('value'): item for item in operation_type_option}
for item in operation_log_list:
if item.get('status') == 0:
item['status'] = '成功'
else:
item['status'] = '失败'
if str(item.get('businessType')) in operation_type_option_dict:
item['businessType'] = operation_type_option_dict.get(str(item.get('businessType'))).get('label')
binary_data = ExcelUtil.export_list2excel(operation_log_list, mapping_dict)
return binary_data
class LoginLogService:
"""
登录日志管理模块服务层
"""
@classmethod
async def get_login_log_list_services(
cls, query_db: AsyncSession, query_object: LoginLogPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取登录日志列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 登录日志列表信息对象
"""
operation_log_list_result = await LoginLogDao.get_login_log_list(query_db, query_object, is_page)
return operation_log_list_result
@classmethod
async def add_login_log_services(cls, query_db: AsyncSession, page_object: LogininforModel) -> CrudResponseModel:
"""
新增登录日志service
:param query_db: orm对象
:param page_object: 新增登录日志对象
:return: 新增登录日志校验结果
"""
try:
await LoginLogDao.add_login_log_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def delete_login_log_services(
cls, query_db: AsyncSession, page_object: DeleteLoginLogModel
) -> CrudResponseModel:
"""
删除操作日志信息service
:param query_db: orm对象
:param page_object: 删除操作日志对象
:return: 删除操作日志校验结果
"""
if page_object.info_ids:
info_id_list = page_object.info_ids.split(',')
try:
for info_id in info_id_list:
await LoginLogDao.delete_login_log_dao(query_db, LogininforModel(infoId=info_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入登录日志id为空')
@classmethod
async def clear_login_log_services(cls, query_db: AsyncSession) -> CrudResponseModel:
"""
清除操作日志信息service
:param query_db: orm对象
:return: 清除操作日志校验结果
"""
try:
await LoginLogDao.clear_login_log_dao(query_db)
await query_db.commit()
return CrudResponseModel(is_success=True, message='清除成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def unlock_user_services(cls, request: Request, unlock_user: UnlockUser) -> CrudResponseModel:
locked_user = await request.app.state.redis.get(f'account_lock:{unlock_user.user_name}')
if locked_user:
await request.app.state.redis.delete(f'account_lock:{unlock_user.user_name}')
return CrudResponseModel(is_success=True, message='解锁成功')
raise ServiceException(message='该用户未锁定')
@staticmethod
async def export_login_log_list_services(login_log_list: list) -> bytes:
"""
导出登录日志信息service
:param login_log_list: 登录日志信息列表
:return: 登录日志信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'infoId': '访问编号',
'userName': '用户名称',
'ipaddr': '登录地址',
'loginLocation': '登录地点',
'browser': '浏览器',
'os': '操作系统',
'status': '登录状态',
'msg': '操作信息',
'loginTime': '登录日期',
}
for item in login_log_list:
if item.get('status') == '0':
item['status'] = '成功'
else:
item['status'] = '失败'
binary_data = ExcelUtil.export_list2excel(login_log_list, mapping_dict)
return binary_data
class LogQueueService:
"""
日志队列服务
"""
@classmethod
def _build_event_id(cls, request_id: str, log_type: str, source: str) -> str:
"""
生成日志事件唯一标识
:param request_id: 请求唯一标识
:param log_type: 日志类型
:param source: 日志来源
:return: 事件唯一标识
"""
if not request_id:
return uuid.uuid4().hex
base = f'{request_id}:{log_type}:{source}'
return hashlib.md5(base.encode('utf-8')).hexdigest()
@classmethod
async def _xadd_event(cls, redis: aioredis.Redis, event_type: str, payload: dict, source: str) -> None:
"""
写入日志事件到Redis Streams
:param redis: Redis连接对象
:param event_type: 事件类型
:param payload: 事件负载
:param source: 日志来源
:return: None
"""
request_id = TraceCtx.get_request_id()
trace_id = TraceCtx.get_trace_id()
span_id = TraceCtx.get_span_id()
event_id = cls._build_event_id(request_id, event_type, source)
await redis.xadd(
LogConfig.log_stream_key,
{
'event_id': event_id,
'event_type': event_type,
'request_id': request_id,
'trace_id': trace_id,
'span_id': span_id,
'payload': json.dumps(payload, ensure_ascii=False, default=str),
},
maxlen=LogConfig.log_stream_maxlen,
approximate=True,
)
@classmethod
async def enqueue_login_log(cls, request: Request, login_log: LogininforModel, source: str) -> None:
"""
登录日志入队
:param request: Request对象
:param login_log: 登录日志模型
:param source: 日志来源
:return: None
"""
payload = LogSanitizer.sanitize_data(login_log.model_dump(by_alias=True, exclude_none=True))
await cls._xadd_event(request.app.state.redis, 'login', payload, source)
@classmethod
async def enqueue_operation_log(cls, request: Request, operation_log: OperLogModel, source: str) -> None:
"""
操作日志入队
:param request: Request对象
:param operation_log: 操作日志模型
:param source: 日志来源
:return: None
"""
payload = LogSanitizer.sanitize_data(operation_log.model_dump(by_alias=True, exclude_none=True))
await cls._xadd_event(request.app.state.redis, 'operation', payload, source)
class LogAggregatorService:
"""
日志聚合消费服务
"""
@classmethod
async def _ensure_group(cls, redis: aioredis.Redis) -> None:
"""
初始化消费组
:param redis: Redis连接对象
:return: None
"""
try:
await redis.xgroup_create(
name=LogConfig.log_stream_key,
groupname=LogConfig.log_stream_group,
id='0-0',
mkstream=True,
)
except Exception as exc:
if 'BUSYGROUP' not in str(exc):
raise
@classmethod
async def _acquire_dedup(cls, redis: aioredis.Redis, event_id: str) -> bool:
"""
获取去重锁
:param redis: Redis连接对象
:param event_id: 事件唯一标识
:return: 是否获取成功
"""
if not event_id:
return False
key = f'{LogConfig.log_stream_dedup_prefix}:{event_id}'
return await redis.set(key, '1', nx=True, ex=LogConfig.log_stream_dedup_ttl)
@classmethod
async def _release_dedup(cls, redis: aioredis.Redis, event_id: str) -> None:
"""
释放去重锁
:param redis: Redis连接对象
:param event_id: 事件唯一标识
:return: None
"""
if not event_id:
return
await redis.delete(f'{LogConfig.log_stream_dedup_prefix}:{event_id}')
@classmethod
async def _claim_pending(cls, redis: aioredis.Redis, consumer_name: str) -> None:
"""
认领并处理超时未确认的消息
:param redis: Redis连接对象
:param consumer_name: 消费者名称
:return: None
"""
if LogConfig.log_stream_claim_idle_ms <= 0:
return
start_id = '0-0'
while True:
result = await redis.xautoclaim(
name=LogConfig.log_stream_key,
groupname=LogConfig.log_stream_group,
consumername=consumer_name,
min_idle_time=LogConfig.log_stream_claim_idle_ms,
start_id=start_id,
count=LogConfig.log_stream_claim_batch_size,
)
if not result:
return
next_start_id, messages = result[0], result[1]
if messages:
await cls._process_messages(redis, LogConfig.log_stream_key, messages)
if not messages or next_start_id == start_id:
return
start_id = next_start_id
@classmethod
async def consume_stream(cls, redis: aioredis.Redis) -> None:
"""
消费日志队列
:param redis: Redis连接对象
:return: None
"""
await cls._ensure_group(redis)
consumer_name = f'{LogConfig.log_stream_consumer_prefix}-{os.getpid()}-{uuid.uuid4().hex[:6]}'
last_claim_time = 0.0
while True:
try:
now = asyncio.get_running_loop().time()
if now - last_claim_time >= LogConfig.log_stream_claim_interval_ms / 1000:
await cls._claim_pending(redis, consumer_name)
last_claim_time = now
result = await redis.xreadgroup(
groupname=LogConfig.log_stream_group,
consumername=consumer_name,
streams={LogConfig.log_stream_key: '>'},
count=LogConfig.log_stream_batch_size,
block=LogConfig.log_stream_block_ms,
)
if not result:
continue
for stream_name, messages in result:
await cls._process_messages(redis, stream_name, messages)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.error(f'日志聚合消费异常: {exc}')
await asyncio.sleep(1)
@classmethod
async def _process_messages(cls, redis: aioredis.Redis, stream_name: str, messages: list[tuple[str, dict]]) -> None:
"""
处理消息并落库
:param redis: Redis连接对象
:param stream_name: Stream名称
:param messages: 消息列表
:return: None
"""
if not messages:
return
async with AsyncSessionLocal() as session:
ack_ids: list[str] = []
dedup_event_ids: list[str] = []
try:
for message_id, data in messages:
event_type = data.get('event_type')
event_id = data.get('event_id')
payload_raw = data.get('payload') or '{}'
if event_type not in {'login', 'operation'}:
ack_ids.append(message_id)
continue
acquired = await cls._acquire_dedup(redis, event_id)
if not acquired:
ack_ids.append(message_id)
continue
dedup_event_ids.append(event_id)
payload = json.loads(payload_raw)
if event_type == 'login':
await LoginLogDao.add_login_log_dao(session, LogininforModel(**payload))
elif event_type == 'operation':
await OperationLogDao.add_operation_log_dao(session, OperLogModel(**payload))
ack_ids.append(message_id)
if ack_ids:
await session.commit()
await redis.xack(stream_name, LogConfig.log_stream_group, *ack_ids)
except Exception:
await session.rollback()
for event_id in dedup_event_ids:
await cls._release_dedup(redis, event_id)
raise
@@ -0,0 +1,656 @@
import random
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any
import jwt
from fastapi import Depends, Form, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from sqlalchemy import Row
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant, MenuConstant
from common.context import RequestContext
from common.enums import RedisInitKeyConfig
from common.vo import CrudResponseModel
from config.env import AppConfig, JwtConfig
from config.get_db import get_db
from exceptions.exception import AuthException, LoginException, ServiceException
from module_admin.dao.login_dao import login_by_account
from module_admin.dao.user_dao import UserDao
from module_admin.entity.do.dept_do import SysDept
from module_admin.entity.do.menu_do import SysMenu
from module_admin.entity.do.user_do import SysUser
from module_admin.entity.vo.login_vo import MenuTreeModel, MetaModel, RouterModel, SmsCode, UserLogin, UserRegister
from module_admin.entity.vo.user_vo import AddUserModel, CurrentUserModel, ResetUserModel, TokenData, UserInfoModel
from module_admin.service.user_service import UserService
from utils.client_ip_util import ClientIPUtil
from utils.common_util import CamelCaseUtil
from utils.log_util import logger
from utils.message_util import message_service
from utils.pwd_util import PwdUtil
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login')
class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm):
"""
自定义OAuth2PasswordRequestForm类,增加验证码及会话编号参数
"""
def __init__(
self,
grant_type: str = Form(default=None, pattern='password'),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=''),
client_id: str | None = Form(default=None),
client_secret: str | None = Form(default=None),
code: str | None = Form(default=''),
uuid: str | None = Form(default=''),
login_info: dict[str, str] | None = Form(default=None),
) -> None:
super().__init__(
grant_type=grant_type,
username=username,
password=password,
scope=scope,
client_id=client_id,
client_secret=client_secret,
)
self.code = code
self.uuid = uuid
self.login_info = login_info
class LoginService:
"""
登录模块服务层
"""
@classmethod
async def authenticate_user(
cls, request: Request, query_db: AsyncSession, login_user: UserLogin
) -> Row[tuple[SysUser, SysDept]]:
"""
根据用户名密码校验用户登录
:param request: Request对象
:param query_db: orm对象
:param login_user: 登录用户对象
:return: 校验结果
"""
await cls.__check_login_ip(request)
account_lock = await request.app.state.redis.get(
f'{RedisInitKeyConfig.ACCOUNT_LOCK.key}:{login_user.user_name}'
)
if login_user.user_name == account_lock:
logger.warning('账号已锁定,请稍后再试')
raise LoginException(data='', message='账号已锁定,请稍后再试')
# 判断请求是否来自于api文档,如果是返回指定格式的结果,用于修复api文档认证成功后token显示undefined的bug
request_from_swagger = (
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False
)
request_from_redoc = (
request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False
)
# 判断是否开启验证码,开启则验证,否则不验证(dev模式下来自API文档的登录请求不检验)
if not login_user.captcha_enabled or (
(request_from_swagger or request_from_redoc) and AppConfig.app_env == 'dev'
):
pass
else:
await cls.__check_login_captcha(request, login_user)
user = await login_by_account(query_db, login_user.user_name)
if not user:
logger.warning('用户不存在')
raise LoginException(data='', message='用户不存在')
if not PwdUtil.verify_password(login_user.password, user[0].password):
cache_password_error_count = await request.app.state.redis.get(
f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}'
)
password_error_counted = 0
if cache_password_error_count:
password_error_counted = cache_password_error_count
password_error_count = int(password_error_counted) + 1
await request.app.state.redis.set(
f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}',
password_error_count,
ex=timedelta(minutes=10),
)
if password_error_count > CommonConstant.PASSWORD_ERROR_COUNT:
await request.app.state.redis.delete(
f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}'
)
await request.app.state.redis.set(
f'{RedisInitKeyConfig.ACCOUNT_LOCK.key}:{login_user.user_name}',
login_user.user_name,
ex=timedelta(minutes=10),
)
logger.warning('10分钟内密码已输错超过5次,账号已锁定,请10分钟后再试')
raise LoginException(data='', message='10分钟内密码已输错超过5次,账号已锁定,请10分钟后再试')
logger.warning('密码错误')
raise LoginException(data='', message='密码错误')
if user[0].status == '1':
logger.warning('用户已停用')
raise LoginException(data='', message='用户已停用')
await request.app.state.redis.delete(f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}')
return user
@classmethod
async def __check_login_ip(cls, request: Request) -> bool:
"""
校验用户登录ip是否在黑名单内
:param request: Request对象
:return: 校验结果
"""
black_ip_value = await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.login.blackIPList')
black_ip_list = black_ip_value.split(',') if black_ip_value else []
if ClientIPUtil.get_client_ip(request) in black_ip_list:
logger.warning('当前IP禁止登录')
raise LoginException(data='', message='当前IP禁止登录')
return True
@classmethod
async def __check_login_captcha(cls, request: Request, login_user: UserLogin) -> bool:
"""
校验用户登录验证码
:param request: Request对象
:param login_user: 登录用户对象
:return: 校验结果
"""
captcha_value = await request.app.state.redis.get(f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{login_user.uuid}')
if not captcha_value:
logger.warning('验证码已失效')
raise LoginException(data='', message='验证码已失效')
if login_user.code != str(captcha_value):
logger.warning('验证码错误')
raise LoginException(data='', message='验证码错误')
return True
@classmethod
async def create_access_token(cls, data: dict, expires_delta: timedelta | None = None) -> str:
"""
根据登录信息创建当前用户token
:param data: 登录信息
:param expires_delta: token有效期
:return: token
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=30)
to_encode.update({'exp': expire})
encoded_jwt = jwt.encode(to_encode, JwtConfig.jwt_secret_key, algorithm=JwtConfig.jwt_algorithm)
return encoded_jwt
@classmethod
async def get_current_user(
cls, request: Request = Request, token: str = Depends(oauth2_scheme), query_db: AsyncSession = Depends(get_db)
) -> CurrentUserModel:
"""
根据token获取当前用户信息
:param request: Request对象
:param token: 用户token
:param query_db: orm对象
:return: 当前用户信息对象
:raise: 令牌异常AuthException
"""
# if token[:6] != 'Bearer':
# logger.warning("用户token不合法")
# raise AuthException(data="", message="用户token不合法")
try:
if token.startswith('Bearer'):
token = token.split(' ')[1]
payload = jwt.decode(token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm])
user_id: str = payload.get('user_id')
session_id: str = payload.get('session_id')
if not user_id:
logger.warning('用户token不合法')
raise AuthException(data='', message='用户token不合法')
token_data = TokenData(user_id=int(user_id))
except InvalidTokenError as e:
logger.warning('用户token已失效,请重新登录')
raise AuthException(data='', message='用户token已失效,请重新登录') from e
query_user = await UserDao.get_user_by_id(query_db, user_id=token_data.user_id)
if query_user.get('user_basic_info') is None:
logger.warning('用户token不合法')
raise AuthException(data='', message='用户token不合法')
if AppConfig.app_same_time_login:
redis_token = await request.app.state.redis.get(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}')
else:
# 此方法可实现同一账号同一时间只能登录一次
redis_token = await request.app.state.redis.get(
f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get("user_basic_info").user_id}'
)
if token == redis_token:
if AppConfig.app_same_time_login:
await request.app.state.redis.set(
f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}',
redis_token,
ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes),
)
else:
await request.app.state.redis.set(
f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get("user_basic_info").user_id}',
redis_token,
ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes),
)
role_id_list = [item.role_id for item in query_user.get('user_role_info')]
if 1 in role_id_list: # noqa: SIM108
permissions = ['*:*:*']
else:
permissions = [row.perms for row in query_user.get('user_menu_info')]
post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')])
role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')])
roles = [row.role_key for row in query_user.get('user_role_info')]
is_default_modify_pwd = await cls.__init_password_is_modify(
request, query_user.get('user_basic_info').pwd_update_date
)
is_password_expired = await cls.__password_is_expired(
request, query_user.get('user_basic_info').pwd_update_date
)
current_user = CurrentUserModel(
permissions=permissions,
roles=roles,
user=UserInfoModel(
**CamelCaseUtil.transform_result(query_user.get('user_basic_info')),
postIds=post_ids,
roleIds=role_ids,
dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')),
role=CamelCaseUtil.transform_result(query_user.get('user_role_info')),
),
isDefaultModifyPwd=is_default_modify_pwd,
isPasswordExpired=is_password_expired,
)
# 设置当前用户信息到上下文
RequestContext.set_current_user(current_user)
return current_user
logger.warning('用户token已失效,请重新登录')
raise AuthException(data='', message='用户token已失效,请重新登录')
@classmethod
async def __init_password_is_modify(cls, request: Request, pwd_update_date: datetime) -> bool:
"""
判断当前用户是否初始密码登录
:param request: Request对象
:param pwd_update_date: 密码最后更新时间
:return: 是否初始密码登录
"""
init_password_is_modify = await request.app.state.redis.get(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.initPasswordModify'
)
return init_password_is_modify == '1' and pwd_update_date is None
@classmethod
async def __password_is_expired(cls, request: Request, pwd_update_date: datetime) -> bool:
"""
判断当前用户密码是否过期
:param request: Request对象
:param pwd_update_date: 密码最后更新时间
:return: 密码是否过期
"""
password_validate_days = await request.app.state.redis.get(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.passwordValidateDays'
)
if password_validate_days and int(password_validate_days) > 0:
if pwd_update_date is None:
return True
expire_date = pwd_update_date + timedelta(days=int(password_validate_days))
if datetime.now() > expire_date:
return True
return False
@classmethod
async def get_current_user_routers(cls, user_id: int, query_db: AsyncSession) -> list[dict[str, Any]]:
"""
根据用户id获取当前用户路由信息
:param user_id: 用户id
:param query_db: orm对象
:return: 当前用户路由信息对象
"""
query_user = await UserDao.get_user_by_id(query_db, user_id=user_id)
user_router_menu = sorted(
[
row
for row in query_user.get('user_menu_info')
if row.menu_type in [MenuConstant.TYPE_DIR, MenuConstant.TYPE_MENU]
],
key=lambda x: x.order_num,
)
menus = cls.__generate_menus(0, user_router_menu)
user_router = cls.__generate_user_router_menu(menus)
return [router.model_dump(exclude_unset=True, by_alias=True) for router in user_router]
@classmethod
def __generate_menus(cls, pid: int, permission_list: list[SysMenu]) -> list[MenuTreeModel]:
"""
工具方法:根据菜单信息生成菜单信息树形嵌套数据
:param pid: 菜单id
:param permission_list: 菜单列表信息
:return: 菜单信息树形嵌套数据
"""
menu_list: list[MenuTreeModel] = []
for permission in permission_list:
if permission.parent_id == pid:
children = cls.__generate_menus(permission.menu_id, permission_list)
menu_list_data = MenuTreeModel(**CamelCaseUtil.transform_result(permission))
if children:
menu_list_data.children = children
menu_list.append(menu_list_data)
return menu_list
@classmethod
def __generate_user_router_menu(cls, permission_list: list[MenuTreeModel]) -> list[RouterModel]:
"""
工具方法:根据菜单树信息生成路由信息树形嵌套数据
:param permission_list: 菜单树列表信息
:return: 路由信息树形嵌套数据
"""
router_list: list[RouterModel] = []
for permission in permission_list:
router = RouterModel(
hidden=permission.visible == '1',
name=RouterUtil.get_router_name(permission),
path=RouterUtil.get_router_path(permission),
component=RouterUtil.get_component(permission),
query=permission.query,
meta=MetaModel(
title=permission.menu_name,
icon=permission.icon,
noCache=permission.is_cache == 1,
link=permission.path if RouterUtil.is_http(permission.path) else None,
),
)
c_menus = permission.children
if c_menus and permission.menu_type == MenuConstant.TYPE_DIR:
router.always_show = True
router.redirect = 'noRedirect'
router.children = cls.__generate_user_router_menu(c_menus)
elif RouterUtil.is_menu_frame(permission):
router.meta = None
children_list: list[RouterModel] = []
children = RouterModel(
path=permission.path,
component=permission.component,
name=RouterUtil.get_route_name(permission.route_name, permission.path),
meta=MetaModel(
title=permission.menu_name,
icon=permission.icon,
noCache=permission.is_cache == 1,
link=permission.path if RouterUtil.is_http(permission.path) else None,
),
query=permission.query,
)
children_list.append(children)
router.children = children_list
elif permission.parent_id == 0 and RouterUtil.is_inner_link(permission):
router.meta = MetaModel(title=permission.menu_name, icon=permission.icon)
router.path = '/'
children_list: list[RouterModel] = []
router_path = RouterUtil.inner_link_replace_each(permission.path)
children = RouterModel(
path=router_path,
component=MenuConstant.INNER_LINK,
name=RouterUtil.get_route_name(permission.route_name, permission.path),
meta=MetaModel(
title=permission.menu_name,
icon=permission.icon,
link=permission.path if RouterUtil.is_http(permission.path) else None,
),
)
children_list.append(children)
router.children = children_list
router_list.append(router)
return router_list
@classmethod
async def register_user_services(
cls, request: Request, query_db: AsyncSession, user_register: UserRegister
) -> CrudResponseModel:
"""
用户注册services
:param request: Request对象
:param query_db: orm对象
:param user_register: 注册用户对象
:return: 注册结果
"""
register_enabled = (
await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.registerUser') == 'true'
)
captcha_enabled = (
await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.captchaEnabled')
== 'true'
)
if user_register.password == user_register.confirm_password:
if register_enabled:
if captcha_enabled:
captcha_value = await request.app.state.redis.get(
f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{user_register.uuid}'
)
if not captcha_value:
raise ServiceException(message='验证码已失效')
if user_register.code != str(captcha_value):
raise ServiceException(message='验证码错误')
add_user = AddUserModel(
userName=user_register.username,
nickName=user_register.username,
password=PwdUtil.get_password_hash(user_register.password),
pwdUpdateDate=datetime.now(),
)
result = await UserService.add_user_services(query_db, add_user)
return result
raise ServiceException(message='注册程序已关闭,禁止注册')
raise ServiceException(message='两次输入的密码不一致')
@classmethod
async def get_sms_code_services(cls, request: Request, query_db: AsyncSession, user: ResetUserModel) -> SmsCode:
"""
获取短信验证码service
:param request: Request对象
:param query_db: orm对象
:param user: 用户对象
:return: 短信验证码对象
"""
redis_sms_result = await request.app.state.redis.get(f'{RedisInitKeyConfig.SMS_CODE.key}:{user.session_id}')
if redis_sms_result:
return SmsCode(is_success=False, sms_code='', session_id='', message='短信验证码仍在有效期内')
is_user = await UserDao.get_user_by_name(query_db, user.user_name)
if is_user:
sms_code = str(random.randint(100000, 999999))
session_id = str(uuid.uuid4())
await request.app.state.redis.set(
f'{RedisInitKeyConfig.SMS_CODE.key}:{session_id}', sms_code, ex=timedelta(minutes=2)
)
# 此处模拟调用短信服务
message_service(sms_code)
return SmsCode(is_success=True, sms_code=sms_code, session_id=session_id, message='获取成功')
return SmsCode(is_success=False, sms_code='', session_id='', message='用户不存在')
@classmethod
async def forget_user_services(
cls, request: Request, query_db: AsyncSession, forget_user: ResetUserModel
) -> CrudResponseModel:
"""
用户忘记密码services
:param request: Request对象
:param query_db: orm对象
:param forget_user: 重置用户对象
:return: 重置结果
"""
redis_sms_result = await request.app.state.redis.get(
f'{RedisInitKeyConfig.SMS_CODE.key}:{forget_user.session_id}'
)
if forget_user.sms_code == redis_sms_result:
forget_user.password = PwdUtil.get_password_hash(forget_user.password)
forget_user.user_id = (await UserDao.get_user_by_name(query_db, forget_user.user_name)).user_id
edit_result = await UserService.reset_user_services(query_db, forget_user)
result = edit_result.dict()
elif not redis_sms_result:
result = {'is_success': False, 'message': '短信验证码已过期'}
else:
await request.app.state.redis.delete(f'{RedisInitKeyConfig.SMS_CODE.key}:{forget_user.session_id}')
result = {'is_success': False, 'message': '短信验证码不正确'}
return CrudResponseModel(**result)
@classmethod
async def logout_services(cls, request: Request, token_id: str) -> bool:
"""
退出登录services
:param request: Request对象
:param token_id: 令牌编号
:return: 退出登录结果
"""
await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}')
# await request.app.state.redis.delete(f'{current_user.user.user_id}_access_token')
# await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id')
return True
class RouterUtil:
"""
路由处理工具类
"""
@classmethod
def get_router_name(cls, menu: MenuTreeModel) -> str:
"""
获取路由名称
:param menu: 菜单数对象
:return: 路由名称
"""
# 非外链并且是一级目录(类型为目录)
if cls.is_menu_frame(menu):
return ''
return cls.get_route_name(menu.route_name, menu.path)
@classmethod
def get_route_name(cls, name: str, path: str) -> str:
"""
获取路由名称,如没有配置路由名称则取路由地址
:param name: 路由名称
:param path: 路由地址
:return: 路由名称(驼峰格式)
"""
router_name = name if name else path
return router_name.capitalize()
@classmethod
def get_router_path(cls, menu: MenuTreeModel) -> str | None:
"""
获取路由地址
:param menu: 菜单数对象
:return: 路由地址
"""
# 内链打开外网方式
router_path = menu.path
if menu.parent_id != 0 and cls.is_inner_link(menu):
router_path = cls.inner_link_replace_each(router_path)
# 非外链并且是一级目录(类型为目录)
if menu.parent_id == 0 and menu.menu_type == MenuConstant.TYPE_DIR and menu.is_frame == MenuConstant.NO_FRAME:
router_path = f'/{menu.path}'
# 非外链并且是一级目录(类型为菜单)
elif cls.is_menu_frame(menu):
router_path = '/'
return router_path
@classmethod
def get_component(cls, menu: MenuTreeModel) -> str:
"""
获取组件信息
:param menu: 菜单数对象
:return: 组件信息
"""
component = MenuConstant.LAYOUT
if menu.component and not cls.is_menu_frame(menu):
component = menu.component
elif (menu.component is None or menu.component == '') and menu.parent_id != 0 and cls.is_inner_link(menu):
component = MenuConstant.INNER_LINK
elif (menu.component is None or menu.component == '') and cls.is_parent_view(menu):
component = MenuConstant.PARENT_VIEW
return component
@classmethod
def is_menu_frame(cls, menu: MenuTreeModel) -> bool:
"""
判断是否为菜单内部跳转
:param menu: 菜单数对象
:return: 是否为菜单内部跳转
"""
return (
menu.parent_id == 0 and menu.menu_type == MenuConstant.TYPE_MENU and menu.is_frame == MenuConstant.NO_FRAME
)
@classmethod
def is_inner_link(cls, menu: MenuTreeModel) -> bool:
"""
判断是否为内链组件
:param menu: 菜单数对象
:return: 是否为内链组件
"""
return menu.is_frame == MenuConstant.NO_FRAME and cls.is_http(menu.path)
@classmethod
def is_parent_view(cls, menu: MenuTreeModel) -> bool:
"""
判断是否为parent_view组件
:param menu: 菜单数对象
:return: 是否为parent_view组件
"""
return menu.parent_id != 0 and menu.menu_type == MenuConstant.TYPE_DIR
@classmethod
def is_http(cls, link: str) -> bool:
"""
判断是否为http(s)://开头
:param link: 链接
:return: 是否为http(s)://开头
"""
return link.startswith((CommonConstant.HTTP, CommonConstant.HTTPS))
@classmethod
def inner_link_replace_each(cls, path: str) -> str:
"""
内链域名特殊字符替换
:param path: 内链域名
:return: 替换后的内链域名
"""
old_values = [CommonConstant.HTTP, CommonConstant.HTTPS, CommonConstant.WWW, '.', ':']
new_values = ['', '', '', '/', '/']
for old, new in zip(old_values, new_values, strict=False):
path = path.replace(old, new)
return path
@@ -0,0 +1,217 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant, MenuConstant
from common.vo import CrudResponseModel
from exceptions.exception import ServiceException, ServiceWarning
from module_admin.dao.menu_dao import MenuDao
from module_admin.dao.role_dao import RoleDao
from module_admin.entity.do.menu_do import SysMenu
from module_admin.entity.vo.menu_vo import DeleteMenuModel, MenuModel, MenuQueryModel, MenuTreeModel
from module_admin.entity.vo.role_vo import RoleMenuQueryModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from utils.common_util import CamelCaseUtil
from utils.string_util import StringUtil
class MenuService:
"""
菜单管理模块服务层
"""
@classmethod
async def get_menu_tree_services(
cls, query_db: AsyncSession, current_user: CurrentUserModel | None = None
) -> list[dict[str, Any]]:
"""
获取菜单树信息service
:param query_db: orm对象
:param current_user: 当前用户对象
:return: 菜单树信息对象
"""
menu_list_result = await MenuDao.get_menu_list_for_tree(
query_db, current_user.user.user_id, current_user.user.role
)
menu_tree_model_result = cls.list_to_tree(menu_list_result)
menu_tree_result = [menu.model_dump(exclude_unset=True, by_alias=True) for menu in menu_tree_model_result]
return menu_tree_result
@classmethod
async def get_role_menu_tree_services(
cls, query_db: AsyncSession, role_id: int, current_user: CurrentUserModel | None = None
) -> RoleMenuQueryModel:
"""
根据角色id获取菜单树信息service
:param query_db: orm对象
:param role_id: 角色id
:param current_user: 当前用户对象
:return: 当前角色id的菜单树信息对象
"""
menu_list_result = await MenuDao.get_menu_list_for_tree(
query_db, current_user.user.user_id, current_user.user.role
)
menu_tree_result = cls.list_to_tree(menu_list_result)
role = await RoleDao.get_role_detail_by_id(query_db, role_id)
role_menu_list = await RoleDao.get_role_menu_dao(query_db, role)
checked_keys = [row.menu_id for row in role_menu_list]
result = RoleMenuQueryModel(menus=menu_tree_result, checkedKeys=checked_keys)
return result
@classmethod
async def get_menu_list_services(
cls, query_db: AsyncSession, page_object: MenuQueryModel, current_user: CurrentUserModel | None = None
) -> list[dict[str, Any]]:
"""
获取菜单列表信息service
:param query_db: orm对象
:param page_object: 分页查询参数对象
:param current_user: 当前用户对象
:return: 菜单列表信息对象
"""
menu_list_result = await MenuDao.get_menu_list(
query_db, page_object, current_user.user.user_id, current_user.user.role
)
return CamelCaseUtil.transform_result(menu_list_result)
@classmethod
async def check_menu_name_unique_services(cls, query_db: AsyncSession, page_object: MenuModel) -> bool:
"""
校验菜单名称是否唯一service
:param query_db: orm对象
:param page_object: 菜单对象
:return: 校验结果
"""
menu_id = -1 if page_object.menu_id is None else page_object.menu_id
menu = await MenuDao.get_menu_detail_by_info(query_db, MenuModel(menuName=page_object.menu_name))
if menu and menu.menu_id != menu_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_menu_services(cls, query_db: AsyncSession, page_object: MenuModel) -> CrudResponseModel:
"""
新增菜单信息service
:param query_db: orm对象
:param page_object: 新增菜单对象
:return: 新增菜单校验结果
"""
if not await cls.check_menu_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,菜单名称已存在')
if page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path):
raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,地址必须以http(s)://开头')
try:
await MenuDao.add_menu_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel) -> CrudResponseModel:
"""
编辑菜单信息service
:param query_db: orm对象
:param page_object: 编辑部门对象
:return: 编辑菜单校验结果
"""
edit_menu = page_object.model_dump(exclude_unset=True)
menu_info = await cls.menu_detail_services(query_db, page_object.menu_id)
if menu_info.menu_id:
if not await cls.check_menu_name_unique_services(query_db, page_object):
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,菜单名称已存在')
if page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path):
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,地址必须以http(s)://开头')
if page_object.menu_id == page_object.parent_id:
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,上级菜单不能选择自己')
try:
await MenuDao.edit_menu_dao(query_db, edit_menu)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='菜单不存在')
@classmethod
async def delete_menu_services(cls, query_db: AsyncSession, page_object: DeleteMenuModel) -> CrudResponseModel:
"""
删除菜单信息service
:param query_db: orm对象
:param page_object: 删除菜单对象
:return: 删除菜单校验结果
"""
if page_object.menu_ids:
menu_id_list = page_object.menu_ids.split(',')
try:
for menu_id in menu_id_list:
if (await MenuDao.has_child_by_menu_id_dao(query_db, int(menu_id))) > 0:
raise ServiceWarning(message='存在子菜单,不允许删除')
if (await MenuDao.check_menu_exist_role_dao(query_db, int(menu_id))) > 0:
raise ServiceWarning(message='菜单已分配,不允许删除')
await MenuDao.delete_menu_dao(query_db, MenuModel(menuId=menu_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入菜单id为空')
@classmethod
async def menu_detail_services(cls, query_db: AsyncSession, menu_id: int) -> MenuModel:
"""
获取菜单详细信息service
:param query_db: orm对象
:param menu_id: 菜单id
:return: 菜单id对应的信息
"""
menu = await MenuDao.get_menu_detail_by_id(query_db, menu_id=menu_id)
result = MenuModel(**CamelCaseUtil.transform_result(menu)) if menu else MenuModel()
return result
@classmethod
def list_to_tree(cls, permission_list: Sequence[SysMenu]) -> list[MenuTreeModel]:
"""
工具方法:根据菜单列表信息生成树形嵌套数据
:param permission_list: 菜单列表信息
:return: 菜单树形嵌套数据
"""
_permission_list = [
MenuTreeModel(id=item.menu_id, label=item.menu_name, parentId=item.parent_id) for item in permission_list
]
# 转成id为key的字典
mapping: dict[int, MenuTreeModel] = dict(zip([i.id for i in _permission_list], _permission_list, strict=False))
# 树容器
container: list[MenuTreeModel] = []
for d in _permission_list:
# 如果找不到父级项,则是根节点
parent = mapping.get(d.parent_id)
if parent is None:
container.append(d)
else:
children: list[MenuTreeModel] = parent.children
if not children:
children = []
children.append(d)
parent.children = children
return container
@@ -0,0 +1,126 @@
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.notice_dao import NoticeDao
from module_admin.entity.vo.notice_vo import DeleteNoticeModel, NoticeModel, NoticePageQueryModel
from utils.common_util import CamelCaseUtil
class NoticeService:
"""
通知公告管理模块服务层
"""
@classmethod
async def get_notice_list_services(
cls, query_db: AsyncSession, query_object: NoticePageQueryModel, is_page: bool = True
) -> PageModel | list[dict[str, Any]]:
"""
获取通知公告列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 通知公告列表信息对象
"""
notice_list_result = await NoticeDao.get_notice_list(query_db, query_object, is_page)
return notice_list_result
@classmethod
async def check_notice_unique_services(cls, query_db: AsyncSession, page_object: NoticeModel) -> bool:
"""
校验通知公告是否存在service
:param query_db: orm对象
:param page_object: 通知公告对象
:return: 校验结果
"""
notice_id = -1 if page_object.notice_id is None else page_object.notice_id
notice = await NoticeDao.get_notice_detail_by_info(query_db, page_object)
if notice and notice.notice_id != notice_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_notice_services(cls, query_db: AsyncSession, page_object: NoticeModel) -> CrudResponseModel:
"""
新增通知公告信息service
:param query_db: orm对象
:param page_object: 新增通知公告对象
:return: 新增通知公告校验结果
"""
if not await cls.check_notice_unique_services(query_db, page_object):
raise ServiceException(message=f'新增通知公告{page_object.notice_title}失败,通知公告已存在')
try:
await NoticeDao.add_notice_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_notice_services(cls, query_db: AsyncSession, page_object: NoticeModel) -> CrudResponseModel:
"""
编辑通知公告信息service
:param query_db: orm对象
:param page_object: 编辑通知公告对象
:return: 编辑通知公告校验结果
"""
edit_notice = page_object.model_dump(exclude_unset=True)
notice_info = await cls.notice_detail_services(query_db, page_object.notice_id)
if notice_info.notice_id:
if not await cls.check_notice_unique_services(query_db, page_object):
raise ServiceException(message=f'修改通知公告{page_object.notice_title}失败,通知公告已存在')
try:
await NoticeDao.edit_notice_dao(query_db, edit_notice)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='通知公告不存在')
@classmethod
async def delete_notice_services(cls, query_db: AsyncSession, page_object: DeleteNoticeModel) -> CrudResponseModel:
"""
删除通知公告信息service
:param query_db: orm对象
:param page_object: 删除通知公告对象
:return: 删除通知公告校验结果
"""
if page_object.notice_ids:
notice_id_list = page_object.notice_ids.split(',')
try:
for notice_id in notice_id_list:
await NoticeDao.delete_notice_dao(query_db, NoticeModel(noticeId=notice_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入通知公告id为空')
@classmethod
async def notice_detail_services(cls, query_db: AsyncSession, notice_id: int) -> NoticeModel:
"""
获取通知公告详细信息service
:param query_db: orm对象
:param notice_id: 通知公告id
:return: 通知公告id对应的信息
"""
notice = await NoticeDao.get_notice_detail_by_id(query_db, notice_id=notice_id)
result = NoticeModel(**CamelCaseUtil.transform_result(notice)) if notice else NoticeModel()
return result
@@ -0,0 +1,78 @@
from typing import Any
import jwt
from fastapi import Request
from common.enums import RedisInitKeyConfig
from common.vo import CrudResponseModel
from config.env import AppConfig, JwtConfig
from exceptions.exception import ServiceException
from module_admin.entity.vo.online_vo import DeleteOnlineModel, OnlineQueryModel
from utils.common_util import CamelCaseUtil
class OnlineService:
"""
在线用户管理模块服务层
"""
@classmethod
async def get_online_list_services(cls, request: Request, query_object: OnlineQueryModel) -> list[dict[str, Any]]:
"""
获取在线用户表信息service
:param request: Request对象
:param query_object: 查询参数对象
:return: 在线用户列表信息
"""
access_token_keys = await request.app.state.redis.keys(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}*')
if not access_token_keys:
access_token_keys = []
access_token_values_list = [await request.app.state.redis.get(key) for key in access_token_keys]
online_info_list = []
for item in access_token_values_list:
payload = jwt.decode(item, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm])
online_dict = {
'token_id': payload.get('session_id') if AppConfig.app_same_time_login else payload.get('user_id'),
'user_name': payload.get('user_name'),
'dept_name': payload.get('dept_name'),
'ipaddr': payload.get('login_info').get('ipaddr'),
'login_location': payload.get('login_info').get('loginLocation'),
'browser': payload.get('login_info').get('browser'),
'os': payload.get('login_info').get('os'),
'login_time': payload.get('login_info').get('loginTime'),
}
if query_object.user_name and not query_object.ipaddr:
if query_object.user_name == payload.get('user_name'):
online_info_list = [online_dict]
break
elif not query_object.user_name and query_object.ipaddr:
if query_object.ipaddr == payload.get('login_info').get('ipaddr'):
online_info_list = [online_dict]
break
elif query_object.user_name and query_object.ipaddr:
if query_object.user_name == payload.get('user_name') and query_object.ipaddr == payload.get(
'login_info'
).get('ipaddr'):
online_info_list = [online_dict]
break
else:
online_info_list.append(online_dict)
return CamelCaseUtil.transform_result(online_info_list)
@classmethod
async def delete_online_services(cls, request: Request, page_object: DeleteOnlineModel) -> CrudResponseModel:
"""
强退在线用户信息service
:param request: Request对象
:param page_object: 强退在线用户对象
:return: 强退在线用户校验结果
"""
if page_object.token_ids:
token_id_list = page_object.token_ids.split(',')
for token_id in token_id_list:
await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}')
return CrudResponseModel(is_success=True, message='强退成功')
raise ServiceException(message='传入session_id为空')
@@ -0,0 +1,180 @@
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.post_dao import PostDao
from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel
from utils.common_util import CamelCaseUtil
from utils.excel_util import ExcelUtil
class PostService:
"""
岗位管理模块服务层
"""
@classmethod
async def get_post_list_services(
cls, query_db: AsyncSession, query_object: PostPageQueryModel, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
获取岗位列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 岗位列表信息对象
"""
post_list_result = await PostDao.get_post_list(query_db, query_object, is_page)
return post_list_result
@classmethod
async def check_post_name_unique_services(cls, query_db: AsyncSession, page_object: PostModel) -> bool:
"""
检查岗位名称是否唯一service
:param query_db: orm对象
:param page_object: 岗位对象
:return: 校验结果
"""
post_id = -1 if page_object.post_id is None else page_object.post_id
post = await PostDao.get_post_detail_by_info(query_db, PostModel(postName=page_object.post_name))
if post and post.post_id != post_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def check_post_code_unique_services(cls, query_db: AsyncSession, page_object: PostModel) -> bool:
"""
检查岗位编码是否唯一service
:param query_db: orm对象
:param page_object: 岗位对象
:return: 校验结果
"""
post_id = -1 if page_object.post_id is None else page_object.post_id
post = await PostDao.get_post_detail_by_info(query_db, PostModel(postCode=page_object.post_code))
if post and post.post_id != post_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_post_services(cls, query_db: AsyncSession, page_object: PostModel) -> CrudResponseModel:
"""
新增岗位信息service
:param query_db: orm对象
:param page_object: 新增岗位对象
:return: 新增岗位校验结果
"""
if not await cls.check_post_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位名称已存在')
if not await cls.check_post_code_unique_services(query_db, page_object):
raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位编码已存在')
try:
await PostDao.add_post_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_post_services(cls, query_db: AsyncSession, page_object: PostModel) -> CrudResponseModel:
"""
编辑岗位信息service
:param query_db: orm对象
:param page_object: 编辑岗位对象
:return: 编辑岗位校验结果
"""
edit_post = page_object.model_dump(exclude_unset=True)
post_info = await cls.post_detail_services(query_db, page_object.post_id)
if post_info.post_id:
if not await cls.check_post_name_unique_services(query_db, page_object):
raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位名称已存在')
if not await cls.check_post_code_unique_services(query_db, page_object):
raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位编码已存在')
try:
await PostDao.edit_post_dao(query_db, edit_post)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='岗位不存在')
@classmethod
async def delete_post_services(cls, query_db: AsyncSession, page_object: DeletePostModel) -> CrudResponseModel:
"""
删除岗位信息service
:param query_db: orm对象
:param page_object: 删除岗位对象
:return: 删除岗位校验结果
"""
if page_object.post_ids:
post_id_list = page_object.post_ids.split(',')
try:
for post_id in post_id_list:
post = await cls.post_detail_services(query_db, int(post_id))
if (await PostDao.count_user_post_dao(query_db, int(post_id))) > 0:
raise ServiceException(message=f'{post.post_name}已分配,不能删除')
await PostDao.delete_post_dao(query_db, PostModel(postId=post_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入岗位id为空')
@classmethod
async def post_detail_services(cls, query_db: AsyncSession, post_id: int) -> PostModel:
"""
获取岗位详细信息service
:param query_db: orm对象
:param post_id: 岗位id
:return: 岗位id对应的信息
"""
post = await PostDao.get_post_detail_by_id(query_db, post_id=post_id)
result = PostModel(**CamelCaseUtil.transform_result(post)) if post else PostModel()
return result
@staticmethod
async def export_post_list_services(post_list: list) -> bytes:
"""
导出岗位信息service
:param post_list: 岗位信息列表
:return: 岗位信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'postId': '岗位编号',
'postCode': '岗位编码',
'postName': '岗位名称',
'postSort': '显示顺序',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in post_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '停用'
binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict)
return binary_data
@@ -0,0 +1,368 @@
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.role_dao import RoleDao
from module_admin.dao.user_dao import UserDao
from module_admin.entity.vo.role_vo import (
AddRoleModel,
DeleteRoleModel,
RoleDeptModel,
RoleDeptQueryModel,
RoleMenuModel,
RoleModel,
RolePageQueryModel,
)
from module_admin.entity.vo.user_vo import UserInfoModel, UserRolePageQueryModel
from utils.common_util import CamelCaseUtil
from utils.excel_util import ExcelUtil
class RoleService:
"""
角色管理模块服务层
"""
@classmethod
async def get_role_select_option_services(cls, query_db: AsyncSession) -> list[dict[str, Any]]:
"""
获取角色列表不分页信息service
:param query_db: orm对象
:return: 角色列表不分页信息对象
"""
role_list_result = await RoleDao.get_role_select_option_dao(query_db)
return CamelCaseUtil.transform_result(role_list_result)
@classmethod
async def get_role_dept_tree_services(cls, query_db: AsyncSession, role_id: int) -> RoleDeptQueryModel:
"""
根据角色id获取部门树信息service
:param query_db: orm对象
:param role_id: 角色id
:return: 当前角色id的部门树信息对象
"""
role = await cls.role_detail_services(query_db, role_id)
role_dept_list = await RoleDao.get_role_dept_dao(query_db, role)
checked_keys = [row.dept_id for row in role_dept_list]
result = RoleDeptQueryModel(checkedKeys=checked_keys)
return result
@classmethod
async def get_role_list_services(
cls,
query_db: AsyncSession,
query_object: RolePageQueryModel,
data_scope_sql: ColumnElement,
is_page: bool = False,
) -> PageModel | list[dict[str, Any]]:
"""
获取角色列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: 角色列表信息对象
"""
role_list_result = await RoleDao.get_role_list(query_db, query_object, data_scope_sql, is_page)
return role_list_result
@classmethod
async def check_role_allowed_services(cls, check_role: RoleModel) -> CrudResponseModel:
"""
校验角色是否允许操作service
:param check_role: 角色信息
:return: 校验结果
"""
if check_role.admin:
raise ServiceException(message='不允许操作超级管理员角色')
return CrudResponseModel(is_success=True, message='校验通过')
@classmethod
async def check_role_data_scope_services(
cls, query_db: AsyncSession, role_ids: str, data_scope_sql: ColumnElement
) -> None:
"""
校验角色是否有数据权限service
:param query_db: orm对象
:param role_ids: 角色id
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 校验结果
"""
role_id_list = role_ids.split(',') if role_ids else []
if role_id_list:
for role_id in role_id_list:
roles = await RoleDao.get_role_list(
query_db, RolePageQueryModel(roleId=int(role_id)), data_scope_sql, is_page=False
)
if roles:
continue
raise ServiceException(message='没有权限访问角色数据')
@classmethod
async def check_role_name_unique_services(cls, query_db: AsyncSession, page_object: RoleModel) -> bool:
"""
校验角色名称是否唯一service
:param query_db: orm对象
:param page_object: 角色对象
:return: 校验结果
"""
role_id = -1 if page_object.role_id is None else page_object.role_id
role = await RoleDao.get_role_by_info(query_db, RoleModel(roleName=page_object.role_name))
if role and role.role_id != role_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def check_role_key_unique_services(cls, query_db: AsyncSession, page_object: RoleModel) -> bool:
"""
校验角色权限字符是否唯一service
:param query_db: orm对象
:param page_object: 角色对象
:return: 校验结果
"""
role_id = -1 if page_object.role_id is None else page_object.role_id
role = await RoleDao.get_role_by_info(query_db, RoleModel(roleKey=page_object.role_key))
if role and role.role_id != role_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_role_services(cls, query_db: AsyncSession, page_object: AddRoleModel) -> CrudResponseModel:
"""
新增角色信息service
:param query_db: orm对象
:param page_object: 新增角色对象
:return: 新增角色校验结果
"""
add_role = RoleModel(**page_object.model_dump(by_alias=True))
if not await cls.check_role_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增角色{page_object.role_name}失败,角色名称已存在')
if not await cls.check_role_key_unique_services(query_db, page_object):
raise ServiceException(message=f'新增角色{page_object.role_name}失败,角色权限已存在')
try:
add_result = await RoleDao.add_role_dao(query_db, add_role)
role_id = add_result.role_id
if page_object.menu_ids:
for menu in page_object.menu_ids:
await RoleDao.add_role_menu_dao(query_db, RoleMenuModel(roleId=role_id, menuId=menu))
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_role_services(cls, query_db: AsyncSession, page_object: AddRoleModel) -> CrudResponseModel:
"""
编辑角色信息service
:param query_db: orm对象
:param page_object: 编辑角色对象
:return: 编辑角色校验结果
"""
edit_role = page_object.model_dump(exclude_unset=True, exclude={'admin'})
if page_object.type != 'status':
del edit_role['menu_ids']
if page_object.type == 'status':
del edit_role['type']
role_info = await cls.role_detail_services(query_db, edit_role.get('role_id'))
if role_info:
if page_object.type != 'status':
if not await cls.check_role_name_unique_services(query_db, page_object):
raise ServiceException(message=f'修改角色{page_object.role_name}失败,角色名称已存在')
if not await cls.check_role_key_unique_services(query_db, page_object):
raise ServiceException(message=f'修改角色{page_object.role_name}失败,角色权限已存在')
try:
await RoleDao.edit_role_dao(query_db, edit_role)
if page_object.type != 'status':
await RoleDao.delete_role_menu_dao(query_db, RoleMenuModel(roleId=page_object.role_id))
if page_object.menu_ids:
for menu in page_object.menu_ids:
await RoleDao.add_role_menu_dao(
query_db, RoleMenuModel(roleId=page_object.role_id, menuId=menu)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='角色不存在')
@classmethod
async def role_datascope_services(cls, query_db: AsyncSession, page_object: AddRoleModel) -> CrudResponseModel:
"""
分配角色数据权限service
:param query_db: orm对象
:param page_object: 角色数据权限对象
:return: 分配角色数据权限结果
"""
edit_role = page_object.model_dump(exclude_unset=True, exclude={'admin', 'dept_ids'})
role_info = await cls.role_detail_services(query_db, page_object.role_id)
if role_info.role_id:
try:
await RoleDao.edit_role_dao(query_db, edit_role)
await RoleDao.delete_role_dept_dao(query_db, RoleDeptModel(roleId=page_object.role_id))
if page_object.dept_ids and page_object.data_scope == '2':
for dept in page_object.dept_ids:
await RoleDao.add_role_dept_dao(
query_db, RoleDeptModel(roleId=page_object.role_id, deptId=dept)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='分配成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='角色不存在')
@classmethod
async def delete_role_services(cls, query_db: AsyncSession, page_object: DeleteRoleModel) -> CrudResponseModel:
"""
删除角色信息service
:param query_db: orm对象
:param page_object: 删除角色对象
:return: 删除角色校验结果
"""
if page_object.role_ids:
role_id_list = page_object.role_ids.split(',')
try:
for role_id in role_id_list:
role = await cls.role_detail_services(query_db, int(role_id))
if (await RoleDao.count_user_role_dao(query_db, int(role_id))) > 0:
raise ServiceException(message=f'角色{role.role_name}已分配,不能删除')
role_id_dict = {
'roleId': role_id,
'updateBy': page_object.update_by,
'updateTime': page_object.update_time,
}
await RoleDao.delete_role_menu_dao(query_db, RoleMenuModel(**role_id_dict))
await RoleDao.delete_role_dept_dao(query_db, RoleDeptModel(**role_id_dict))
await RoleDao.delete_role_dao(query_db, RoleModel(**role_id_dict))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入角色id为空')
@classmethod
async def role_detail_services(cls, query_db: AsyncSession, role_id: int) -> RoleModel:
"""
获取角色详细信息service
:param query_db: orm对象
:param role_id: 角色id
:return: 角色id对应的信息
"""
role = await RoleDao.get_role_detail_by_id(query_db, role_id=role_id)
result = RoleModel(**CamelCaseUtil.transform_result(role)) if role else RoleModel()
return result
@staticmethod
async def export_role_list_services(role_list: list) -> bytes:
"""
导出角色列表信息service
:param role_list: 角色信息列表
:return: 角色列表信息对象
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'roleId': '角色编号',
'roleName': '角色名称',
'roleKey': '权限字符',
'roleSort': '显示顺序',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in role_list:
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '停用'
binary_data = ExcelUtil.export_list2excel(role_list, mapping_dict)
return binary_data
@classmethod
async def get_role_user_allocated_list_services(
cls,
query_db: AsyncSession,
page_object: UserRolePageQueryModel,
data_scope_sql: ColumnElement,
is_page: bool = False,
) -> PageModel:
"""
根据角色id获取已分配用户列表
:param query_db: orm对象
:param page_object: 用户关联角色对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: 已分配用户列表
"""
query_user_list = await UserDao.get_user_role_allocated_list_by_role_id(
query_db, page_object, data_scope_sql, is_page
)
allocated_list = PageModel[UserInfoModel](
**{
**query_user_list.model_dump(by_alias=True),
'rows': [UserInfoModel(**row) for row in query_user_list.rows],
}
)
return allocated_list
@classmethod
async def get_role_user_unallocated_list_services(
cls,
query_db: AsyncSession,
page_object: UserRolePageQueryModel,
data_scope_sql: ColumnElement,
is_page: bool = False,
) -> PageModel:
"""
根据角色id获取未分配用户列表
:param query_db: orm对象
:param page_object: 用户关联角色对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: 未分配用户列表
"""
query_user_list = await UserDao.get_user_role_unallocated_list_by_role_id(
query_db, page_object, data_scope_sql, is_page
)
unallocated_list = PageModel[UserInfoModel](
**{
**query_user_list.model_dump(by_alias=True),
'rows': [UserInfoModel(**row) for row in query_user_list.rows],
}
)
return unallocated_list
@@ -0,0 +1,103 @@
import os
import platform
import socket
import time
import anyio
import psutil
from module_admin.entity.vo.server_vo import CpuInfo, MemoryInfo, PyInfo, ServerMonitorModel, SysFiles, SysInfo
from utils.common_util import bytes2human
class ServerService:
"""
服务监控模块服务层
"""
@staticmethod
async def get_server_monitor_info() -> ServerMonitorModel:
# CPU信息
# 获取CPU总核心数
cpu_num = psutil.cpu_count(logical=True)
cpu_usage_percent = psutil.cpu_times_percent()
cpu_used = cpu_usage_percent.user
cpu_sys = cpu_usage_percent.system
cpu_free = cpu_usage_percent.idle
cpu = CpuInfo(cpuNum=cpu_num, used=cpu_used, sys=cpu_sys, free=cpu_free)
# 内存信息
memory_info = psutil.virtual_memory()
memory_total = bytes2human(memory_info.total)
memory_used = bytes2human(memory_info.used)
memory_free = bytes2human(memory_info.free)
memory_usage = memory_info.percent
mem = MemoryInfo(total=memory_total, used=memory_used, free=memory_free, usage=memory_usage)
# 主机信息
# 获取主机名
hostname = socket.gethostname()
# 获取IP
computer_ip = socket.gethostbyname(hostname)
os_name = platform.platform()
computer_name = platform.node()
os_arch = platform.machine()
user_dir = str(await anyio.Path.cwd())
sys = SysInfo(
computerIp=computer_ip, computerName=computer_name, osArch=os_arch, osName=os_name, userDir=user_dir
)
# python解释器信息
current_pid = os.getpid()
current_process = psutil.Process(current_pid)
python_name = current_process.name()
python_version = platform.python_version()
python_home = current_process.exe()
start_time_stamp = current_process.create_time()
start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp))
current_time_stamp = time.time()
difference = current_time_stamp - start_time_stamp
# 将时间差转换为天、小时和分钟数
days = int(difference // (24 * 60 * 60)) # 每天的秒数
hours = int((difference % (24 * 60 * 60)) // (60 * 60)) # 每小时的秒数
minutes = int((difference % (60 * 60)) // 60) # 每分钟的秒数
run_time = f'{days}天{hours}小时{minutes}分钟'
# 获取当前Python程序的pid
pid = os.getpid()
# 获取该进程的内存信息
current_process_memory_info = psutil.Process(pid).memory_info()
py = PyInfo(
name=python_name,
version=python_version,
startTime=start_time,
runTime=run_time,
home=python_home,
total=bytes2human(memory_info.available),
used=bytes2human(current_process_memory_info.rss),
free=bytes2human(memory_info.available - current_process_memory_info.rss),
usage=round((current_process_memory_info.rss / memory_info.available) * 100, 2),
)
# 磁盘信息
io = psutil.disk_partitions()
sys_files = []
for i in io:
try:
o = psutil.disk_usage(i.device)
disk_data = SysFiles(
dirName=i.device,
sysTypeName=i.fstype,
typeName='本地固定磁盘(' + i.mountpoint.replace('\\', '') + ')',
total=bytes2human(o.total),
used=bytes2human(o.used),
free=bytes2human(o.free),
usage=f'{psutil.disk_usage(i.device).percent}%',
)
sys_files.append(disk_data)
except Exception: # noqa: PERF203
# 忽略所有异常,跳过有问题的磁盘
continue
result = ServerMonitorModel(cpu=cpu, mem=mem, sys=sys, py=py, sysFiles=sys_files)
return result
@@ -0,0 +1,44 @@
from fastapi import Request
from module_admin.entity.vo.transport_crypto_vo import (
TransportCryptoFrontendConfigModel,
TransportCryptoMonitorModel,
TransportCryptoPublicKeyModel,
)
from utils.transport_crypto_util import TransportCryptoMonitorUtil, TransportCryptoUtil
class TransportCryptoService:
"""
传输加密模块服务层
"""
@classmethod
async def get_transport_frontend_config_services(cls) -> TransportCryptoFrontendConfigModel:
"""
获取前端传输加密运行配置service
:return: 前端传输加密运行配置
"""
return TransportCryptoFrontendConfigModel.model_validate(TransportCryptoUtil.build_frontend_config_payload())
@classmethod
async def get_transport_public_key_services(cls) -> TransportCryptoPublicKeyModel:
"""
获取传输加密公钥service
:return: 传输加密公钥信息
"""
return TransportCryptoPublicKeyModel.model_validate(TransportCryptoUtil.build_public_key_payload())
@classmethod
async def get_transport_crypto_monitor_info_services(cls, request: Request) -> TransportCryptoMonitorModel:
"""
获取传输加密监控信息service
:param request: Request对象
:return: 传输加密监控信息
"""
transport_crypto_monitor_info = await TransportCryptoMonitorUtil.get_snapshot(request.app)
return TransportCryptoMonitorModel.model_validate(transport_crypto_monitor_info)
@@ -0,0 +1,679 @@
import io
from datetime import datetime
from typing import Any
import pandas as pd
from fastapi import Request, UploadFile
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from common.constant import CommonConstant
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_admin.dao.user_dao import UserDao
from module_admin.entity.do.user_do import SysUserRole
from module_admin.entity.vo.post_vo import PostPageQueryModel
from module_admin.entity.vo.user_vo import (
AddUserModel,
CrudUserRoleModel,
CurrentUserModel,
DeleteUserModel,
EditUserModel,
ResetUserModel,
SelectedRoleModel,
UserDetailModel,
UserInfoModel,
UserModel,
UserPageQueryModel,
UserPostModel,
UserProfileModel,
UserRoleModel,
UserRoleQueryModel,
UserRoleResponseModel,
UserRowModel,
)
from module_admin.service.config_service import ConfigService
from module_admin.service.dept_service import DeptService
from module_admin.service.post_service import PostService
from module_admin.service.role_service import RoleService
from utils.common_util import CamelCaseUtil
from utils.excel_util import ExcelUtil
from utils.pwd_util import PwdUtil
class UserService:
"""
用户管理模块服务层
"""
@classmethod
async def get_user_list_services(
cls,
query_db: AsyncSession,
query_object: UserPageQueryModel,
data_scope_sql: ColumnElement,
is_page: bool = False,
) -> PageModel[UserRowModel] | list[dict[str, Any]]:
"""
获取用户列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: 用户列表信息对象
"""
query_result = await UserDao.get_user_list(query_db, query_object, data_scope_sql, is_page)
if is_page:
user_list_result = PageModel[UserRowModel](
**{
**query_result.model_dump(by_alias=True),
'rows': [{**row[0], 'dept': row[1]} for row in query_result.rows],
}
)
else:
user_list_result = []
if query_result:
user_list_result = [{**row[0], 'dept': row[1]} for row in query_result]
return user_list_result
@classmethod
async def check_user_allowed_services(cls, check_user: UserModel) -> CrudResponseModel:
"""
校验用户是否允许操作service
:param check_user: 用户信息
:return: 校验结果
"""
if check_user.admin:
raise ServiceException(message='不允许操作超级管理员用户')
return CrudResponseModel(is_success=True, message='校验通过')
@classmethod
async def check_user_data_scope_services(
cls, query_db: AsyncSession, user_id: int, data_scope_sql: ColumnElement
) -> CrudResponseModel:
"""
校验用户数据权限service
:param query_db: orm对象
:param user_id: 用户id
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 校验结果
"""
users = await UserDao.get_user_list(query_db, UserPageQueryModel(userId=user_id), data_scope_sql, is_page=False)
if users:
return CrudResponseModel(is_success=True, message='校验通过')
raise ServiceException(message='没有权限访问用户数据')
@classmethod
async def check_user_name_unique_services(cls, query_db: AsyncSession, page_object: UserModel) -> bool:
"""
校验用户名是否唯一service
:param query_db: orm对象
:param page_object: 用户对象
:return: 校验结果
"""
user_id = -1 if page_object.user_id is None else page_object.user_id
user = await UserDao.get_user_by_info(query_db, UserModel(userName=page_object.user_name))
if user and user.user_id != user_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def check_phonenumber_unique_services(cls, query_db: AsyncSession, page_object: UserModel) -> bool:
"""
校验用户手机号是否唯一service
:param query_db: orm对象
:param page_object: 用户对象
:return: 校验结果
"""
user_id = -1 if page_object.user_id is None else page_object.user_id
user = await UserDao.get_user_by_info(query_db, UserModel(phonenumber=page_object.phonenumber))
if user and user.user_id != user_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def check_email_unique_services(cls, query_db: AsyncSession, page_object: UserModel) -> bool:
"""
校验用户邮箱是否唯一service
:param query_db: orm对象
:param page_object: 用户对象
:return: 校验结果
"""
user_id = -1 if page_object.user_id is None else page_object.user_id
user = await UserDao.get_user_by_info(query_db, UserModel(email=page_object.email))
if user and user.user_id != user_id:
return CommonConstant.NOT_UNIQUE
return CommonConstant.UNIQUE
@classmethod
async def add_user_services(cls, query_db: AsyncSession, page_object: AddUserModel) -> CrudResponseModel:
"""
新增用户信息service
:param query_db: orm对象
:param page_object: 新增用户对象
:return: 新增用户校验结果
"""
add_user = UserModel(**page_object.model_dump(by_alias=True))
if not await cls.check_user_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增用户{page_object.user_name}失败,登录账号已存在')
if page_object.phonenumber and not await cls.check_phonenumber_unique_services(query_db, page_object):
raise ServiceException(message=f'新增用户{page_object.user_name}失败,手机号码已存在')
if page_object.email and not await cls.check_email_unique_services(query_db, page_object):
raise ServiceException(message=f'新增用户{page_object.user_name}失败,邮箱账号已存在')
try:
add_result = await UserDao.add_user_dao(query_db, add_user)
user_id = add_result.user_id
if page_object.role_ids:
for role in page_object.role_ids:
await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=user_id, roleId=role))
if page_object.post_ids:
for post in page_object.post_ids:
await UserDao.add_user_post_dao(query_db, UserPostModel(userId=user_id, postId=post))
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
def _deal_edit_user(cls, page_object: EditUserModel, edit_user: dict[str, Any]) -> None:
"""
处理编辑用户字典
:param page_object: 编辑用户对象
:param edit_user: 编辑用户字典
:return: None
"""
if page_object.type not in ['status', 'avatar', 'pwd']:
del edit_user['role_ids']
del edit_user['post_ids']
del edit_user['role']
else:
del edit_user['type']
@classmethod
async def edit_user_services(cls, query_db: AsyncSession, page_object: EditUserModel) -> CrudResponseModel:
"""
编辑用户信息service
:param query_db: orm对象
:param page_object: 编辑用户对象
:return: 编辑用户校验结果
"""
edit_user = page_object.model_dump(exclude_unset=True, exclude={'admin'})
cls._deal_edit_user(page_object, edit_user)
user_info = await cls.user_detail_services(query_db, edit_user.get('user_id'))
if user_info.data and user_info.data.user_id:
if page_object.type not in ['status', 'avatar', 'pwd']:
if not await cls.check_user_name_unique_services(query_db, page_object):
raise ServiceException(message=f'修改用户{page_object.user_name}失败,登录账号已存在')
if page_object.phonenumber and not await cls.check_phonenumber_unique_services(query_db, page_object):
raise ServiceException(message=f'修改用户{page_object.user_name}失败,手机号码已存在')
if page_object.email and not await cls.check_email_unique_services(query_db, page_object):
raise ServiceException(message=f'修改用户{page_object.user_name}失败,邮箱账号已存在')
try:
await UserDao.edit_user_dao(query_db, edit_user)
if page_object.type not in {'status', 'avatar', 'pwd'}:
await UserDao.delete_user_role_dao(query_db, UserRoleModel(userId=page_object.user_id))
await UserDao.delete_user_post_dao(query_db, UserPostModel(userId=page_object.user_id))
if page_object.role_ids:
for role in page_object.role_ids:
await UserDao.add_user_role_dao(
query_db, UserRoleModel(userId=page_object.user_id, roleId=role)
)
if page_object.post_ids:
for post in page_object.post_ids:
await UserDao.add_user_post_dao(
query_db, UserPostModel(userId=page_object.user_id, postId=post)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='用户不存在')
@classmethod
async def delete_user_services(cls, query_db: AsyncSession, page_object: DeleteUserModel) -> CrudResponseModel:
"""
删除用户信息service
:param query_db: orm对象
:param page_object: 删除用户对象
:return: 删除用户校验结果
"""
if page_object.user_ids:
user_id_list = page_object.user_ids.split(',')
try:
for user_id in user_id_list:
user_id_dict = {
'userId': user_id,
'updateBy': page_object.update_by,
'updateTime': page_object.update_time,
}
await UserDao.delete_user_role_dao(query_db, UserRoleModel(**user_id_dict))
await UserDao.delete_user_post_dao(query_db, UserPostModel(**user_id_dict))
await UserDao.delete_user_dao(query_db, UserModel(**user_id_dict))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入用户id为空')
@classmethod
async def user_detail_services(cls, query_db: AsyncSession, user_id: int | str) -> UserDetailModel:
"""
获取用户详细信息service
:param query_db: orm对象
:param user_id: 用户id
:return: 用户id对应的信息
"""
posts = await PostService.get_post_list_services(query_db, PostPageQueryModel(), is_page=False)
roles = await RoleService.get_role_select_option_services(query_db)
if user_id != '':
query_user = await UserDao.get_user_detail_by_id(query_db, user_id=user_id)
post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')])
post_ids_list = [row.post_id for row in query_user.get('user_post_info')]
role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')])
role_ids_list = [row.role_id for row in query_user.get('user_role_info')]
return UserDetailModel(
data=UserInfoModel(
**CamelCaseUtil.transform_result(query_user.get('user_basic_info')),
postIds=post_ids,
roleIds=role_ids,
dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')),
role=CamelCaseUtil.transform_result(query_user.get('user_role_info')),
),
postIds=post_ids_list,
posts=posts,
roleIds=role_ids_list,
roles=roles,
)
return UserDetailModel(posts=posts, roles=roles)
@classmethod
async def user_profile_services(cls, query_db: AsyncSession, user_id: int) -> UserProfileModel:
"""
获取用户个人详细信息service
:param query_db: orm对象
:param user_id: 用户id
:return: 用户id对应的信息
"""
query_user = await UserDao.get_user_detail_by_id(query_db, user_id=user_id)
post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')])
post_group = ','.join([row.post_name for row in query_user.get('user_post_info')])
role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')])
role_group = ','.join([row.role_name for row in query_user.get('user_role_info')])
return UserProfileModel(
data=UserInfoModel(
**CamelCaseUtil.transform_result(query_user.get('user_basic_info')),
postIds=post_ids,
roleIds=role_ids,
dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')),
role=CamelCaseUtil.transform_result(query_user.get('user_role_info')),
),
postGroup=post_group,
roleGroup=role_group,
)
@classmethod
async def reset_user_services(cls, query_db: AsyncSession, page_object: ResetUserModel) -> CrudResponseModel:
"""
重置用户密码service
:param query_db: orm对象
:param page_object: 重置用户对象
:return: 重置用户校验结果
"""
reset_user = page_object.model_dump(exclude_unset=True, exclude={'admin'})
if page_object.old_password:
user = (await UserDao.get_user_detail_by_id(query_db, user_id=page_object.user_id)).get('user_basic_info')
if not PwdUtil.verify_password(page_object.old_password, user.password):
raise ServiceException(message='修改密码失败,旧密码错误')
if PwdUtil.verify_password(page_object.password, user.password):
raise ServiceException(message='新密码不能与旧密码相同')
del reset_user['old_password']
if page_object.sms_code and page_object.session_id:
del reset_user['sms_code']
del reset_user['session_id']
try:
reset_user['password'] = PwdUtil.get_password_hash(page_object.password)
await UserDao.edit_user_dao(query_db, reset_user)
await query_db.commit()
return CrudResponseModel(is_success=True, message='重置成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
def _set_row_sex_value(cls, row: pd.Series) -> None:
"""
设置行性别值
:param row: 行数据
:return: None
"""
if row['sex'] == '男':
row['sex'] = '0'
if row['sex'] == '女':
row['sex'] = '1'
if row['sex'] == '未知':
row['sex'] = '2'
@classmethod
def _set_row_status_value(cls, row: pd.Series) -> None:
"""
设置行状态值
:param row: 行数据
:return: None
"""
if row['status'] == '正常':
row['status'] = '0'
if row['status'] == '停用':
row['status'] = '1'
@classmethod
async def batch_import_user_services(
cls,
request: Request,
query_db: AsyncSession,
file: UploadFile,
update_support: bool,
current_user: CurrentUserModel,
user_data_scope_sql: ColumnElement,
dept_data_scope_sql: ColumnElement,
) -> CrudResponseModel:
"""
批量导入用户service
:param request: Request对象
:param query_db: orm对象
:param file: 用户导入文件对象
:param update_support: 用户存在时是否更新
:param current_user: 当前用户对象
:param user_data_scope_sql: 用户数据权限sql
:param dept_data_scope_sql: 部门数据权限sql
:return: 批量导入用户结果
"""
header_dict = {
'部门编号': 'dept_id',
'登录名称': 'user_name',
'用户名称': 'nick_name',
'用户邮箱': 'email',
'手机号码': 'phonenumber',
'用户性别': 'sex',
'帐号状态': 'status',
}
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
await file.close()
df.rename(columns=header_dict, inplace=True)
add_error_result = []
count = 0
try:
for _index, row in df.iterrows():
count = count + 1
cls._set_row_sex_value(row)
cls._set_row_status_value(row)
add_user = UserModel(
deptId=row['dept_id'],
userName=row['user_name'],
password=PwdUtil.get_password_hash(
await ConfigService.query_config_list_from_cache_services(
request.app.state.redis, 'sys.user.initPassword'
)
),
nickName=row['nick_name'],
email=row['email'],
phonenumber=str(row['phonenumber']),
sex=row['sex'],
status=row['status'],
createBy=current_user.user.user_name,
createTime=datetime.now(),
updateBy=current_user.user.user_name,
updateTime=datetime.now(),
)
user_info = await UserDao.get_user_by_info(query_db, UserModel(userName=row['user_name']))
if user_info:
if update_support:
edit_user_model = UserModel(
userId=user_info.user_id,
deptId=row['dept_id'],
userName=row['user_name'],
nickName=row['nick_name'],
email=row['email'],
phonenumber=str(row['phonenumber']),
sex=row['sex'],
status=row['status'],
updateBy=current_user.user.user_name,
updateTime=datetime.now(),
)
edit_user_model.validate_fields()
await cls.check_user_allowed_services(edit_user_model)
if not current_user.user.admin:
await cls.check_user_data_scope_services(
query_db, edit_user_model.user_id, user_data_scope_sql
)
await DeptService.check_dept_data_scope_services(
query_db, edit_user_model.dept_id, dept_data_scope_sql
)
edit_user = edit_user_model.model_dump(exclude_unset=True)
await UserDao.edit_user_dao(query_db, edit_user)
else:
add_error_result.append(f'{count}.用户账号{row["user_name"]}已存在')
else:
add_user.validate_fields()
if not current_user.user.admin:
await DeptService.check_dept_data_scope_services(
query_db, add_user.dept_id, dept_data_scope_sql
)
await UserDao.add_user_dao(query_db, add_user)
await query_db.commit()
return CrudResponseModel(is_success=True, message='\n'.join(add_error_result))
except Exception as e:
await query_db.rollback()
raise e
@staticmethod
async def get_user_import_template_services() -> bytes:
"""
获取用户导入模板service
:return: 用户导入模板excel的二进制数据
"""
header_list = ['部门编号', '登录名称', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态']
selector_header_list = ['用户性别', '帐号状态']
option_list = [{'用户性别': ['男', '女', '未知']}, {'帐号状态': ['正常', '停用']}]
binary_data = ExcelUtil.get_excel_template(
header_list=header_list, selector_header_list=selector_header_list, option_list=option_list
)
return binary_data
@staticmethod
async def export_user_list_services(user_list: list) -> bytes:
"""
导出用户信息service
:param user_list: 用户信息列表
:return: 用户信息对应excel的二进制数据
"""
# 创建一个映射字典,将英文键映射到中文键
mapping_dict = {
'userId': '用户编号',
'userName': '用户名称',
'nickName': '用户昵称',
'deptName': '部门',
'email': '邮箱地址',
'phonenumber': '手机号码',
'sex': '性别',
'status': '状态',
'createBy': '创建者',
'createTime': '创建时间',
'updateBy': '更新者',
'updateTime': '更新时间',
'remark': '备注',
}
for item in user_list:
item['deptName'] = item.get('dept').get('deptName')
if item.get('status') == '0':
item['status'] = '正常'
else:
item['status'] = '停用'
if item.get('sex') == '0':
item['sex'] = '男'
elif item.get('sex') == '1':
item['sex'] = '女'
else:
item['sex'] = '未知'
binary_data = ExcelUtil.export_list2excel(user_list, mapping_dict)
return binary_data
@classmethod
async def get_user_role_allocated_list_services(
cls, query_db: AsyncSession, page_object: UserRoleQueryModel
) -> UserRoleResponseModel:
"""
根据用户id获取已分配角色列表
:param query_db: orm对象
:param page_object: 用户关联角色对象
:return: 已分配角色列表
"""
query_user = await UserDao.get_user_detail_by_id(query_db, page_object.user_id)
post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')])
role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')])
user = UserInfoModel(
**CamelCaseUtil.transform_result(query_user.get('user_basic_info')),
postIds=post_ids,
roleIds=role_ids,
dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')),
role=CamelCaseUtil.transform_result(query_user.get('user_role_info')),
)
query_role_list = [
SelectedRoleModel(**row) for row in await RoleService.get_role_select_option_services(query_db)
]
for model_a in query_role_list:
for model_b in user.role:
if model_a.role_id == model_b.role_id:
model_a.flag = True
result = UserRoleResponseModel(roles=query_role_list, user=user)
return result
@classmethod
async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudUserRoleModel) -> CrudResponseModel:
"""
新增用户关联角色信息service
:param query_db: orm对象
:param page_object: 新增用户关联角色对象
:return: 新增用户关联角色校验结果
"""
if page_object.user_id and page_object.role_ids:
role_id_list = page_object.role_ids.split(',')
try:
await UserDao.delete_user_role_by_user_and_role_dao(query_db, UserRoleModel(userId=page_object.user_id))
for role_id in role_id_list:
await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=page_object.user_id, roleId=role_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='分配成功')
except Exception as e:
await query_db.rollback()
raise e
elif page_object.user_id and not page_object.role_ids:
try:
await UserDao.delete_user_role_by_user_and_role_dao(query_db, UserRoleModel(userId=page_object.user_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='分配成功')
except Exception as e:
await query_db.rollback()
raise e
elif page_object.user_ids and page_object.role_id:
user_id_list = page_object.user_ids.split(',')
try:
for user_id in user_id_list:
user_role = await cls.detail_user_role_services(
query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id)
)
if user_role:
continue
await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='不满足新增条件')
@classmethod
async def delete_user_role_services(
cls, query_db: AsyncSession, page_object: CrudUserRoleModel
) -> CrudResponseModel:
"""
删除用户关联角色信息service
:param query_db: orm对象
:param page_object: 删除用户关联角色对象
:return: 删除用户关联角色校验结果
"""
if (page_object.user_id and page_object.role_id) or (page_object.user_ids and page_object.role_id):
if page_object.user_id and page_object.role_id:
try:
await UserDao.delete_user_role_by_user_and_role_dao(
query_db, UserRoleModel(userId=page_object.user_id, roleId=page_object.role_id)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
elif page_object.user_ids and page_object.role_id:
user_id_list = page_object.user_ids.split(',')
try:
for user_id in user_id_list:
await UserDao.delete_user_role_by_user_and_role_dao(
query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id)
)
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='不满足删除条件')
else:
raise ServiceException(message='传入用户角色关联信息为空')
@classmethod
async def detail_user_role_services(cls, query_db: AsyncSession, page_object: UserRoleModel) -> SysUserRole | None:
"""
获取用户关联角色详细信息service
:param query_db: orm对象
:param page_object: 用户关联角色对象
:return: 用户关联角色详细信息
"""
user_role = await UserDao.get_user_role_detail(query_db, page_object)
return user_role