feat(sync): 每小时后台自动同步 + 手动拉取最新接口
- services/scheduler.py:通过 job_locks 表跨 worker 抢占,
gunicorn 多进程下一个周期只跑一次;claim 超时 30 分钟自动释放,
避免 worker 中途挂掉把任务永久卡死
- POST /api/garmin/sync-latest:同步执行,窗口 clamp 到 1..7 天
- GET /api/garmin/auto-sync:返回上次/下次运行时间
- db.py:注释里的分号会被 SCHEMA.split(";") 截断,改为先剥注释再切分
19 项调度器测试,全量 324 项通过
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from auth import require_auth
|
||||
from db import query_one
|
||||
from services import garmin as garmin_svc
|
||||
from services import garmin_auth
|
||||
from services import scheduler
|
||||
|
||||
bp = Blueprint("garmin", __name__)
|
||||
|
||||
@@ -119,3 +120,31 @@ def cancel_login():
|
||||
@require_auth
|
||||
def status():
|
||||
return jsonify(garmin_svc.get_sync_status(g.user_id))
|
||||
|
||||
|
||||
@bp.route("/sync-latest", methods=["POST"])
|
||||
@require_auth
|
||||
def sync_latest():
|
||||
"""Pull just the last couple of days.
|
||||
|
||||
Separate from /sync because it is fast enough to wait for (a few seconds
|
||||
rather than minutes), so the UI can report the result directly instead of
|
||||
handing back a job to poll.
|
||||
"""
|
||||
if not garmin_svc.has_token(g.user_id):
|
||||
return jsonify({"error": "尚未绑定 Garmin 账号"}), 400
|
||||
|
||||
days = request.get_json(silent=True) or {}
|
||||
try:
|
||||
window = max(1, min(int(days.get("days", scheduler.SYNC_DAYS)), 7))
|
||||
except (TypeError, ValueError):
|
||||
window = scheduler.SYNC_DAYS
|
||||
|
||||
return jsonify(garmin_svc.sync_data(g.user_id, {}, days=window))
|
||||
|
||||
|
||||
@bp.route("/auto-sync", methods=["GET"])
|
||||
@require_auth
|
||||
def auto_sync_status():
|
||||
"""When the scheduler last ran and when it runs next."""
|
||||
return jsonify(scheduler.status())
|
||||
|
||||
Reference in New Issue
Block a user