mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
feat: 重构任务调度系统和工作流节点管理
- 重构任务调度系统,将定时任务改为调度器监控,优化状态管理和日志记录 - 重构工作流节点模型,简化分类并增加执行相关字段 - 优化工作流执行逻辑,移除对开始/结束节点的强制要求 - 前端新增终端组件和工作流节点操作按钮优化 - 移除无用代码和文件,清理后端任务调度相关代码 - 更新系统菜单结构,调整任务管理相关权限和名称
This commit is contained in:
@@ -0,0 +1,695 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from apscheduler.events import (
|
||||
EVENT_ALL,
|
||||
EVENT_JOB_ERROR,
|
||||
EVENT_JOB_EXECUTED,
|
||||
EVENT_JOB_MISSED,
|
||||
EVENT_JOB_REMOVED,
|
||||
EVENT_JOB_SUBMITTED,
|
||||
JobEvent,
|
||||
JobExecutionEvent,
|
||||
)
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.executors.pool import ProcessPoolExecutor, ThreadPoolExecutor
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.jobstores.base import ConflictingIdError
|
||||
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 apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.config.setting import settings
|
||||
from app.core.database import engine
|
||||
from app.core.logger import log
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.utils.cron_util import CronUtil
|
||||
from app.plugin.module_task.node.model import NodeModel
|
||||
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.configure(
|
||||
jobstores={
|
||||
"default": MemoryJobStore(),
|
||||
"sqlalchemy": SQLAlchemyJobStore(url=settings.DB_URI, engine=engine),
|
||||
"redis": RedisJobStore(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
)
|
||||
},
|
||||
executors={
|
||||
"default": AsyncIOExecutor(),
|
||||
"threadpool": ThreadPoolExecutor(max_workers=10),
|
||||
"processpool": ProcessPoolExecutor(max_workers=1),
|
||||
},
|
||||
job_defaults={
|
||||
"coalesce": True,
|
||||
"max_instances": 1,
|
||||
},
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
|
||||
|
||||
class SchedulerUtil:
|
||||
"""
|
||||
定时任务相关方法
|
||||
"""
|
||||
|
||||
redis_instance: Redis | None = None
|
||||
|
||||
@classmethod
|
||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||
"""
|
||||
监听任务执行事件,记录执行日志
|
||||
每次执行都创建新记录,保留所有历史执行记录
|
||||
"""
|
||||
try:
|
||||
if not hasattr(event, "job_id"):
|
||||
return
|
||||
|
||||
job_id = str(event.job_id)
|
||||
|
||||
if event.code == EVENT_JOB_SUBMITTED:
|
||||
job = cls.get_job(job_id=job_id)
|
||||
cls._create_job_log(
|
||||
job_id=job_id,
|
||||
job_name=job.name if job else None,
|
||||
trigger_type=cls._get_trigger_type(job_id),
|
||||
status="running",
|
||||
)
|
||||
elif event.code == EVENT_JOB_EXECUTED:
|
||||
retval = getattr(event, "retval", None)
|
||||
cls._update_latest_job_log(
|
||||
job_id=job_id,
|
||||
status="success",
|
||||
result=str(retval) if retval else None,
|
||||
)
|
||||
elif event.code == EVENT_JOB_ERROR:
|
||||
exception = getattr(event, "exception", None)
|
||||
cls._update_latest_job_log(
|
||||
job_id=job_id,
|
||||
status="failed",
|
||||
error=str(exception) if exception else "未知错误",
|
||||
)
|
||||
elif event.code == EVENT_JOB_MISSED:
|
||||
cls._update_latest_job_log(
|
||||
job_id=job_id,
|
||||
status="timeout",
|
||||
error="任务错过执行时间",
|
||||
)
|
||||
elif event.code == EVENT_JOB_REMOVED:
|
||||
cls._update_job_log_on_removed(job_id=job_id)
|
||||
except Exception as e:
|
||||
log.error(f"处理任务执行事件失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
def _get_trigger_type(cls, job_id: str) -> str:
|
||||
"""
|
||||
获取任务的触发类型
|
||||
"""
|
||||
job = cls.get_job(job_id=job_id)
|
||||
if not job:
|
||||
return "manual"
|
||||
trigger = job.trigger
|
||||
if isinstance(trigger, CronTrigger):
|
||||
return "cron"
|
||||
elif isinstance(trigger, IntervalTrigger):
|
||||
return "interval"
|
||||
elif isinstance(trigger, DateTrigger):
|
||||
if trigger.run_date:
|
||||
now = datetime.now(trigger.run_date.tzinfo)
|
||||
diff = abs((trigger.run_date - now).total_seconds())
|
||||
if diff < 60:
|
||||
return "manual"
|
||||
return "date"
|
||||
return "manual"
|
||||
|
||||
@classmethod
|
||||
async def init_scheduler(cls, redis: Redis | None = None) -> None:
|
||||
"""
|
||||
应用启动时初始化定时任务。
|
||||
"""
|
||||
if redis:
|
||||
cls.redis_instance = redis
|
||||
scheduler.start()
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||
scheduler.resume()
|
||||
|
||||
@classmethod
|
||||
def _task_wrapper(cls, job_id: str | int, code_block: str | None, *args, **kwargs):
|
||||
"""
|
||||
任务执行包装器,执行自定义代码块(同步版本,用于 ThreadPoolExecutor)
|
||||
"""
|
||||
|
||||
def run_sync_handler():
|
||||
if code_block:
|
||||
local_vars = {}
|
||||
exec(code_block, {"__builtins__": __builtins__}, local_vars)
|
||||
handler = local_vars.get("handler")
|
||||
if handler and callable(handler):
|
||||
return handler(*args, **kwargs)
|
||||
raise ValueError("代码块必须定义 handler(*args, **kwargs) 函数")
|
||||
return None
|
||||
|
||||
try:
|
||||
result = run_sync_handler()
|
||||
return result
|
||||
except Exception as e:
|
||||
log.error(f"任务 {job_id} 执行失败: {e!s}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def _get_job_state(cls, job) -> str | None:
|
||||
"""
|
||||
获取任务状态(解析为可读的JSON格式)
|
||||
"""
|
||||
import json
|
||||
import pickle
|
||||
|
||||
if not job:
|
||||
return None
|
||||
|
||||
state = job.__getstate__()
|
||||
|
||||
def serialize_value(obj):
|
||||
if obj is None:
|
||||
return None
|
||||
if isinstance(obj, (str, int, float, bool)):
|
||||
return obj
|
||||
if isinstance(obj, bytes):
|
||||
try:
|
||||
decoded = pickle.loads(obj)
|
||||
return serialize_value(decoded)
|
||||
except Exception:
|
||||
return obj.decode("utf-8", errors="replace")
|
||||
if isinstance(obj, dict):
|
||||
return {k: serialize_value(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [serialize_value(item) for item in obj]
|
||||
if hasattr(obj, "__dict__"):
|
||||
obj_dict = {}
|
||||
for k, v in obj.__dict__.items():
|
||||
if not k.startswith("_"):
|
||||
obj_dict[k] = serialize_value(v)
|
||||
return {"__class__": obj.__class__.__name__, **obj_dict}
|
||||
try:
|
||||
return str(obj)
|
||||
except Exception:
|
||||
return f"<{type(obj).__name__}>"
|
||||
|
||||
parsed_state = serialize_value(state)
|
||||
return json.dumps(parsed_state, ensure_ascii=False, indent=2)
|
||||
|
||||
@classmethod
|
||||
def get_job_state_from_blob(cls, blob_data: bytes) -> Any:
|
||||
"""
|
||||
从 BLOB 数据反序列化任务状态
|
||||
|
||||
参数:
|
||||
- blob_data: apscheduler_jobs 表中的 job_state 字段(BLOB 类型)
|
||||
|
||||
返回:
|
||||
- 反序列化后的任务状态
|
||||
"""
|
||||
import pickle
|
||||
|
||||
if not blob_data:
|
||||
return None
|
||||
|
||||
def serialize_value(obj: Any) -> Any:
|
||||
if obj is None:
|
||||
return None
|
||||
if isinstance(obj, (str, int, float, bool)):
|
||||
return obj
|
||||
if isinstance(obj, bytes):
|
||||
try:
|
||||
decoded = pickle.loads(obj)
|
||||
return serialize_value(decoded)
|
||||
except Exception:
|
||||
return obj.decode("utf-8", errors="replace")
|
||||
if isinstance(obj, dict):
|
||||
return {k: serialize_value(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [serialize_value(item) for item in obj]
|
||||
if hasattr(obj, "__dict__"):
|
||||
obj_dict = {}
|
||||
for k, v in obj.__dict__.items():
|
||||
if not k.startswith("_"):
|
||||
obj_dict[k] = serialize_value(v)
|
||||
return {"__class__": obj.__class__.__name__, **obj_dict}
|
||||
try:
|
||||
return str(obj)
|
||||
except Exception:
|
||||
return f"<{type(obj).__name__}>"
|
||||
|
||||
try:
|
||||
state = pickle.loads(blob_data)
|
||||
return serialize_value(state)
|
||||
except Exception as e:
|
||||
return {"error": str(e), "raw_data": str(blob_data[:200])}
|
||||
|
||||
@classmethod
|
||||
def _create_job_log(cls, job_id: str, job_name: str | None = None, trigger_type: str = "manual", status: str = "running") -> int:
|
||||
"""
|
||||
创建执行日志
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
|
||||
job = cls.get_job(job_id=job_id)
|
||||
next_run_time = str(job.next_run_time) if job and job.next_run_time else None
|
||||
job_state = cls._get_job_state(job)
|
||||
|
||||
with Session(engine) as session:
|
||||
job_log = JobModel(
|
||||
job_id=job_id,
|
||||
job_name=job_name,
|
||||
trigger_type=trigger_type,
|
||||
status=status,
|
||||
next_run_time=next_run_time,
|
||||
job_state=job_state,
|
||||
)
|
||||
session.add(job_log)
|
||||
session.commit()
|
||||
return job_log.id
|
||||
|
||||
@classmethod
|
||||
def _update_job_log(cls, job_id: str, status: str, result: str | None = None, error: str | None = None) -> None:
|
||||
"""
|
||||
更新执行日志(更新该 job_id 最新的 pending 或 running 状态日志)
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
|
||||
job = cls.get_job(job_id=job_id)
|
||||
next_run_time = str(job.next_run_time) if job and job.next_run_time else None
|
||||
job_state = cls._get_job_state(job)
|
||||
|
||||
with Session(engine) as session:
|
||||
job_log = (
|
||||
session.query(JobModel)
|
||||
.filter(JobModel.job_id == job_id, JobModel.status.in_(["pending", "running"]))
|
||||
.order_by(JobModel.created_time.desc())
|
||||
.first()
|
||||
)
|
||||
if job_log:
|
||||
job_log.status = status
|
||||
if next_run_time:
|
||||
job_log.next_run_time = next_run_time
|
||||
if job_state:
|
||||
job_log.job_state = job_state
|
||||
if result:
|
||||
job_log.result = result
|
||||
if error:
|
||||
job_log.error = error
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def _update_latest_job_log(cls, job_id: str, status: str, result: str | None = None, error: str | None = None) -> None:
|
||||
"""
|
||||
更新最新的执行日志(更新该 job_id 最新的一条 running 状态日志)
|
||||
用于每次执行完成后更新状态
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
|
||||
job = cls.get_job(job_id=job_id)
|
||||
next_run_time = str(job.next_run_time) if job and job.next_run_time else None
|
||||
job_state = cls._get_job_state(job)
|
||||
|
||||
with Session(engine) as session:
|
||||
job_log = (
|
||||
session.query(JobModel)
|
||||
.filter(JobModel.job_id == job_id, JobModel.status == "running")
|
||||
.order_by(JobModel.created_time.desc())
|
||||
.first()
|
||||
)
|
||||
if job_log:
|
||||
job_log.status = status
|
||||
if next_run_time:
|
||||
job_log.next_run_time = next_run_time
|
||||
if job_state:
|
||||
job_log.job_state = job_state
|
||||
if result:
|
||||
job_log.result = result
|
||||
if error:
|
||||
job_log.error = error
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def _update_job_log_on_removed(cls, job_id: str) -> None:
|
||||
"""
|
||||
任务被移除时,更新最新的 pending 状态日志为 cancelled
|
||||
注意:
|
||||
- 只有当任务还在 pending 状态时才更新为 cancelled
|
||||
- 一次性任务(trigger_type 为 date 或 manual)执行后会自动触发 REMOVED 事件,此时不应该标记为 cancelled
|
||||
- REMOVED 事件可能在 SUBMITTED 之前触发,此时状态还是 pending
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
|
||||
with Session(engine) as session:
|
||||
job_log = (
|
||||
session.query(JobModel)
|
||||
.filter(JobModel.job_id == job_id, JobModel.status == "pending")
|
||||
.order_by(JobModel.created_time.desc())
|
||||
.first()
|
||||
)
|
||||
if job_log:
|
||||
if job_log.trigger_type in ["date", "manual"]:
|
||||
return
|
||||
job_log.status = "cancelled"
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_job_status(cls, job_id: str | int) -> str:
|
||||
"""
|
||||
获取单个任务的当前状态。
|
||||
"""
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
return "未知"
|
||||
|
||||
if job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
return "暂停中"
|
||||
|
||||
if scheduler.state == 0:
|
||||
return "已停止"
|
||||
|
||||
return "运行中"
|
||||
|
||||
@classmethod
|
||||
def add_and_run_job_now(cls, job_info: NodeModel) -> Job:
|
||||
"""
|
||||
立即执行任务(添加到调度器并立即运行)
|
||||
"""
|
||||
trigger = DateTrigger(run_date=datetime.now())
|
||||
return cls._add_job_with_trigger(job_info, trigger)
|
||||
|
||||
@classmethod
|
||||
def add_cron_job(
|
||||
cls,
|
||||
job_info: NodeModel,
|
||||
trigger_args: str | None = None,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
) -> Job:
|
||||
"""
|
||||
创建Cron定时任务
|
||||
|
||||
参数:
|
||||
- job_info: 任务信息
|
||||
- trigger_args: Cron表达式
|
||||
- start_date: 开始时间
|
||||
- end_date: 结束时间
|
||||
"""
|
||||
cron_expr = trigger_args or job_info.trigger_args
|
||||
if not cron_expr:
|
||||
raise ValueError("Cron触发器缺少参数")
|
||||
|
||||
fields = cron_expr.strip().split()
|
||||
if len(fields) not in (6, 7):
|
||||
raise ValueError("无效的 Cron 表达式")
|
||||
if not CronUtil.validate_cron_expression(cron_expr):
|
||||
raise ValueError(f"Cron表达式不正确: {cron_expr}")
|
||||
|
||||
parsed_fields = [field if field != "?" else "*" for field in fields]
|
||||
if len(fields) == 6:
|
||||
parsed_fields.append("*")
|
||||
|
||||
second, minute, hour, day, month, day_of_week, year = tuple(parsed_fields)
|
||||
|
||||
if second == "*" and minute == "*" and hour == "*" and day == "*" and month == "*" and day_of_week in ("*", "?"):
|
||||
raise ValueError("Cron表达式不允许每秒执行,请至少指定秒数(如:0 * * * * ? * 表示每分钟执行)")
|
||||
|
||||
trigger = CronTrigger(
|
||||
second=second,
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
year=year,
|
||||
start_date=start_date or job_info.start_date,
|
||||
end_date=end_date or job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
return cls._add_job_with_trigger(job_info, trigger)
|
||||
|
||||
@classmethod
|
||||
def add_interval_job(
|
||||
cls,
|
||||
job_info: NodeModel,
|
||||
trigger_args: str | None = None,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
) -> Job:
|
||||
"""
|
||||
创建间隔执行任务
|
||||
|
||||
参数:
|
||||
- job_info: 任务信息
|
||||
- trigger_args: 间隔参数 (秒 分 时 天 周)
|
||||
- start_date: 开始时间
|
||||
- end_date: 结束时间
|
||||
"""
|
||||
interval_args = trigger_args or job_info.trigger_args
|
||||
if not interval_args:
|
||||
raise ValueError("interval触发器缺少参数")
|
||||
|
||||
fields = interval_args.strip().split()
|
||||
if len(fields) != 5:
|
||||
raise ValueError("无效的 interval 表达式,格式: 秒 分 时 天 周")
|
||||
|
||||
second, minute, hour, day, week = tuple(
|
||||
int(field) if field != "*" else 0 for field in fields
|
||||
)
|
||||
trigger = IntervalTrigger(
|
||||
weeks=week,
|
||||
days=day,
|
||||
hours=hour,
|
||||
minutes=minute,
|
||||
seconds=second,
|
||||
start_date=start_date or job_info.start_date,
|
||||
end_date=end_date or job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
return cls._add_job_with_trigger(job_info, trigger)
|
||||
|
||||
@classmethod
|
||||
def add_date_job(cls, job_info: NodeModel, run_date: str | None = None) -> Job:
|
||||
"""
|
||||
创建指定时间执行任务
|
||||
|
||||
参数:
|
||||
- job_info: 任务信息
|
||||
- run_date: 执行时间
|
||||
"""
|
||||
date_str = run_date or job_info.trigger_args
|
||||
if not date_str:
|
||||
raise ValueError("date触发器缺少执行时间参数")
|
||||
|
||||
trigger = DateTrigger(run_date=date_str, timezone="Asia/Shanghai")
|
||||
return cls._add_job_with_trigger(job_info, trigger)
|
||||
|
||||
@classmethod
|
||||
def _add_job_with_trigger(cls, job_info: NodeModel, trigger) -> Job:
|
||||
"""
|
||||
添加任务到调度器
|
||||
"""
|
||||
code_block = job_info.func
|
||||
if not code_block or not code_block.strip():
|
||||
raise ValueError("任务代码块不能为空")
|
||||
|
||||
jobstore = job_info.jobstore or "sqlalchemy"
|
||||
executor = job_info.executor or "threadpool"
|
||||
|
||||
job_args = []
|
||||
if job_info.args:
|
||||
args_str = str(job_info.args).strip()
|
||||
if args_str:
|
||||
job_args = [arg.strip() for arg in args_str.split(",") if arg.strip()]
|
||||
|
||||
job_kwargs = {}
|
||||
if job_info.kwargs:
|
||||
kwargs_str = str(job_info.kwargs).strip()
|
||||
if kwargs_str:
|
||||
try:
|
||||
job_kwargs = json.loads(kwargs_str)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"关键字参数JSON格式无效: {kwargs_str}")
|
||||
|
||||
try:
|
||||
job = scheduler.add_job(
|
||||
func=cls._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), code_block, *job_args],
|
||||
kwargs=job_kwargs,
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1,
|
||||
jobstore=jobstore,
|
||||
executor=executor,
|
||||
)
|
||||
log.info(f"任务 {job_info.id} 添加到 {jobstore} 存储器成功")
|
||||
return job
|
||||
except ConflictingIdError:
|
||||
scheduler.remove_job(job_id=str(job_info.id), jobstore=jobstore)
|
||||
job = scheduler.add_job(
|
||||
func=cls._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), code_block, *job_args],
|
||||
kwargs=job_kwargs,
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1,
|
||||
jobstore=jobstore,
|
||||
executor=executor,
|
||||
)
|
||||
log.info(f"任务 {job_info.id} 已存在,已移除旧任务并重新添加")
|
||||
return job
|
||||
|
||||
@classmethod
|
||||
def start(cls, paused: bool = False) -> None:
|
||||
scheduler.start(paused=paused)
|
||||
|
||||
@classmethod
|
||||
async def shutdown(cls, wait: bool = False):
|
||||
return scheduler.shutdown(wait=wait)
|
||||
|
||||
@classmethod
|
||||
def configure(cls, gconfig: dict | None = None, prefix: str = "apscheduler.", **options) -> None:
|
||||
scheduler.configure(gconfig or {}, prefix, **options)
|
||||
|
||||
@classmethod
|
||||
def pause(cls) -> None:
|
||||
scheduler.pause()
|
||||
|
||||
@classmethod
|
||||
def resume(cls) -> None:
|
||||
scheduler.resume()
|
||||
|
||||
@classmethod
|
||||
def is_running(cls) -> bool:
|
||||
return scheduler.running
|
||||
|
||||
@classmethod
|
||||
def get_scheduler_state(cls) -> str:
|
||||
if scheduler.state == 0:
|
||||
return "停止"
|
||||
if scheduler.state == 1:
|
||||
return "运行中"
|
||||
if scheduler.state == 2:
|
||||
return "暂停"
|
||||
return "未知"
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||
return scheduler.get_job(str(job_id), jobstore)
|
||||
|
||||
@classmethod
|
||||
def get_jobs(cls, jobstore: str | None = None) -> list[Job]:
|
||||
return scheduler.get_jobs(jobstore)
|
||||
|
||||
@classmethod
|
||||
def get_all_jobs(cls) -> list[Job]:
|
||||
return scheduler.get_jobs()
|
||||
|
||||
@classmethod
|
||||
def remove_job(cls, job_id: str | int, jobstore: str | None = None) -> None:
|
||||
scheduler.remove_job(str(job_id), jobstore)
|
||||
|
||||
@classmethod
|
||||
def clear_jobs(cls) -> None:
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
@classmethod
|
||||
def print_jobs(cls, jobstore: str | None = None) -> str:
|
||||
"""
|
||||
打印调度器任务信息
|
||||
|
||||
参数:
|
||||
- jobstore: 存储器别名,None 表示所有存储器
|
||||
|
||||
返回:
|
||||
- str: 格式化的任务信息
|
||||
"""
|
||||
import io
|
||||
|
||||
output = io.StringIO()
|
||||
scheduler.print_jobs(jobstore=jobstore, out=output)
|
||||
return output.getvalue()
|
||||
|
||||
@classmethod
|
||||
def sync_jobs_to_db(cls) -> int:
|
||||
"""
|
||||
将调度器中的任务同步到数据库
|
||||
|
||||
返回:
|
||||
- int: 同步的任务数量
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
|
||||
jobs = cls.get_all_jobs()
|
||||
sync_count = 0
|
||||
|
||||
with Session(engine) as session:
|
||||
for job in jobs:
|
||||
existing_log = (
|
||||
session.query(JobModel)
|
||||
.filter(JobModel.job_id == str(job.id), JobModel.status == "pending")
|
||||
.first()
|
||||
)
|
||||
if not existing_log:
|
||||
job_log = JobModel(
|
||||
job_id=str(job.id),
|
||||
job_name=job.name,
|
||||
trigger_type=cls._get_trigger_type(str(job.id)),
|
||||
status="pending",
|
||||
next_run_time=str(job.next_run_time) if job.next_run_time else None,
|
||||
job_state=cls._get_job_state(job),
|
||||
)
|
||||
session.add(job_log)
|
||||
sync_count += 1
|
||||
session.commit()
|
||||
|
||||
return sync_count
|
||||
|
||||
@classmethod
|
||||
def pause_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||
return scheduler.pause_job(str(job_id), jobstore)
|
||||
|
||||
@classmethod
|
||||
def resume_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||
return scheduler.resume_job(str(job_id), jobstore)
|
||||
|
||||
@classmethod
|
||||
def modify_job(cls, job_id: str | int, jobstore: str | None = None, **changes) -> Job | None:
|
||||
return scheduler.modify_job(str(job_id), jobstore, **changes)
|
||||
|
||||
@classmethod
|
||||
def run_job_now(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||
job = cls.get_job(job_id=job_id, jobstore=jobstore)
|
||||
if not job:
|
||||
return None
|
||||
trigger = DateTrigger(run_date=datetime.now(), timezone="Asia/Shanghai")
|
||||
return scheduler.modify_job(str(job_id), jobstore, trigger=trigger)
|
||||
@@ -81,7 +81,6 @@ def get_dynamic_router() -> APIRouter:
|
||||
if router_id not in seen_router_ids:
|
||||
seen_router_ids.add(router_id)
|
||||
container_router.include_router(attr_value)
|
||||
log.debug(f"📌 注册路由 {attr_name} 到容器 {prefix}")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"❌️ 处理模块 {module_path} 失败: {e!s}")
|
||||
|
||||
@@ -36,7 +36,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
||||
"""
|
||||
from app.api.v1.module_system.dict.service import DictDataService
|
||||
from app.api.v1.module_system.params.service import ParamsService
|
||||
from app.plugin.module_task.job.tools.ap_scheduler import SchedulerUtil
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
|
||||
try:
|
||||
await InitializeData().init_db()
|
||||
@@ -49,7 +49,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
||||
log.info("✅ Redis系统配置初始化完成")
|
||||
await DictDataService().init_dict_service(redis=app.state.redis)
|
||||
log.info("✅ Redis数据字典初始化完成")
|
||||
await SchedulerUtil.init_system_scheduler(redis=app.state.redis)
|
||||
await SchedulerUtil.init_scheduler(redis=app.state.redis)
|
||||
log.info("✅ 定时任务调度器初始化完成")
|
||||
await FastAPILimiter.init(
|
||||
redis=app.state.redis,
|
||||
@@ -62,15 +62,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
||||
# 导入并显示最终的启动信息面板
|
||||
from app.common.enums import EnvironmentEnum
|
||||
|
||||
scheduler_jobs_count = len(SchedulerUtil.get_all_jobs())
|
||||
scheduler_status = SchedulerUtil.get_job_status()
|
||||
console_run(
|
||||
host=settings.SERVER_HOST,
|
||||
port=settings.SERVER_PORT,
|
||||
reload=settings.ENVIRONMENT == EnvironmentEnum.DEV,
|
||||
database_ready=True,
|
||||
redis_ready=True,
|
||||
scheduler_jobs=scheduler_jobs_count,
|
||||
scheduler_status=scheduler_status,
|
||||
scheduler_ready=SchedulerUtil.is_running(),
|
||||
limiter_ready=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -80,11 +79,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
||||
yield
|
||||
|
||||
try:
|
||||
await import_modules_async(
|
||||
modules=settings.EVENT_LIST, desc="全局事件", app=app, status=False
|
||||
)
|
||||
await import_modules_async(modules=settings.EVENT_LIST, desc="全局事件", app=app, status=False)
|
||||
log.info("✅ 全局事件模块卸载完成")
|
||||
await SchedulerUtil.close_system_scheduler()
|
||||
await SchedulerUtil.shutdown(wait=False)
|
||||
log.info("✅ 定时任务调度器已关闭")
|
||||
await FastAPILimiter.close()
|
||||
log.info("✅ 请求限制器已关闭")
|
||||
|
||||
@@ -1,348 +1,332 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.common.request import PaginationService
|
||||
from app.common.response import ErrorResponse, ResponseSchema, StreamResponse, SuccessResponse
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobLogOutSchema,
|
||||
JobLogQueryParam,
|
||||
JobOutSchema,
|
||||
JobQueryParam,
|
||||
JobUpdateSchema,
|
||||
)
|
||||
from .service import JobLogService, JobService
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
from .schema import JobOutSchema, JobQueryParam
|
||||
from .service import JobService
|
||||
|
||||
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/job", tags=["定时任务"])
|
||||
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/job", tags=["调度器监控"])
|
||||
|
||||
|
||||
# ==================== 调度器状态和操作 ====================
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/detail/{id}",
|
||||
summary="获取定时任务详情",
|
||||
description="获取定时任务详情",
|
||||
response_model=ResponseSchema[JobOutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="定时任务ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含定时任务详情的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.get_job_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务详情成功")
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/list",
|
||||
summary="查询定时任务",
|
||||
description="查询定时任务",
|
||||
response_model=ResponseSchema[list[JobOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[JobQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含分页后的定时任务列表的JSON响应
|
||||
"""
|
||||
result_dict_list = await JobService.get_job_list_service(
|
||||
auth=auth, search=search, order_by=page.order_by
|
||||
)
|
||||
result_dict = await PaginationService.paginate(
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
)
|
||||
log.info("查询定时任务列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务列表成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/create",
|
||||
summary="创建定时任务",
|
||||
description="创建定时任务",
|
||||
response_model=ResponseSchema[JobOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
data: JobCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.create_job_service(auth=auth, data=data)
|
||||
log.info(f"创建定时任务成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建定时任务成功")
|
||||
|
||||
|
||||
@JobRouter.put(
|
||||
"/update/{id}",
|
||||
summary="修改定时任务",
|
||||
description="修改定时任务",
|
||||
response_model=ResponseSchema[JobOutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
data: JobUpdateSchema,
|
||||
id: Annotated[int, Path(description="定时任务ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改定时任务
|
||||
|
||||
参数:
|
||||
- data (JobUpdateSchema): 更新参数模型
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.update_job_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改定时任务成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改定时任务成功")
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/delete",
|
||||
summary="删除定时任务",
|
||||
description="删除定时任务",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.delete_job_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/export",
|
||||
summary="导出定时任务",
|
||||
description="导出定时任务",
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[JobQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务
|
||||
|
||||
参数:
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出定时任务结果的流式响应
|
||||
"""
|
||||
result_dict_list = await JobService.get_job_list_service(search=search, auth=auth)
|
||||
export_result = await JobService.export_job_service(data_list=result_dict_list)
|
||||
log.info("导出定时任务成功")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=job.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/clear",
|
||||
summary="清空定时任务日志",
|
||||
description="清空定时任务日志",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def clear_obj_log_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobService.clear_job_service(auth=auth)
|
||||
log.info("清空定时任务日志成功")
|
||||
return SuccessResponse(msg="清空定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.put(
|
||||
"/option",
|
||||
summary="暂停/恢复/重启定时任务",
|
||||
description="暂停/恢复/重启定时任务",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def option_obj_controller(
|
||||
id: Annotated[int, Body(description="定时任务ID")],
|
||||
option: Annotated[int, Body(description="操作类型 1: 暂停 2: 恢复 3: 重启")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
暂停/恢复/重启定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型 1: 暂停 2: 恢复 3: 重启
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.option_job_service(auth=auth, id=id, option=option)
|
||||
log.info(f"操作定时任务成功: {id}")
|
||||
return SuccessResponse(msg="操作定时任务成功")
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/log",
|
||||
summary="获取定时任务日志",
|
||||
description="获取定时任务日志",
|
||||
response_model=ResponseSchema[list[JobLogOutSchema]],
|
||||
"/scheduler/status",
|
||||
summary="获取调度器状态",
|
||||
description="获取调度器运行状态",
|
||||
response_model=ResponseSchema[dict],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:query"]))],
|
||||
)
|
||||
async def get_job_log_controller():
|
||||
async def get_scheduler_status_controller() -> JSONResponse:
|
||||
"""
|
||||
获取定时任务日志
|
||||
获取调度器状态
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志的JSON响应
|
||||
- JSONResponse: 调度器状态信息
|
||||
"""
|
||||
data = [
|
||||
{
|
||||
"id": i.id,
|
||||
"name": i.name,
|
||||
"trigger": i.trigger.__class__.__name__,
|
||||
"executor": i.executor,
|
||||
"func": i.func,
|
||||
"func_ref": i.func_ref,
|
||||
"args": i.args,
|
||||
"kwargs": i.kwargs,
|
||||
"misfire_grace_time": i.misfire_grace_time,
|
||||
"coalesce": i.coalesce,
|
||||
"max_instances": i.max_instances,
|
||||
"next_run_time": i.next_run_time,
|
||||
"state": SchedulerUtil.get_single_job_status(job_id=i.id),
|
||||
}
|
||||
for i in SchedulerUtil.get_all_jobs()
|
||||
]
|
||||
|
||||
return SuccessResponse(msg="获取定时任务日志成功", data=data)
|
||||
data = JobService.get_scheduler_status_service()
|
||||
return SuccessResponse(data=data, msg="获取调度器状态成功")
|
||||
|
||||
|
||||
# 定时任务日志管理接口
|
||||
@JobRouter.get(
|
||||
"/log/detail/{id}",
|
||||
summary="获取定时任务日志详情",
|
||||
description="获取定时任务日志详情",
|
||||
response_model=ResponseSchema[JobLogOutSchema],
|
||||
"/scheduler/jobs",
|
||||
summary="获取调度器任务列表",
|
||||
description="获取调度器中的任务列表",
|
||||
response_model=ResponseSchema[list[dict]],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:query"]))],
|
||||
)
|
||||
async def get_job_log_detail_controller(
|
||||
id: Annotated[int, Path(description="定时任务日志ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:query"]))],
|
||||
async def get_scheduler_jobs_controller() -> JSONResponse:
|
||||
"""
|
||||
获取调度器中的任务列表
|
||||
|
||||
返回:
|
||||
- JSONResponse: 调度器任务列表
|
||||
"""
|
||||
data = JobService.get_scheduler_jobs_service()
|
||||
return SuccessResponse(data=data, msg="获取调度器任务列表成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/scheduler/start",
|
||||
summary="启动调度器",
|
||||
description="启动调度器",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def start_scheduler_controller() -> JSONResponse:
|
||||
"""
|
||||
启动调度器
|
||||
"""
|
||||
SchedulerUtil.start()
|
||||
log.info("调度器已启动")
|
||||
return SuccessResponse(msg="调度器已启动")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/scheduler/pause",
|
||||
summary="暂停调度器",
|
||||
description="暂停调度器",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def pause_scheduler_controller() -> JSONResponse:
|
||||
"""
|
||||
暂停调度器
|
||||
"""
|
||||
SchedulerUtil.pause()
|
||||
log.info("调度器已暂停")
|
||||
return SuccessResponse(msg="调度器已暂停")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/scheduler/resume",
|
||||
summary="恢复调度器",
|
||||
description="恢复调度器",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def resume_scheduler_controller() -> JSONResponse:
|
||||
"""
|
||||
恢复调度器
|
||||
"""
|
||||
SchedulerUtil.resume()
|
||||
log.info("调度器已恢复")
|
||||
return SuccessResponse(msg="调度器已恢复")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/scheduler/shutdown",
|
||||
summary="关闭调度器",
|
||||
description="关闭调度器",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def shutdown_scheduler_controller() -> JSONResponse:
|
||||
"""
|
||||
关闭调度器
|
||||
"""
|
||||
await SchedulerUtil.shutdown()
|
||||
log.info("调度器已关闭")
|
||||
return SuccessResponse(msg="调度器已关闭")
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/scheduler/jobs/clear",
|
||||
summary="清空所有任务",
|
||||
description="清空调度器中的所有任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
)
|
||||
async def clear_jobs_controller() -> JSONResponse:
|
||||
"""
|
||||
清空调度器中的所有任务
|
||||
"""
|
||||
SchedulerUtil.clear_jobs()
|
||||
log.info("已清空所有任务")
|
||||
return SuccessResponse(msg="已清空所有任务")
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/scheduler/console",
|
||||
summary="获取调度器控制台信息",
|
||||
description="获取调度器任务的控制台输出",
|
||||
response_model=ResponseSchema[str],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:query"]))],
|
||||
)
|
||||
async def get_scheduler_console_controller() -> JSONResponse:
|
||||
"""
|
||||
获取调度器控制台信息
|
||||
|
||||
返回:
|
||||
- JSONResponse: 调度器任务的控制台输出
|
||||
"""
|
||||
console_output = SchedulerUtil.print_jobs()
|
||||
return SuccessResponse(data=console_output, msg="获取控制台信息成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/scheduler/sync",
|
||||
summary="同步调度器任务到数据库",
|
||||
description="将调度器中的任务同步到执行日志表",
|
||||
response_model=ResponseSchema[int],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def sync_jobs_controller() -> JSONResponse:
|
||||
"""
|
||||
同步调度器任务到数据库
|
||||
|
||||
返回:
|
||||
- JSONResponse: 同步的任务数量
|
||||
"""
|
||||
sync_count = SchedulerUtil.sync_jobs_to_db()
|
||||
log.info(f"同步任务完成,共同步 {sync_count} 个任务")
|
||||
return SuccessResponse(data=sync_count, msg=f"同步完成,共同步 {sync_count} 个任务")
|
||||
|
||||
|
||||
# ==================== 调度器任务操作 ====================
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/task/pause/{job_id}",
|
||||
summary="暂停任务",
|
||||
description="暂停调度器中的任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def pause_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
暂停调度器中的任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志详情的JSON响应
|
||||
- job_id (str): 调度器任务ID
|
||||
"""
|
||||
result_dict = await JobLogService.get_job_log_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务日志详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务日志详情成功")
|
||||
SchedulerUtil.pause_job(job_id=job_id)
|
||||
log.info(f"暂停任务成功: {job_id}")
|
||||
return SuccessResponse(msg="暂停任务成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/task/resume/{job_id}",
|
||||
summary="恢复任务",
|
||||
description="恢复调度器中的任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def resume_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
恢复调度器中的任务
|
||||
|
||||
参数:
|
||||
- job_id (str): 调度器任务ID
|
||||
"""
|
||||
SchedulerUtil.resume_job(job_id=job_id)
|
||||
log.info(f"恢复任务成功: {job_id}")
|
||||
return SuccessResponse(msg="恢复任务成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/task/run/{job_id}",
|
||||
summary="立即执行任务",
|
||||
description="立即执行调度器中的任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def run_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
立即执行调度器中的任务
|
||||
|
||||
参数:
|
||||
- job_id (str): 调度器任务ID
|
||||
"""
|
||||
SchedulerUtil.run_job_now(job_id=job_id)
|
||||
log.info(f"立即执行任务成功: {job_id}")
|
||||
return SuccessResponse(msg="立即执行任务成功")
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/task/remove/{job_id}",
|
||||
summary="移除任务",
|
||||
description="从调度器中移除任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
)
|
||||
async def remove_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
从调度器中移除任务
|
||||
|
||||
参数:
|
||||
- job_id (str): 调度器任务ID
|
||||
"""
|
||||
SchedulerUtil.remove_job(job_id=job_id)
|
||||
log.info(f"移除任务成功: {job_id}")
|
||||
return SuccessResponse(msg="移除任务成功")
|
||||
|
||||
|
||||
# ==================== 执行日志 ====================
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/log/list",
|
||||
summary="查询定时任务日志",
|
||||
description="查询定时任务日志",
|
||||
response_model=ResponseSchema[list[JobLogOutSchema]],
|
||||
summary="查询执行日志列表",
|
||||
description="查询执行日志列表",
|
||||
response_model=ResponseSchema[list[JobOutSchema]],
|
||||
)
|
||||
async def get_job_log_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[JobLogQueryParam, Depends()],
|
||||
search: Annotated[JobQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务日志
|
||||
查询执行日志列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 查询定时任务日志列表的JSON响应
|
||||
- JSONResponse: 包含分页后的执行日志列表
|
||||
"""
|
||||
order_by = [{"created_time": "desc"}]
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(
|
||||
auth=auth, search=search, order_by=order_by
|
||||
result_dict_list = await JobService.get_job_log_list_service(
|
||||
auth=auth, search=search
|
||||
)
|
||||
result_dict = await PaginationService.paginate(
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
)
|
||||
log.info("查询定时任务日志列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功")
|
||||
log.info("查询执行日志列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询执行日志列表成功")
|
||||
|
||||
|
||||
@JobRouter.get(
|
||||
"/log/detail/{id}",
|
||||
summary="获取执行日志详情",
|
||||
description="获取执行日志详情",
|
||||
response_model=ResponseSchema[JobOutSchema],
|
||||
)
|
||||
async def get_job_log_detail_controller(
|
||||
id: Annotated[int, Path(description="日志ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取执行日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 日志ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含执行日志详情
|
||||
"""
|
||||
result_dict = await JobService.get_job_log_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取执行日志详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取执行日志详情成功")
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/log/delete",
|
||||
summary="删除定时任务日志",
|
||||
description="删除定时任务日志",
|
||||
summary="删除执行日志",
|
||||
description="删除执行日志",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_job_log_controller(
|
||||
@@ -350,96 +334,29 @@ async def delete_job_log_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务日志
|
||||
删除执行日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobLogService.delete_job_log_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务日志成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务日志成功")
|
||||
await JobService.delete_job_log_service(auth=auth, ids=ids)
|
||||
log.info(f"删除执行日志成功: {ids}")
|
||||
return SuccessResponse(msg="删除执行日志成功")
|
||||
|
||||
|
||||
@JobRouter.delete(
|
||||
"/log/clear",
|
||||
summary="清空定时任务日志",
|
||||
description="清空定时任务日志",
|
||||
summary="清空执行日志",
|
||||
description="清空所有执行日志",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def clear_job_log_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务日志结果的JSON响应
|
||||
清空所有执行日志
|
||||
"""
|
||||
await JobLogService.clear_job_log_service(auth=auth)
|
||||
log.info("清空定时任务日志成功")
|
||||
return SuccessResponse(msg="清空定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.post(
|
||||
"/log/export",
|
||||
summary="导出定时任务日志",
|
||||
description="导出定时任务日志",
|
||||
)
|
||||
async def export_job_log_list_controller(
|
||||
search: Annotated[JobLogQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:job:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务日志
|
||||
|
||||
参数:
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出定时任务日志结果的流式响应
|
||||
"""
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(search=search, auth=auth)
|
||||
export_result = await JobLogService.export_job_log_service(data_list=result_dict_list)
|
||||
log.info("导出定时任务日志成功")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=job_log.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@JobRouter.put(
|
||||
"/run/{id}",
|
||||
summary="立即执行定时任务",
|
||||
description="立即执行指定的定时任务",
|
||||
response_model=ResponseSchema[None],
|
||||
dependencies=[Depends(AuthPermission(["module_task:job:update"]))],
|
||||
)
|
||||
async def run_job_now_controller(
|
||||
id: Annotated[int, Path(description="定时任务ID")],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
立即执行定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作结果的JSON响应
|
||||
"""
|
||||
try:
|
||||
SchedulerUtil.run_job_now(job_id=id)
|
||||
log.info(f"立即执行定时任务成功: {id}")
|
||||
return SuccessResponse(msg="立即执行定时任务成功")
|
||||
except Exception as e:
|
||||
log.error(f"立即执行定时任务失败: {id}, 错误信息: {e!s}")
|
||||
return ErrorResponse(msg=f"立即执行定时任务失败: {e!s}")
|
||||
await JobService.clear_job_log_service(auth=auth)
|
||||
log.info("清空执行日志成功")
|
||||
return SuccessResponse(msg="清空执行日志成功")
|
||||
|
||||
@@ -4,21 +4,16 @@ from typing import Any
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from .model import JobLogModel, JobModel
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobLogCreateSchema,
|
||||
JobLogUpdateSchema,
|
||||
JobUpdateSchema,
|
||||
)
|
||||
from .model import JobModel
|
||||
from .schema import JobCreateSchema, JobUpdateSchema
|
||||
|
||||
|
||||
class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
"""定时任务数据层"""
|
||||
"""任务执行日志数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化定时任务CRUD
|
||||
初始化任务执行日志CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
@@ -30,14 +25,14 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
self, id: int, preload: list[str | Any] | None = None
|
||||
) -> JobModel | None:
|
||||
"""
|
||||
获取定时任务详情
|
||||
获取执行日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- id (int): 日志ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobModel | None: 定时任务模型,如果不存在则为None
|
||||
- JobModel | None: 执行日志模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
@@ -48,7 +43,7 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
preload: list[str | Any] | None = None,
|
||||
) -> Sequence[JobModel]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
获取执行日志列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
@@ -56,125 +51,46 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobModel]: 定时任务模型序列
|
||||
- Sequence[JobModel]: 执行日志模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: JobCreateSchema) -> JobModel | None:
|
||||
"""
|
||||
创建定时任务
|
||||
创建执行日志
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建定时任务模型
|
||||
- data (JobCreateSchema): 创建执行日志模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 创建的定时任务模型,如果创建失败则为None
|
||||
- JobModel | None: 创建的执行日志模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> JobModel | None:
|
||||
"""
|
||||
更新定时任务
|
||||
更新执行日志
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 更新定时任务模型
|
||||
- id (int): 日志ID
|
||||
- data (JobUpdateSchema): 更新执行日志模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 更新后的定时任务模型,如果更新失败则为None
|
||||
- JobModel | None: 更新后的执行日志模型,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
删除执行日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
- ids (list[int]): 日志ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""
|
||||
设置定时任务的可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
"""
|
||||
return await self.set(ids=ids, **kwargs)
|
||||
|
||||
async def clear_obj_crud(self) -> None:
|
||||
"""
|
||||
清除定时任务日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有定时任务日志,请谨慎操作
|
||||
"""
|
||||
return await self.clear()
|
||||
|
||||
|
||||
class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
|
||||
"""定时任务日志数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化定时任务日志CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=JobLogModel, auth=auth)
|
||||
|
||||
async def get_obj_log_by_id_crud(
|
||||
self, id: int, preload: list[str | Any] | None = None
|
||||
) -> JobLogModel | None:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobLogModel | None: 定时任务日志模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_log_list_crud(
|
||||
self,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
preload: list[str | Any] | None = None,
|
||||
) -> Sequence[JobLogModel]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobLogModel]: 定时任务日志模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def delete_obj_log_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_obj_log_crud(self) -> None:
|
||||
"""
|
||||
清除定时任务日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有定时任务日志,请谨慎操作
|
||||
清空所有执行日志
|
||||
"""
|
||||
return await self.clear()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
def job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行同步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
time.sleep(3)
|
||||
print(f"{datetime.now()}同步函数执行完成")
|
||||
except Exception as e:
|
||||
log.error(f"同步任务执行失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def async_job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行异步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
await asyncio.sleep(3)
|
||||
print(f"{datetime.now()}异步函数执行完成")
|
||||
except Exception as e:
|
||||
log.error(f"异步任务执行失败: {e}")
|
||||
raise
|
||||
@@ -1,86 +1,35 @@
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
import enum
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class JobModel(ModelMixin, UserMixin):
|
||||
class JobStatusEnum(enum.Enum):
|
||||
"""执行状态枚举"""
|
||||
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
TIMEOUT = "timeout"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class JobModel(ModelMixin):
|
||||
"""
|
||||
定时任务调度表
|
||||
- 0: 运行中
|
||||
- 1: 暂停中
|
||||
任务执行日志表
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_job"
|
||||
__table_args__: dict[str, str] = {"comment": "定时任务调度表"}
|
||||
__loader_options__: list[str] = ["job_logs", "created_by", "updated_by"]
|
||||
__table_args__: dict[str, str] = {"comment": "任务执行日志表"}
|
||||
|
||||
name: Mapped[str | None] = mapped_column(
|
||||
String(64), nullable=True, default="", comment="任务名称"
|
||||
)
|
||||
jobstore: Mapped[str | None] = mapped_column(
|
||||
String(64), nullable=True, default="default", comment="存储器"
|
||||
)
|
||||
executor: Mapped[str | None] = mapped_column(
|
||||
String(64), nullable=True, default="default", comment="执行器"
|
||||
)
|
||||
trigger: Mapped[str] = mapped_column(String(64), nullable=False, comment="触发器")
|
||||
trigger_args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="触发器参数")
|
||||
func: Mapped[str] = mapped_column(Text, nullable=False, comment="任务函数")
|
||||
args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="位置参数")
|
||||
kwargs: Mapped[str | None] = mapped_column(Text, nullable=True, comment="关键字参数")
|
||||
coalesce: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=True,
|
||||
default=False,
|
||||
comment="是否合并运行",
|
||||
)
|
||||
max_instances: Mapped[int] = mapped_column(
|
||||
Integer, nullable=True, default=1, comment="最大实例数"
|
||||
)
|
||||
start_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="开始时间")
|
||||
end_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="结束时间")
|
||||
|
||||
# 关联关系
|
||||
job_logs: Mapped[list["JobLogModel"] | None] = relationship(
|
||||
back_populates="job", lazy="selectin"
|
||||
)
|
||||
|
||||
|
||||
class JobLogModel(ModelMixin):
|
||||
"""
|
||||
定时任务调度日志表
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_job_log"
|
||||
__table_args__: dict[str, str] = {"comment": "定时任务调度日志表"}
|
||||
__loader_options__: list[str] = ["job"]
|
||||
|
||||
job_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="任务名称")
|
||||
job_group: Mapped[str] = mapped_column(String(64), nullable=False, comment="任务组名")
|
||||
job_executor: Mapped[str] = mapped_column(String(64), nullable=False, comment="任务执行器")
|
||||
invoke_target: Mapped[str] = mapped_column(
|
||||
String(500), nullable=False, comment="调用目标字符串"
|
||||
)
|
||||
job_args: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, default="", comment="位置参数"
|
||||
)
|
||||
job_kwargs: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, default="", comment="关键字参数"
|
||||
)
|
||||
job_trigger: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, default="", comment="任务触发器"
|
||||
)
|
||||
job_message: Mapped[str | None] = mapped_column(
|
||||
String(500), nullable=True, default="", comment="日志信息"
|
||||
)
|
||||
exception_info: Mapped[str | None] = mapped_column(
|
||||
String(2000), nullable=True, default="", comment="异常信息"
|
||||
)
|
||||
|
||||
# 任务关联
|
||||
job_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("task_job.id", ondelete="CASCADE"), nullable=True, index=True, comment="任务ID"
|
||||
)
|
||||
|
||||
job: Mapped["JobModel | None"] = relationship(back_populates="job_logs", lazy="selectin")
|
||||
job_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="任务ID")
|
||||
job_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="任务名称")
|
||||
trigger_type: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="触发方式: cron/interval/date/manual")
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default=JobStatusEnum.PENDING.value, comment="执行状态")
|
||||
next_run_time: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="下次执行时间")
|
||||
job_state: Mapped[str | None] = mapped_column(Text, nullable=True, comment="任务状态信息")
|
||||
result: Mapped[str | None] = mapped_column(Text, nullable=True, comment="执行结果")
|
||||
error: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息")
|
||||
|
||||
@@ -3,173 +3,54 @@ from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, datetime_validator
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class JobCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度表对应pydantic模型
|
||||
"""
|
||||
"""执行日志创建模型"""
|
||||
|
||||
name: str = Field(..., max_length=64, description="任务名称")
|
||||
func: str = Field(..., description="任务函数")
|
||||
trigger: str = Field(..., description="触发器:控制此作业计划的 trigger 对象")
|
||||
args: str | None = Field(default=None, description="位置参数")
|
||||
kwargs: str | None = Field(default=None, description="关键字参数")
|
||||
coalesce: bool | None = Field(
|
||||
..., description="是否合并运行:是否在多个运行时间到期时仅运行作业一次"
|
||||
)
|
||||
max_instances: int | None = Field(
|
||||
default=1, ge=1, description="最大实例数:允许的最大并发执行实例数"
|
||||
)
|
||||
jobstore: str | None = Field(..., max_length=64, description="任务存储")
|
||||
executor: str | None = Field(
|
||||
...,
|
||||
max_length=64,
|
||||
description="任务执行器:将运行此作业的执行程序的名称",
|
||||
)
|
||||
trigger_args: str | None = Field(default=None, description="触发器参数")
|
||||
start_date: str | None = Field(default=None, description="开始时间")
|
||||
end_date: str | None = Field(default=None, description="结束时间")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
status: str = Field(default="0", description="任务状态:启动,停止")
|
||||
|
||||
@field_validator("trigger")
|
||||
@classmethod
|
||||
def _validate_trigger(cls, v: str) -> str:
|
||||
allowed = {"cron", "interval", "date"}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError("触发器必须为 cron/interval/date")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_dates(self):
|
||||
"""跨字段校验:结束时间不得早于开始时间。"""
|
||||
if self.start_date and self.end_date:
|
||||
try:
|
||||
start = datetime_validator(self.start_date)
|
||||
end = datetime_validator(self.end_date)
|
||||
except Exception:
|
||||
raise ValueError("时间格式必须为 YYYY-MM-DD HH:MM:SS")
|
||||
if end < start:
|
||||
raise ValueError("结束时间不能早于开始时间")
|
||||
return self
|
||||
job_id: str = Field(..., description="任务ID")
|
||||
job_name: str | None = Field(default=None, description="任务名称")
|
||||
trigger_type: str | None = Field(default=None, description="触发方式")
|
||||
status: str = Field(default="pending", description="执行状态")
|
||||
next_run_time: str | None = Field(default=None, description="下次执行时间")
|
||||
job_state: str | None = Field(default=None, description="任务状态信息")
|
||||
result: str | None = Field(default=None, description="执行结果")
|
||||
error: str | None = Field(default=None, description="错误信息")
|
||||
|
||||
|
||||
class JobUpdateSchema(JobCreateSchema):
|
||||
"""定时任务更新模型"""
|
||||
class JobUpdateSchema(BaseModel):
|
||||
"""执行日志更新模型"""
|
||||
|
||||
status: str | None = Field(default=None, description="执行状态")
|
||||
next_run_time: str | None = Field(default=None, description="下次执行时间")
|
||||
job_state: str | None = Field(default=None, description="任务状态信息")
|
||||
result: str | None = Field(default=None, description="执行结果")
|
||||
error: str | None = Field(default=None, description="错误信息")
|
||||
|
||||
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务响应模型"""
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema):
|
||||
"""执行日志响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
|
||||
|
||||
class JobLogCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度日志表对应pydantic模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
job_name: str = Field(..., description="任务名称")
|
||||
job_group: str | None = Field(default=None, description="任务组名")
|
||||
job_executor: str | None = Field(default=None, description="任务执行器")
|
||||
invoke_target: str | None = Field(default=None, description="调用目标字符串")
|
||||
job_args: str | None = Field(default=None, description="位置参数")
|
||||
job_kwargs: str | None = Field(default=None, description="关键字参数")
|
||||
job_trigger: str | None = Field(default=None, description="任务触发器")
|
||||
job_message: str | None = Field(default=None, description="日志信息")
|
||||
exception_info: str | None = Field(default=None, description="异常信息")
|
||||
status: str = Field(default="0", description="任务状态:正常,失败")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
created_time: DateTimeStr | None = Field(default=None, description="创建时间")
|
||||
updated_time: DateTimeStr | None = Field(default=None, description="更新时间")
|
||||
|
||||
|
||||
class JobLogUpdateSchema(JobLogCreateSchema):
|
||||
"""定时任务调度日志表更新模型"""
|
||||
|
||||
id: int | None = Field(default=None, description="任务日志ID")
|
||||
|
||||
|
||||
class JobLogOutSchema(JobLogUpdateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务调度日志表响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class JobQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
"""执行日志查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 启动,停止"),
|
||||
created_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="创建时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
updated_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="更新时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = (QueueEnum.like.value, name)
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = (QueueEnum.eq.value, created_id)
|
||||
self.updated_id = (QueueEnum.eq.value, updated_id)
|
||||
self.status = (QueueEnum.eq.value, status)
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class JobLogQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: int | None = Query(None, description="定时任务ID"),
|
||||
job_id: str | None = Query(None, description="任务ID"),
|
||||
job_name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 正常,失败"),
|
||||
created_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="创建时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
updated_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="更新时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
status: str | None = Query(None, description="执行状态"),
|
||||
trigger_type: str | None = Query(None, description="触发方式"),
|
||||
) -> None:
|
||||
# 定时任务ID查询
|
||||
self.job_id = (QueueEnum.eq.value, job_id)
|
||||
# 模糊查询字段
|
||||
self.job_name = (QueueEnum.like.value, job_name)
|
||||
# 精确查询字段
|
||||
self.status = (QueueEnum.eq.value, status)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
self.trigger_type = (QueueEnum.eq.value, trigger_type)
|
||||
|
||||
@@ -1,66 +1,47 @@
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.cron_util import CronUtil
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import JobCRUD, JobLogCRUD
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobLogOutSchema,
|
||||
JobLogQueryParam,
|
||||
JobOutSchema,
|
||||
JobQueryParam,
|
||||
JobUpdateSchema,
|
||||
)
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
|
||||
|
||||
def validate_job_func(func: str) -> None:
|
||||
"""
|
||||
校验任务函数格式是否有效。
|
||||
|
||||
参数:
|
||||
- func (str): 任务函数字符串,格式应为 "module.function"
|
||||
|
||||
异常:
|
||||
- CustomException: 当 func 格式无效时抛出
|
||||
"""
|
||||
if not func or "." not in func:
|
||||
raise CustomException(msg=f"任务函数格式无效: {func},必须包含模块名和函数名(如: module.function)")
|
||||
parts = func.rsplit(".", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
raise CustomException(msg=f"任务函数格式无效: {func},模块名和函数名不能为空")
|
||||
from .crud import JobCRUD
|
||||
from .schema import JobCreateSchema, JobOutSchema, JobQueryParam, JobUpdateSchema
|
||||
|
||||
|
||||
class JobService:
|
||||
"""
|
||||
定时任务管理模块服务层
|
||||
调度器监控模块服务层
|
||||
|
||||
职责:
|
||||
1. 执行日志的 CRUD 操作
|
||||
2. 调度器状态和任务列表的获取
|
||||
3. 任务操作(暂停、恢复、执行、移除)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务详情
|
||||
获取执行日志详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- id (int): 日志ID
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
- Dict: 执行日志详情字典
|
||||
"""
|
||||
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="执行日志不存在")
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_list_service(
|
||||
async def get_job_log_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: JobQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
获取执行日志列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
@@ -68,281 +49,135 @@ class JobService:
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- List[Dict]: 定时任务详情字典列表
|
||||
- List[Dict]: 执行日志详情字典列表
|
||||
"""
|
||||
obj_list = await JobCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
if order_by is None:
|
||||
order_by = [{"created_time": "desc"}]
|
||||
obj_list = await JobCRUD(auth).get_obj_list_crud(
|
||||
search=search.__dict__ if search else None, order_by=order_by
|
||||
)
|
||||
return [JobOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_job_service(cls, auth: AuthSchema, data: JobCreateSchema) -> dict:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (JobCreateSchema): 定时任务创建模型
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get(name=data.name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg="创建失败,该定时任务已存在")
|
||||
|
||||
validate_job_func(data.func)
|
||||
|
||||
obj = await JobCRUD(auth).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建失败,该数据定时任务不存在")
|
||||
SchedulerUtil().add_job(job_info=obj)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_job_service(cls, auth: AuthSchema, id: int, data: JobUpdateSchema) -> dict:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 定时任务更新模型
|
||||
|
||||
返回:
|
||||
- dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该定时任务不存在")
|
||||
if (
|
||||
data.trigger == "cron"
|
||||
and data.trigger_args
|
||||
and not CronUtil.validate_cron_expression(data.trigger_args)
|
||||
):
|
||||
raise CustomException(msg=f"新增定时任务{data.name}失败, Cron表达式不正确")
|
||||
|
||||
validate_job_func(data.func)
|
||||
|
||||
obj = await JobCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新失败,该数据定时任务不存在")
|
||||
SchedulerUtil().modify_job(job_id=obj.id)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_job_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="删除失败,该数据定时任务不存在")
|
||||
obj = await JobLogCRUD(auth).get(job_id=id)
|
||||
if obj:
|
||||
raise CustomException(msg=f"删除失败,该定时任务存 {exist_obj.name} 在日志记录")
|
||||
|
||||
SchedulerUtil().remove_job(job_id=id)
|
||||
await JobCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def clear_job_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空所有定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
SchedulerUtil().clear_jobs()
|
||||
await JobLogCRUD(auth).clear_obj_log_crud()
|
||||
await JobCRUD(auth).clear_obj_crud()
|
||||
|
||||
@classmethod
|
||||
async def option_job_service(cls, auth: AuthSchema, id: int, option: int) -> None:
|
||||
"""
|
||||
操作定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型, 1: 暂停 2: 恢复 3: 重启
|
||||
"""
|
||||
# 1: 暂停 2: 恢复 3: 重启
|
||||
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="操作失败,该数据定时任务不存在")
|
||||
if option == 1:
|
||||
SchedulerUtil().pause_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status="1") # 更新为暂停状态(1)
|
||||
elif option == 2:
|
||||
SchedulerUtil().resume_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status="0") # 更新为运行状态(0)
|
||||
elif option == 3:
|
||||
# 重启任务:先移除再添加,确保使用最新的任务配置
|
||||
SchedulerUtil().remove_job(job_id=id)
|
||||
# 获取最新的任务配置
|
||||
updated_job = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if updated_job:
|
||||
# 重新添加任务
|
||||
SchedulerUtil().add_job(job_info=updated_job)
|
||||
# 设置状态为运行中
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status="0")
|
||||
|
||||
@classmethod
|
||||
async def export_job_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务列表
|
||||
|
||||
参数:
|
||||
- data_list (list[dict]): 定时任务列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
"id": "编号",
|
||||
"name": "任务名称",
|
||||
"func": "任务函数",
|
||||
"trigger": "触发器",
|
||||
"args": "位置参数",
|
||||
"kwargs": "关键字参数",
|
||||
"coalesce": "是否合并运行",
|
||||
"max_instances": "最大实例数",
|
||||
"jobstore": "任务存储",
|
||||
"executor": "任务执行器",
|
||||
"trigger_args": "触发器参数",
|
||||
"status": "任务状态",
|
||||
"message": "日志信息",
|
||||
"description": "备注",
|
||||
"created_time": "创建时间",
|
||||
"updated_time": "更新时间",
|
||||
"created_id": "创建者ID",
|
||||
"updated_id": "更新者ID",
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item["status"] = (
|
||||
"运行中"
|
||||
if item["status"] == "0"
|
||||
else "暂停中"
|
||||
if item["status"] == "1"
|
||||
else "未知状态"
|
||||
)
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
|
||||
class JobLogService:
|
||||
"""
|
||||
定时任务日志管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务日志ID
|
||||
|
||||
返回:
|
||||
- dict: 定时任务日志详情字典
|
||||
"""
|
||||
obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
return JobLogOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_list_service(
|
||||
async def create_job_log_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: JobLogQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
job_id: str,
|
||||
job_name: str | None = None,
|
||||
trigger_type: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
创建执行日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (JobLogQueryParam | None): 查询参数模型, 包含分页信息和查询条件
|
||||
- order_by (list[dict] | None): 排序参数列表, 每个元素为一个字典, 包含字段名和排序方向
|
||||
- job_id (str): 任务ID
|
||||
- job_name (str | None): 任务名称
|
||||
- trigger_type (str | None): 触发方式
|
||||
|
||||
返回:
|
||||
- list[dict]: 定时任务日志详情字典列表
|
||||
- Dict: 执行日志详情字典
|
||||
"""
|
||||
obj_list = await JobLogCRUD(auth).get_obj_log_list_crud(
|
||||
search=search.__dict__, order_by=order_by
|
||||
data = JobCreateSchema(
|
||||
job_id=job_id,
|
||||
job_name=job_name,
|
||||
trigger_type=trigger_type,
|
||||
status="running",
|
||||
)
|
||||
return [JobLogOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
obj = await JobCRUD(auth).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建执行日志失败")
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_job_log_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
id: int,
|
||||
status: str,
|
||||
result: str | None = None,
|
||||
error: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
更新执行日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 日志ID
|
||||
- status (str): 执行状态
|
||||
- result (str | None): 执行结果
|
||||
- error (str | None): 错误信息
|
||||
|
||||
返回:
|
||||
- Dict: 执行日志详情字典
|
||||
"""
|
||||
data = JobUpdateSchema(
|
||||
status=status,
|
||||
result=result,
|
||||
error=error,
|
||||
)
|
||||
obj = await JobCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新执行日志失败")
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_job_log_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
删除执行日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
- ids (list[int]): 日志ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
exist_obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg=f"删除失败,该定时任务日志ID为{id}的记录不存在")
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
await JobCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def clear_job_log_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空定时任务日志
|
||||
清空所有执行日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
# 获取所有日志ID并批量删除
|
||||
all_logs = await JobLogCRUD(auth).get_obj_log_list_crud()
|
||||
if all_logs:
|
||||
ids = [log.id for log in all_logs]
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
await JobCRUD(auth).clear_obj_crud()
|
||||
|
||||
@classmethod
|
||||
async def export_job_log_service(cls, data_list: list[dict]) -> bytes:
|
||||
def get_scheduler_status_service(cls) -> dict:
|
||||
"""
|
||||
导出定时任务日志列表
|
||||
|
||||
参数:
|
||||
- data_list (List[Dict[str, Any]]): 定时任务日志列表
|
||||
获取调度器状态
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
- Dict: 调度器状态信息
|
||||
"""
|
||||
mapping_dict = {
|
||||
"id": "编号",
|
||||
"job_name": "任务名称",
|
||||
"job_group": "任务组名",
|
||||
"job_executor": "任务执行器",
|
||||
"invoke_target": "调用目标字符串",
|
||||
"job_args": "位置参数",
|
||||
"job_kwargs": "关键字参数",
|
||||
"job_trigger": "任务触发器",
|
||||
"job_message": "日志信息",
|
||||
"exception_info": "异常信息",
|
||||
"status": "执行状态",
|
||||
"created_time": "创建时间",
|
||||
"updated_time": "更新时间",
|
||||
status = SchedulerUtil.get_scheduler_state()
|
||||
is_running = SchedulerUtil.is_running()
|
||||
jobs = SchedulerUtil.get_all_jobs()
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"is_running": is_running,
|
||||
"job_count": len(jobs),
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item["status"] = "成功" if item.get("status") == "0" else "失败"
|
||||
@classmethod
|
||||
def get_scheduler_jobs_service(cls) -> list[dict]:
|
||||
"""
|
||||
获取调度器中的任务列表
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
返回:
|
||||
- List[Dict]: 任务列表
|
||||
"""
|
||||
jobs = SchedulerUtil.get_all_jobs()
|
||||
return [
|
||||
{
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"trigger": str(job.trigger),
|
||||
"next_run_time": str(job.next_run_time) if job.next_run_time else None,
|
||||
"status": SchedulerUtil.get_job_status(job_id=job.id),
|
||||
}
|
||||
for job in jobs
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,694 +0,0 @@
|
||||
import importlib
|
||||
import json
|
||||
from asyncio import iscoroutinefunction
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from apscheduler.events import EVENT_ALL, JobEvent, JobExecutionEvent
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.executors.pool import ProcessPoolExecutor
|
||||
from apscheduler.job import Job
|
||||
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 apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.config.setting import settings
|
||||
from app.core.database import async_db_session, db_session, engine
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.plugin.module_task.job.model import JobModel
|
||||
from app.utils.cron_util import CronUtil
|
||||
|
||||
job_stores = {
|
||||
"default": MemoryJobStore(),
|
||||
"sqlalchemy": SQLAlchemyJobStore(url=settings.DB_URI, engine=engine),
|
||||
"redis": RedisJobStore(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
),
|
||||
}
|
||||
# 配置执行器
|
||||
executors = {
|
||||
"default": AsyncIOExecutor(),
|
||||
"processpool": ProcessPoolExecutor(max_workers=1), # 减少进程数量以减少资源消耗
|
||||
}
|
||||
# 配置默认参数
|
||||
job_defaults = {
|
||||
"coalesce": True, # 合并执行错过的任务
|
||||
"max_instances": 1, # 最大实例数
|
||||
}
|
||||
# 配置调度器
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.configure(
|
||||
jobstores=job_stores,
|
||||
executors=executors,
|
||||
job_defaults=job_defaults,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
|
||||
|
||||
class SchedulerUtil:
|
||||
"""
|
||||
定时任务相关方法
|
||||
"""
|
||||
|
||||
# 类变量,存储应用的Redis连接
|
||||
redis_instance = None
|
||||
|
||||
@classmethod
|
||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||
"""
|
||||
监听任务执行事件并记录详细执行信息。
|
||||
|
||||
参数:
|
||||
- event (JobEvent | JobExecutionEvent): 任务事件对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
# 只处理任务执行相关事件,不处理任务添加、删除等事件
|
||||
if not isinstance(event, JobExecutionEvent):
|
||||
return
|
||||
|
||||
# 延迟导入避免循环导入
|
||||
from app.plugin.module_task.job.model import JobLogModel
|
||||
|
||||
# 获取事件类型和任务ID
|
||||
event_type = event.__class__.__name__
|
||||
|
||||
# 初始化任务状态
|
||||
status = "0"
|
||||
exception_info = ""
|
||||
if hasattr(event, "exception") and event.exception:
|
||||
exception_info = str(event.exception)
|
||||
status = "1"
|
||||
|
||||
if hasattr(event, "job_id"):
|
||||
job_id = event.job_id
|
||||
query_job = cls.get_job(job_id=job_id)
|
||||
|
||||
if query_job:
|
||||
# 解析任务的实际执行函数和参数
|
||||
actual_func = None
|
||||
actual_args = []
|
||||
actual_kwargs = {}
|
||||
|
||||
try:
|
||||
if hasattr(query_job, "args") and len(query_job.args) >= 2:
|
||||
actual_func = query_job.args[0]
|
||||
actual_args = query_job.args[2:]
|
||||
|
||||
if hasattr(query_job, "kwargs"):
|
||||
actual_kwargs = query_job.kwargs
|
||||
except Exception as e:
|
||||
log.error(f"解析任务 {job_id} 参数失败: {e!s}")
|
||||
|
||||
# 格式化参数显示
|
||||
formatted_args = str(actual_args) if actual_args else "()"
|
||||
formatted_kwargs = str(actual_kwargs) if actual_kwargs else "{}"
|
||||
|
||||
# 获取实际的执行函数信息
|
||||
actual_func_module = ""
|
||||
actual_func_name = ""
|
||||
try:
|
||||
if actual_func:
|
||||
actual_func_module = getattr(actual_func, "__module__", "")
|
||||
actual_func_name = getattr(actual_func, "__name__", "")
|
||||
except Exception as e:
|
||||
log.error(f"获取任务 {job_id} 函数信息失败: {e!s}")
|
||||
|
||||
# 构建详细的任务消息
|
||||
scheduled_time_str = "未知"
|
||||
try:
|
||||
if hasattr(event, "scheduled_run_time") and event.scheduled_run_time:
|
||||
scheduled_time_str = event.scheduled_run_time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
except Exception:
|
||||
scheduled_time_str = str(event.scheduled_run_time)
|
||||
|
||||
try:
|
||||
event_type = event_type
|
||||
func_info = (
|
||||
f"{actual_func_module}.{actual_func_name}" if actual_func else "未知"
|
||||
)
|
||||
job_message = f"任务 {job_id} ({query_job.name}) 执行完成: "
|
||||
job_message += f"状态={'成功' if status == '0' else '失败'}, "
|
||||
job_message += f"执行函数={func_info}, "
|
||||
job_message += f"参数={formatted_args}, "
|
||||
job_message += f"关键字参数={formatted_kwargs}, "
|
||||
job_message += f"计划时间={scheduled_time_str}, "
|
||||
job_message += (
|
||||
f"实际执行时间={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
if exception_info:
|
||||
job_message += f", 错误={exception_info[:500]}..."
|
||||
except Exception as e:
|
||||
job_message = (
|
||||
f"任务 {job_id} 执行事件,状态={'成功' if status == '0' else '失败'}"
|
||||
)
|
||||
log.error(f"构建任务 {job_id} 消息失败: {e!s}")
|
||||
|
||||
# 创建日志记录
|
||||
try:
|
||||
# 获取执行函数信息
|
||||
invoke_target = func_info
|
||||
if not invoke_target:
|
||||
try:
|
||||
invoke_target = f"{getattr(query_job.func, '__module__', '')}.{getattr(query_job.func, '__name__', '')}"
|
||||
except Exception:
|
||||
invoke_target = "未知"
|
||||
|
||||
job_log = JobLogModel(
|
||||
job_name=query_job.name,
|
||||
job_group=query_job._jobstore_alias,
|
||||
job_executor=query_job.executor,
|
||||
invoke_target=invoke_target,
|
||||
job_args=formatted_args,
|
||||
job_kwargs=formatted_kwargs,
|
||||
job_trigger=str(query_job.trigger),
|
||||
job_message=job_message,
|
||||
status=status,
|
||||
exception_info=exception_info,
|
||||
created_time=datetime.now(),
|
||||
updated_time=datetime.now(),
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
# 保存到数据库
|
||||
with db_session.begin() as session:
|
||||
try:
|
||||
session.add(job_log)
|
||||
session.commit()
|
||||
log.info(f"任务 {job_id} 执行日志已保存")
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
log.error(f"保存任务 {job_id} 执行日志失败: {e!s}")
|
||||
except Exception as e:
|
||||
log.error(f"创建任务 {job_id} 日志记录失败: {e!s}")
|
||||
except Exception as e:
|
||||
log.error(f"处理任务执行事件失败: {e!s}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
@classmethod
|
||||
async def init_system_scheduler(cls, redis: Redis) -> None:
|
||||
"""
|
||||
应用启动时初始化定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.plugin.module_task.job.crud import JobCRUD
|
||||
|
||||
log.info("🔎 开始启动定时任务...")
|
||||
# 保存Redis连接到类变量
|
||||
cls.redis_instance = redis
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
# 添加事件监听器
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
auth = AuthSchema(db=session)
|
||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||
# 使用Redis锁确保只有一个实例执行任务初始化
|
||||
redis_client = RedisCURD(redis)
|
||||
lock_key = f"{RedisInitKeyConfig.APSCHEDULER_LOCK_KEY.key}:job"
|
||||
# 尝试获取锁,过期时间10秒
|
||||
lock_acquired, lock_value = await redis_client.lock(lock_key, 10)
|
||||
if lock_acquired:
|
||||
try:
|
||||
for item in job_list:
|
||||
# 检查任务是否已经存在
|
||||
existing_job = cls.get_job(job_id=item.id)
|
||||
if existing_job:
|
||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||
# 添加新任务
|
||||
cls.add_job(item)
|
||||
# 根据数据库中保存的状态来设置任务状态
|
||||
if item.status == "1":
|
||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||
cls.pause_job(job_id=item.id)
|
||||
finally:
|
||||
# 释放锁
|
||||
await redis_client.unlock(lock_key, lock_value)
|
||||
else:
|
||||
# 等待其他实例完成初始化
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(2)
|
||||
log.info("✅️ 定时任务已由其他实例初始化完成")
|
||||
|
||||
@classmethod
|
||||
async def close_system_scheduler(cls) -> None:
|
||||
"""
|
||||
关闭系统定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
# 移除所有任务
|
||||
scheduler.remove_all_jobs()
|
||||
# 等待所有任务完成后再关闭
|
||||
scheduler.shutdown(wait=True)
|
||||
log.info("✅️ 关闭定时任务成功")
|
||||
except Exception as e:
|
||||
log.error(f"关闭定时任务失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: str | int) -> Job | None:
|
||||
"""
|
||||
根据任务ID获取任务对象。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job | None: 任务对象,未找到则为 None。
|
||||
"""
|
||||
return scheduler.get_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def get_all_jobs(cls) -> list[Job]:
|
||||
"""
|
||||
获取全部调度任务列表。
|
||||
|
||||
返回:
|
||||
- list[Job]: 任务列表。
|
||||
"""
|
||||
return scheduler.get_jobs()
|
||||
|
||||
@classmethod
|
||||
async def _task_wrapper(cls, func: Callable, job_id: str | int, *args, **kwargs):
|
||||
"""任务执行包装器,添加分布式锁防止并发执行"""
|
||||
import asyncio
|
||||
|
||||
# 使用类变量中的Redis连接
|
||||
if not cls.redis_instance:
|
||||
log.error(f"任务 {job_id} 执行失败:Redis连接未初始化")
|
||||
return None
|
||||
|
||||
redis_client = RedisCURD(redis=cls.redis_instance)
|
||||
lock_key = f"{RedisInitKeyConfig.APSCHEDULER_LOCK_KEY.key}:{job_id}"
|
||||
lock_acquired = False
|
||||
lock_value = ""
|
||||
renewal_task = None
|
||||
|
||||
# 定义锁续约函数
|
||||
async def renew_lock() -> None:
|
||||
"""定期续约锁的过期时间"""
|
||||
try:
|
||||
while True:
|
||||
# 等待锁过期时间的2/3后进行续约
|
||||
await asyncio.sleep(20) # 30秒的2/3
|
||||
# 使用redis_client.renew_lock续约锁,验证锁持有者
|
||||
success = await redis_client.renew_lock(lock_key, 30, lock_value)
|
||||
if success:
|
||||
log.info(f"任务 {job_id} 锁续约成功")
|
||||
else:
|
||||
log.warning(f"任务 {job_id} 锁续约失败:锁可能已被其他实例获取")
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
log.info(f"任务 {job_id} 锁续约任务已取消")
|
||||
except Exception as e:
|
||||
log.error(f"任务 {job_id} 锁续约失败: {e!s}")
|
||||
|
||||
try:
|
||||
# 获取分布式锁,使用原子性的lock方法
|
||||
lock_acquired, lock_value = await redis_client.lock(lock_key, 30)
|
||||
if lock_acquired:
|
||||
log.info(f"任务 {job_id} 获取执行锁成功")
|
||||
# 启动锁续约任务
|
||||
renewal_task = asyncio.create_task(renew_lock())
|
||||
|
||||
# 执行任务
|
||||
if iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
# 对于同步函数,使用线程池执行
|
||||
log.info(f"任务 {job_id} 开始执行同步函数: {func.__name__}, 参数: {args}-{kwargs}")
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# 使用lambda包装函数调用,以支持关键字参数
|
||||
result = await loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
||||
log.info(f"任务 {job_id} 同步函数执行完成,结果: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
log.error(f"任务 {job_id} 同步函数执行失败: {e!s}")
|
||||
raise
|
||||
else:
|
||||
# 获取锁失败,记录日志
|
||||
log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行")
|
||||
return None
|
||||
finally:
|
||||
# 取消锁续约任务
|
||||
if renewal_task and not renewal_task.done():
|
||||
renewal_task.cancel()
|
||||
try:
|
||||
await renewal_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 释放锁
|
||||
if lock_acquired:
|
||||
await redis_client.unlock(lock_key, lock_value)
|
||||
log.info(f"任务 {job_id} 释放执行锁")
|
||||
|
||||
@classmethod
|
||||
def add_job(cls, job_info: JobModel) -> Job:
|
||||
"""
|
||||
根据任务配置创建并添加调度任务。
|
||||
|
||||
参数:
|
||||
- job_info (JobModel): 任务对象信息(包含触发器、函数、参数等)。
|
||||
|
||||
返回:
|
||||
- Job: 新增的任务对象。
|
||||
"""
|
||||
# 动态导入模块
|
||||
# 1. 解析调用目标
|
||||
func_str = str(job_info.func)
|
||||
if "." not in func_str:
|
||||
log.error(f"任务 {job_info.id} 的 func 格式无效: {func_str},必须包含模块名和函数名(如: module.function)")
|
||||
raise CustomException(msg=f"任务函数格式无效: {func_str},必须包含模块名和函数名(如: module.function)")
|
||||
|
||||
try:
|
||||
module_path, func_name = func_str.rsplit(".", 1)
|
||||
except ValueError as e:
|
||||
log.error(f"任务 {job_info.id} 的 func 解析失败: {func_str}, 错误: {e}")
|
||||
raise CustomException(msg=f"任务函数格式无效: {func_str}") from e
|
||||
|
||||
module_path = "app.plugin.module_task.job.function_task." + module_path
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
job_func = getattr(module, func_name)
|
||||
|
||||
# 2. 确定任务存储器:优先使用redis,确保分布式环境中任务同步
|
||||
if job_info.jobstore is None:
|
||||
job_info.jobstore = "redis" # 改为默认使用redis存储
|
||||
|
||||
# 3. 确定执行器
|
||||
job_executor = job_info.executor
|
||||
if job_executor is None:
|
||||
job_executor = "default"
|
||||
|
||||
# 异步函数必须使用默认执行器
|
||||
if iscoroutinefunction(job_func):
|
||||
job_executor = "default"
|
||||
|
||||
# 4. 创建触发器
|
||||
trigger = None
|
||||
if job_info.trigger is None or job_info.trigger.lower() == "now":
|
||||
# 立即执行作业:省略trigger或使用'now'时,使用date触发器立即执行
|
||||
trigger = DateTrigger(run_date=datetime.now())
|
||||
elif job_info.trigger == "date":
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("date触发器缺少执行时间参数")
|
||||
trigger = DateTrigger(run_date=job_info.trigger_args)
|
||||
elif job_info.trigger == "interval":
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("interval触发器缺少参数")
|
||||
# 将传入的 interval 表达式拆分为不同的字段
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) != 5:
|
||||
raise ValueError("无效的 interval 表达式")
|
||||
second, minute, hour, day, week = tuple(
|
||||
int(field) if field != "*" else 0 for field in fields
|
||||
)
|
||||
# 秒、分、时、天、周(* * * * 1)
|
||||
trigger = IntervalTrigger(
|
||||
weeks=week,
|
||||
days=day,
|
||||
hours=hour,
|
||||
minutes=minute,
|
||||
seconds=second,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
jitter=None,
|
||||
)
|
||||
elif job_info.trigger == "cron":
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("cron触发器缺少参数")
|
||||
# 秒、分、时、天、月、星期几、年 ()
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) not in (6, 7):
|
||||
raise ValueError("无效的 Cron 表达式")
|
||||
if not CronUtil.validate_cron_expression(job_info.trigger_args):
|
||||
raise ValueError(f"定时任务{job_info.name}, Cron表达式不正确")
|
||||
|
||||
# 将Cron表达式中的"?"替换为"*"以兼容APScheduler
|
||||
parsed_fields = [field if field != "?" else "*" for field in fields]
|
||||
if len(fields) == 6:
|
||||
parsed_fields.append("*") # 如果没有年份字段,添加None
|
||||
|
||||
second, minute, hour, day, month, day_of_week, year = tuple(parsed_fields)
|
||||
trigger = CronTrigger(
|
||||
second=second,
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
year=year,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
else:
|
||||
raise ValueError("无效的 trigger 触发器")
|
||||
|
||||
# 5. 添加任务(使用包装器函数)
|
||||
# 处理任务参数,确保空参数时返回空列表
|
||||
job_args = []
|
||||
if job_info.args:
|
||||
args_str = str(job_info.args).strip()
|
||||
if args_str:
|
||||
job_args = args_str.split(",")
|
||||
|
||||
job = scheduler.add_job(
|
||||
func=cls._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[job_func, str(job_info.id), *job_args],
|
||||
kwargs=json.loads(job_info.kwargs) if job_info.kwargs else {},
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1, # 确保只有一个实例执行
|
||||
jobstore=job_info.jobstore,
|
||||
executor=job_executor,
|
||||
)
|
||||
log.info(f"任务 {job_info.id} 添加到 {job_info.jobstore} 存储器成功")
|
||||
return job
|
||||
except ModuleNotFoundError:
|
||||
raise ValueError(f"未找到该模块:{module_path}")
|
||||
except AttributeError:
|
||||
raise ValueError(f"未找到该模块下的方法:{func_name}")
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f"添加任务失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
def remove_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
根据任务ID删除调度任务。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if query_job:
|
||||
scheduler.remove_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def clear_jobs(cls) -> None:
|
||||
"""
|
||||
删除所有调度任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
@classmethod
|
||||
def modify_job(cls, job_id: str | int) -> Job:
|
||||
"""
|
||||
更新指定任务的配置(运行中的任务下次执行生效)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job: 更新后的任务对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
return scheduler.modify_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def pause_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
暂停指定任务(仅运行中可暂停,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.pause_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def resume_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
恢复指定任务(仅暂停中可恢复,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.resume_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def reschedule_job(
|
||||
cls, job_id: str | int, trigger: str | None = None, **trigger_args
|
||||
) -> Job | None:
|
||||
"""
|
||||
重启指定任务的触发器。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- trigger: 触发器类型('date', 'interval', 'cron')
|
||||
- **trigger_args: 触发器参数
|
||||
|
||||
返回:
|
||||
- Job | None: 更新后的任务对象,未找到任务时返回 None。
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
|
||||
# 如果没有提供新的触发器,则使用现有触发器
|
||||
if trigger is None:
|
||||
# 获取当前任务的触发器配置
|
||||
current_trigger = query_job.trigger
|
||||
# 重新调度任务,使用当前的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=current_trigger)
|
||||
# 使用新提供的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=trigger, **trigger_args)
|
||||
|
||||
@classmethod
|
||||
def get_single_job_status(cls, job_id: str | int) -> str:
|
||||
"""
|
||||
获取单个任务的当前状态。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID
|
||||
|
||||
返回:
|
||||
- str: 任务状态('running' | 'paused' | 'stopped' | 'unknown')
|
||||
"""
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
return "unknown"
|
||||
|
||||
# 检查任务是否在暂停列表中
|
||||
if job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
return "paused"
|
||||
|
||||
# 检查调度器状态
|
||||
if scheduler.state == 0: # STATE_STOPPED
|
||||
return "stopped"
|
||||
|
||||
return "running"
|
||||
|
||||
@classmethod
|
||||
def print_jobs(cls, jobstore: Any | None = None, out: Any | None = None) -> None:
|
||||
"""
|
||||
打印调度任务列表。
|
||||
|
||||
参数:
|
||||
- jobstore (Any | None): 任务存储别名。
|
||||
- out (Any | None): 输出目标。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.print_jobs(jobstore=jobstore, out=out)
|
||||
|
||||
@classmethod
|
||||
def get_job_status(cls) -> str:
|
||||
"""
|
||||
获取调度器当前状态。
|
||||
|
||||
返回:
|
||||
- str: 状态字符串('stopped' | 'running' | 'paused' | 'unknown')。
|
||||
"""
|
||||
if scheduler.state == 0:
|
||||
return "stopped"
|
||||
if scheduler.state == 1:
|
||||
return "running"
|
||||
if scheduler.state == 2:
|
||||
return "paused"
|
||||
return "unknown"
|
||||
|
||||
@classmethod
|
||||
def run_job_now(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
立即执行指定任务。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
|
||||
# 立即执行任务
|
||||
scheduler.modify_job(job_id=str(job_id), next_run_time=datetime.now())
|
||||
log.info(f"任务 {job_id} 已设置为立即执行")
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.common.request import PaginationService
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
@@ -12,13 +13,37 @@ from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import (
|
||||
NodeCreateSchema,
|
||||
NodeExecuteSchema,
|
||||
NodeOutSchema,
|
||||
NodeQueryParam,
|
||||
NodeUpdateSchema,
|
||||
)
|
||||
from .service import NodeService
|
||||
|
||||
NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/node", tags=["节点管理"])
|
||||
NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/node", tags=["节点"])
|
||||
|
||||
|
||||
@NodeRouter.get(
|
||||
"/options",
|
||||
summary="获取节点类型选项",
|
||||
description="获取节点类型选项列表,用于流程编排",
|
||||
response_model=ResponseSchema[list[dict]],
|
||||
)
|
||||
async def get_node_options_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取节点类型选项
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含节点类型选项列表的JSON响应
|
||||
"""
|
||||
result = await NodeService.get_node_options_service(auth=auth)
|
||||
log.info("获取节点类型选项成功")
|
||||
return SuccessResponse(data=result, msg="获取节点类型选项成功")
|
||||
|
||||
|
||||
@NodeRouter.get(
|
||||
@@ -27,88 +52,66 @@ NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/node", tags=["节
|
||||
description="获取节点详情",
|
||||
response_model=ResponseSchema[NodeOutSchema],
|
||||
)
|
||||
async def get_node_type_detail_controller(
|
||||
id: Annotated[int, Path(description="节点类型ID")],
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取节点类型详情
|
||||
获取节点详情
|
||||
|
||||
参数:
|
||||
- id (int): 节点类型ID
|
||||
- id (int): 节点ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含节点类型详情的JSON响应
|
||||
- JSONResponse: 包含节点详情的JSON响应
|
||||
"""
|
||||
result_dict = await NodeService.detail_service(auth=auth, id=id)
|
||||
result_dict = await NodeService.get_node_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取节点详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取节点详情成功")
|
||||
|
||||
|
||||
@NodeRouter.get(
|
||||
"/list",
|
||||
summary="查询节点列表",
|
||||
description="查询节点列表",
|
||||
summary="查询节点",
|
||||
description="查询节点",
|
||||
response_model=ResponseSchema[list[NodeOutSchema]],
|
||||
)
|
||||
async def get_node_type_list_controller(
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[NodeQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询节点列表
|
||||
查询节点
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数
|
||||
- search (NodeQueryParam): 查询参数
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (NodeQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含节点列表分页信息的JSON响应
|
||||
- JSONResponse: 包含分页后的节点列表的JSON响应
|
||||
"""
|
||||
result_dict = await NodeService.page_service(
|
||||
auth=auth,
|
||||
result_dict_list = await NodeService.get_node_list_service(
|
||||
auth=auth, search=search, order_by=page.order_by
|
||||
)
|
||||
result_dict = await PaginationService.paginate(
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search.__dict__ if search else None,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
log.info("查询节点列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询节点列表成功")
|
||||
|
||||
|
||||
@NodeRouter.get(
|
||||
"/options",
|
||||
summary="获取节点选项",
|
||||
description="获取节点选项",
|
||||
response_model=ResponseSchema[list[NodeOutSchema]],
|
||||
)
|
||||
async def get_node_options_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取节点选项
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含节点选项的JSON响应
|
||||
"""
|
||||
result_list = await NodeService.list_service(auth=auth)
|
||||
log.info("获取节点选项成功")
|
||||
return SuccessResponse(data=result_list, msg="获取节点选项成功")
|
||||
|
||||
|
||||
@NodeRouter.post(
|
||||
"/create",
|
||||
summary="创建节点",
|
||||
description="创建节点",
|
||||
description="创建节点(仅保存到数据库,不创建调度器任务)",
|
||||
response_model=ResponseSchema[NodeOutSchema],
|
||||
)
|
||||
async def create_node_controller(
|
||||
async def create_obj_controller(
|
||||
data: NodeCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:create"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -116,24 +119,24 @@ async def create_node_controller(
|
||||
创建节点
|
||||
|
||||
参数:
|
||||
- data (NodeCreateSchema): 节点创建模型
|
||||
- data (NodeCreateSchema): 创建参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建节点详情的JSON响应
|
||||
- JSONResponse: 包含创建节点结果的JSON响应
|
||||
"""
|
||||
result_dict = await NodeService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建节点成功: {result_dict.get('name')}")
|
||||
result_dict = await NodeService.create_node_service(auth=auth, data=data)
|
||||
log.info(f"创建节点成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建节点成功")
|
||||
|
||||
|
||||
@NodeRouter.put(
|
||||
"/update/{id}",
|
||||
summary="修改节点",
|
||||
description="修改节点",
|
||||
description="修改节点(仅更新数据库,不修改调度器任务)",
|
||||
response_model=ResponseSchema[NodeOutSchema],
|
||||
)
|
||||
async def update_node_controller(
|
||||
async def update_obj_controller(
|
||||
data: NodeUpdateSchema,
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:update"]))],
|
||||
@@ -142,15 +145,15 @@ async def update_node_controller(
|
||||
修改节点
|
||||
|
||||
参数:
|
||||
- data (NodeUpdateSchema): 节点更新模型
|
||||
- data (NodeUpdateSchema): 更新参数模型
|
||||
- id (int): 节点ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改节点详情的JSON响应
|
||||
- JSONResponse: 包含修改节点结果的JSON响应
|
||||
"""
|
||||
result_dict = await NodeService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改节点成功: {result_dict.get('name')}")
|
||||
result_dict = await NodeService.update_node_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改节点成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改节点成功")
|
||||
|
||||
|
||||
@@ -160,7 +163,7 @@ async def update_node_controller(
|
||||
description="删除节点",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_node_controller(
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:delete"]))],
|
||||
) -> JSONResponse:
|
||||
@@ -168,12 +171,62 @@ async def delete_node_controller(
|
||||
删除节点
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 节点ID列表
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除节点详情的JSON响应
|
||||
- JSONResponse: 包含删除节点结果的JSON响应
|
||||
"""
|
||||
await NodeService.delete_service(auth=auth, ids=ids)
|
||||
await NodeService.delete_node_service(auth=auth, ids=ids)
|
||||
log.info(f"删除节点成功: {ids}")
|
||||
return SuccessResponse(msg="删除节点成功")
|
||||
|
||||
|
||||
@NodeRouter.delete(
|
||||
"/clear",
|
||||
summary="清空节点",
|
||||
description="清空所有节点",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def clear_obj_controller(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空所有节点
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空节点结果的JSON响应
|
||||
"""
|
||||
await NodeService.clear_node_service(auth=auth)
|
||||
log.info("清空节点成功")
|
||||
return SuccessResponse(msg="清空节点成功")
|
||||
|
||||
|
||||
@NodeRouter.post(
|
||||
"/execute/{id}",
|
||||
summary="调试节点",
|
||||
description="调试节点(创建调度器任务并执行)",
|
||||
response_model=ResponseSchema[dict],
|
||||
)
|
||||
async def execute_job_controller(
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
data: NodeExecuteSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:node:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
调试节点
|
||||
|
||||
参数:
|
||||
- id (int): 节点ID
|
||||
- data (NodeExecuteSchema): 执行参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含调试结果的JSON响应
|
||||
"""
|
||||
result = await NodeService.execute_node_service(auth=auth, id=id, execute_data=data)
|
||||
log.info(f"调试节点成功: {id}")
|
||||
return SuccessResponse(data=result, msg="调试节点成功")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
@@ -6,7 +7,6 @@ from app.core.base_crud import CRUDBase
|
||||
from .model import NodeModel
|
||||
from .schema import (
|
||||
NodeCreateSchema,
|
||||
NodeOutSchema,
|
||||
NodeUpdateSchema,
|
||||
)
|
||||
|
||||
@@ -15,102 +15,98 @@ class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
|
||||
"""节点数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化节点CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=NodeModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(
|
||||
async def get_obj_by_id_crud(
|
||||
self, id: int, preload: list[str | Any] | None = None
|
||||
) -> NodeModel | None:
|
||||
"""
|
||||
获取节点类型详情
|
||||
获取节点详情
|
||||
|
||||
参数:
|
||||
- id (int): 节点类型ID
|
||||
- preload (list[str | Any] | None): 预加载关联数据
|
||||
- id (int): 节点ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- NodeTypeModel | None: 节点类型模型实例
|
||||
- NodeModel | None: 节点模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_code_crud(self, code: str) -> NodeModel | None:
|
||||
"""
|
||||
根据编码获取节点类型
|
||||
|
||||
参数:
|
||||
- code (str): 节点类型编码
|
||||
|
||||
返回:
|
||||
- NodeModel | None: 节点模型实例
|
||||
"""
|
||||
return await self.get(code=code)
|
||||
|
||||
async def page_crud(
|
||||
async def get_obj_list_crud(
|
||||
self,
|
||||
offset: int,
|
||||
limit: int,
|
||||
order_by: list[dict] | None = None,
|
||||
search: dict | None = None,
|
||||
preload: list | None = None,
|
||||
) -> dict:
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
preload: list[str | Any] | None = None,
|
||||
) -> Sequence[NodeModel]:
|
||||
"""
|
||||
分页查询节点类型
|
||||
获取节点列表
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 限制数量
|
||||
- order_by (list[dict] | None): 排序条件
|
||||
- search (dict | None): 查询条件
|
||||
- preload (list | None): 预加载关联数据
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- dict: 分页查询结果
|
||||
- Sequence[NodeModel]: 节点模型序列
|
||||
"""
|
||||
order_by_list = order_by or [{"sort_order": "asc"}, {"id": "desc"}]
|
||||
search_dict = search or {}
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=NodeOutSchema,
|
||||
preload=preload,
|
||||
)
|
||||
|
||||
async def create_crud(self, data: NodeCreateSchema) -> NodeModel | None:
|
||||
async def create_obj_crud(self, data: NodeCreateSchema) -> NodeModel | None:
|
||||
"""
|
||||
创建节点类型
|
||||
创建节点
|
||||
|
||||
参数:
|
||||
- data (NodeCreateSchema): 节点类型创建模型
|
||||
- data (NodeCreateSchema): 创建节点模型
|
||||
|
||||
返回:
|
||||
- NodeModel | None: 节点类型模型实例
|
||||
- NodeModel | None: 创建的节点模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: NodeUpdateSchema) -> NodeModel | None:
|
||||
async def update_obj_crud(self, id: int, data: NodeUpdateSchema) -> NodeModel | None:
|
||||
"""
|
||||
更新节点类型
|
||||
更新节点
|
||||
|
||||
参数:
|
||||
- id (int): 节点类型ID
|
||||
- data (NodeUpdateSchema): 节点类型更新模型
|
||||
- id (int): 节点ID
|
||||
- data (NodeUpdateSchema): 更新节点模型
|
||||
|
||||
返回:
|
||||
- NodeModel | None: 节点类型模型实例
|
||||
- NodeModel | None: 更新后的节点模型,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除节点类型
|
||||
删除节点
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 节点类型ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
- ids (list[int]): 节点ID列表
|
||||
"""
|
||||
await self.delete(ids=ids)
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""
|
||||
设置节点的可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 节点ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
"""
|
||||
return await self.set(ids=ids, **kwargs)
|
||||
|
||||
async def clear_obj_crud(self) -> None:
|
||||
"""
|
||||
清除节点日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有节点日志,请谨慎操作
|
||||
"""
|
||||
return await self.clear()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import JSON, String
|
||||
from sqlalchemy import Boolean, Integer, String, Text, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
@@ -13,8 +13,6 @@ class NodeCategoryEnum(enum.Enum):
|
||||
ACTION = "action"
|
||||
CONDITION = "condition"
|
||||
CONTROL = "control"
|
||||
INTEGRATION = "integration"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class NodeModel(ModelMixin, UserMixin):
|
||||
@@ -26,14 +24,18 @@ class NodeModel(ModelMixin, UserMixin):
|
||||
__table_args__: dict[str, str] = {"comment": "节点类型表"}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment="节点类型名称")
|
||||
code: Mapped[str] = mapped_column(String(32), nullable=False, unique=True, comment="节点类型编码")
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(32), default=NodeCategoryEnum.ACTION.value, comment="节点分类"
|
||||
)
|
||||
config_schema: Mapped[dict] = mapped_column(JSON, nullable=False, comment="配置表单Schema(JSON Schema)")
|
||||
input_schema: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="输入数据Schema")
|
||||
output_schema: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="输出数据Schema")
|
||||
handler: Mapped[str] = mapped_column(String(256), nullable=False, comment="处理器路径")
|
||||
description: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="描述")
|
||||
meta_data: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="元数据")
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="节点名称")
|
||||
code: Mapped[str] = mapped_column(String(32), nullable=False, unique=True, comment="节点编码")
|
||||
category: Mapped[str] = mapped_column(String(32), default=NodeCategoryEnum.ACTION.value, comment="节点分类")
|
||||
config_schema: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="配置表单Schema(JSON Schema)")
|
||||
jobstore: Mapped[str | None] = mapped_column(String(64), nullable=True, default="default", comment="存储器")
|
||||
executor: Mapped[str | None] = mapped_column(String(64), nullable=True, default="default", comment="执行器")
|
||||
trigger: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="触发器")
|
||||
trigger_args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="触发器参数")
|
||||
func: Mapped[str | None] = mapped_column(Text, nullable=True, comment="代码块")
|
||||
args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="位置参数")
|
||||
kwargs: Mapped[str | None] = mapped_column(Text, nullable=True, comment="关键字参数")
|
||||
coalesce: Mapped[bool] = mapped_column(Boolean, nullable=True, default=False, comment="是否合并运行")
|
||||
max_instances: Mapped[int] = mapped_column(Integer, nullable=True, default=1, comment="最大实例数")
|
||||
start_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="开始时间")
|
||||
end_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="结束时间")
|
||||
|
||||
@@ -1,65 +1,121 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, datetime_validator
|
||||
|
||||
|
||||
class NodeCreateSchema(BaseModel):
|
||||
"""创建节点模型"""
|
||||
"""
|
||||
节点创建/编辑时只设置节点基本信息,节点参数在执行时设置
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="节点类型名称", min_length=2, max_length=64)
|
||||
code: str = Field(..., description="节点类型编码", min_length=2, max_length=32)
|
||||
category: str = Field(default="action", description="节点分类(trigger/action/condition/control/integration/custom)")
|
||||
config_schema: dict = Field(default_factory=dict, description="配置表单Schema(JSON Schema)")
|
||||
input_schema: dict | None = Field(default=None, description="输入数据Schema")
|
||||
output_schema: dict | None = Field(default=None, description="输出数据Schema")
|
||||
handler: str = Field(default="", description="处理器路径(如: app.workflow.handlers.custom_handler)")
|
||||
meta_data: dict | None = Field(default=None, description="元数据")
|
||||
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, description="描述")
|
||||
|
||||
|
||||
class NodeUpdateSchema(BaseModel):
|
||||
"""更新节点模型"""
|
||||
|
||||
name: str | None = Field(default=None, description="节点类型名称", min_length=2, max_length=64)
|
||||
code: str | None = Field(default=None, description="节点类型编码", min_length=2, max_length=32)
|
||||
name: str = Field(..., max_length=64, description="任务名称")
|
||||
func: str | None = Field(default=None, description="代码块")
|
||||
args: str | None = Field(default=None, description="位置参数")
|
||||
kwargs: str | None = Field(default=None, description="关键字参数")
|
||||
coalesce: bool | None = Field(default=False, description="是否合并运行:是否在多个运行时间到期时仅运行作业一次")
|
||||
max_instances: int | None = Field(default=1, ge=1, description="最大实例数:允许的最大并发执行实例数")
|
||||
jobstore: str | None = Field(default="default", max_length=64, description="任务存储")
|
||||
executor: str | None = Field(default="default", max_length=64, description="任务执行器:将运行此作业的执行程序的名称",)
|
||||
start_date: str | None = Field(default=None, description="开始时间")
|
||||
end_date: str | None = Field(default=None, description="结束时间")
|
||||
code: str | None = Field(default=None, description="节点编码")
|
||||
category: str | None = Field(default=None, description="节点分类")
|
||||
config_schema: dict | None = Field(default=None, description="配置表单Schema")
|
||||
input_schema: dict | None = Field(default=None, description="输入数据Schema")
|
||||
output_schema: dict | None = Field(default=None, description="输出数据Schema")
|
||||
handler: str | None = Field(default=None, description="处理器路径")
|
||||
meta_data: dict | None = Field(default=None, description="元数据")
|
||||
status: str | None = Field(default=None, description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, description="描述")
|
||||
config_schema: dict | None = Field(default=None, description="节点配置")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_func(self):
|
||||
if not self.func or not self.func.strip():
|
||||
raise ValueError("必须提供代码块(func)")
|
||||
return self
|
||||
|
||||
|
||||
class NodeUpdateSchema(NodeCreateSchema):
|
||||
"""节点更新模型"""
|
||||
|
||||
|
||||
class NodeOutSchema(NodeCreateSchema, BaseSchema, UserBySchema):
|
||||
"""节点响应模型"""
|
||||
|
||||
trigger: str | None = Field(default=None, description="触发器")
|
||||
trigger_args: str | None = Field(default=None, description="触发器参数")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeQueryParam:
|
||||
"""节点查询参数"""
|
||||
|
||||
name: str | None = field(default=None, metadata={"description": "节点名称"})
|
||||
code: str | None = field(default=None, metadata={"description": "节点编码"})
|
||||
category: str | None = field(default=None, metadata={"description": "节点分类"})
|
||||
status: str | None = field(default=None, metadata={"description": "是否启用(0:启用 1:禁用)"})
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="节点名称"),
|
||||
status: str | None = Query(None, description="状态: 启动,停止"),
|
||||
created_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="创建时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
updated_time: list[DateTimeStr] | None = Query(
|
||||
None,
|
||||
description="更新时间范围",
|
||||
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||
),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
def to_search_dict(self) -> dict:
|
||||
"""转换为搜索字典"""
|
||||
search_dict = {}
|
||||
if self.name:
|
||||
search_dict["name"] = (QueueEnum.like.value, self.name)
|
||||
if self.code:
|
||||
search_dict["code"] = (QueueEnum.like.value, self.code)
|
||||
if self.category:
|
||||
search_dict["category"] = (QueueEnum.eq.value, self.category)
|
||||
if self.status:
|
||||
search_dict["status"] = (QueueEnum.eq.value, self.status)
|
||||
return search_dict
|
||||
self.name = (QueueEnum.like.value, name)
|
||||
self.created_id = (QueueEnum.eq.value, created_id)
|
||||
self.updated_id = (QueueEnum.eq.value, updated_id)
|
||||
self.status = (QueueEnum.eq.value, status)
|
||||
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class NodeExecuteSchema(BaseModel):
|
||||
"""节点执行参数"""
|
||||
|
||||
trigger: str = Field(default="now", description="触发方式: now/cron/interval/date")
|
||||
trigger_args: str | None = Field(default=None, description="触发器参数")
|
||||
start_date: str | None = Field(default=None, description="开始时间")
|
||||
end_date: str | None = Field(default=None, description="结束时间")
|
||||
|
||||
@field_validator("trigger")
|
||||
@classmethod
|
||||
def _validate_trigger(cls, v: str) -> str:
|
||||
allowed = {"now", "cron", "interval", "date"}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError("触发器必须为 now/cron/interval/date")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_trigger_args(self):
|
||||
"""非立即执行时必须提供触发器参数"""
|
||||
if self.trigger != "now" and not self.trigger_args:
|
||||
raise ValueError("非立即执行时必须提供触发器参数")
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_dates(self):
|
||||
"""跨字段校验:结束时间不得早于开始时间。"""
|
||||
if self.start_date and self.end_date:
|
||||
try:
|
||||
start = datetime_validator(self.start_date)
|
||||
end = datetime_validator(self.end_date)
|
||||
except Exception:
|
||||
raise ValueError("时间格式必须为 YYYY-MM-DD HH:MM:SS")
|
||||
if end < start:
|
||||
raise ValueError("结束时间不能早于开始时间")
|
||||
return self
|
||||
|
||||
@@ -1,151 +1,211 @@
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.utils.cron_util import CronUtil
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
|
||||
from .crud import NodeCRUD
|
||||
from .schema import (
|
||||
NodeCreateSchema,
|
||||
NodeExecuteSchema,
|
||||
NodeOutSchema,
|
||||
NodeQueryParam,
|
||||
NodeUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class NodeService:
|
||||
"""节点管理模块服务层"""
|
||||
"""
|
||||
节点管理模块服务层
|
||||
|
||||
设计原则:
|
||||
1. 节点CRUD只操作数据库,不直接操作调度器
|
||||
2. 调度器节点通过"执行"操作来创建和管理
|
||||
3. 支持预设函数和自定义代码块两种执行方式
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
async def get_node_options_service(cls, auth: AuthSchema) -> list[dict]:
|
||||
"""
|
||||
获取节点类型详情
|
||||
获取节点类型选项列表,用于流程编排
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 节点类型ID
|
||||
|
||||
返回:
|
||||
- dict: 节点类型模型实例字典
|
||||
- list[dict]: 节点类型选项列表
|
||||
"""
|
||||
obj = await NodeCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该节点不存在")
|
||||
obj_list = await NodeCRUD(auth).get_obj_list_crud()
|
||||
return [
|
||||
{
|
||||
"id": obj.id,
|
||||
"name": obj.name,
|
||||
"code": obj.code,
|
||||
"category": obj.category,
|
||||
"config_schema": obj.config_schema if obj.config_schema else {"fields": []},
|
||||
"func": obj.func,
|
||||
"args": obj.args,
|
||||
"kwargs": obj.kwargs,
|
||||
}
|
||||
for obj in obj_list
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def get_node_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取节点详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 节点ID
|
||||
|
||||
返回:
|
||||
- Dict: 节点详情字典
|
||||
"""
|
||||
obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
return NodeOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def page_service(
|
||||
async def get_node_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: dict | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> dict:
|
||||
search: NodeQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
分页查询节点类型
|
||||
获取节点列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
- search (dict | None): 查询条件
|
||||
- order_by (list[dict] | None): 排序条件
|
||||
- search (NodeQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- dict: 分页查询结果
|
||||
- List[Dict]: 节点详情字典列表
|
||||
"""
|
||||
offset = (page_no - 1) * page_size
|
||||
return await NodeCRUD(auth).page_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by,
|
||||
search=search,
|
||||
)
|
||||
obj_list = await NodeCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
return [NodeOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: NodeCreateSchema) -> dict:
|
||||
async def create_node_service(cls, auth: AuthSchema, data: NodeCreateSchema) -> dict:
|
||||
"""
|
||||
创建节点类型
|
||||
创建节点 - 只保存到数据库,不创建调度器任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (NodeCreateSchema): 节点类型创建模型
|
||||
- data (NodeCreateSchema): 节点创建模型
|
||||
|
||||
返回:
|
||||
- dict: 节点类型模型实例字典
|
||||
- Dict: 节点详情字典
|
||||
"""
|
||||
existing = await NodeCRUD(auth).get_by_code_crud(code=data.code)
|
||||
if existing:
|
||||
raise CustomException(msg=f"节点类型编码 {data.code} 已存在")
|
||||
exist_obj = await NodeCRUD(auth).get(name=data.name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg="创建失败,该节点已存在")
|
||||
|
||||
obj = await NodeCRUD(auth).create_crud(data=data)
|
||||
obj = await NodeCRUD(auth).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建失败,节点类型创建失败")
|
||||
log.info(f"创建节点类型成功: {data.name}")
|
||||
raise CustomException(msg="创建失败")
|
||||
return NodeOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: NodeUpdateSchema) -> dict:
|
||||
async def update_node_service(cls, auth: AuthSchema, id: int, data: NodeUpdateSchema) -> dict:
|
||||
"""
|
||||
更新节点类型
|
||||
更新节点 - 只更新数据库,不修改调度器任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 节点类型ID
|
||||
- data (NodeUpdateSchema): 节点类型更新模型
|
||||
- id (int): 节点ID
|
||||
- data (NodeUpdateSchema): 节点更新模型
|
||||
|
||||
返回:
|
||||
- dict: 节点类型模型实例字典
|
||||
- dict: 节点详情字典
|
||||
"""
|
||||
obj = await NodeCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新失败,该节点类型不存在")
|
||||
exist_obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该节点不存在")
|
||||
|
||||
if data.code and data.code != obj.code:
|
||||
existing = await NodeCRUD(auth).get_by_code_crud(code=data.code)
|
||||
if existing:
|
||||
raise CustomException(msg=f"节点类型编码 {data.code} 已存在")
|
||||
|
||||
obj = await NodeCRUD(auth).update_crud(id=id, data=data)
|
||||
obj = await NodeCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新失败,节点类型更新失败")
|
||||
log.info(f"更新节点类型成功: {obj.name}")
|
||||
raise CustomException(msg="更新失败")
|
||||
return NodeOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
async def delete_node_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除节点类型
|
||||
删除节点 - 只删除数据库记录,同时移除调度器中的任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 节点类型ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
- ids (list[int]): 节点ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for id in ids:
|
||||
obj = await NodeCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f"删除失败,节点类型ID {id} 不存在")
|
||||
|
||||
await NodeCRUD(auth).delete_crud(ids=ids)
|
||||
log.info(f"删除节点类型成功: {ids}")
|
||||
exist_obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="删除失败,该节点不存在")
|
||||
SchedulerUtil.remove_job(job_id=id)
|
||||
await NodeCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema) -> list[dict]:
|
||||
async def clear_node_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
获取节点类型列表
|
||||
清空所有节点
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
SchedulerUtil.clear_jobs()
|
||||
await NodeCRUD(auth).clear_obj_crud()
|
||||
|
||||
@classmethod
|
||||
async def execute_node_service(cls, auth: AuthSchema, id: int, execute_data: NodeExecuteSchema) -> dict:
|
||||
"""
|
||||
调试节点 - 根据任务配置创建调度器任务并执行
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 节点ID
|
||||
- execute_data (NodeExecuteSchema): 执行参数模型
|
||||
|
||||
返回:
|
||||
- list[dict]: 节点类型模型实例字典列表
|
||||
- dict: 调试结果
|
||||
"""
|
||||
result = await NodeCRUD(auth).page_crud(
|
||||
offset=0,
|
||||
limit=1000,
|
||||
search={"status": ("eq", "0")},
|
||||
order_by=[{"updated_time": "desc"}],
|
||||
obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="调试失败,该节点不存在")
|
||||
|
||||
trigger = execute_data.trigger
|
||||
trigger_args = execute_data.trigger_args
|
||||
start_date = execute_data.start_date
|
||||
end_date = execute_data.end_date
|
||||
|
||||
if trigger == "now":
|
||||
SchedulerUtil.add_and_run_job_now(job_info=obj)
|
||||
elif trigger == "cron":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="Cron执行需要提供Cron表达式")
|
||||
if not CronUtil.validate_cron_expression(trigger_args):
|
||||
raise CustomException(msg=f"Cron表达式不正确: {trigger_args}")
|
||||
SchedulerUtil.add_cron_job(
|
||||
job_info=obj,
|
||||
trigger_args=trigger_args,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
return result.get("items", [])
|
||||
elif trigger == "interval":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="间隔执行需要提供间隔参数")
|
||||
SchedulerUtil.add_interval_job(
|
||||
job_info=obj,
|
||||
trigger_args=trigger_args,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
elif trigger == "date":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="指定时间执行需要提供执行时间")
|
||||
SchedulerUtil.add_date_job(job_info=obj, run_date=trigger_args)
|
||||
else:
|
||||
raise CustomException(msg=f"不支持的触发方式: {trigger}")
|
||||
|
||||
return {"job_id": id, "status": "executed", "trigger": trigger}
|
||||
|
||||
@@ -245,19 +245,6 @@ class WorkflowService:
|
||||
if len(nodes) == 0:
|
||||
errors.append("流程中没有节点")
|
||||
|
||||
start_nodes = [n for n in nodes if n.get("type") == "input"]
|
||||
end_nodes = [n for n in nodes if n.get("type") == "output"]
|
||||
|
||||
if len(start_nodes) == 0:
|
||||
errors.append("流程缺少开始节点")
|
||||
elif len(start_nodes) > 1:
|
||||
warnings.append("流程有多个开始节点")
|
||||
|
||||
if len(end_nodes) == 0:
|
||||
errors.append("流程缺少结束节点")
|
||||
elif len(end_nodes) > 1:
|
||||
warnings.append("流程有多个结束节点")
|
||||
|
||||
node_ids = {n.get("id") for n in nodes}
|
||||
for edge in edges:
|
||||
source = edge.get("source")
|
||||
@@ -526,7 +513,7 @@ class WorkflowRunService:
|
||||
|
||||
update_data = WorkflowRunUpdateSchema(
|
||||
status="cancelled",
|
||||
end_time=datetime.now().isoformat(),
|
||||
end_time=datetime.now(),
|
||||
)
|
||||
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
||||
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
||||
@@ -575,7 +562,7 @@ class WorkflowRunService:
|
||||
|
||||
update_data = WorkflowRunUpdateSchema(
|
||||
status="terminated",
|
||||
end_time=datetime.now().isoformat(),
|
||||
end_time=datetime.now(),
|
||||
)
|
||||
obj = await WorkflowRunCRUD(auth).update_crud(id=id, data=update_data)
|
||||
return WorkflowRunOutSchema.model_validate(obj).model_dump()
|
||||
@@ -672,7 +659,7 @@ class WorkflowRunService:
|
||||
|
||||
update_data = WorkflowRunUpdateSchema(
|
||||
status="running",
|
||||
start_time=start_time.isoformat(),
|
||||
start_time=start_time,
|
||||
)
|
||||
await WorkflowRunCRUD(auth).update_crud(id=task_run_id, data=update_data)
|
||||
|
||||
@@ -690,7 +677,7 @@ class WorkflowRunService:
|
||||
|
||||
update_data = WorkflowRunUpdateSchema(
|
||||
status="completed",
|
||||
end_time=end_time.isoformat(),
|
||||
end_time=end_time,
|
||||
duration=duration,
|
||||
)
|
||||
await WorkflowRunCRUD(auth).update_crud(id=task_run_id, data=update_data)
|
||||
@@ -708,7 +695,7 @@ class WorkflowRunService:
|
||||
|
||||
update_data = WorkflowRunUpdateSchema(
|
||||
status="failed",
|
||||
end_time=end_time.isoformat(),
|
||||
end_time=end_time,
|
||||
duration=duration,
|
||||
error_message=str(e),
|
||||
)
|
||||
@@ -737,15 +724,20 @@ class WorkflowRunService:
|
||||
"""执行节点"""
|
||||
node_map = {node.get("id"): node for node in nodes}
|
||||
edge_map = {}
|
||||
target_nodes = set()
|
||||
for edge in edges:
|
||||
source = edge.get("source")
|
||||
target = edge.get("target")
|
||||
if source not in edge_map:
|
||||
edge_map[source] = []
|
||||
edge_map[source].append(edge)
|
||||
target_nodes.add(target)
|
||||
|
||||
start_nodes = [node for node in nodes if node.get("type") == "input"]
|
||||
start_nodes = [node for node in nodes if node.get("id") not in target_nodes]
|
||||
if not start_nodes:
|
||||
raise CustomException(msg="工作流缺少开始节点")
|
||||
start_nodes = [node for node in nodes if node.get("type") == "trigger"]
|
||||
if not start_nodes:
|
||||
raise CustomException(msg="工作流缺少起始节点")
|
||||
|
||||
executed_nodes = set()
|
||||
queue = start_nodes.copy()
|
||||
@@ -829,8 +821,6 @@ class WorkflowRunService:
|
||||
await cls._execute_timer_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
||||
elif node_type == "parallel":
|
||||
await cls._execute_parallel_node(auth, task_run_id, node_id, node_name, node_config, variables)
|
||||
elif node_type in ["input", "output"]:
|
||||
pass
|
||||
else:
|
||||
await cls._add_log(
|
||||
auth=auth,
|
||||
|
||||
@@ -2594,7 +2594,7 @@
|
||||
"description": "任务管理",
|
||||
"children": [
|
||||
{
|
||||
"name": "定时任务",
|
||||
"name": "调度器监控",
|
||||
"type": 2,
|
||||
"icon": "el-icon-DataLine",
|
||||
"order": 1,
|
||||
@@ -2606,18 +2606,18 @@
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "定时任务",
|
||||
"title": "调度器监控",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "定时任务",
|
||||
"description": "调度器监控",
|
||||
"children": [
|
||||
{
|
||||
"name": "创建定时任务",
|
||||
"name": "查询调度器",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
"permission": "module_task:job:create",
|
||||
"permission": "module_task:job:query",
|
||||
"route_name": null,
|
||||
"route_path": null,
|
||||
"component_path": null,
|
||||
@@ -2625,14 +2625,14 @@
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "创建定时任务",
|
||||
"title": "查询调度器",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "创建定时任务"
|
||||
"description": "查询调度器"
|
||||
},
|
||||
{
|
||||
"name": "修改和操作定时任务",
|
||||
"name": "操作调度器",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 2,
|
||||
@@ -2644,14 +2644,14 @@
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "修改和操作定时任务",
|
||||
"title": "操作调度器",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "修改和操作定时任务"
|
||||
"description": "操作调度器"
|
||||
},
|
||||
{
|
||||
"name": "删除和清除定时任务",
|
||||
"name": "删除执行日志",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 3,
|
||||
@@ -2663,36 +2663,17 @@
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "删除和清除定时任务",
|
||||
"title": "删除执行日志",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "删除和清除定时任务"
|
||||
"description": "删除执行日志"
|
||||
},
|
||||
{
|
||||
"name": "导出定时任务",
|
||||
"name": "详情执行日志",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 4,
|
||||
"permission": "module_task:job:export",
|
||||
"route_name": null,
|
||||
"route_path": null,
|
||||
"component_path": null,
|
||||
"status": "0",
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "导出定时任务",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "初始化数据"
|
||||
},
|
||||
{
|
||||
"name": "详情定时任务",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 5,
|
||||
"permission": "module_task:job:detail",
|
||||
"route_name": null,
|
||||
"route_path": null,
|
||||
@@ -2701,30 +2682,11 @@
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "详情定时任务",
|
||||
"title": "详情执行日志",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "详情定时任务"
|
||||
},
|
||||
{
|
||||
"name": "查询定时任务",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 6,
|
||||
"permission": "module_task:job:query",
|
||||
"route_name": null,
|
||||
"route_path": null,
|
||||
"component_path": null,
|
||||
"status": "0",
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "查询定时任务",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "初始化数据"
|
||||
"description": "详情执行日志"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -15,9 +15,10 @@ def console_run(
|
||||
port: int,
|
||||
reload: bool,
|
||||
*,
|
||||
database_ready: bool | None = None,
|
||||
redis_ready: bool | None = None,
|
||||
scheduler_jobs: int | None = None,
|
||||
scheduler_status: str | None = None,
|
||||
scheduler_ready: bool | None = None,
|
||||
limiter_ready: bool | None = None,
|
||||
) -> None:
|
||||
"""显示启动信息面板"""
|
||||
|
||||
@@ -37,20 +38,27 @@ def console_run(
|
||||
style="bold red",
|
||||
)
|
||||
service_info.append(
|
||||
f"\n重载配置: {'✅ 开启' if reload else '❌ 关闭'}",
|
||||
f"\n重载配置: {'✅ 启动' if reload else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(
|
||||
f"\n调试模式: {'✅ 开启' if settings.DEBUG else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(f"\n数据库类型: {settings.DATABASE_TYPE} 数据库", style="bold italic")
|
||||
service_info.append(
|
||||
f"\nRedis: {'✅ 已连接' if redis_ready else '❌ 未连接'}",
|
||||
f"\n调试模式: {'✅ 启动' if settings.DEBUG else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(
|
||||
f"\n定时任务 {'✅ 运行中' if scheduler_status == 'running' else '⏸️ 暂停'} {scheduler_jobs}",
|
||||
f"\n{settings.DATABASE_TYPE}: {'✅ 启动' if database_ready else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(
|
||||
f"\nRedis: {'✅ 启动' if redis_ready else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(
|
||||
f"\n调度器: {'✅ 启动' if scheduler_ready else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
service_info.append(
|
||||
f"\n限流器: {'✅ 启动' if limiter_ready else '❌ 关闭'}",
|
||||
style="bold italic",
|
||||
)
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"clipboard": "^2.0.11",
|
||||
"codemirror": "^5.65.19",
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.1",
|
||||
"echarts": "^5.6.0",
|
||||
@@ -96,6 +97,7 @@
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-json-pretty": "^2.5.0",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-web-terminal": "^3.4.1",
|
||||
"vue3-cron-plus": "^0.1.9",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
@@ -103,6 +105,7 @@
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/dagre": "^0.7.53",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
|
||||
@@ -3,87 +3,98 @@ import request from "@/utils/request";
|
||||
const API_PATH = "/task/job";
|
||||
|
||||
const JobAPI = {
|
||||
listJob(query: JobPageQuery) {
|
||||
return request<ApiResponse<PageResult<JobTable[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
detailJob(query: number) {
|
||||
return request<ApiResponse<JobTable>>({
|
||||
url: `${API_PATH}/detail/${query}`,
|
||||
getSchedulerStatus() {
|
||||
return request<ApiResponse<SchedulerStatus>>({
|
||||
url: `${API_PATH}/scheduler/status`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
createJob(body: JobForm) {
|
||||
getSchedulerJobs() {
|
||||
return request<ApiResponse<SchedulerJob[]>>({
|
||||
url: `${API_PATH}/scheduler/jobs`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
startScheduler() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/create`,
|
||||
url: `${API_PATH}/scheduler/start`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
updateJob(id: number, body: JobForm) {
|
||||
pauseScheduler() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
deleteJob(body: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
exportJob(body: JobPageQuery) {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/export`,
|
||||
url: `${API_PATH}/scheduler/pause`,
|
||||
method: "post",
|
||||
data: body,
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
clearJob() {
|
||||
resumeScheduler() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/clear`,
|
||||
url: `${API_PATH}/scheduler/resume`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
shutdownScheduler() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/scheduler/shutdown`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
clearAllJobs() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/scheduler/jobs/clear`,
|
||||
method: "delete",
|
||||
});
|
||||
},
|
||||
|
||||
OptionJob(params: JobOptionData) {
|
||||
getSchedulerConsole() {
|
||||
return request<ApiResponse<string>>({
|
||||
url: `${API_PATH}/scheduler/console`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
syncJobsToDb() {
|
||||
return request<ApiResponse<number>>({
|
||||
url: `${API_PATH}/scheduler/sync`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
pauseJob(jobId: string) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/option`,
|
||||
method: "put",
|
||||
data: params,
|
||||
url: `${API_PATH}/task/pause/${jobId}`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
// 获取定时任务运行日志(实时状态)
|
||||
getJobRunLog() {
|
||||
return request<ApiResponse<JobRunLog[]>>({
|
||||
url: `${API_PATH}/log`,
|
||||
method: "get",
|
||||
resumeJob(jobId: string) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/task/resume/${jobId}`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
// 获取定时任务日志详情
|
||||
detailJobLog(id: number) {
|
||||
return request<ApiResponse<JobLogDetail>>({
|
||||
url: `${API_PATH}/log/detail/${id}`,
|
||||
method: "get",
|
||||
runJobNow(jobId: string) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/task/run/${jobId}`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
// 查询定时任务日志列表
|
||||
listJobLog(query: JobLogPageQuery) {
|
||||
removeJob(jobId: string) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/task/remove/${jobId}`,
|
||||
method: "delete",
|
||||
});
|
||||
},
|
||||
|
||||
getJobLogList(query: JobLogPageQuery) {
|
||||
return request<ApiResponse<PageResult<JobLogTable[]>>>({
|
||||
url: `${API_PATH}/log/list`,
|
||||
method: "get",
|
||||
@@ -91,7 +102,13 @@ const JobAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
// 删除定时任务日志
|
||||
getJobLogDetail(id: number) {
|
||||
return request<ApiResponse<JobLogTable>>({
|
||||
url: `${API_PATH}/log/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
deleteJobLog(ids: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/log/delete`,
|
||||
@@ -100,126 +117,44 @@ const JobAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
// 清空定时任务日志
|
||||
clearJobLog() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/log/clear`,
|
||||
method: "delete",
|
||||
});
|
||||
},
|
||||
|
||||
// 导出定时任务日志
|
||||
exportJobLog(query: JobLogPageQuery) {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/log/export`,
|
||||
method: "post",
|
||||
data: query,
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
//立即执行定时任务
|
||||
runJob(id: number) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/run/${id}`,
|
||||
method: "put",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default JobAPI;
|
||||
|
||||
export interface JobPageQuery extends PageQuery {
|
||||
name?: string;
|
||||
status?: string;
|
||||
created_id?: number;
|
||||
updated_id?: number;
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
export interface SchedulerStatus {
|
||||
status: string;
|
||||
is_running: boolean;
|
||||
job_count: number;
|
||||
}
|
||||
|
||||
export interface SchedulerJob {
|
||||
id: string;
|
||||
name: string;
|
||||
trigger: string;
|
||||
next_run_time?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface JobLogPageQuery extends PageQuery {
|
||||
job_id?: number;
|
||||
job_id?: string;
|
||||
job_name?: string;
|
||||
status?: string;
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
trigger_type?: string;
|
||||
}
|
||||
|
||||
export interface JobOptionData {
|
||||
id?: number;
|
||||
option?: number; //操作类型 1: 暂停 2: 恢复 3: 重启
|
||||
}
|
||||
|
||||
export interface JobTable extends BaseType {
|
||||
name: string;
|
||||
func?: string;
|
||||
trigger?: string;
|
||||
args?: string;
|
||||
kwargs?: string;
|
||||
coalesce?: boolean;
|
||||
max_instances?: number;
|
||||
jobstore?: string;
|
||||
executor?: string;
|
||||
trigger_args?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
created_by?: CommonType;
|
||||
updated_by?: CommonType;
|
||||
}
|
||||
|
||||
export interface JobForm extends BaseFormType {
|
||||
name?: string;
|
||||
func?: string;
|
||||
trigger?: string;
|
||||
args?: string;
|
||||
kwargs?: string;
|
||||
coalesce?: boolean;
|
||||
max_instances?: number;
|
||||
jobstore?: string;
|
||||
executor?: string;
|
||||
trigger_args?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
// 定时任务运行日志接口(对应Scheduler实时状态)
|
||||
export interface JobRunLog extends BaseType {
|
||||
name: string;
|
||||
trigger: string;
|
||||
executor: string;
|
||||
func: string;
|
||||
func_ref: string;
|
||||
args: any[];
|
||||
kwargs: any;
|
||||
misfire_grace_time: number;
|
||||
coalesce: boolean;
|
||||
max_instances: number;
|
||||
next_run_time: string;
|
||||
}
|
||||
|
||||
// 定时任务日志详情接口(对应数据库日志表)
|
||||
export interface JobLogDetail extends BaseType {
|
||||
job_name: string;
|
||||
job_group: string;
|
||||
job_executor: string;
|
||||
invoke_target: string;
|
||||
job_args?: string;
|
||||
job_kwargs?: string;
|
||||
job_trigger?: string;
|
||||
job_message?: string;
|
||||
exception_info?: string;
|
||||
}
|
||||
|
||||
// 定时任务日志列表接口(对应数据库日志表)
|
||||
export interface JobLogTable extends BaseType {
|
||||
job_name: string;
|
||||
job_group: string;
|
||||
job_executor: string;
|
||||
invoke_target: string;
|
||||
job_args?: string;
|
||||
job_kwargs?: string;
|
||||
job_trigger?: string;
|
||||
job_message?: string;
|
||||
exception_info?: string;
|
||||
job_id: string;
|
||||
job_name?: string;
|
||||
trigger_type?: string;
|
||||
status: string;
|
||||
result?: string;
|
||||
error?: string;
|
||||
created_time?: string;
|
||||
updated_time?: string;
|
||||
}
|
||||
|
||||
@@ -3,21 +3,6 @@ import request from "@/utils/request";
|
||||
const API_PATH = "/task/node";
|
||||
|
||||
const NodeAPI = {
|
||||
getNodeTypes(query: NodeTypePageQuery) {
|
||||
return request<ApiResponse<PageResult<NodeType[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
getNodeTypeDetail(id: number) {
|
||||
return request<ApiResponse<NodeType>>({
|
||||
url: `${API_PATH}/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
getNodeTypeOptions() {
|
||||
return request<ApiResponse<NodeType[]>>({
|
||||
url: `${API_PATH}/options`,
|
||||
@@ -25,64 +10,129 @@ const NodeAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
createNodeType(body: NodeTypeForm) {
|
||||
return request<ApiResponse<NodeType>>({
|
||||
listNode(query: NodePageQuery) {
|
||||
return request<ApiResponse<PageResult<NodeTable[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
detailNode(query: number) {
|
||||
return request<ApiResponse<NodeTable>>({
|
||||
url: `${API_PATH}/detail/${query}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
createNode(body: NodeForm) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
updateNodeType(id: number, body: NodeTypeForm) {
|
||||
return request<ApiResponse<NodeType>>({
|
||||
updateNode(id: number, body: NodeForm) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
deleteNodeType(ids: number[]) {
|
||||
return request<ApiResponse<null>>({
|
||||
deleteNode(body: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: { ids },
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
clearNode() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/clear`,
|
||||
method: "delete",
|
||||
});
|
||||
},
|
||||
|
||||
executeNode(id: number, params: ExecuteNodeParams = { trigger: "now" }) {
|
||||
return request<ApiResponse<ExecuteNodeResult>>({
|
||||
url: `${API_PATH}/execute/${id}`,
|
||||
method: "post",
|
||||
data: params,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default NodeAPI;
|
||||
|
||||
export interface NodeType extends BaseType {
|
||||
code: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description?: string;
|
||||
config_schema?: Record<string, any>;
|
||||
input_schema?: Record<string, any>;
|
||||
output_schema?: Record<string, any>;
|
||||
handler?: string;
|
||||
is_system?: boolean;
|
||||
is_active?: boolean;
|
||||
sort_order?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NodeTypeForm extends BaseFormType {
|
||||
code: string;
|
||||
name: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
config_schema?: Record<string, any>;
|
||||
input_schema?: Record<string, any>;
|
||||
output_schema?: Record<string, any>;
|
||||
handler?: string;
|
||||
is_system?: boolean;
|
||||
is_active?: boolean;
|
||||
sort_order?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NodeTypePageQuery extends PageQuery {
|
||||
code?: string;
|
||||
export interface NodePageQuery extends PageQuery {
|
||||
name?: string;
|
||||
code?: string;
|
||||
category?: string;
|
||||
created_id?: number;
|
||||
updated_id?: number;
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
}
|
||||
|
||||
export type TriggerType = "now" | "cron" | "interval" | "date";
|
||||
|
||||
export interface ExecuteNodeParams {
|
||||
trigger: TriggerType;
|
||||
trigger_args?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
export interface ExecuteNodeResult {
|
||||
job_id: number;
|
||||
status: string;
|
||||
trigger: TriggerType;
|
||||
}
|
||||
|
||||
export interface NodeTable extends BaseType {
|
||||
name: string;
|
||||
code: string;
|
||||
category?: string;
|
||||
config_schema?: Record<string, unknown>;
|
||||
jobstore?: string;
|
||||
executor?: string;
|
||||
trigger?: TriggerType;
|
||||
trigger_args?: string;
|
||||
func?: string;
|
||||
args?: string;
|
||||
kwargs?: string;
|
||||
coalesce?: boolean;
|
||||
max_instances?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
created_by?: CommonType;
|
||||
updated_by?: CommonType;
|
||||
}
|
||||
|
||||
export interface NodeForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
code?: string;
|
||||
category?: string;
|
||||
config_schema?: Record<string, unknown>;
|
||||
jobstore?: string;
|
||||
executor?: string;
|
||||
func?: string;
|
||||
args?: string;
|
||||
kwargs?: string;
|
||||
coalesce?: boolean;
|
||||
max_instances?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
export interface NodeType {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
category?: string;
|
||||
config_schema?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-if="isJson"
|
||||
:data="parsed"
|
||||
:show-line="true"
|
||||
:show-icon="true"
|
||||
:show-double-quotes="false"
|
||||
:show-length="true"
|
||||
:deep="3"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import setupPlugins from "@/plugins";
|
||||
import { createTerminal } from "vue-web-terminal";
|
||||
|
||||
// 暗黑主题样式
|
||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
@@ -19,6 +20,8 @@ import { useConfigStore } from "@/store";
|
||||
const app = createApp(App);
|
||||
// 注册插件
|
||||
app.use(setupPlugins);
|
||||
// 注册终端组件
|
||||
app.use(createTerminal());
|
||||
// 封装设置 title 和 favicon 的函数
|
||||
const setTitleAndFavicon = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,538 +0,0 @@
|
||||
<!-- 任务日志抽屉 -->
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="'【' + props.jobName + '】任务日志'"
|
||||
:size="drawerSize"
|
||||
>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:model="queryFormData"
|
||||
:inline="true"
|
||||
label-suffix=":"
|
||||
@submit.prevent="handleQuery"
|
||||
>
|
||||
<el-form-item prop="status" label="执行状态">
|
||||
<el-select
|
||||
v-model="queryFormData.status"
|
||||
placeholder="请选择执行状态"
|
||||
style="width: 167.5px"
|
||||
clearable
|
||||
>
|
||||
<el-option :value="true" label="成功" />
|
||||
<el-option :value="false" label="失败" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="执行时间">
|
||||
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button
|
||||
v-hasPerm="['module_task:job:query']"
|
||||
type="primary"
|
||||
icon="search"
|
||||
@click="handleQuery"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button v-hasPerm="['module_task:job:query']" icon="refresh" @click="handleResetQuery">
|
||||
重置
|
||||
</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip
|
||||
content="任务执行日志记录每次定时任务的执行情况,包括成功、失败状态及错误信息。"
|
||||
>
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
任务执行日志列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@click="handleDelete(selectIds)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['module_task:job:delete']"
|
||||
type="warning"
|
||||
icon="delete"
|
||||
@click="handleClearLog"
|
||||
>
|
||||
清空日志
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<!-- 将直接导出改为打开导出弹窗 -->
|
||||
<el-button
|
||||
v-hasPerm="['module_task:job:export']"
|
||||
type="warning"
|
||||
icon="download"
|
||||
circle
|
||||
@click="handleOpenExportsModal"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="default" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:任务日志列表 -->
|
||||
<el-table
|
||||
ref="dataTableRef"
|
||||
v-loading="loading"
|
||||
:data="pageTableData"
|
||||
highlight-current-row
|
||||
class="data-table__content"
|
||||
height="460"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务名称" prop="job_name" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="任务组名" prop="job_group" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="执行状态" prop="status" min-width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === "0" ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="执行信息"
|
||||
prop="job_message"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="异常信息"
|
||||
prop="exception_info"
|
||||
min-width="250"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="执行器" prop="job_executor" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="调用目标"
|
||||
prop="invoke_target"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="位置参数" prop="job_args" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="关键字参数"
|
||||
prop="job_kwargs"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="触发器" prop="job_trigger" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" prop="created_time" min-width="180" sortable />
|
||||
<el-table-column label="更新时间" prop="updated_time" min-width="180" sortable />
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['module_task:job:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_task:job:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination
|
||||
v-model:total="total"
|
||||
v-model:page="queryFormData.page_no"
|
||||
v-model:limit="queryFormData.page_size"
|
||||
@pagination="loadingData"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="2" border label-width="120px">
|
||||
<el-descriptions-item label="日志ID" :span="2">
|
||||
{{ detailFormData.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务名称" :span="2">
|
||||
{{ detailFormData.job_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务组名" :span="2">
|
||||
{{ detailFormData.job_group }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行状态" :span="2">
|
||||
<el-tag :type="detailFormData.status === '0' ? 'success' : 'danger'">
|
||||
{{ detailFormData.status === "0" ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行信息" :span="2">
|
||||
{{ detailFormData.job_message || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息" :span="2">
|
||||
{{ detailFormData.exception_info || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行器" :span="2">
|
||||
{{ detailFormData.job_executor || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="调用目标" :span="2">
|
||||
{{ detailFormData.invoke_target || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="位置参数" :span="2">
|
||||
{{ detailFormData.job_args || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="关键字参数" :span="2">
|
||||
{{ detailFormData.job_kwargs || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="触发器" :span="2">
|
||||
{{ detailFormData.job_trigger || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ detailFormData.created_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ detailFormData.updated_time }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 添加 props 来接收 jobId 和 jobName
|
||||
const props = defineProps({
|
||||
jobId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
jobName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/module_task/job";
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataTableRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
const drawerVisible = ref<boolean>(false);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<JobLogTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<JobLogTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<JobLogTable>({} as JobLogTable);
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<JobLogPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
status: undefined,
|
||||
created_time: undefined,
|
||||
job_id: props.jobId,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: "detail",
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.created_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用任务日志列表接口
|
||||
const response = await JobAPI.listJobLog(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
queryFormData.status = undefined;
|
||||
queryFormData.created_time = undefined;
|
||||
dateRange.value = [];
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
// 提取有效的数字ID,过滤掉 null/undefined 并转为 number
|
||||
selectIds.value = selection
|
||||
.map((item: any) => item?.id)
|
||||
.filter((id: any) => id !== null && id !== undefined)
|
||||
.map((id: any) => Number(id));
|
||||
// 记录选中行数据供导出弹窗使用
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
}
|
||||
|
||||
// 打开详情弹窗
|
||||
async function handleOpenDialog(type: "detail", id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await JobAPI.detailJobLog(id);
|
||||
dialogVisible.title = "任务日志详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
// 如果没有有效ID,直接返回
|
||||
const validIds = ids.filter((id) => id !== null && id !== undefined) as number[];
|
||||
if (validIds.length === 0) return;
|
||||
|
||||
ElMessageBox.confirm("确认删除该任务日志?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.deleteJobLog(validIds);
|
||||
// 删除后刷新并清空选择状态
|
||||
handleResetQuery();
|
||||
selectIds.value = [];
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async function handleClearLog() {
|
||||
ElMessageBox.confirm("确认清空所有任务日志?此操作不可恢复!", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.clearJobLog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: "job_name", label: "任务名称" },
|
||||
{ prop: "job_group", label: "任务组名" },
|
||||
{ prop: "status", label: "执行状态" },
|
||||
{ prop: "job_message", label: "执行信息" },
|
||||
{ prop: "exception_info", label: "异常信息" },
|
||||
{ prop: "job_executor", label: "执行器" },
|
||||
{ prop: "invoke_target", label: "调用目标" },
|
||||
{ prop: "job_args", label: "位置参数" },
|
||||
{ prop: "job_kwargs", label: "关键字参数" },
|
||||
{ prop: "job_trigger", label: "触发器" },
|
||||
{ prop: "create_time", label: "创建时间" },
|
||||
];
|
||||
|
||||
// 导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: "application:job_log",
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await JobAPI.listJobLog(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
// 打开抽屉
|
||||
function openDrawer() {
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 关闭抽屉
|
||||
function closeDrawer() {
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 抽屉打开时会自动加载数据
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,208 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑节点类型' : '创建节点类型'"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="formData" label-width="120px">
|
||||
<el-form-item label="节点编码" required>
|
||||
<el-input v-model="formData.code" placeholder="请输入节点编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点名称" required>
|
||||
<el-input v-model="formData.name" placeholder="请输入节点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点分类">
|
||||
<el-select v-model="formData.category" placeholder="请选择节点分类" style="width: 100%">
|
||||
<el-option label="触发器" value="trigger" />
|
||||
<el-option label="动作" value="action" />
|
||||
<el-option label="条件" value="condition" />
|
||||
<el-option label="控制" value="control" />
|
||||
<el-option label="集成" value="integration" />
|
||||
<el-option label="自定义" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置Schema">
|
||||
<el-input
|
||||
v-model="configSchemaJson"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入配置Schema(JSON格式)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点描述">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入节点描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleOk">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import NodeAPI, { type NodeType, type NodeTypeForm } from "@/api/module_task/node";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodeType: {
|
||||
type: Object as () => NodeType | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "refresh"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const configSchemaJson = ref("{}");
|
||||
const formData = reactive<Partial<NodeTypeForm>>({
|
||||
code: "",
|
||||
name: "",
|
||||
category: "action",
|
||||
description: "",
|
||||
config_schema: {},
|
||||
});
|
||||
|
||||
const isEdit = computed(() => !!props.nodeType?.id);
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.nodeType,
|
||||
(newNodeType) => {
|
||||
if (newNodeType) {
|
||||
Object.assign(formData, {
|
||||
code: newNodeType.code,
|
||||
name: newNodeType.name,
|
||||
category: newNodeType.category,
|
||||
description: newNodeType.description,
|
||||
});
|
||||
configSchemaJson.value = JSON.stringify(newNodeType.config_schema || {}, null, 2);
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
code: "",
|
||||
name: "",
|
||||
category: "action",
|
||||
description: "",
|
||||
});
|
||||
configSchemaJson.value = "{}";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!formData.code || !formData.name) {
|
||||
ElMessage.error("请填写必填项");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
formData.config_schema = JSON.parse(configSchemaJson.value);
|
||||
} catch {
|
||||
ElMessage.error("配置Schema格式错误,请输入有效的JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await NodeAPI.updateNodeType(props.nodeType!.id!, formData as NodeTypeForm);
|
||||
} else {
|
||||
await NodeAPI.createNodeType(formData as NodeTypeForm);
|
||||
}
|
||||
emit("refresh");
|
||||
handleCancel();
|
||||
} catch {
|
||||
ElMessage.error(isEdit.value ? "更新失败" : "创建失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit("update:visible", false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.color-presets {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.color-preset {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #ecf5ff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #ecf5ff;
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="dynamic-node" :class="nodeClass">
|
||||
<div
|
||||
class="dynamic-node"
|
||||
:class="nodeClass"
|
||||
@mouseenter="showHandles = true"
|
||||
@mouseleave="showHandles = false"
|
||||
>
|
||||
<div class="node-content">
|
||||
<span class="node-label">{{ data.label }}</span>
|
||||
</div>
|
||||
@@ -8,6 +13,7 @@
|
||||
:id="'top-' + id"
|
||||
type="target"
|
||||
position="top"
|
||||
:class="{ 'handle-visible': showHandles }"
|
||||
:style="{ background: nodeType.color || '#3b82f6' }"
|
||||
/>
|
||||
<Handle
|
||||
@@ -15,6 +21,7 @@
|
||||
:id="'left-' + id"
|
||||
type="target"
|
||||
position="left"
|
||||
:class="{ 'handle-visible': showHandles }"
|
||||
:style="{ background: nodeType.color || '#3b82f6' }"
|
||||
/>
|
||||
<Handle
|
||||
@@ -22,6 +29,7 @@
|
||||
:id="'right-' + id"
|
||||
type="source"
|
||||
position="right"
|
||||
:class="{ 'handle-visible': showHandles }"
|
||||
:style="{ background: nodeType.color || '#3b82f6' }"
|
||||
/>
|
||||
<Handle
|
||||
@@ -29,13 +37,14 @@
|
||||
:id="'bottom-' + id"
|
||||
type="source"
|
||||
position="bottom"
|
||||
:class="{ 'handle-visible': showHandles }"
|
||||
:style="{ background: nodeType.color || '#3b82f6' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { Handle } from "@vue-flow/core";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -44,6 +53,8 @@ const props = defineProps({
|
||||
nodeStatus: String,
|
||||
});
|
||||
|
||||
const showHandles = ref(false);
|
||||
|
||||
const nodeType = computed(() => {
|
||||
if (props.data?.type === "input") {
|
||||
return {
|
||||
@@ -141,4 +152,15 @@ const nodeClass = computed(() => {
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.vue-flow__handle.handle-visible,
|
||||
.vue-flow__handle.vue-flow__handle-connecting,
|
||||
.vue-flow__handle.vue-flow__handle-valid {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<el-splitter direction="horizontal" style="height: 100%">
|
||||
<el-splitter-panel size="250px" :min="200" :max="400">
|
||||
<el-scrollbar style="height: 100%">
|
||||
<div class="basic-info-section">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">基础信息</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -67,8 +67,8 @@
|
||||
<el-space direction="vertical" :size="8" fill style="width: 100%; margin-top: 8px">
|
||||
<el-tag
|
||||
v-for="item in filteredNodes"
|
||||
:key="'id' in item ? item.id : item.type"
|
||||
:type="getNodeType(item)"
|
||||
:key="item.id"
|
||||
:type="getCategoryType(item.category) as any"
|
||||
effect="plain"
|
||||
draggable="true"
|
||||
style="justify-content: center; cursor: move; user-select: none"
|
||||
@@ -76,6 +76,9 @@
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span style="margin-left: 4px; font-size: 10px; opacity: 0.7">
|
||||
[{{ getCategoryText(item.category) }}]
|
||||
</span>
|
||||
</el-tag>
|
||||
</el-space>
|
||||
</div>
|
||||
@@ -101,6 +104,65 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background pattern-color="#aaa" :gap="16" />
|
||||
<Panel position="top-right" class="workflow-toolbar">
|
||||
<el-dropdown trigger="click" @command="handleEdgeStyleChange">
|
||||
<el-button class="vue-flow__controls-button" title="连线样式" :icon="Share" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
command="bezier"
|
||||
:class="{ active: edgeStyle === 'bezier' }"
|
||||
>
|
||||
平滑曲线
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
command="smoothstep"
|
||||
:class="{ active: edgeStyle === 'smoothstep' }"
|
||||
>
|
||||
阶梯折线
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
command="straight"
|
||||
:class="{ active: edgeStyle === 'straight' }"
|
||||
>
|
||||
直线
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
class="vue-flow__controls-button"
|
||||
:title="edgeAnimated ? '关闭动画' : '开启动画'"
|
||||
:icon="VideoPlay"
|
||||
@click="handleEdgeAnimatedChange(!edgeAnimated)"
|
||||
/>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button class="vue-flow__controls-button" title="布局">
|
||||
<el-icon><Rank /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
@click="
|
||||
layoutDirection = 'LR';
|
||||
handleLayout();
|
||||
"
|
||||
>
|
||||
横向布局
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click="
|
||||
layoutDirection = 'TB';
|
||||
handleLayout();
|
||||
"
|
||||
>
|
||||
纵向布局
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</Panel>
|
||||
<MiniMap pannable zoomable />
|
||||
</VueFlow>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,14 +199,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted, markRaw, type Component } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { VueFlow, useVueFlow } from "@vue-flow/core";
|
||||
import { Panel, VueFlow, useVueFlow } from "@vue-flow/core";
|
||||
import { Background } from "@vue-flow/background";
|
||||
import { MiniMap } from "@vue-flow/minimap";
|
||||
import { Controls } from "@vue-flow/controls";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import { Search, Share, VideoPlay, Rank } from "@element-plus/icons-vue";
|
||||
import type { Node, Edge, DefaultEdgeOptions, MarkerType } from "@vue-flow/core";
|
||||
import dagre from "dagre";
|
||||
import "@vue-flow/core/dist/style.css";
|
||||
import "@vue-flow/core/dist/theme-default.css";
|
||||
import "@vue-flow/controls/dist/style.css";
|
||||
import "@vue-flow/minimap/dist/style.css";
|
||||
import "element-plus/dist/index.css";
|
||||
|
||||
import DynamicNode from "./DynamicNode.vue";
|
||||
@@ -219,36 +284,101 @@ const defaultEdgeOptions: DefaultEdgeOptions = {
|
||||
markerEnd: "arrowclosed" as MarkerType,
|
||||
};
|
||||
|
||||
const edgeStyle = ref<string>("smoothstep");
|
||||
const edgeAnimated = ref<boolean>(true);
|
||||
|
||||
const handleEdgeStyleChange = (value: string) => {
|
||||
edgeStyle.value = value;
|
||||
defaultEdgeOptions.type = value;
|
||||
setEdges(
|
||||
getEdgesRef.value.map((edge) => ({
|
||||
...edge,
|
||||
type: value,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdgeAnimatedChange = (value: boolean) => {
|
||||
edgeAnimated.value = value;
|
||||
defaultEdgeOptions.animated = value;
|
||||
setEdges(
|
||||
getEdgesRef.value.map((edge) => ({
|
||||
...edge,
|
||||
animated: value,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const layoutDirection = ref<"LR" | "TB">("LR");
|
||||
|
||||
const handleLayout = () => {
|
||||
const currentNodes = getNodesRef.value;
|
||||
const currentEdges = getEdgesRef.value;
|
||||
|
||||
if (currentNodes.length === 0) {
|
||||
ElMessage.warning("画布中没有节点,无法布局");
|
||||
return;
|
||||
}
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const nodeWidth = 180;
|
||||
const nodeHeight = 60;
|
||||
|
||||
dagreGraph.setGraph({
|
||||
rankdir: layoutDirection.value,
|
||||
nodesep: 80,
|
||||
ranksep: 120,
|
||||
marginx: 50,
|
||||
marginy: 50,
|
||||
});
|
||||
|
||||
currentNodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
|
||||
currentEdges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
const layoutedNodes = currentNodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
setNodes(layoutedNodes);
|
||||
setEdges(
|
||||
currentEdges.map((edge) => ({
|
||||
...edge,
|
||||
type: edgeStyle.value,
|
||||
animated: edgeAnimated.value,
|
||||
}))
|
||||
);
|
||||
ElMessage.success("画布布局完成");
|
||||
};
|
||||
|
||||
const nodes = ref<Node[]>([]);
|
||||
const edges = ref<Edge[]>([]);
|
||||
|
||||
const searchKeyword = ref("");
|
||||
|
||||
type BaseNodeType = {
|
||||
type: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type LoadedNodeType = {
|
||||
id: number;
|
||||
type: string;
|
||||
name: string;
|
||||
class: string;
|
||||
category: string;
|
||||
};
|
||||
|
||||
const allNodes = ref<(BaseNodeType | LoadedNodeType)[]>([
|
||||
{
|
||||
type: "input",
|
||||
name: "开始",
|
||||
color: "#67c23a",
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
name: "结束",
|
||||
color: "#f56c6c",
|
||||
},
|
||||
]);
|
||||
const allNodes = ref<LoadedNodeType[]>([]);
|
||||
|
||||
const filteredNodes = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
@@ -258,16 +388,27 @@ const filteredNodes = computed(() => {
|
||||
return allNodes.value.filter((node) => node.name.toLowerCase().includes(keyword));
|
||||
});
|
||||
|
||||
const getNodeType = (item: BaseNodeType | LoadedNodeType) => {
|
||||
if (item.type === "input") return "success";
|
||||
if (item.type === "output") return "danger";
|
||||
return undefined;
|
||||
const getCategoryType = (category: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
trigger: "warning",
|
||||
action: "",
|
||||
condition: "success",
|
||||
control: "info",
|
||||
};
|
||||
return typeMap[category] || "";
|
||||
};
|
||||
|
||||
const nodeTypesRegistry = ref<Record<string, Component>>({
|
||||
input: markRaw(DynamicNode),
|
||||
output: markRaw(DynamicNode),
|
||||
});
|
||||
const getCategoryText = (category: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
trigger: "触发器",
|
||||
action: "动作",
|
||||
condition: "条件",
|
||||
control: "控制",
|
||||
};
|
||||
return textMap[category] || category;
|
||||
};
|
||||
|
||||
const nodeTypesRegistry = ref<Record<string, Component>>({});
|
||||
|
||||
const updateState = ref("");
|
||||
const selectedEdge = ref<Edge>();
|
||||
@@ -296,27 +437,19 @@ const loadNodeTypes = async () => {
|
||||
try {
|
||||
const res = await NodeAPI.getNodeTypeOptions();
|
||||
if (res.data && res.data.data) {
|
||||
const loadedNodesWithColor: (BaseNodeType | LoadedNodeType)[] = res.data.data.map(
|
||||
(nodeType: any) => ({
|
||||
allNodes.value = res.data.data.map((nodeType: any) => ({
|
||||
id: nodeType.id,
|
||||
type: nodeType.code,
|
||||
name: nodeType.name,
|
||||
class: "custom-drag-item",
|
||||
color: "#409eff",
|
||||
})
|
||||
);
|
||||
|
||||
allNodes.value = [...allNodes.value, ...loadedNodesWithColor];
|
||||
category: nodeType.category || "action",
|
||||
}));
|
||||
|
||||
const newTypes: Record<string, Component> = {};
|
||||
res.data.data.forEach((nodeType: any) => {
|
||||
newTypes[nodeType.code] = markRaw(DynamicNode);
|
||||
});
|
||||
|
||||
nodeTypesRegistry.value = {
|
||||
...nodeTypesRegistry.value,
|
||||
...newTypes,
|
||||
};
|
||||
nodeTypesRegistry.value = newTypes;
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error("加载节点类型失败");
|
||||
@@ -337,9 +470,6 @@ onInit((vueFlowInstance) => {
|
||||
if (res.data && res.data.data) {
|
||||
nodes.value = res.data.data.nodes || [];
|
||||
edges.value = res.data.data.edges || [];
|
||||
if (nodes.value.length === 0) {
|
||||
initializeDefaultNodes();
|
||||
}
|
||||
saveToHistory(nodes.value as any, edges.value as any);
|
||||
}
|
||||
})
|
||||
@@ -352,7 +482,11 @@ onInit((vueFlowInstance) => {
|
||||
});
|
||||
|
||||
onConnect((connection) => {
|
||||
addEdges(connection);
|
||||
addEdges({
|
||||
...connection,
|
||||
type: edgeStyle.value,
|
||||
animated: edgeAnimated.value,
|
||||
});
|
||||
saveToHistory(nodes.value as any, edges.value as any);
|
||||
});
|
||||
|
||||
@@ -367,21 +501,6 @@ function handleValidate() {
|
||||
errors.push("流程中没有节点");
|
||||
}
|
||||
|
||||
const startNodes = allNodesList.filter((n: Node) => n.type === "input");
|
||||
const endNodes = allNodesList.filter((n: Node) => n.type === "output");
|
||||
|
||||
if (startNodes.length === 0) {
|
||||
errors.push("流程缺少开始节点");
|
||||
} else if (startNodes.length > 1) {
|
||||
warnings.push("流程有多个开始节点");
|
||||
}
|
||||
|
||||
if (endNodes.length === 0) {
|
||||
errors.push("流程缺少结束节点");
|
||||
} else if (endNodes.length > 1) {
|
||||
warnings.push("流程有多个结束节点");
|
||||
}
|
||||
|
||||
const nodeIds = new Set(allNodesList.map((n: Node) => n.id));
|
||||
allEdgesList.forEach((edge: Edge) => {
|
||||
if (!nodeIds.has(edge.source)) {
|
||||
@@ -543,23 +662,6 @@ function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeDefaultNodes() {
|
||||
nodes.value = [
|
||||
{
|
||||
id: `node-${Date.now()}`,
|
||||
type: "input",
|
||||
position: { x: 100, y: 100 },
|
||||
data: { label: "开始" },
|
||||
},
|
||||
{
|
||||
id: `node-${Date.now() + 1}`,
|
||||
type: "output",
|
||||
position: { x: 400, y: 100 },
|
||||
data: { label: "结束" },
|
||||
},
|
||||
] as Node[];
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.workflow,
|
||||
(newWorkflow) => {
|
||||
@@ -595,7 +697,6 @@ const handleFinish = async () => {
|
||||
await formRef.value.validate();
|
||||
await handleValidate();
|
||||
await handleSave();
|
||||
ElMessage.success("流程保存成功");
|
||||
emit("refresh");
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
@@ -667,6 +768,35 @@ const handleClose = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__controls) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__controls-button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.workflow-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -153,23 +153,8 @@
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-space class="flex">
|
||||
<el-button type="primary" size="small" link icon="edit" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleViewRuns(row)"
|
||||
>
|
||||
执行记录
|
||||
</el-button>
|
||||
<el-dropdown @command="(e) => handleExecute(e, row)">
|
||||
<el-button type="primary" size="small" link icon="video-play">
|
||||
<el-button type="warning" size="small" link icon="video-play">
|
||||
执行
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
@@ -182,6 +167,15 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="info" size="small" link icon="document" @click="handleViewRuns(row)">
|
||||
执行记录
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" link icon="edit" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
Reference in New Issue
Block a user