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:
胡绍康andClaude Opus 4.6 committed 2026-02-25 08:19:27 +08:00
1 parent fa99c48ef6
commit 2ab1721946
3 files changed
+160 -3

No files matched your search

+1 -1
View File
@@ -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())
+154
View File
@@ -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()
+5 -2
View File
@@ -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