Browse Source

!22 RuoYi-Vue3-FastAPI v1.5.1

Merge pull request !22 from insistence/develop
master
insistence 3 months ago
committed by Gitee
parent
commit
591dbe06a2
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 4
      README.md
  2. 2
      ruoyi-fastapi-backend/.env.dev
  3. 2
      ruoyi-fastapi-backend/.env.prod
  4. 30
      ruoyi-fastapi-backend/config/get_scheduler.py
  5. 191
      ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py
  6. 12
      ruoyi-fastapi-backend/module_admin/service/job_service.py
  7. 10
      ruoyi-fastapi-backend/module_admin/service/menu_service.py
  8. 14
      ruoyi-fastapi-backend/module_task/scheduler_test.py
  9. 2
      ruoyi-fastapi-frontend/package.json
  10. 2
      ruoyi-fastapi-frontend/src/components/DictTag/index.vue
  11. 7
      ruoyi-fastapi-frontend/src/components/FileUpload/index.vue
  12. 8
      ruoyi-fastapi-frontend/src/components/ImageUpload/index.vue
  13. 19
      ruoyi-fastapi-frontend/src/views/monitor/job/index.vue

4
README.md

@ -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">

2
ruoyi-fastapi-backend/.env.dev

@ -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归属区域查询

2
ruoyi-fastapi-backend/.env.prod

@ -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归属区域查询

30
ruoyi-fastapi-backend/config/get_scheduler.py

@ -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
@ -109,9 +111,9 @@ job_stores = {
)
),
}
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)
@ -132,9 +134,7 @@ class SchedulerUtil:
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('系统初始定时任务加载成功')
@ -169,6 +169,10 @@ class SchedulerUtil:
: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),
@ -180,7 +184,7 @@ class SchedulerUtil:
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
@ -191,6 +195,10 @@ class SchedulerUtil:
: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',
@ -203,7 +211,7 @@ class SchedulerUtil:
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
@ -214,7 +222,9 @@ class SchedulerUtil:
: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):

191
ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py

@ -3,19 +3,18 @@ import json
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
@ -201,188 +200,6 @@ class Log:
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):
"""

12
ruoyi-fastapi-backend/module_admin/service/job_service.py

@ -129,9 +129,7 @@ class JobService:
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)
@ -152,9 +150,7 @@ class JobService:
: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)
@ -176,9 +172,7 @@ class JobService:
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:

10
ruoyi-fastapi-backend/module_admin/service/menu_service.py

@ -99,9 +99,9 @@ class MenuService:
: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)
@ -124,11 +124,11 @@ class MenuService:
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)

14
ruoyi-fastapi-backend/module_task/scheduler_test.py

@ -2,6 +2,18 @@ from datetime import datetime
def job(*args, **kwargs):
"""
定时任务执行同步函数示例
"""
print(args)
print(kwargs)
print(f'{datetime.now()}执行了')
print(f'{datetime.now()}同步函数执行了')
async def async_job(*args, **kwargs):
"""
定时任务执行异步函数示例
"""
print(args)
print(kwargs)
print(f'{datetime.now()}异步函数执行了')

2
ruoyi-fastapi-frontend/package.json

@ -1,6 +1,6 @@
{
"name": "vfadmin",
"version": "1.5.0",
"version": "1.5.1",
"description": "vfadmin管理系统",
"author": "insistence",
"license": "MIT",

2
ruoyi-fastapi-frontend/src/components/DictTag/index.vue

@ -55,7 +55,7 @@ const values = computed(() => {
const unmatch = computed(() => {
unmatchArray.value = [];
// value
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || props.options.length === 0) return false
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
//
let unmatch = false //
values.value.forEach(item => {

7
ruoyi-fastapi-frontend/src/components/FileUpload/index.vue

@ -104,10 +104,15 @@ function handleBeforeUpload(file) {
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
proxy.$modal.msgError(`文件格式不正确请上传${props.fileType.join("/")}格式文件!`);
return false;
}
}
//
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
//
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;

8
ruoyi-fastapi-frontend/src/components/ImageUpload/index.vue

@ -125,9 +125,11 @@ function handleBeforeUpload(file) {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) {
proxy.$modal.msgError(
`文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
);
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (props.fileSize) {

19
ruoyi-fastapi-frontend/src/views/monitor/job/index.vue

@ -159,7 +159,20 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务执行器" prop="jobGroup">
<el-form-item prop="jobGroup">
<template #label>
<span>
任务执行器
<el-tooltip placement="top">
<template #content>
<div>
调用方法为异步函数时此选项无效
</div>
</template>
<el-icon><question-filled /></el-icon>
</el-tooltip>
</span>
</template>
<el-select v-model="form.jobExecutor" placeholder="请选择任务执行器">
<el-option
v-for="dict in sys_job_executor"
@ -178,9 +191,7 @@
<el-tooltip placement="top">
<template #content>
<div>
Bean调用示例ryTask.ryParams('ry')
<br />Class类调用示例com.ruoyi.quartz.task.RyTask.ryParams('ry')
<br />参数说明支持字符串布尔类型长整型浮点型整型
调用示例module_task.scheduler_test.job
</div>
</template>
<el-icon><question-filled /></el-icon>

Loading…
Cancel
Save