fix: 实机验证发现的七个缺陷
部署与时区 - client/.env.production 写死 REACT_APP_API_URL=/api。之前没有这个文件, 构建靠命令行临时传参,一旦忘了就把开发默认值 localhost:5000 打进包里, 部署后整站 Network Error。 - 新增 lib/day.ts,所有日期改用本地日历日。原先用 toISOString() 取的是 UTC 日期, 在 UTC+8 每天前 8 小时都会少查一天——当天的数据佳明已经有了,应用却够不到。 界面 - 覆盖 Framework7 9 给 .navbar .left/.right 加的 frosted pill, 就是各页右上角和返回键旁边那个半透明椭圆。 - .metric-tab 显式 width:auto。F7 把每个 button 渲染成整宽块元素, 运动详情的四个 Tab 因此竖着堆成四行。 - 主要收益为 UNKNOWN 时不显示该区块,那是「没有结论」的哨兵值。 正确性 - 心率区间百分比改用整次运动时长作分母。原先除以「落在区间内的总时长」, 把低于区间 1 的时间挤掉了:44:06 的登山里区间 1 占 23:03, 手表显示 52%,我算成了 90%。现在对上了。 性能 - 按进程缓存已认证的 Garmin 会话(15 分钟 TTL)。实测 _connect 单次 11 秒, 而七个数据接口加起来才 4 秒——瓶颈全在每次重新认证。 冷启 16s → 热 7s → 命中缓存 0.8s,不再撞客户端超时。 - get_activity_details 的 maxchart 由 2000 降到 500,反正写入时抽稀到 300。 - 重新绑定账号时丢弃缓存会话。 删除前端重做前遗留的 5 个无引用页面文件。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,12 +123,44 @@ def save_token(user_id, token, garmin_email=None):
|
||||
f"ON CONFLICT(user_id) DO UPDATE SET {updates}")
|
||||
execute(sql, [user_id, token, garmin_email,
|
||||
datetime.datetime.utcnow().isoformat(timespec="seconds")])
|
||||
# A re-bind means the old session is stale; the next call must build a
|
||||
# fresh one rather than keep using the session the old token minted.
|
||||
forget_client(user_id)
|
||||
|
||||
|
||||
def has_token(user_id):
|
||||
return load_token(user_id) is not None
|
||||
|
||||
|
||||
# An authenticated client, reused across requests in this process.
|
||||
#
|
||||
# Building one costs ~11s against Garmin — loading the token, refreshing the
|
||||
# OAuth2 grant and fetching the profile — which dwarfed the ~4s of actual data
|
||||
# fetching behind an activity-detail request. The session is a requests.Session
|
||||
# underneath, so it is reusable; it is dropped after CLIENT_TTL so a refreshed
|
||||
# or revoked token is picked up rather than being cached indefinitely.
|
||||
CLIENT_TTL_SECONDS = 900
|
||||
_clients = {}
|
||||
_clients_lock = threading.Lock()
|
||||
|
||||
|
||||
def _cached_client(user_id):
|
||||
entry = _clients.get(user_id)
|
||||
if entry and (datetime.datetime.utcnow() - entry[1]).total_seconds() < CLIENT_TTL_SECONDS:
|
||||
return entry[0]
|
||||
return None
|
||||
|
||||
|
||||
def _cache_client(user_id, client):
|
||||
if user_id:
|
||||
_clients[user_id] = (client, datetime.datetime.utcnow())
|
||||
|
||||
|
||||
def forget_client(user_id):
|
||||
"""Drop the cached session — call after re-binding an account."""
|
||||
_clients.pop(user_id, None)
|
||||
|
||||
|
||||
def _connect(creds, user_id=None):
|
||||
"""Obtain a logged-in Garmin client.
|
||||
|
||||
@@ -138,6 +170,12 @@ def _connect(creds, user_id=None):
|
||||
"EOFError: EOF when reading a line"). Tokens are minted once by
|
||||
`garmin_login.py`, which runs in a terminal where a code can be typed.
|
||||
"""
|
||||
if user_id:
|
||||
with _clients_lock:
|
||||
cached = _cached_client(user_id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
Garmin = _import_garmin()
|
||||
client = Garmin(is_cn=_is_cn())
|
||||
|
||||
@@ -150,6 +188,8 @@ def _connect(creds, user_id=None):
|
||||
# garminconnect builds most of its URLs from display_name, so leaving
|
||||
# it unset sends every request to ".../None".
|
||||
client.display_name = client.garth.profile["displayName"]
|
||||
with _clients_lock:
|
||||
_cache_client(user_id, client)
|
||||
return client
|
||||
|
||||
if not creds.get("garminPassword"):
|
||||
@@ -167,6 +207,8 @@ def _connect(creds, user_id=None):
|
||||
"系统会提示你输入验证码。"
|
||||
) from e
|
||||
_use_api_user_agent(client)
|
||||
with _clients_lock:
|
||||
_cache_client(user_id, client)
|
||||
return client
|
||||
|
||||
|
||||
@@ -519,8 +561,10 @@ def _build_detail(client, activity_id):
|
||||
rest of the page intact rather than fail the request.
|
||||
"""
|
||||
summary = _safe(lambda: client.get_activity_evaluation(activity_id), {}) or {}
|
||||
# 500 is already more samples than the 300 we keep, and asking for 2000
|
||||
# triples the payload for points that get thinned away anyway.
|
||||
details = _safe(
|
||||
lambda: client.get_activity_details(activity_id, maxchart=2000, maxpoly=0), {}
|
||||
lambda: client.get_activity_details(activity_id, maxchart=500, maxpoly=0), {}
|
||||
) or {}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user