Skip to content

Commit

Permalink
!22 RuoYi-Vue3-FastAPI v1.5.1
Browse files Browse the repository at this point in the history
Merge pull request !22 from insistence/develop
  • Loading branch information
insistence authored and gitee-org committed Nov 12, 2024
2 parents 8202e8c + 43b7931 commit 591dbe0
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 226 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.5.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.5.1</h1>
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a>
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.5.0-brightgreen.svg"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.5.1-brightgreen.svg"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
<img src="https://img.shields.io/badge/python-≥3.9-blue">
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
Expand Down
2 changes: 1 addition & 1 deletion ruoyi-fastapi-backend/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.5.0'
APP_VERSION= '1.5.1'
# 应用是否开启热重载
APP_RELOAD = true
# 应用是否开启IP归属区域查询
Expand Down
2 changes: 1 addition & 1 deletion ruoyi-fastapi-backend/.env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.5.0'
APP_VERSION= '1.5.1'
# 应用是否开启热重载
APP_RELOAD = false
# 应用是否开启IP归属区域查询
Expand Down
30 changes: 20 additions & 10 deletions ruoyi-fastapi-backend/config/get_scheduler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json
from apscheduler.events import EVENT_ALL
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.asyncio import AsyncIOExecutor
from apscheduler.executors.pool import ProcessPoolExecutor
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.jobstores.redis import RedisJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from asyncio import iscoroutinefunction
from datetime import datetime, timedelta
from sqlalchemy.engine import create_engine
from sqlalchemy.orm import sessionmaker
Expand Down Expand Up @@ -109,9 +111,9 @@ def __find_recent_workday(cls, day: int):
)
),
}
executors = {'default': ThreadPoolExecutor(20), 'processpool': ProcessPoolExecutor(5)}
executors = {'default': AsyncIOExecutor(), 'processpool': ProcessPoolExecutor(5)}
job_defaults = {'coalesce': False, 'max_instance': 1}
scheduler = BackgroundScheduler()
scheduler = AsyncIOScheduler()
scheduler.configure(jobstores=job_stores, executors=executors, job_defaults=job_defaults)


Expand All @@ -132,9 +134,7 @@ async def init_system_scheduler(cls):
async with AsyncSessionLocal() as session:
job_list = await JobDao.get_job_list_for_scheduler(session)
for item in job_list:
query_job = cls.get_scheduler_job(job_id=str(item.job_id))
if query_job:
cls.remove_scheduler_job(job_id=str(item.job_id))
cls.remove_scheduler_job(job_id=str(item.job_id))
cls.add_scheduler_job(item)
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
logger.info('系统初始定时任务加载成功')
Expand Down Expand Up @@ -169,6 +169,10 @@ def add_scheduler_job(cls, job_info: JobModel):
:param job_info: 任务对象信息
:return:
"""
job_func = eval(job_info.invoke_target)
job_executor = job_info.job_executor
if iscoroutinefunction(job_func):
job_executor = 'default'
scheduler.add_job(
func=eval(job_info.invoke_target),
trigger=MyCronTrigger.from_crontab(job_info.cron_expression),
Expand All @@ -180,7 +184,7 @@ def add_scheduler_job(cls, job_info: JobModel):
coalesce=True if job_info.misfire_policy == '2' else False,
max_instances=3 if job_info.concurrent == '0' else 1,
jobstore=job_info.job_group,
executor=job_info.job_executor,
executor=job_executor,
)

@classmethod
Expand All @@ -191,6 +195,10 @@ def execute_scheduler_job_once(cls, job_info: JobModel):
:param job_info: 任务对象信息
:return:
"""
job_func = eval(job_info.invoke_target)
job_executor = job_info.job_executor
if iscoroutinefunction(job_func):
job_executor = 'default'
scheduler.add_job(
func=eval(job_info.invoke_target),
trigger='date',
Expand All @@ -203,7 +211,7 @@ def execute_scheduler_job_once(cls, job_info: JobModel):
coalesce=True if job_info.misfire_policy == '2' else False,
max_instances=3 if job_info.concurrent == '0' else 1,
jobstore=job_info.job_group,
executor=job_info.job_executor,
executor=job_executor,
)

