313 lines
11 KiB
Python
313 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""调度任务数据模型"""
|
|
|
|
import json
|
|
from dataclasses import dataclass, field, asdict
|
|
from datetime import datetime, timedelta
|
|
from enum import Enum
|
|
from typing import Optional, List, Dict, Any
|
|
from pathlib import Path
|
|
|
|
|
|
class ScheduleType(Enum):
|
|
"""调度类型"""
|
|
ONCE = "once" # 一次性
|
|
INTERVAL = "interval" # 固定间隔
|
|
DAILY = "daily" # 每天
|
|
WEEKLY = "weekly" # 每周
|
|
CRON = "cron" # Cron 表达式
|
|
|
|
|
|
class IntervalUnit(Enum):
|
|
"""间隔单位"""
|
|
MINUTES = "minutes"
|
|
HOURS = "hours"
|
|
DAYS = "days"
|
|
|
|
|
|
@dataclass
|
|
class ScheduleConfig:
|
|
"""调度配置"""
|
|
schedule_type: ScheduleType = ScheduleType.ONCE
|
|
|
|
# 间隔调度
|
|
interval_value: int = 1
|
|
interval_unit: IntervalUnit = IntervalUnit.HOURS
|
|
|
|
# 每日调度
|
|
daily_time: str = "04:00" # HH:MM
|
|
|
|
# 每周调度
|
|
weekly_days: List[int] = field(default_factory=lambda: [1]) # 1-7, 1=周一
|
|
weekly_time: str = "04:00"
|
|
|
|
# Cron 表达式
|
|
cron_expression: str = "0 4 * * *"
|
|
|
|
# 通用设置
|
|
enabled: bool = True
|
|
start_date: Optional[str] = None # YYYY-MM-DD
|
|
end_date: Optional[str] = None # YYYY-MM-DD
|
|
|
|
def to_dict(self) -> dict:
|
|
"""转换为字典"""
|
|
return {
|
|
"schedule_type": self.schedule_type.value,
|
|
"interval_value": self.interval_value,
|
|
"interval_unit": self.interval_unit.value,
|
|
"daily_time": self.daily_time,
|
|
"weekly_days": self.weekly_days,
|
|
"weekly_time": self.weekly_time,
|
|
"cron_expression": self.cron_expression,
|
|
"enabled": self.enabled,
|
|
"start_date": self.start_date,
|
|
"end_date": self.end_date,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "ScheduleConfig":
|
|
"""从字典创建"""
|
|
return cls(
|
|
schedule_type=ScheduleType(data.get("schedule_type", "once")),
|
|
interval_value=data.get("interval_value", 1),
|
|
interval_unit=IntervalUnit(data.get("interval_unit", "hours")),
|
|
daily_time=data.get("daily_time", "04:00"),
|
|
weekly_days=data.get("weekly_days", [1]),
|
|
weekly_time=data.get("weekly_time", "04:00"),
|
|
cron_expression=data.get("cron_expression", "0 4 * * *"),
|
|
enabled=data.get("enabled", True),
|
|
start_date=data.get("start_date"),
|
|
end_date=data.get("end_date"),
|
|
)
|
|
|
|
def get_description(self) -> str:
|
|
"""获取调度描述"""
|
|
if self.schedule_type == ScheduleType.ONCE:
|
|
return "一次性执行"
|
|
elif self.schedule_type == ScheduleType.INTERVAL:
|
|
unit_names = {"minutes": "分钟", "hours": "小时", "days": "天"}
|
|
return f"每 {self.interval_value} {unit_names[self.interval_unit.value]}"
|
|
elif self.schedule_type == ScheduleType.DAILY:
|
|
return f"每天 {self.daily_time}"
|
|
elif self.schedule_type == ScheduleType.WEEKLY:
|
|
day_names = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"}
|
|
days = "、".join(f"周{day_names[d]}" for d in sorted(self.weekly_days))
|
|
return f"每周 {days} {self.weekly_time}"
|
|
elif self.schedule_type == ScheduleType.CRON:
|
|
return f"Cron: {self.cron_expression}"
|
|
return "未知"
|
|
|
|
def get_next_run_time(self, last_run: Optional[datetime] = None) -> Optional[datetime]:
|
|
"""计算下次运行时间"""
|
|
now = datetime.now()
|
|
|
|
# 检查日期范围
|
|
if self.start_date:
|
|
start = datetime.strptime(self.start_date, "%Y-%m-%d")
|
|
if now < start:
|
|
now = start
|
|
|
|
if self.end_date:
|
|
end = datetime.strptime(self.end_date, "%Y-%m-%d") + timedelta(days=1)
|
|
if now >= end:
|
|
return None
|
|
|
|
if self.schedule_type == ScheduleType.ONCE:
|
|
return None if last_run else now
|
|
|
|
elif self.schedule_type == ScheduleType.INTERVAL:
|
|
if not last_run:
|
|
return now
|
|
if self.interval_unit == IntervalUnit.MINUTES:
|
|
delta = timedelta(minutes=self.interval_value)
|
|
elif self.interval_unit == IntervalUnit.HOURS:
|
|
delta = timedelta(hours=self.interval_value)
|
|
else:
|
|
delta = timedelta(days=self.interval_value)
|
|
return last_run + delta
|
|
|
|
elif self.schedule_type == ScheduleType.DAILY:
|
|
hour, minute = map(int, self.daily_time.split(":"))
|
|
next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
if next_run <= now:
|
|
next_run += timedelta(days=1)
|
|
return next_run
|
|
|
|
elif self.schedule_type == ScheduleType.WEEKLY:
|
|
hour, minute = map(int, self.weekly_time.split(":"))
|
|
# 找到下一个匹配的日期
|
|
for i in range(8):
|
|
check_date = now + timedelta(days=i)
|
|
weekday = check_date.isoweekday() # 1-7
|
|
if weekday in self.weekly_days:
|
|
next_run = check_date.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
if next_run > now:
|
|
return next_run
|
|
return None
|
|
|
|
elif self.schedule_type == ScheduleType.CRON:
|
|
# 简化版 Cron 解析(只支持基本格式)
|
|
try:
|
|
return self._parse_simple_cron(now)
|
|
except Exception:
|
|
return None
|
|
|
|
return None
|
|
|
|
def _parse_simple_cron(self, now: datetime) -> Optional[datetime]:
|
|
"""简化版 Cron 解析"""
|
|
parts = self.cron_expression.split()
|
|
if len(parts) != 5:
|
|
return None
|
|
|
|
minute, hour, day, month, weekday = parts
|
|
|
|
# 只处理简单情况
|
|
if minute.isdigit() and hour.isdigit():
|
|
next_run = now.replace(
|
|
hour=int(hour),
|
|
minute=int(minute),
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if next_run <= now:
|
|
next_run += timedelta(days=1)
|
|
return next_run
|
|
|
|
return None
|
|
|
|
|
|
@dataclass
|
|
class ScheduledTask:
|
|
"""调度任务"""
|
|
id: str
|
|
name: str
|
|
task_codes: List[str]
|
|
schedule: ScheduleConfig
|
|
task_config: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 运行状态
|
|
enabled: bool = True
|
|
last_run: Optional[datetime] = None
|
|
next_run: Optional[datetime] = None
|
|
run_count: int = 0
|
|
last_status: str = ""
|
|
|
|
created_at: datetime = field(default_factory=datetime.now)
|
|
updated_at: datetime = field(default_factory=datetime.now)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""转换为字典"""
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"task_codes": self.task_codes,
|
|
"schedule": self.schedule.to_dict(),
|
|
"task_config": self.task_config,
|
|
"enabled": self.enabled,
|
|
"last_run": self.last_run.isoformat() if self.last_run else None,
|
|
"next_run": self.next_run.isoformat() if self.next_run else None,
|
|
"run_count": self.run_count,
|
|
"last_status": self.last_status,
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "ScheduledTask":
|
|
"""从字典创建"""
|
|
return cls(
|
|
id=data["id"],
|
|
name=data["name"],
|
|
task_codes=data["task_codes"],
|
|
schedule=ScheduleConfig.from_dict(data.get("schedule", {})),
|
|
task_config=data.get("task_config", {}),
|
|
enabled=data.get("enabled", True),
|
|
last_run=datetime.fromisoformat(data["last_run"]) if data.get("last_run") else None,
|
|
next_run=datetime.fromisoformat(data["next_run"]) if data.get("next_run") else None,
|
|
run_count=data.get("run_count", 0),
|
|
last_status=data.get("last_status", ""),
|
|
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(),
|
|
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(),
|
|
)
|
|
|
|
def update_next_run(self):
|
|
"""更新下次运行时间"""
|
|
if self.enabled and self.schedule.enabled:
|
|
self.next_run = self.schedule.get_next_run_time(self.last_run)
|
|
else:
|
|
self.next_run = None
|
|
self.updated_at = datetime.now()
|
|
|
|
|
|
class ScheduleStore:
|
|
"""调度任务存储"""
|
|
|
|
def __init__(self, storage_path: Optional[Path] = None):
|
|
if storage_path is None:
|
|
storage_path = Path(__file__).resolve().parents[2] / "scheduled_tasks.json"
|
|
self.storage_path = storage_path
|
|
self.tasks: Dict[str, ScheduledTask] = {}
|
|
self.load()
|
|
|
|
def load(self):
|
|
"""加载任务"""
|
|
if self.storage_path.exists():
|
|
try:
|
|
data = json.loads(self.storage_path.read_text(encoding="utf-8"))
|
|
self.tasks = {
|
|
task_id: ScheduledTask.from_dict(task_data)
|
|
for task_id, task_data in data.get("tasks", {}).items()
|
|
}
|
|
except Exception:
|
|
self.tasks = {}
|
|
|
|
def save(self):
|
|
"""保存任务"""
|
|
data = {
|
|
"tasks": {
|
|
task_id: task.to_dict()
|
|
for task_id, task in self.tasks.items()
|
|
}
|
|
}
|
|
self.storage_path.write_text(
|
|
json.dumps(data, ensure_ascii=False, indent=2),
|
|
encoding="utf-8"
|
|
)
|
|
|
|
def add_task(self, task: ScheduledTask):
|
|
"""添加任务"""
|
|
task.update_next_run()
|
|
self.tasks[task.id] = task
|
|
self.save()
|
|
|
|
def remove_task(self, task_id: str):
|
|
"""移除任务"""
|
|
if task_id in self.tasks:
|
|
del self.tasks[task_id]
|
|
self.save()
|
|
|
|
def update_task(self, task: ScheduledTask):
|
|
"""更新任务"""
|
|
task.update_next_run()
|
|
task.updated_at = datetime.now()
|
|
self.tasks[task.id] = task
|
|
self.save()
|
|
|
|
def get_task(self, task_id: str) -> Optional[ScheduledTask]:
|
|
"""获取任务"""
|
|
return self.tasks.get(task_id)
|
|
|
|
def get_all_tasks(self) -> List[ScheduledTask]:
|
|
"""获取所有任务"""
|
|
return list(self.tasks.values())
|
|
|
|
def get_due_tasks(self) -> List[ScheduledTask]:
|
|
"""获取到期需要执行的任务"""
|
|
now = datetime.now()
|
|
due_tasks = []
|
|
for task in self.tasks.values():
|
|
if task.enabled and task.next_run and task.next_run <= now:
|
|
due_tasks.append(task)
|
|
return due_tasks
|