fix: 修复定时任务模型字段与数据库表不匹配的问题
All checks were successful
continuous-integration/drone/push Build is passing

- task_type -> execution_type
- status -> is_enabled
- 移除不存在的字段 webhook_method, webhook_headers, script_timeout
- time_points/input_params 适配 JSON 类型
This commit is contained in:
2026-01-28 16:45:58 +08:00
parent 104487f082
commit d9fa9708ce
3 changed files with 66 additions and 81 deletions

View File

@@ -1,6 +1,6 @@
"""定时任务相关模型""" """定时任务相关模型"""
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, BigInteger, Integer, String, Text, Enum, SmallInteger, TIMESTAMP, DateTime from sqlalchemy import Column, BigInteger, Integer, String, Text, Enum, SmallInteger, TIMESTAMP, DateTime, JSON, Boolean
from ..database import Base from ..database import Base
@@ -11,40 +11,43 @@ class ScheduledTask(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
tenant_id = Column(String(50)) tenant_id = Column(String(50))
task_name = Column(String(100), nullable=False) task_name = Column(String(100), nullable=False)
task_type = Column(Enum('webhook', 'script'), nullable=False, default='script') task_desc = Column(String(500))
# 调度配置 # 调度配置
schedule_type = Column(Enum('simple', 'cron'), nullable=False, default='simple') schedule_type = Column(Enum('simple', 'cron'), nullable=False, default='simple')
time_points = Column(Text) # JSON数组 ["08:00", "12:00"] time_points = Column(JSON) # JSON数组 ["08:00", "12:00"]
cron_expression = Column(String(100)) cron_expression = Column(String(100))
timezone = Column(String(50), default='Asia/Shanghai')
# 执行类型
execution_type = Column(Enum('webhook', 'script'), nullable=False, default='script')
# Webhook配置 # Webhook配置
webhook_url = Column(String(500)) webhook_url = Column(String(500))
webhook_method = Column(String(10), default='POST')
webhook_headers = Column(Text) # JSON格式
# 脚本配置 # 脚本配置
script_content = Column(Text) script_content = Column(Text)
script_timeout = Column(Integer, default=300) script_deps = Column(Text) # 脚本依赖
# 输入参数 # 输入参数
input_params = Column(Text) # JSON格式 input_params = Column(JSON) # JSON格式
# 重试配置 # 重试配置
retry_count = Column(Integer, default=0) retry_count = Column(Integer, default=0)
retry_interval = Column(Integer, default=60) retry_interval = Column(Integer, default=60)
# 告警配置 # 告警配置
alert_on_failure = Column(SmallInteger, default=0) alert_on_failure = Column(Boolean, default=False)
alert_webhook = Column(String(500)) alert_webhook = Column(String(500))
# 状态 # 状态
status = Column(SmallInteger, default=1) # 0-禁用 1-启用 is_enabled = Column(Boolean, default=True)
last_run_at = Column(DateTime) last_run_at = Column(DateTime)
last_run_status = Column(String(20)) last_run_status = Column(Enum('success', 'failed', 'running'))
last_run_message = Column(Text)
created_at = Column(TIMESTAMP, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class TaskLog(Base): class TaskLog(Base):

View File

@@ -21,15 +21,13 @@ router = APIRouter(prefix="/api/scheduled-tasks", tags=["scheduled-tasks"])
class TaskCreate(BaseModel): class TaskCreate(BaseModel):
tenant_id: Optional[str] = None tenant_id: Optional[str] = None
task_name: str task_name: str
task_type: str = 'script' task_desc: Optional[str] = None
execution_type: str = 'script'
schedule_type: str = 'simple' schedule_type: str = 'simple'
time_points: Optional[List[str]] = None time_points: Optional[List[str]] = None
cron_expression: Optional[str] = None cron_expression: Optional[str] = None
webhook_url: Optional[str] = None webhook_url: Optional[str] = None
webhook_method: Optional[str] = 'POST'
webhook_headers: Optional[dict] = None
script_content: Optional[str] = None script_content: Optional[str] = None
script_timeout: Optional[int] = 300
input_params: Optional[dict] = None input_params: Optional[dict] = None
retry_count: Optional[int] = 0 retry_count: Optional[int] = 0
retry_interval: Optional[int] = 60 retry_interval: Optional[int] = 60
@@ -40,21 +38,19 @@ class TaskCreate(BaseModel):
class TaskUpdate(BaseModel): class TaskUpdate(BaseModel):
tenant_id: Optional[str] = None tenant_id: Optional[str] = None
task_name: Optional[str] = None task_name: Optional[str] = None
task_type: Optional[str] = None task_desc: Optional[str] = None
execution_type: Optional[str] = None
schedule_type: Optional[str] = None schedule_type: Optional[str] = None
time_points: Optional[List[str]] = None time_points: Optional[List[str]] = None
cron_expression: Optional[str] = None cron_expression: Optional[str] = None
webhook_url: Optional[str] = None webhook_url: Optional[str] = None
webhook_method: Optional[str] = None
webhook_headers: Optional[dict] = None
script_content: Optional[str] = None script_content: Optional[str] = None
script_timeout: Optional[int] = None
input_params: Optional[dict] = None input_params: Optional[dict] = None
retry_count: Optional[int] = None retry_count: Optional[int] = None
retry_interval: Optional[int] = None retry_interval: Optional[int] = None
alert_on_failure: Optional[bool] = None alert_on_failure: Optional[bool] = None
alert_webhook: Optional[str] = None alert_webhook: Optional[str] = None
status: Optional[int] = None is_enabled: Optional[bool] = None
class SecretCreate(BaseModel): class SecretCreate(BaseModel):
@@ -91,7 +87,8 @@ async def list_tasks(
if tenant_id: if tenant_id:
query = query.filter(ScheduledTask.tenant_id == tenant_id) query = query.filter(ScheduledTask.tenant_id == tenant_id)
if status is not None: if status is not None:
query = query.filter(ScheduledTask.status == status) is_enabled = status == 1
query = query.filter(ScheduledTask.is_enabled == is_enabled)
total = query.count() total = query.count()
items = query.order_by(desc(ScheduledTask.created_at)).offset((page - 1) * size).limit(size).all() items = query.order_by(desc(ScheduledTask.created_at)).offset((page - 1) * size).limit(size).all()
@@ -117,21 +114,19 @@ async def create_task(data: TaskCreate, db: Session = Depends(get_db)):
task = ScheduledTask( task = ScheduledTask(
tenant_id=data.tenant_id, tenant_id=data.tenant_id,
task_name=data.task_name, task_name=data.task_name,
task_type=data.task_type, task_desc=data.task_desc,
execution_type=data.execution_type,
schedule_type=data.schedule_type, schedule_type=data.schedule_type,
time_points=json.dumps(data.time_points) if data.time_points else None, time_points=data.time_points,
cron_expression=data.cron_expression, cron_expression=data.cron_expression,
webhook_url=data.webhook_url, webhook_url=data.webhook_url,
webhook_method=data.webhook_method,
webhook_headers=json.dumps(data.webhook_headers) if data.webhook_headers else None,
script_content=data.script_content, script_content=data.script_content,
script_timeout=data.script_timeout, input_params=data.input_params,
input_params=json.dumps(data.input_params) if data.input_params else None,
retry_count=data.retry_count, retry_count=data.retry_count,
retry_interval=data.retry_interval, retry_interval=data.retry_interval,
alert_on_failure=1 if data.alert_on_failure else 0, alert_on_failure=data.alert_on_failure,
alert_webhook=data.alert_webhook, alert_webhook=data.alert_webhook,
status=1 is_enabled=True
) )
db.add(task) db.add(task)
@@ -156,41 +151,37 @@ async def update_task(task_id: int, data: TaskUpdate, db: Session = Depends(get_
task.tenant_id = data.tenant_id task.tenant_id = data.tenant_id
if data.task_name is not None: if data.task_name is not None:
task.task_name = data.task_name task.task_name = data.task_name
if data.task_type is not None: if data.task_desc is not None:
task.task_type = data.task_type task.task_desc = data.task_desc
if data.execution_type is not None:
task.execution_type = data.execution_type
if data.schedule_type is not None: if data.schedule_type is not None:
task.schedule_type = data.schedule_type task.schedule_type = data.schedule_type
if data.time_points is not None: if data.time_points is not None:
task.time_points = json.dumps(data.time_points) task.time_points = data.time_points
if data.cron_expression is not None: if data.cron_expression is not None:
task.cron_expression = data.cron_expression task.cron_expression = data.cron_expression
if data.webhook_url is not None: if data.webhook_url is not None:
task.webhook_url = data.webhook_url task.webhook_url = data.webhook_url
if data.webhook_method is not None:
task.webhook_method = data.webhook_method
if data.webhook_headers is not None:
task.webhook_headers = json.dumps(data.webhook_headers)
if data.script_content is not None: if data.script_content is not None:
task.script_content = data.script_content task.script_content = data.script_content
if data.script_timeout is not None:
task.script_timeout = data.script_timeout
if data.input_params is not None: if data.input_params is not None:
task.input_params = json.dumps(data.input_params) task.input_params = data.input_params
if data.retry_count is not None: if data.retry_count is not None:
task.retry_count = data.retry_count task.retry_count = data.retry_count
if data.retry_interval is not None: if data.retry_interval is not None:
task.retry_interval = data.retry_interval task.retry_interval = data.retry_interval
if data.alert_on_failure is not None: if data.alert_on_failure is not None:
task.alert_on_failure = 1 if data.alert_on_failure else 0 task.alert_on_failure = data.alert_on_failure
if data.alert_webhook is not None: if data.alert_webhook is not None:
task.alert_webhook = data.alert_webhook task.alert_webhook = data.alert_webhook
if data.status is not None: if data.is_enabled is not None:
task.status = data.status task.is_enabled = data.is_enabled
db.commit() db.commit()
# 更新调度器 # 更新调度器
if task.status == 1: if task.is_enabled:
scheduler_service.add_task(task.id) scheduler_service.add_task(task.id)
else: else:
scheduler_service.remove_task(task.id) scheduler_service.remove_task(task.id)
@@ -224,15 +215,15 @@ async def toggle_task(task_id: int, db: Session = Depends(get_db)):
if not task: if not task:
raise HTTPException(status_code=404, detail="任务不存在") raise HTTPException(status_code=404, detail="任务不存在")
task.status = 0 if task.status == 1 else 1 task.is_enabled = not task.is_enabled
db.commit() db.commit()
if task.status == 1: if task.is_enabled:
scheduler_service.add_task(task.id) scheduler_service.add_task(task.id)
else: else:
scheduler_service.remove_task(task.id) scheduler_service.remove_task(task.id)
return {"success": True, "status": task.status} return {"success": True, "status": 1 if task.is_enabled else 0}
@router.post("/{task_id}/run") @router.post("/{task_id}/run")
@@ -492,15 +483,22 @@ async def delete_secret(secret_id: int, db: Session = Depends(get_db)):
def format_task(task: ScheduledTask, include_content: bool = False) -> dict: def format_task(task: ScheduledTask, include_content: bool = False) -> dict:
"""格式化任务数据""" """格式化任务数据"""
time_points = task.time_points
if isinstance(time_points, str):
try:
time_points = json.loads(time_points)
except:
time_points = []
data = { data = {
"id": task.id, "id": task.id,
"tenant_id": task.tenant_id, "tenant_id": task.tenant_id,
"task_name": task.task_name, "task_name": task.task_name,
"task_type": task.task_type, "task_type": task.execution_type, # 前端使用 task_type
"schedule_type": task.schedule_type, "schedule_type": task.schedule_type,
"time_points": json.loads(task.time_points) if task.time_points else [], "time_points": time_points or [],
"cron_expression": task.cron_expression, "cron_expression": task.cron_expression,
"status": task.status, "status": 1 if task.is_enabled else 0, # 前端使用 status
"last_run_at": task.last_run_at, "last_run_at": task.last_run_at,
"last_run_status": task.last_run_status, "last_run_status": task.last_run_status,
"retry_count": task.retry_count, "retry_count": task.retry_count,
@@ -513,11 +511,14 @@ def format_task(task: ScheduledTask, include_content: bool = False) -> dict:
if include_content: if include_content:
data["webhook_url"] = task.webhook_url data["webhook_url"] = task.webhook_url
data["webhook_method"] = task.webhook_method
data["webhook_headers"] = json.loads(task.webhook_headers) if task.webhook_headers else None
data["script_content"] = task.script_content data["script_content"] = task.script_content
data["script_timeout"] = task.script_timeout input_params = task.input_params
data["input_params"] = json.loads(task.input_params) if task.input_params else None if isinstance(input_params, str):
try:
input_params = json.loads(input_params)
except:
input_params = None
data["input_params"] = input_params
return data return data

View File

@@ -49,7 +49,7 @@ class SchedulerService:
"""从数据库加载所有启用的任务""" """从数据库加载所有启用的任务"""
db = SessionLocal() db = SessionLocal()
try: try:
tasks = db.query(ScheduledTask).filter(ScheduledTask.status == 1).all() tasks = db.query(ScheduledTask).filter(ScheduledTask.is_enabled == True).all()
for task in tasks: for task in tasks:
self._add_task_to_scheduler(task) self._add_task_to_scheduler(task)
print(f"已加载 {len(tasks)} 个定时任务") print(f"已加载 {len(tasks)} 个定时任务")
@@ -81,7 +81,7 @@ class SchedulerService:
elif task.schedule_type == 'simple' and task.time_points: elif task.schedule_type == 'simple' and task.time_points:
# 简单模式 - 多个时间点 # 简单模式 - 多个时间点
try: try:
time_points = json.loads(task.time_points) time_points = task.time_points if isinstance(task.time_points, list) else json.loads(task.time_points)
for i, time_point in enumerate(time_points): for i, time_point in enumerate(time_points):
hour, minute = map(int, time_point.split(':')) hour, minute = map(int, time_point.split(':'))
sub_job_id = f"{job_id}_{i}" sub_job_id = f"{job_id}_{i}"
@@ -100,7 +100,7 @@ class SchedulerService:
db = SessionLocal() db = SessionLocal()
try: try:
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first() task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if task and task.status == 1: if task and task.is_enabled:
self._add_task_to_scheduler(task) self._add_task_to_scheduler(task)
finally: finally:
db.close() db.close()
@@ -172,12 +172,9 @@ class SchedulerService:
# 解析输入参数 # 解析输入参数
params = {} params = {}
if task.input_params: if task.input_params:
try: params = task.input_params if isinstance(task.input_params, dict) else {}
params = json.loads(task.input_params)
except:
pass
if task.task_type == 'webhook': if task.execution_type == 'webhook':
success, output, error = await self._execute_webhook(task) success, output, error = await self._execute_webhook(task)
else: else:
success, output, error = await self._execute_script(db, task, trace_id, params) success, output, error = await self._execute_script(db, task, trace_id, params)
@@ -206,20 +203,12 @@ class SchedulerService:
async def _execute_webhook(self, task: ScheduledTask): async def _execute_webhook(self, task: ScheduledTask):
"""执行Webhook任务""" """执行Webhook任务"""
try: try:
headers = {}
if task.webhook_headers:
headers = json.loads(task.webhook_headers)
body = {} body = {}
if task.input_params: if task.input_params:
body = json.loads(task.input_params) body = task.input_params if isinstance(task.input_params, dict) else {}
async with httpx.AsyncClient(timeout=30) as client: async with httpx.AsyncClient(timeout=30) as client:
if task.webhook_method.upper() == 'GET': response = await client.post(task.webhook_url, json=body)
response = await client.get(task.webhook_url, headers=headers, params=body)
else:
response = await client.post(task.webhook_url, headers=headers, json=body)
response.raise_for_status() response.raise_for_status()
return True, response.text[:5000], '' return True, response.text[:5000], ''
@@ -238,7 +227,7 @@ class SchedulerService:
tenant_id=task.tenant_id, tenant_id=task.tenant_id,
trace_id=trace_id, trace_id=trace_id,
params=params, params=params,
timeout=task.script_timeout or 300 timeout=300 # 默认超时
) )
return success, output, error return success, output, error
@@ -285,14 +274,6 @@ class SchedulerService:
if not task: if not task:
return {"success": False, "error": "任务不存在"} return {"success": False, "error": "任务不存在"}
# 解析参数
params = {}
if task.input_params:
try:
params = json.loads(task.input_params)
except:
pass
success, output, error = await self._execute_task_once(db, task) success, output, error = await self._execute_task_once(db, task)
return { return {