From c39375b776723f3ecc356ed347e81ccb10edadb4 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 14 Jul 2024 22:37:35 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9D=97service=E5=B1=82=E5=8F=8A?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/config/constant.py | 27 +++ ruoyi-fastapi-backend/config/get_scheduler.py | 171 ++++++++++++++- .../module_admin/controller/job_controller.py | 206 ++++++------------ .../module_admin/dao/job_dao.py | 11 +- .../module_admin/service/job_service.py | 100 ++++++--- ruoyi-fastapi-backend/utils/string_util.py | 55 +++++ .../src/views/monitor/job/index.vue | 2 +- 7 files changed, 398 insertions(+), 174 deletions(-) diff --git a/ruoyi-fastapi-backend/config/constant.py b/ruoyi-fastapi-backend/config/constant.py index 46ad2ea..9129fe0 100644 --- a/ruoyi-fastapi-backend/config/constant.py +++ b/ruoyi-fastapi-backend/config/constant.py @@ -5,6 +5,9 @@ class CommonConstant: WWW: www主域 HTTP: http请求 HTTPS: https请求 + LOOKUP_RMI: RMI远程方法调用 + LOOKUP_LDAP: LDAP远程方法调用 + LOOKUP_LDAPS: LDAPS远程方法调用 YES: 是否为系统默认(是) NO: 是否为系统默认(否) DEPT_NORMAL: 部门正常状态 @@ -16,6 +19,9 @@ class CommonConstant: WWW = 'www.' HTTP = 'http://' HTTPS = 'https://' + LOOKUP_RMI = 'rmi:' + LOOKUP_LDAP = 'ldap:' + LOOKUP_LDAPS = 'ldaps:' YES = 'Y' NO = 'N' DEPT_NORMAL = '0' @@ -66,6 +72,27 @@ class HttpStatusConstant: WARN = 601 +class JobConstant: + """ + 定时任务常量 + + JOB_ERROR_LIST: 定时任务禁止调用模块列表 + JOB_WHITE_LIST: 定时任务允许调用模块列表 + """ + + JOB_ERROR_LIST = [ + 'app', + 'config', + 'exceptions', + 'middlewares', + 'module_admin', + 'server', + 'sub_applications', + 'utils', + ] + JOB_WHITE_LIST = ['module_task'] + + class MenuConstant: """ 菜单常量 diff --git a/ruoyi-fastapi-backend/config/get_scheduler.py b/ruoyi-fastapi-backend/config/get_scheduler.py index b9cfa00..9b6ed2a 100644 --- a/ruoyi-fastapi-backend/config/get_scheduler.py +++ b/ruoyi-fastapi-backend/config/get_scheduler.py @@ -1,4 +1,5 @@ import json +import re from apscheduler.events import EVENT_ALL from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor from apscheduler.schedulers.background import BackgroundScheduler @@ -9,10 +10,12 @@ from apscheduler.triggers.cron import CronTrigger from datetime import datetime, timedelta from sqlalchemy.engine import create_engine from sqlalchemy.orm import sessionmaker +from typing import Union from config.database import AsyncSessionLocal, quote_plus from config.env import DataBaseConfig, RedisConfig +from exceptions.exception import ServiceException from module_admin.dao.job_dao import JobDao -from module_admin.entity.vo.job_vo import JobLogModel +from module_admin.entity.vo.job_vo import JobLogModel, JobModel from module_admin.service.job_log_service import JobLogService from utils.log_util import logger import module_task # noqa: F401 @@ -21,7 +24,7 @@ import module_task # noqa: F401 # 重写Cron定时 class MyCronTrigger(CronTrigger): @classmethod - def from_crontab(cls, expr, timezone=None): + def from_crontab(cls, expr: str, timezone=None): values = expr.split() if len(values) != 6 and len(values) != 7: raise ValueError('Wrong number of fields; got {}, expected 6 or 7'.format(len(values))) @@ -62,7 +65,7 @@ class MyCronTrigger(CronTrigger): ) @classmethod - def __find_recent_workday(cls, day): + def __find_recent_workday(cls, day: int): now = datetime.now() date = datetime(now.year, now.month, day) if date.weekday() < 5: @@ -144,7 +147,7 @@ class SchedulerUtil: logger.info('关闭定时任务成功') @classmethod - def get_scheduler_job(cls, job_id): + def get_scheduler_job(cls, job_id: Union[str, int]): """ 根据任务id获取任务对象 @@ -156,7 +159,7 @@ class SchedulerUtil: return query_job @classmethod - def add_scheduler_job(cls, job_info): + def add_scheduler_job(cls, job_info: JobModel): """ 根据输入的任务对象信息添加任务 @@ -178,7 +181,7 @@ class SchedulerUtil: ) @classmethod - def execute_scheduler_job_once(cls, job_info): + def execute_scheduler_job_once(cls, job_info: JobModel): """ 根据输入的任务对象执行一次任务 @@ -201,7 +204,7 @@ class SchedulerUtil: ) @classmethod - def remove_scheduler_job(cls, job_id): + def remove_scheduler_job(cls, job_id: Union[str, int]): """ 根据任务id移除任务 @@ -210,6 +213,160 @@ class SchedulerUtil: """ scheduler.remove_job(job_id=str(job_id)) + @classmethod + def __valid_range(cls, search_str: str, start_range: int, end_range: int): + match = re.match(r'^(\d+)-(\d+)$', search_str) + if match: + start, end = int(match.group(1)), int(match.group(2)) + return start_range <= start < end <= end_range + return False + + @classmethod + def __valid_sum( + cls, search_str: str, start_range_a: int, start_range_b: int, end_range_a: int, end_range_b: int, sum_range: int + ): + match = re.match(r'^(\d+)/(\d+)$', search_str) + if match: + start, end = int(match.group(1)), int(match.group(2)) + return ( + start_range_a <= start <= start_range_b + and end_range_a <= end <= end_range_b + and start + end <= sum_range + ) + return False + + @classmethod + def __validate_second_or_minute(cls, second_or_minute: str): + """ + 校验秒或分钟值是否正确 + + :param second_or_minute: 秒或分钟值 + :return: 校验结果 + """ + if ( + second_or_minute == '*' + or ('-' in second_or_minute and cls.__valid_range(second_or_minute, 0, 59)) + or ('/' in second_or_minute and cls.__valid_sum(second_or_minute, 0, 58, 1, 59)) + or re.match(r'^(?:[0-5]?\d|59)(?:,[0-5]?\d|59)*$', second_or_minute) + ): + return True + return False + + @classmethod + def __validate_hour(cls, hour: str): + """ + 校验小时值是否正确 + :param hour: 小时值 + :return: 校验结果 + """ + if ( + hour == '*' + or ('-' in hour and cls.__valid_range(hour, 0, 23)) + or ('/' in hour and cls.__valid_sum(hour, 0, 22, 1, 23, 23)) + or re.match(r'^(?:0|[1-9]|1\d|2[0-3])(?:,(?:0|[1-9]|1\d|2[0-3]))*$', hour) + ): + return True + return False + + @classmethod + def __validate_day(cls, day: str): + """ + 校验日值是否正确 + :param day: 日值 + :return: 校验结果 + """ + if ( + day in ['*', '?', 'L'] + or ('-' in day and cls.__valid_range(day, 1, 31)) + or ('/' in day and cls.__valid_sum(day, 1, 30, 1, 30, 31)) + or ('W' in day and re.match(r'^(?:[1-9]|1\d|2\d|3[01])W$', day)) + or re.match(r'^(?:0|[1-9]|1\d|2[0-9]|3[0-1])(?:,(?:0|[1-9]|1\d|2[0-9]|3[0-1]))*$', day) + ): + return True + return False + + @classmethod + def __validate_month(cls, month: str): + """ + 校验月值是否正确 + :param month: 月值 + :return: 校验结果 + """ + if ( + month == '*' + or ('-' in month and cls.__valid_range(month, 1, 12)) + or ('/' in month and cls.__valid_sum(month, 1, 11, 1, 11, 12)) + or re.match(r'^(?:0|[1-9]|1[0-2])(?:,(?:0|[1-9]|1[0-2]))*$', month) + ): + return True + return False + + @classmethod + def __validate_week(cls, week: str): + """ + 校验周值是否正确 + :param week: 周值 + :return: 校验结果 + """ + if ( + week in ['*', '?'] + or ('-' in week and cls.__valid_range(week, 1, 7)) + or re.match(r'^[1-7]#[1-4]$', week) + or re.match(r'^[1-7]L$', week) + or re.match(r'^[1-7](?:(,[1-7]))*$', week) + ): + return True + return False + + @classmethod + def __validate_year(cls, year: str): + """ + 校验年值是否正确 + :param year: 年值 + :return: 校验结果 + """ + current_year = int(datetime.now().year) + if ( + year == '*' + or ('-' in year and cls.__valid_range(year, current_year, 2099)) + or ('/' in year and cls.__valid_sum(year, current_year, 2098, 1, 2099 - current_year, 2099)) + or re.match(r'^[1-7]#[1-4]$', year) + or re.match(r'^[1-7]L$', year) + ): + return True + return False + + @classmethod + def validate_cron_expression(cls, cron_expression: str): + """ + 校验Cron表达式是否正确 + + :param cron_expression: Cron表达式 + :return: 校验结果 + """ + values = cron_expression.split() + if len(values) != 6 and len(values) != 7: + return False + second_validation = cls.__validate_second_or_minute(values[0]) + minute_validation = cls.__validate_second_or_minute(values[1]) + hour_validation = cls.__validate_hour(values[2]) + day_validation = cls.__validate_day(values[3]) + month_validation = cls.__validate_month(values[4]) + week_validation = cls.__validate_week(values[5]) + validation = ( + second_validation + and minute_validation + and hour_validation + and day_validation + and month_validation + and week_validation + ) + if len(values) == 6: + return validation + if len(values) == 7: + year_validation = cls.__validate_year(values[6]) + return validation and year_validation + @classmethod def scheduler_event_listener(cls, event): # 获取事件类型和任务ID diff --git a/ruoyi-fastapi-backend/module_admin/controller/job_controller.py b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py index 961c472..3e3b095 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/job_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py @@ -35,14 +35,11 @@ async def get_system_job_list( job_page_query: JobPageQueryModel = Depends(JobPageQueryModel.as_query), query_db: AsyncSession = Depends(get_db), ): - try: - # 获取分页数据 - notice_page_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=True) - logger.info('获取成功') - return ResponseUtil.success(model_content=notice_page_query_result) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + # 获取分页数据 + notice_page_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=notice_page_query_result) @jobController.post('/job', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:add'))]) @@ -54,21 +51,14 @@ async def add_system_job( query_db: AsyncSession = Depends(get_db), current_user: CurrentUserModel = Depends(LoginService.get_current_user), ): - try: - add_job.create_by = current_user.user.user_name - add_job.create_time = datetime.now() - add_job.update_by = current_user.user.user_name - add_job.update_time = datetime.now() - add_job_result = await JobService.add_job_services(query_db, add_job) - if add_job_result.is_success: - logger.info(add_job_result.message) - return ResponseUtil.success(msg=add_job_result.message) - else: - logger.warning(add_job_result.message) - return ResponseUtil.failure(msg=add_job_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + add_job.create_by = current_user.user.user_name + add_job.create_time = datetime.now() + add_job.update_by = current_user.user.user_name + add_job.update_time = datetime.now() + add_job_result = await JobService.add_job_services(query_db, add_job) + logger.info(add_job_result.message) + + return ResponseUtil.success(msg=add_job_result.message) @jobController.put('/job', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:edit'))]) @@ -80,89 +70,62 @@ async def edit_system_job( query_db: AsyncSession = Depends(get_db), current_user: CurrentUserModel = Depends(LoginService.get_current_user), ): - try: - edit_job.update_by = current_user.user.user_name - edit_job.update_time = datetime.now() - edit_job_result = await JobService.edit_job_services(query_db, edit_job) - if edit_job_result.is_success: - logger.info(edit_job_result.message) - return ResponseUtil.success(msg=edit_job_result.message) - else: - logger.warning(edit_job_result.message) - return ResponseUtil.failure(msg=edit_job_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + edit_job.update_by = current_user.user.user_name + edit_job.update_time = datetime.now() + edit_job_result = await JobService.edit_job_services(query_db, edit_job) + logger.info(edit_job_result.message) + + return ResponseUtil.success(msg=edit_job_result.message) @jobController.put('/job/changeStatus', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:changeStatus'))]) @log_decorator(title='定时任务管理', business_type=BusinessType.UPDATE) async def change_system_job_status( request: Request, - edit_job: EditJobModel, + change_job: EditJobModel, query_db: AsyncSession = Depends(get_db), current_user: CurrentUserModel = Depends(LoginService.get_current_user), ): - try: - edit_job.update_by = current_user.user.user_name - edit_job.update_time = datetime.now() - edit_job.type = 'status' - edit_job_result = await JobService.edit_job_services(query_db, edit_job) - if edit_job_result.is_success: - logger.info(edit_job_result.message) - return ResponseUtil.success(msg=edit_job_result.message) - else: - logger.warning(edit_job_result.message) - return ResponseUtil.failure(msg=edit_job_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + edit_job = EditJobModel( + jobId=change_job.job_id, + status=change_job.status, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='status', + ) + edit_job_result = await JobService.edit_job_services(query_db, edit_job) + logger.info(edit_job_result.message) + + return ResponseUtil.success(msg=edit_job_result.message) @jobController.put('/job/run', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:changeStatus'))]) @log_decorator(title='定时任务管理', business_type=BusinessType.UPDATE) async def execute_system_job(request: Request, execute_job: JobModel, query_db: AsyncSession = Depends(get_db)): - try: - execute_job_result = await JobService.execute_job_once_services(query_db, execute_job) - if execute_job_result.is_success: - logger.info(execute_job_result.message) - return ResponseUtil.success(msg=execute_job_result.message) - else: - logger.warning(execute_job_result.message) - return ResponseUtil.failure(msg=execute_job_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + execute_job_result = await JobService.execute_job_once_services(query_db, execute_job) + logger.info(execute_job_result.message) + + return ResponseUtil.success(msg=execute_job_result.message) @jobController.delete('/job/{job_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) @log_decorator(title='定时任务管理', business_type=BusinessType.DELETE) async def delete_system_job(request: Request, job_ids: str, query_db: AsyncSession = Depends(get_db)): - try: - delete_job = DeleteJobModel(jobIds=job_ids) - delete_job_result = await JobService.delete_job_services(query_db, delete_job) - if delete_job_result.is_success: - logger.info(delete_job_result.message) - return ResponseUtil.success(msg=delete_job_result.message) - else: - logger.warning(delete_job_result.message) - return ResponseUtil.failure(msg=delete_job_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + delete_job = DeleteJobModel(jobIds=job_ids) + delete_job_result = await JobService.delete_job_services(query_db, delete_job) + logger.info(delete_job_result.message) + + return ResponseUtil.success(msg=delete_job_result.message) @jobController.get( '/job/{job_id}', response_model=JobModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:query'))] ) async def query_detail_system_job(request: Request, job_id: int, query_db: AsyncSession = Depends(get_db)): - try: - job_detail_result = await JobService.job_detail_services(query_db, job_id) - logger.info(f'获取job_id为{job_id}的信息成功') - return ResponseUtil.success(data=job_detail_result) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + job_detail_result = await JobService.job_detail_services(query_db, job_id) + logger.info(f'获取job_id为{job_id}的信息成功') + + return ResponseUtil.success(data=job_detail_result) @jobController.post('/job/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:export'))]) @@ -172,15 +135,12 @@ async def export_system_job_list( job_page_query: JobPageQueryModel = Depends(JobPageQueryModel.as_form), query_db: AsyncSession = Depends(get_db), ): - try: - # 获取全量数据 - job_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=False) - job_export_result = await JobService.export_job_list_services(request, job_query_result) - logger.info('导出成功') - return ResponseUtil.streaming(data=bytes2file_response(job_export_result)) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + # 获取全量数据 + job_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=False) + job_export_result = await JobService.export_job_list_services(request, job_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(job_export_result)) @jobController.get( @@ -191,49 +151,32 @@ async def get_system_job_log_list( job_log_page_query: JobLogPageQueryModel = Depends(JobLogPageQueryModel.as_query), query_db: AsyncSession = Depends(get_db), ): - try: - # 获取分页数据 - job_log_page_query_result = await JobLogService.get_job_log_list_services( - query_db, job_log_page_query, is_page=True - ) - logger.info('获取成功') - return ResponseUtil.success(model_content=job_log_page_query_result) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + # 获取分页数据 + job_log_page_query_result = await JobLogService.get_job_log_list_services( + query_db, job_log_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=job_log_page_query_result) @jobController.delete('/jobLog/clean', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) @log_decorator(title='定时任务日志管理', business_type=BusinessType.CLEAN) async def clear_system_job_log(request: Request, query_db: AsyncSession = Depends(get_db)): - try: - clear_job_log_result = await JobLogService.clear_job_log_services(query_db) - if clear_job_log_result.is_success: - logger.info(clear_job_log_result.message) - return ResponseUtil.success(msg=clear_job_log_result.message) - else: - logger.warning(clear_job_log_result.message) - return ResponseUtil.failure(msg=clear_job_log_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + clear_job_log_result = await JobLogService.clear_job_log_services(query_db) + logger.info(clear_job_log_result.message) + + return ResponseUtil.success(msg=clear_job_log_result.message) @jobController.delete('/jobLog/{job_log_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) @log_decorator(title='定时任务日志管理', business_type=BusinessType.DELETE) async def delete_system_job_log(request: Request, job_log_ids: str, query_db: AsyncSession = Depends(get_db)): - try: - delete_job_log = DeleteJobLogModel(jobLogIds=job_log_ids) - delete_job_log_result = await JobLogService.delete_job_log_services(query_db, delete_job_log) - if delete_job_log_result.is_success: - logger.info(delete_job_log_result.message) - return ResponseUtil.success(msg=delete_job_log_result.message) - else: - logger.warning(delete_job_log_result.message) - return ResponseUtil.failure(msg=delete_job_log_result.message) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + delete_job_log = DeleteJobLogModel(jobLogIds=job_log_ids) + delete_job_log_result = await JobLogService.delete_job_log_services(query_db, delete_job_log) + logger.info(delete_job_log_result.message) + + return ResponseUtil.success(msg=delete_job_log_result.message) @jobController.post('/jobLog/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:export'))]) @@ -243,14 +186,9 @@ async def export_system_job_log_list( job_log_page_query: JobLogPageQueryModel = Depends(JobLogPageQueryModel.as_form), query_db: AsyncSession = Depends(get_db), ): - try: - # 获取全量数据 - job_log_query_result = await JobLogService.get_job_log_list_services( - query_db, job_log_page_query, is_page=False - ) - job_log_export_result = await JobLogService.export_job_log_list_services(request, job_log_query_result) - logger.info('导出成功') - return ResponseUtil.streaming(data=bytes2file_response(job_log_export_result)) - except Exception as e: - logger.exception(e) - return ResponseUtil.error(msg=str(e)) + # 获取全量数据 + job_log_query_result = await JobLogService.get_job_log_list_services(query_db, job_log_page_query, is_page=False) + job_log_export_result = await JobLogService.export_job_log_list_services(request, job_log_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(job_log_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/dao/job_dao.py b/ruoyi-fastapi-backend/module_admin/dao/job_dao.py index c2488c1..7f4f4a3 100644 --- a/ruoyi-fastapi-backend/module_admin/dao/job_dao.py +++ b/ruoyi-fastapi-backend/module_admin/dao/job_dao.py @@ -36,10 +36,13 @@ class JobDao: ( await db.execute( select(SysJob).where( - SysJob.job_name == job.job_name if job.job_name else True, - SysJob.job_group == job.job_group if job.job_group else True, - SysJob.invoke_target == job.invoke_target if job.invoke_target else True, - SysJob.cron_expression == job.cron_expression if job.cron_expression else True, + SysJob.job_name == job.job_name, + SysJob.job_group == job.job_group, + SysJob.job_executor == job.job_executor, + SysJob.invoke_target == job.invoke_target, + SysJob.job_args == job.job_args, + SysJob.job_kwargs == job.job_kwargs, + SysJob.cron_expression == job.cron_expression, ) ) ) diff --git a/ruoyi-fastapi-backend/module_admin/service/job_service.py b/ruoyi-fastapi-backend/module_admin/service/job_service.py index 12a9b93..76c2dfa 100644 --- a/ruoyi-fastapi-backend/module_admin/service/job_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/job_service.py @@ -1,12 +1,15 @@ from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from typing import List +from config.constant import CommonConstant, JobConstant from config.get_scheduler import SchedulerUtil +from exceptions.exception import ServiceException from module_admin.dao.job_dao import JobDao from module_admin.entity.vo.common_vo import CrudResponseModel 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, export_list2excel +from utils.string_util import StringUtil class JobService: @@ -30,6 +33,21 @@ class JobService: return job_list_result + @classmethod + async def check_job_unique_services(cls, query_db: AsyncSession, page_object: JobModel): + """ + 校验定时任务是否存在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): """ @@ -39,13 +57,28 @@ class JobService: :param page_object: 新增定时任务对象 :return: 新增定时任务校验结果 """ - job = await JobDao.get_job_detail_by_info(query_db, page_object) - if job: - result = dict(is_success=False, message='定时任务已存在') + if not SchedulerUtil.validate_cron_expression(page_object.cron_expression): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,Cron表达式不正确') + elif not await cls.check_job_unique_services(query_db, page_object): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,定时任务已存在') + elif StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许rmi调用') + elif 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)调用') + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS] + ): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用') + elif StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串存在违规') + elif not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不在白名单内') else: try: - await JobDao.add_job_dao(query_db, page_object) - job_info = await JobDao.get_job_detail_by_info(query_db, page_object) + 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() @@ -68,18 +101,31 @@ class JobService: edit_job = page_object.model_dump(exclude_unset=True) if page_object.type == 'status': del edit_job['type'] - job_info = await cls.job_detail_services(query_db, edit_job.get('job_id')) + job_info = await cls.job_detail_services(query_db, page_object.job_id) if job_info: - if page_object.type != 'status' and ( - job_info.job_name != page_object.job_name - or job_info.job_group != page_object.job_group - or job_info.invoke_target != page_object.invoke_target - or job_info.cron_expression != page_object.cron_expression - ): - job = await JobDao.get_job_detail_by_info(query_db, page_object) - if job: - result = dict(is_success=False, message='定时任务已存在') - return CrudResponseModel(**result) + if page_object.type != 'status': + if not SchedulerUtil.validate_cron_expression(page_object.cron_expression): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,Cron表达式不正确') + elif not await cls.check_job_unique_services(query_db, page_object): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,定时任务已存在') + elif StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许rmi调用') + elif 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)调用' + ) + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS] + ): + raise ServiceException( + message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用' + ) + elif StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串存在违规') + elif not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST): + 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')) @@ -89,14 +135,12 @@ class JobService: 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() - result = dict(is_success=True, message='更新成功') + return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() raise e else: - result = dict(is_success=False, message='定时任务不存在') - - return CrudResponseModel(**result) + raise ServiceException(message='定时任务不存在') @classmethod async def execute_job_once_services(cls, query_db: AsyncSession, page_object: JobModel): @@ -113,11 +157,9 @@ class JobService: 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) - result = dict(is_success=True, message='执行成功') + return CrudResponseModel(is_success=True, message='执行成功') else: - result = dict(is_success=False, message='定时任务不存在') - - return CrudResponseModel(**result) + raise ServiceException(message='定时任务不存在') @classmethod async def delete_job_services(cls, query_db: AsyncSession, page_object: DeleteJobModel): @@ -137,13 +179,12 @@ class JobService: if query_job: SchedulerUtil.remove_scheduler_job(job_id=job_id) await query_db.commit() - result = dict(is_success=True, message='删除成功') + return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() raise e else: - result = dict(is_success=False, message='传入定时任务id为空') - return CrudResponseModel(**result) + raise ServiceException(message='传入定时任务id为空') @classmethod async def job_detail_services(cls, query_db: AsyncSession, job_id: int): @@ -155,7 +196,10 @@ class JobService: :return: 定时任务id对应的信息 """ job = await JobDao.get_job_detail_by_id(query_db, job_id=job_id) - result = JobModel(**CamelCaseUtil.transform_result(job)) + if job: + result = JobModel(**CamelCaseUtil.transform_result(job)) + else: + result = JobModel(**dict()) return result diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py index 0a9d25d..0be9e65 100644 --- a/ruoyi-fastapi-backend/utils/string_util.py +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -1,3 +1,4 @@ +from typing import List from config.constant import CommonConstant @@ -44,3 +45,57 @@ class StringUtil: :return: 是否为http(s)://开头 """ return link.startswith(CommonConstant.HTTP) or link.startswith(CommonConstant.HTTPS) + + @classmethod + def contains_ignore_case(cls, search_str: str, compare_str: str): + """ + 查找指定字符串是否包含指定字符串同时串忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 查找结果 + """ + if compare_str and search_str: + return compare_str.lower() in search_str.lower() + return False + + @classmethod + def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): + """ + 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 查找结果 + """ + if search_str and compare_str_list: + for compare_str in compare_str_list: + return cls.contains_ignore_case(search_str, compare_str) + return False + + @classmethod + def startswith_case(cls, search_str: str, compare_str: str): + """ + 查找指定字符串是否以指定字符串开头 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 查找结果 + """ + if compare_str and search_str: + return search_str.startswith(compare_str) + return False + + @classmethod + def startswith_any_case(cls, search_str: str, compare_str_list: List[str]): + """ + 查找指定字符串是否以指定字符串列表中的任意一个字符串开头 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 查找结果 + """ + if search_str and compare_str_list: + for compare_str in compare_str_list: + return cls.startswith_case(search_str, compare_str) + return False diff --git a/ruoyi-fastapi-frontend/src/views/monitor/job/index.vue b/ruoyi-fastapi-frontend/src/views/monitor/job/index.vue index f820e5c..1b6b2ce 100644 --- a/ruoyi-fastapi-frontend/src/views/monitor/job/index.vue +++ b/ruoyi-fastapi-frontend/src/views/monitor/job/index.vue @@ -385,7 +385,7 @@ function reset() { cronExpression: undefined, misfirePolicy: "1", concurrent: "1", - status: "0" + status: "1" }; proxy.resetForm("jobRef"); }