@classmethod
Expand All @@ -214,7 +222,9 @@ def remove_scheduler_job(cls, job_id: Union[str, int]):
:param job_id: 任务id
:return:
"""
scheduler.remove_job(job_id=str(job_id))
query_job = cls.get_scheduler_job(job_id=job_id)
if query_job:
scheduler.remove_job(job_id=str(job_id))

@classmethod
def scheduler_event_listener(cls, event):
Expand Down
191 changes: 4 additions & 187 deletions ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
import os
import requests
import time
import warnings
from datetime import datetime
from fastapi import Request
from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse
from functools import lru_cache, wraps
from typing import Literal, Optional, Union
from typing import Literal, Optional
from user_agents import parse
from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel
from module_admin.service.log_service import LoginLogService, OperationLogService
from module_admin.service.login_service import LoginService
from config.enums import BusinessType
from config.env import AppConfig
from exceptions.exception import LoginException, ServiceException, ServiceWarning
from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel
from module_admin.service.log_service import LoginLogService, OperationLogService
from module_admin.service.login_service import LoginService
from utils.log_util import logger
from utils.response_util import ResponseUtil

Expand Down Expand Up @@ -201,188 +200,6 @@ async def wrapper(*args, **kwargs):
return wrapper


def log_decorator(
title: str,
business_type: Union[Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], BusinessType],
log_type: Optional[Literal['login', 'operation']] = 'operation',
):
"""
日志装饰器
:param title: 当前日志装饰器装饰的模块标题
:param business_type: 业务类型(0其它 1新增 2修改 3删除 4授权 5导出 6导入 7强退 8生成代码 9清空数据)
:param log_type: 日志类型(login表示登录日志,operation表示为操作日志)
:return:
"""
warnings.simplefilter('always', category=DeprecationWarning)
if isinstance(business_type, BusinessType):
business_type = business_type.value
warnings.warn(
'未来版本将会移除@log_decorator装饰器,请使用@Log装饰器',
category=DeprecationWarning,
stacklevel=2,
)

def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.time()
# 获取被装饰函数的文件路径
file_path = inspect.getfile(func)
# 获取项目根路径
project_root = os.getcwd()
# 处理文件路径,去除项目根路径部分
relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.')
# 获取当前被装饰函数所在路径
func_path = f'{relative_path}{func.__name__}()'
# 获取上下文信息
request: Request = kwargs.get('request')
token = request.headers.get('Authorization')
query_db = kwargs.get('query_db')
request_method = request.method
operator_type = 0
user_agent = request.headers.get('User-Agent')
if 'Windows' in user_agent or 'Macintosh' in user_agent or 'Linux' in user_agent:
operator_type = 1
if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent:
operator_type = 2
# 获取请求的url
oper_url = request.url.path
# 获取请求的ip及ip归属区域
oper_ip = request.headers.get('X-Forwarded-For')
oper_location = '内网IP'
if AppConfig.app_ip_location_query:
oper_location = get_ip_location(oper_ip)
# 根据不同的请求类型使用不同的方法获取请求参数
content_type = request.headers.get('Content-Type')
if content_type and (
'multipart/form-data' in content_type or 'application/x-www-form-urlencoded' in content_type
):
payload = await request.form()
oper_param = '\n'.join([f'{key}: {value}' for key, value in payload.items()])
else:
payload = await request.body()
# 通过 request.path_params 直接访问路径参数
path_params = request.path_params
oper_param = {}
if payload:
oper_param.update(json.loads(str(payload, 'utf-8')))
if path_params:
oper_param.update(path_params)
oper_param = json.dumps(oper_param, ensure_ascii=False)
# 日志表请求参数字段长度最大为2000,因此在此处判断长度
if len(oper_param) > 2000:
oper_param = '请求参数过长'

# 获取操作时间
oper_time = datetime.now()
# 此处在登录之前向原始函数传递一些登录信息,用于监测在线用户的相关信息
login_log = {}
if log_type == 'login':
user_agent_info = parse(user_agent)
browser = f'{user_agent_info.browser.family}'
system_os = f'{user_agent_info.os.family}'
if user_agent_info.browser.version != ():
browser += f' {user_agent_info.browser.version[0]}'
if user_agent_info.os.version != ():
system_os += f' {user_agent_info.os.version[0]}'
login_log = dict(
ipaddr=oper_ip,
loginLocation=oper_location,
browser=browser,
os=system_os,
loginTime=oper_time.strftime('%Y-%m-%d %H:%M:%S'),
)
kwargs['form_data'].login_info = login_log
try:
# 调用原始函数
result = await func(*args, **kwargs)
except (LoginException, ServiceWarning) as e:
logger.warning(e.message)
result = ResponseUtil.failure(data=e.data, msg=e.message)
except ServiceException as e:
logger.error(e.message)
result = ResponseUtil.error(data=e.data, msg=e.message)
except Exception as e:
logger.exception(e)
result = ResponseUtil.error(msg=str(e))
# 获取请求耗时
cost_time = float(time.time() - start_time) * 100
# 判断请求是否来自api文档
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
)
# 根据响应结果的类型使用不同的方法获取响应结果参数
if (
isinstance(result, JSONResponse)
or isinstance(result, ORJSONResponse)
or isinstance(result, UJSONResponse)
):
result_dict = json.loads(str(result.body, 'utf-8'))
else:
if request_from_swagger or request_from_redoc:
result_dict = {}
else:
if result.status_code == 200:
result_dict = {'code': result.status_code, 'message': '获取成功'}
else:
result_dict = {'code': result.status_code, 'message': '获取失败'}
json_result = json.dumps(result_dict, ensure_ascii=False)
# 根据响应结果获取响应状态及异常信息
status = 1
error_msg = ''
if result_dict.get('code') == 200:
status = 0
else:
error_msg = result_dict.get('msg')
# 根据日志类型向对应的日志表插入数据
if log_type == 'login':
# 登录请求来自于api文档时不记录登录日志,其余情况则记录
if request_from_swagger or request_from_redoc:
pass
else:
user = kwargs.get('form_data')
user_name = user.username
login_log['loginTime'] = oper_time
login_log['userName'] = user_name
login_log['status'] = str(status)
login_log['msg'] = result_dict.get('msg')

await LoginLogService.add_login_log_services(query_db, LogininforModel(**login_log))
else:
current_user = await LoginService.get_current_user(request, token, query_db)
oper_name = current_user.user.user_name
dept_name = current_user.user.dept.dept_name if current_user.user.dept else None
operation_log = OperLogModel(
title=title,
businessType=business_type,
method=func_path,
requestMethod=request_method,
operatorType=operator_type,
operName=oper_name,
deptName=dept_name,
operUrl=oper_url,
operIp=oper_ip,
operLocation=oper_location,
operParam=oper_param,
jsonResult=json_result,
status=status,
errorMsg=error_msg,
operTime=oper_time,
costTime=int(cost_time),
)
await OperationLogService.add_operation_log_services(query_db, operation_log)

return result

return wrapper

return decorator


@lru_cache()
def get_ip_location(oper_ip: str):
"""
Expand Down
12 changes: 3 additions & 9 deletions ruoyi-fastapi-backend/module_admin/service/job_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,7 @@ async def edit_job_services(cls, query_db: AsyncSession, page_object: EditJobMod
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,定时任务已存在')
try:
await JobDao.edit_job_dao(query_db, edit_job)
query_job = SchedulerUtil.get_scheduler_job(job_id=edit_job.get('job_id'))
if query_job:
SchedulerUtil.remove_scheduler_job(job_id=edit_job.get('job_id'))
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)
Expand All @@ -152,9 +150,7 @@ async def execute_job_once_services(cls, query_db: AsyncSession, page_object: Jo
:param page_object: 定时任务对象
:return: 执行一次定时任务结果
"""
query_job = SchedulerUtil.get_scheduler_job(job_id=page_object.job_id)
if query_job:
SchedulerUtil.remove_scheduler_job(job_id=page_object.job_id)
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)
Expand All @@ -176,9 +172,7 @@ async def delete_job_services(cls, query_db: AsyncSession, page_object: DeleteJo
try:
for job_id in job_id_list:
await JobDao.delete_job_dao(query_db, JobModel(jobId=job_id))
query_job = SchedulerUtil.get_scheduler_job(job_id=job_id)
if query_job:
SchedulerUtil.remove_scheduler_job(job_id=job_id)
SchedulerUtil.remove_scheduler_job(job_id=job_id)
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
Expand Down
10 changes: 5 additions & 5 deletions ruoyi-fastapi-backend/module_admin/service/menu_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ async def add_menu_services(cls, query_db: AsyncSession, page_object: MenuModel)
:return: 新增菜单校验结果
"""
if not await cls.check_menu_name_unique_services(query_db, page_object):
raise ServiceException(message=f'新增菜单{page_object.post_name}失败,菜单名称已存在')
raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,菜单名称已存在')
elif page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path):
raise ServiceException(message=f'新增菜单{page_object.post_name}失败,地址必须以http(s)://开头')
raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,地址必须以http(s)://开头')
else:
try:
await MenuDao.add_menu_dao(query_db, page_object)
Expand All @@ -124,11 +124,11 @@ async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel
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.post_name}失败,菜单名称已存在')
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,菜单名称已存在')
elif page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path):
raise ServiceException(message=f'修改菜单{page_object.post_name}失败,地址必须以http(s)://开头')
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,地址必须以http(s)://开头')
elif page_object.menu_id == page_object.parent_id:
raise ServiceException(message=f'修改菜单{page_object.post_name}失败,上级菜单不能选择自己')
raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,上级菜单不能选择自己')
else:
try:
await MenuDao.edit_menu_dao(query_db, edit_menu)
Expand Down
Loading

0 comments on commit 591dbe0

Please sign in to comment.