feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ from ..schemas.schedules import ScheduleConfigSchema
|
||||
from ..schemas.tasks import TaskConfigSchema
|
||||
from .task_queue import task_queue
|
||||
|
||||
from app.trace.decorators import trace_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 调度器轮询间隔(秒)
|
||||
@@ -34,6 +36,23 @@ def _parse_time(time_str: str) -> tuple[int, int]:
|
||||
return int(parts[0]), int(parts[1])
|
||||
|
||||
|
||||
def _convert_interval_to_seconds(value: int, unit: str) -> int:
|
||||
"""将间隔值转换为秒数。
|
||||
|
||||
Args:
|
||||
value: 间隔数值(0 = 无限制)
|
||||
unit: 间隔单位,支持 "minutes"、"hours"、"days"
|
||||
|
||||
Returns:
|
||||
对应的秒数;value <= 0 时返回 0
|
||||
"""
|
||||
if value <= 0:
|
||||
return 0
|
||||
multipliers = {"minutes": 60, "hours": 3600, "days": 86400}
|
||||
return value * multipliers.get(unit, 60)
|
||||
|
||||
|
||||
@trace_service(description_zh="calculate_next_run", description_en="Calculate Next Run")
|
||||
def calculate_next_run(
|
||||
schedule_config: ScheduleConfigSchema,
|
||||
now: datetime | None = None,
|
||||
@@ -188,7 +207,9 @@ class Scheduler:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, site_id, task_config, schedule_config
|
||||
SELECT id, site_id, task_config, schedule_config,
|
||||
min_run_interval_value, min_run_interval_unit,
|
||||
last_run_at, last_status, min_run_intervals
|
||||
FROM scheduled_tasks
|
||||
WHERE enabled = TRUE
|
||||
AND next_run_at IS NOT NULL
|
||||
@@ -198,11 +219,32 @@ class Scheduler:
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for row in rows:
|
||||
task_id = str(row[0])
|
||||
site_id = row[1]
|
||||
task_config_raw = row[2] if isinstance(row[2], dict) else json.loads(row[2])
|
||||
schedule_config_raw = row[3] if isinstance(row[3], dict) else json.loads(row[3])
|
||||
min_interval_value = row[4] or 0
|
||||
min_interval_unit = row[5] or "minutes"
|
||||
last_run_at = row[6]
|
||||
last_status = row[7]
|
||||
# per-task 间隔:取所有任务中最大的间隔作为有效间隔
|
||||
min_run_intervals_raw = row[8] if isinstance(row[8], dict) else json.loads(row[8]) if row[8] else {}
|
||||
|
||||
# 计算有效间隔:per-task 最大值 vs schedule 级别,取较大者
|
||||
effective_interval_seconds = _convert_interval_to_seconds(
|
||||
min_interval_value, min_interval_unit
|
||||
)
|
||||
for _task_code, interval_cfg in min_run_intervals_raw.items():
|
||||
if isinstance(interval_cfg, dict):
|
||||
task_seconds = _convert_interval_to_seconds(
|
||||
interval_cfg.get("value", 0),
|
||||
interval_cfg.get("unit", "minutes"),
|
||||
)
|
||||
if task_seconds > effective_interval_seconds:
|
||||
effective_interval_seconds = task_seconds
|
||||
|
||||
try:
|
||||
config = TaskConfigSchema(**task_config_raw)
|
||||
@@ -211,7 +253,44 @@ class Scheduler:
|
||||
logger.exception("调度任务 [%s] 配置反序列化失败,跳过", task_id)
|
||||
continue
|
||||
|
||||
# 入队
|
||||
# 1. 并发检查:上次仍在运行中 → 跳过
|
||||
if last_status == "running":
|
||||
logger.warning(
|
||||
"调度任务 [%s] skipped_concurrent:上次执行仍在运行中",
|
||||
task_id,
|
||||
)
|
||||
continue
|
||||
|
||||
# 2. 间隔检查:最小运行间隔未到 → 跳过并推进 next_run_at
|
||||
if effective_interval_seconds > 0 and last_run_at is not None:
|
||||
elapsed = (now - last_run_at).total_seconds()
|
||||
if elapsed < effective_interval_seconds:
|
||||
# 推进 next_run_at = last_run_at + interval
|
||||
next_run_at_pushed = last_run_at + timedelta(
|
||||
seconds=effective_interval_seconds
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE scheduled_tasks
|
||||
SET next_run_at = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(next_run_at_pushed, task_id),
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(
|
||||
"调度任务 [%s] skipped_interval:最小间隔未到"
|
||||
"(已过 %.0fs / 需 %ds),next_run_at 推进至 %s",
|
||||
task_id,
|
||||
elapsed,
|
||||
effective_interval_seconds,
|
||||
next_run_at_pushed,
|
||||
)
|
||||
continue
|
||||
|
||||
# 3. 正常入队
|
||||
try:
|
||||
queue_id = task_queue.enqueue(config, site_id, schedule_id=task_id)
|
||||
logger.info(
|
||||
@@ -224,7 +303,6 @@ class Scheduler:
|
||||
continue
|
||||
|
||||
# 更新调度任务状态
|
||||
now = datetime.now(timezone.utc)
|
||||
next_run = calculate_next_run(schedule_cfg, now)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
@@ -269,6 +347,9 @@ class Scheduler:
|
||||
# 在线程池中执行同步数据库操作,避免阻塞事件循环
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, self.check_and_enqueue)
|
||||
# CHANGE 2026-03-23 | 同时检查 trigger_jobs 中到期的 cron/interval 任务
|
||||
from app.services.trigger_scheduler import check_scheduled_jobs
|
||||
await loop.run_in_executor(None, check_scheduled_jobs)
|
||||
except Exception:
|
||||
logger.exception("Scheduler 循环迭代异常")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user