fix(sync): 限流拦截不再把'上次同步'刷成现在 + 设置页新增立即同步按钮
- _set_sync_status 支持保留旧 last_sync_time;两处限流拦截(start_sync / sync_data)改为传旧值——被拒的同步零请求发生,不能把 UI 的'上次同步' 拨快成'刚刚',掩盖数据早已停更的事实 - SettingsPage 同步区块新增'立即同步'行:走 /garmin/sync-latest(最近2天, 秒级阻塞返回),成功/限流/未绑定三种状态都有明确反馈;限流中后端零请求 拦截并回传恢复倒计时文案
This commit is contained in:
@@ -34,7 +34,15 @@ from services import garmin_extras as extras
|
|||||||
DEFAULT_SYNC_DAYS = int(os.environ.get("GARMIN_SYNC_DAYS") or 7)
|
DEFAULT_SYNC_DAYS = int(os.environ.get("GARMIN_SYNC_DAYS") or 7)
|
||||||
|
|
||||||
|
|
||||||
def _set_sync_status(user_id, status, now, **fields):
|
def _set_sync_status(user_id, status, now, last_sync_time=None, **fields):
|
||||||
|
"""Write the status row; `now` becomes the new last_sync_time by default.
|
||||||
|
|
||||||
|
Callers that merely *report* being blocked (a rate-limit refusal, a
|
||||||
|
refused start) pass the previous last_sync_time explicitly: a sync that
|
||||||
|
never happened must not move "上次同步" forward, or the UI reads "刚刚"
|
||||||
|
while the data actually stopped syncing hours ago.
|
||||||
|
"""
|
||||||
|
last = last_sync_time if last_sync_time is not None else now
|
||||||
cols = ["user_id", "status", "last_sync_time"] + list(fields.keys())
|
cols = ["user_id", "status", "last_sync_time"] + list(fields.keys())
|
||||||
placeholders = ", ".join(["?"] * len(cols))
|
placeholders = ", ".join(["?"] * len(cols))
|
||||||
if DB_TYPE == "mariadb":
|
if DB_TYPE == "mariadb":
|
||||||
@@ -49,7 +57,7 @@ def _set_sync_status(user_id, status, now, **fields):
|
|||||||
f"INSERT INTO sync_status ({', '.join(cols)}) VALUES ({placeholders}) "
|
f"INSERT INTO sync_status ({', '.join(cols)}) VALUES ({placeholders}) "
|
||||||
f"ON CONFLICT(user_id) DO UPDATE SET {updates}"
|
f"ON CONFLICT(user_id) DO UPDATE SET {updates}"
|
||||||
)
|
)
|
||||||
execute(sql, [user_id, status, now] + list(fields.values()))
|
execute(sql, [user_id, status, last] + list(fields.values()))
|
||||||
|
|
||||||
|
|
||||||
def get_sync_status(user_id):
|
def get_sync_status(user_id):
|
||||||
@@ -971,8 +979,15 @@ def start_sync(user_id, creds, days=None):
|
|||||||
now = datetime.datetime.utcnow().isoformat(timespec="seconds")
|
now = datetime.datetime.utcnow().isoformat(timespec="seconds")
|
||||||
until, msg = _rate_limit_block(user_id)
|
until, msg = _rate_limit_block(user_id)
|
||||||
if until:
|
if until:
|
||||||
|
# A refused start synced nothing — keep the previous last_sync_time so
|
||||||
|
# the UI's 上次同步 keeps telling the truth (the data is stale since
|
||||||
|
# the last *successful* sync, and that date is what the user needs).
|
||||||
|
row = query_one(
|
||||||
|
"SELECT last_sync_time FROM sync_status WHERE user_id = ?", [user_id]
|
||||||
|
)
|
||||||
_set_sync_status(
|
_set_sync_status(
|
||||||
user_id, "rate_limited", now,
|
user_id, "rate_limited", now,
|
||||||
|
last_sync_time=row["last_sync_time"] if row else None,
|
||||||
records_synced=0, progress_current=0, progress_total=days,
|
records_synced=0, progress_current=0, progress_total=days,
|
||||||
started_at=now, last_error=msg,
|
started_at=now, last_error=msg,
|
||||||
)
|
)
|
||||||
@@ -1005,9 +1020,14 @@ def sync_data(user_id, creds, days=None, client=None):
|
|||||||
until, msg = _rate_limit_block(user_id)
|
until, msg = _rate_limit_block(user_id)
|
||||||
if until:
|
if until:
|
||||||
# A blocked account must issue zero Garmin requests — that is the whole
|
# A blocked account must issue zero Garmin requests — that is the whole
|
||||||
# point. Return immediately without touching the network.
|
# point. Return immediately without touching the network, and keep the
|
||||||
|
# previous last_sync_time (this was not a sync).
|
||||||
|
row = query_one(
|
||||||
|
"SELECT last_sync_time FROM sync_status WHERE user_id = ?", [user_id]
|
||||||
|
)
|
||||||
_set_sync_status(
|
_set_sync_status(
|
||||||
user_id, "rate_limited", now,
|
user_id, "rate_limited", now,
|
||||||
|
last_sync_time=row["last_sync_time"] if row else None,
|
||||||
records_synced=0, last_error=msg,
|
records_synced=0, last_error=msg,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ function SettingsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [saved, setSaved] = useState('');
|
const [saved, setSaved] = useState('');
|
||||||
|
/** A sync is running right now (blocks the button). */
|
||||||
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
/** A Garmin account is bound; sync-latest refuses to run without one. */
|
||||||
|
const [hasToken, setHasToken] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([apiClient.getSettings(), apiClient.getSettingsOptions()])
|
Promise.all([apiClient.getSettings(), apiClient.getSettingsOptions()])
|
||||||
@@ -36,6 +40,8 @@ function SettingsPage() {
|
|||||||
.catch((err) => setError(errorMessage(err, '加载设置失败')))
|
.catch((err) => setError(errorMessage(err, '加载设置失败')))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
|
||||||
|
apiClient.getGarminAuthStatus().then(setHasToken).catch(() => setHasToken(false));
|
||||||
|
|
||||||
if (FEATURES.ai) {
|
if (FEATURES.ai) {
|
||||||
apiClient.getModels().then(setModels).catch(() => setModels([]));
|
apiClient.getModels().then(setModels).catch(() => setModels([]));
|
||||||
}
|
}
|
||||||
@@ -64,6 +70,30 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 立即同步最近几天(秒级阻塞返回,不用进同步页)。
|
||||||
|
/garmin/sync-latest 只做最近 1-7 天的增量拉取,速度足够直接等待;
|
||||||
|
正在限流冷却时后端零请求直接拦截并返回恢复倒计时。 */
|
||||||
|
const syncNow = async () => {
|
||||||
|
setError('');
|
||||||
|
setSaved('');
|
||||||
|
setSyncing(true);
|
||||||
|
try {
|
||||||
|
const res = await apiClient.syncLatest(2);
|
||||||
|
if (res.status === 'rate_limited') {
|
||||||
|
setError(res.message || 'Garmin 正在限流,请耐心等待自动恢复。');
|
||||||
|
} else if (res.status === 'success') {
|
||||||
|
setSaved(res.message || '已同步最近数据');
|
||||||
|
window.setTimeout(() => setSaved(''), 3200);
|
||||||
|
} else {
|
||||||
|
setError(res.message || '同步没有完成,去同步页看具体原因。');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(errorMessage(err, '同步失败'));
|
||||||
|
} finally {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pick = (
|
const pick = (
|
||||||
title: string,
|
title: string,
|
||||||
values: Array<string | number>,
|
values: Array<string | number>,
|
||||||
@@ -224,6 +254,24 @@ function SettingsPage() {
|
|||||||
<span className="set-chevron" aria-hidden="true">›</span>
|
<span className="set-chevron" aria-hidden="true">›</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="set-row"
|
||||||
|
onClick={syncNow}
|
||||||
|
disabled={syncing || !hasToken}
|
||||||
|
>
|
||||||
|
<span className="set-label">
|
||||||
|
立即同步
|
||||||
|
<span className="set-sub">
|
||||||
|
{!hasToken
|
||||||
|
? '还没绑定 Garmin 账号,先在下面的「数据同步」里绑定'
|
||||||
|
: '不等自动同步,马上拉取最近 2 天的数据'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="set-value">
|
||||||
|
{syncing ? '正在同步…' : ''}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Link href="/sync/" className="set-row">
|
<Link href="/sync/" className="set-row">
|
||||||
<span className="set-label">
|
<span className="set-label">
|
||||||
数据同步
|
数据同步
|
||||||
|
|||||||
Reference in New Issue
Block a user