import logging from datetime import datetime, timezone from shared.enums import JOB_STATUS_LIST, JOB_EXECUTE_POLICY_LIST from shared.constants import PAGE_SIZE from modules.job.schemas import JobForm, JobStatusForm from modules.job.models import Job from modules.job.repository import job_repo, joblog_repo from modules.job.scheduler import ( add_job_to_scheduler, remove_job_from_scheduler, pause_job_in_scheduler, get_scheduler, run_job_once as scheduler_run_job_once, reload_scheduler, ) from core.database import get_db_session from core import R from core.security import get_user_id from utils.request import like_contains, parse_id_list logger = logging.getLogger(__name__) # 查询定时任务分页数据 def get_job_page(request): # 页码 page = int(request.query_params.get("pageNo", 1)) # 每页数 limit = int(request.query_params.get("pageSize", PAGE_SIZE)) # 实例化查询对象 query = job_repo.filter(Job.is_delete == 0) # 任务名称 job_name = request.query_params.get('jobName') if job_name: query = query.filter(Job.job_name.like(like_contains(job_name), escape='\\')) # 任务状态 status = request.query_params.get('status') if status: query = query.filter(Job.status == status) # 任务分组 job_group = request.query_params.get('jobGroup') if job_group: query = query.filter(Job.job_group == job_group) # 排序 query = query.order_by(Job.id.desc()) # 记录总数 count = query.count() # 分页查询 job_list = query.limit(limit).offset((page - 1) * limit).all() # 若当前页超出末页(数据被并发删除),自动回退到第一页 if page > 1 and not job_list: page = 1 count = query.count() job_list = query.limit(limit).offset(0).all() # 实例化结果 scheduler = get_scheduler() result = [] if len(job_list) > 0: for item in job_list: data = item.to_dict() data['status_name'] = JOB_STATUS_LIST.get(item.status) data['execute_policy_name'] = JOB_EXECUTE_POLICY_LIST.get(item.execute_policy) data['next_run_time'] = _get_next_run_time(scheduler, item.id) result.append(data) return R.ok(data=result, count=count) def _get_next_run_time(scheduler, job_id): """从调度器获取任务下次执行时间""" try: scheduler_job = scheduler.get_job(f"job_{job_id}") if scheduler_job and scheduler_job.next_run_time: return scheduler_job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') except Exception: pass return None # 根据ID查询任务详情 def get_job_detail(job_id): job = job_repo.get_by_id(job_id) if not job: return None data = job.to_dict() data['status_name'] = JOB_STATUS_LIST.get(job.status) data['execute_policy_name'] = JOB_EXECUTE_POLICY_LIST.get(job.execute_policy) return data # 添加定时任务 def add_job(request, data: JobForm): # 表单数据赋值给对象并插入 job = job_repo.create(**data.model_dump(exclude={'id'}), create_user=get_user_id(request)) # 状态为运行中且有Cron表达式,添加到调度器 if job.status == 1 and job.cron_expression: add_job_to_scheduler(job.to_dict()) return R.ok(msg="添加成功") # 更新定时任务 def update_job(request, data: JobForm): # 记录ID判空 if not data.id or data.id <= 0: return R.failed("记录ID不能为空") # 根据ID查询记录 job = job_repo.get_by_id(data.id) if not job: return R.failed("任务不存在") old_status = job.status old_cron = job.cron_expression # 构造更新数据(排除 id) updData = data.model_dump(exclude={'id'}) updData['update_user'] = get_user_id(request) updData['update_time'] = datetime.now(tz=timezone.utc) result = job_repo.update(data.id, updData) if not result: return R.failed("更新失败") # 处理调度器更新 _handle_scheduler_update(data.model_dump(), old_status, old_cron) return R.ok(msg="更新成功") def _handle_scheduler_update(job_data, old_status, old_cron): """处理调度器更新(job_data 为完整任务数据,含 execute_policy/params)""" job_id = job_data.get('id') new_status = job_data.get('status') new_cron = job_data.get('cron_expression') if old_status != 1 and new_status == 1 and new_cron: add_job_to_scheduler(job_data) elif old_status == 1 and new_status != 1: remove_job_from_scheduler(job_id) elif new_status == 1 and old_cron != new_cron: if new_cron: add_job_to_scheduler(job_data) else: remove_job_from_scheduler(job_id) # 删除定时任务 def delete_job(job_id): # 从调度器移除 id_list = parse_id_list(job_id) for jid in id_list: remove_job_from_scheduler(jid) return Job.batch_delete_r(job_id) # 设置任务状态 def update_job_status(request, data: JobStatusForm): # 根据ID查询记录 job = job_repo.get_by_id(data.id) if not job: return R.failed("任务不存在") old_status = job.status # 更新记录 result = job_repo.update(data.id, { "status": data.status, "update_user": get_user_id(request), "update_time": datetime.now(tz=timezone.utc) }) if not result: return R.failed("设置失败") # 处理调度器 if data.status == 1 and job.cron_expression: job_dict = job.to_dict() job_dict['status'] = data.status add_job_to_scheduler(job_dict) elif data.status == 2: pause_job_in_scheduler(data.id) elif old_status == 1 and data.status != 1: remove_job_from_scheduler(data.id) return R.ok(msg="设置成功") # 暂停任务 def pause_job_by_id(job_id): db = get_db_session() job = job_repo.get_by_id(job_id) if not job: return R.failed("任务不存在") if job.status == 2: return R.ok(msg="任务已处于暂停状态") job.status = 2 job.update_user = job.create_user job.update_time = datetime.now(tz=timezone.utc) db.commit() pause_job_in_scheduler(job_id) return R.ok(msg="暂停成功") # 恢复任务 def resume_job_by_id(job_id): db = get_db_session() job = job_repo.get_by_id(job_id) if not job: return R.failed("任务不存在") if job.status == 1: return R.ok(msg="任务已处于运行中状态") if not job.cron_expression: return R.failed("Cron表达式为空,无法恢复") job.status = 1 job.update_user = job.create_user job.update_time = datetime.now(tz=timezone.utc) db.commit() add_job_to_scheduler(job.to_dict()) return R.ok(msg="恢复成功") # 立即执行一次 def run_job_once_api(job_id): result = scheduler_run_job_once(job_id) if result.get('success'): return R.ok(data=result, msg="执行成功") else: return R.failed(msg=result.get('error', '执行失败')) # 重新加载调度器 def reload_all_jobs(): count = reload_scheduler() return R.ok(data=count, msg=f"已重新加载 {count} 个任务") import logging from shared.enums import JOB_LOG_STATUS_LIST from shared.constants import PAGE_SIZE from modules.job.models import Job from modules.job.models import JobLog from core.database import SessionLocal from core import R from utils.request import like_contains, parse_id_list logger = logging.getLogger(__name__) # ============================================================ # 日志写入(供调度器调用,可在请求上下文外执行) # ============================================================ def record_job_log(job_id, start_time, end_time, status, exception_info=None, job_message=None): """ 记录任务执行日志 使用独立会话写入,调度器线程等无请求上下文场景也可调用。 """ session = SessionLocal() try: job = session.query(Job).filter(Job.id == job_id, Job.is_delete == 0).first() consume_time = 0 if end_time and start_time: consume_time = int((end_time - start_time).total_seconds() * 1000) log = JobLog( job_id=job_id, job_name=job.job_name if job else '', job_group=job.job_group if job else None, job_trigger=job.job_trigger if job else None, cron_expression=job.cron_expression if job else None, job_message=job_message, status=status, start_time=start_time, end_time=end_time, consume_time=consume_time, exception_info=exception_info, ) session.add(log) session.commit() return log except Exception as e: session.rollback() logger.error(f"记录任务执行日志异常: {str(e)}") return None finally: session.close() # ============================================================ # 日志查询/删除 # ============================================================ def get_job_log_page(request): # 页码 page = int(request.query_params.get("pageNo", 1)) # 每页数 limit = int(request.query_params.get("pageSize", PAGE_SIZE)) # 实例化查询对象 query = joblog_repo.filter(JobLog.del_flag == 0) # 任务名称 job_name = request.query_params.get('jobName') if job_name: query = query.filter(JobLog.job_name.like(like_contains(job_name), escape='\\')) # 任务分组 job_group = request.query_params.get('jobGroup') if job_group: query = query.filter(JobLog.job_group == job_group) # 执行状态 status = request.query_params.get('status') if status: query = query.filter(JobLog.status == status) # 排序 query = query.order_by(JobLog.id.desc()) # 记录总数 count = query.count() # 分页查询 log_list = query.limit(limit).offset((page - 1) * limit).all() # 若当前页超出末页(数据被并发删除),自动回退到第一页 if page > 1 and not log_list: page = 1 count = query.count() log_list = query.limit(limit).offset(0).all() # 实例化结果 result = [] for item in log_list: data = item.to_dict() data['status_name'] = JOB_LOG_STATUS_LIST.get(item.status) result.append(data) return R.ok(data=result, count=count) # 根据ID查询日志详情 def get_job_log_detail(job_log_id): log = joblog_repo.get_by_id(job_log_id) if not log: return None data = log.to_dict() data['status_name'] = JOB_LOG_STATUS_LIST.get(log.status) return data # 删除任务日志(软删除) def delete_job_log(ids_str): id_list = parse_id_list(ids_str) if not id_list: return R.failed("记录ID不存在") result = joblog_repo.filter(JobLog.id.in_(id_list), JobLog.del_flag == 0).update( {"del_flag": 1}, synchronize_session=False ) return R.ok(msg="本次共删除{0}条数据".format(result))