feat: 迁移 scheduler 模块到 controllers 层
- 迁移 api_get_scheduler_status 到 controllers/scheduler.py - 更新 routes/scheduler.py 移除 impl 参数 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa99c48ef6
commit
2ab1721946
3 files changed
+160
-3
No files matched your search
+1
-1
@@ -77,7 +77,7 @@ def create_app(*, autostart_scheduler: Optional[bool] = None):
|
||||
app.register_blueprint(temp_emails.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(oauth.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(settings.create_blueprint())
|
||||
app.register_blueprint(scheduler.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(scheduler.create_blueprint())
|
||||
app.register_blueprint(system.create_blueprint())
|
||||
app.register_blueprint(audit.create_blueprint())
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
from outlook_web import config
|
||||
from outlook_web.db import create_sqlite_connection
|
||||
from outlook_web.repositories import settings as settings_repo
|
||||
from outlook_web.security.auth import login_required
|
||||
|
||||
|
||||
# 常量
|
||||
REFRESH_LOCK_NAME = "token_refresh"
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
"""返回 naive UTC 时间(等价于旧的 datetime.utcnow())"""
|
||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
# ==================== 调度器 API ====================
|
||||
|
||||
|
||||
@login_required
|
||||
def api_get_scheduler_status() -> Any:
|
||||
"""获取调度器/定时刷新状态(用于验证"看起来已开启但实际未运行"的问题)"""
|
||||
conn = create_sqlite_connection()
|
||||
try:
|
||||
enable_scheduled = settings_repo.get_setting('enable_scheduled_refresh', 'true').lower() == 'true'
|
||||
use_cron = settings_repo.get_setting('use_cron_schedule', 'false').lower() == 'true'
|
||||
refresh_interval_days = int(settings_repo.get_setting('refresh_interval_days', '30'))
|
||||
refresh_cron = settings_repo.get_setting('refresh_cron', '0 2 * * *')
|
||||
|
||||
# 心跳
|
||||
heartbeat_row = conn.execute('''
|
||||
SELECT value, updated_at
|
||||
FROM settings
|
||||
WHERE key = 'scheduler_heartbeat'
|
||||
''').fetchone()
|
||||
|
||||
heartbeat = None
|
||||
heartbeat_age_seconds = None
|
||||
if heartbeat_row:
|
||||
try:
|
||||
heartbeat = json.loads(heartbeat_row['value']) if heartbeat_row['value'] else None
|
||||
except Exception:
|
||||
heartbeat = {"raw": heartbeat_row['value']}
|
||||
try:
|
||||
hb_time = datetime.fromisoformat(heartbeat_row['updated_at'])
|
||||
heartbeat_age_seconds = int((utcnow() - hb_time).total_seconds())
|
||||
except Exception:
|
||||
heartbeat_age_seconds = None
|
||||
|
||||
# 锁状态
|
||||
lock_row = conn.execute('''
|
||||
SELECT owner_id, acquired_at, expires_at
|
||||
FROM distributed_locks
|
||||
WHERE name = ?
|
||||
''', (REFRESH_LOCK_NAME,)).fetchone()
|
||||
now_ts = time.time()
|
||||
lock_info = None
|
||||
if lock_row and lock_row['expires_at'] and lock_row['expires_at'] > now_ts:
|
||||
lock_info = {
|
||||
"locked": True,
|
||||
"owner_id": lock_row['owner_id'],
|
||||
"acquired_at": lock_row['acquired_at'],
|
||||
"expires_at": lock_row['expires_at']
|
||||
}
|
||||
else:
|
||||
lock_info = {"locked": False}
|
||||
|
||||
# 最近一次定时刷新(含手动触发 scheduled_manual)
|
||||
last_scheduled_run = conn.execute('''
|
||||
SELECT id, trigger_source, status, started_at, finished_at,
|
||||
total, success_count, failed_count, message, trace_id, requested_by_ip
|
||||
FROM refresh_runs
|
||||
WHERE trigger_source IN ('scheduled', 'scheduled_manual')
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
''').fetchone()
|
||||
|
||||
last_scheduled = dict(last_scheduled_run) if last_scheduled_run else None
|
||||
|
||||
running_run = conn.execute('''
|
||||
SELECT id, trigger_source, status, started_at, total, success_count, failed_count, trace_id
|
||||
FROM refresh_runs
|
||||
WHERE status = 'running'
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
''').fetchone()
|
||||
|
||||
running = dict(running_run) if running_run else None
|
||||
|
||||
# 未来触发时间预览
|
||||
future_runs = []
|
||||
next_due = None
|
||||
if enable_scheduled:
|
||||
if use_cron:
|
||||
try:
|
||||
from croniter import croniter
|
||||
base_time = datetime.now()
|
||||
it = croniter(refresh_cron, base_time)
|
||||
for _ in range(5):
|
||||
future_runs.append(it.get_next(datetime).isoformat())
|
||||
except Exception:
|
||||
future_runs = []
|
||||
else:
|
||||
# 按天数策略:基于最近一次已完成的 scheduled/scheduled_manual 计算 next_due
|
||||
row = conn.execute('''
|
||||
SELECT finished_at
|
||||
FROM refresh_runs
|
||||
WHERE trigger_source IN ('scheduled', 'scheduled_manual')
|
||||
AND status IN ('completed', 'failed')
|
||||
AND finished_at IS NOT NULL
|
||||
ORDER BY finished_at DESC
|
||||
LIMIT 1
|
||||
''').fetchone()
|
||||
last_finished_at = row['finished_at'] if row else None
|
||||
try:
|
||||
last_time = datetime.fromisoformat(last_finished_at) if last_finished_at else None
|
||||
except Exception:
|
||||
last_time = None
|
||||
|
||||
base = last_time if last_time else utcnow()
|
||||
next_due_dt = base + timedelta(days=refresh_interval_days)
|
||||
next_due = next_due_dt.isoformat()
|
||||
future_runs.append(next_due_dt.isoformat())
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'scheduler': {
|
||||
'autostart': config.get_scheduler_autostart_default(),
|
||||
'enabled': enable_scheduled,
|
||||
'use_cron': use_cron,
|
||||
'refresh_cron': refresh_cron,
|
||||
'refresh_interval_days': refresh_interval_days,
|
||||
'future_runs': future_runs,
|
||||
'next_due': next_due,
|
||||
'heartbeat': heartbeat,
|
||||
'heartbeat_updated_at': heartbeat_row['updated_at'] if heartbeat_row else None,
|
||||
'heartbeat_age_seconds': heartbeat_age_seconds
|
||||
},
|
||||
'refresh': {
|
||||
'lock': lock_info,
|
||||
'running': running,
|
||||
'last_scheduled': last_scheduled
|
||||
}
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -2,8 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from outlook_web.controllers import scheduler as scheduler_controller
|
||||
|
||||
def create_blueprint(*, impl) -> Blueprint:
|
||||
|
||||
def create_blueprint() -> Blueprint:
|
||||
"""创建 scheduler Blueprint"""
|
||||
bp = Blueprint("scheduler", __name__)
|
||||
bp.add_url_rule("/api/scheduler/status", view_func=impl.api_get_scheduler_status, methods=["GET"])
|
||||
bp.add_url_rule("/api/scheduler/status", view_func=scheduler_controller.api_get_scheduler_status, methods=["GET"])
|
||||
return bp
|
||||
Reference in new issue
Block a user