ericwyuan 682936b0b6 fix(garmin): 刷新出来的令牌从来没写回库,于是每次连接都重换一次
「又被限流了」的根因找到了,不是请求量,是令牌。

`_connect` 里 `refresh_oauth2()` 换来的新 OAuth2 令牌只活在进程内存里——
`save_token` 只在绑定账号时调用过一次。于是每次客户端缓存过期(15 分钟)、
每个 gunicorn worker、每次部署重启,都从库里读回**同一个已过期的令牌**,
然后再做一次真实 SSO 换令牌。而 SSO 端点是按账号限流最狠的那个,社区报告能
封 48 小时(garth #217、python-garminconnect #337)。我今天为了部署重启了
八次服务,每次都清掉缓存。

- `refresh_oauth2()` 成功后 `_persist_token()` 写回。拆出这个函数是因为它和
  `save_token` 想要的正好相反:重新绑定要作废现有会话,持久化刷新结果必须
  保住刚刚产出它的那个会话
- 写回时不带 garmin_email,否则 upsert 会把绑定邮箱刷成 NULL,数据同步页会
  忘记绑的是哪个账号
- 刷新加进程内锁,并在拿到锁后重读一次库:另一个线程刚换过就直接用它的,
  不再自己去换一次
- 五条测试盯住这个不变量,包括「冷缓存不该再换一次」(这条如果回归,就是同一
  个 bug 再来一遍)

顺带把数据端点也节流了——那是另外一半问题,不是这次的病因,但一天历史要 9 次
调用,730 天全历史 6600 个请求全速打出去,不该指望佳明一直容忍:

- services/garmin_throttle.py:代理包住 client,所有调用(含以后新加的)都经
  同一个收口,按间隔排队并计数
- 0.5s 是查过的:garmin-data-export 默认 0.15s、garmin-connect-scraper 默认
  3s、官方合作方 API 100 次/分钟(0.6s)。依据写在文件顶部
- 单次同步 1200 个请求预算,跑满就干净收尾、下次接着跑(已存的天数本来就跳过)
- 运动详情每次最多补 40 条——新账号几百条,不限量就是一次性打光预算

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 23:24:48 +08:00

佳明健康数据分析平台 (Garmin Health Lab)

一个完整的健康数据分析平台用于获取、分析和可视化你的佳明Garmin设备数据。

📊 主要功能

  • 数据同步: 通过 Garmin API 自动同步你的健康数据
  • 综合分析: 步数、心率、睡眠、运动、压力等多维度分析
  • 数据可视化: 交互式图表和仪表板展示健康数据趋势
  • 智能建议: 基于数据分析的个性化健康建议
  • 数据导出: 支持数据导出为 CSV/JSON 格式

🛠 技术栈

前端

  • React 18 + TypeScriptCRA / react-scripts 构建)
  • Framework7 9framework7-reactiOS 风格 UI
  • Recharts (数据可视化)
  • Axios (API 请求JWT 拦截器)

后端

  • Python 3.10+ + Flask应用工厂
  • Gunicorn生产运行NAS :8124
  • 可插拔数据层SQLite本地开发/ MariaDB生产NAS 10.11 库 garmin_health_lab,经 PyMySQL/socket
  • JWT 鉴权;登录走 auth-hub 统一 SSOOAuth2/OIDC本地密码登录已移除
  • garminconnectGarmin API 集成)

早期 Node.js/TypeScript 后端(server/)已整体重写为 backend/ 下的 Python/Flask 单体,目录已删除。

📁 项目结构

GarminHealthLab/
├── client/                 # 前端应用 (React 18 + TypeScript)
│   └── src/services/api.ts # API 客户端(含 JWT 拦截器)
├── backend/                # 后端应用 (Python + Flask) —— 当前可运行实现
│   ├── app.py              # 应用工厂 / 路由装配
│   ├── wsgi.py             # Gunicorn 入口
│   ├── config.py           # 配置(读 .env
│   ├── db.py               # 可插拔数据层 (SQLite / MariaDB)
│   ├── auth.py             # JWT 签发/校验 + require_auth
│   ├── services/           # 业务逻辑 (health / analysis / garmin)
│   ├── routes/             # 蓝图 (auth / garmin / health / analysis)
│   ├── tests/smoke.py      # 冒烟测试
│   ├── requirements.txt
│   └── .env.example
├── server/                 # (已删除)原 Node/TS 后端,已重写为 backend/
├── docs/                   # 文档
├── package.json           # 工作空间根配置
└── README.md

🚀 快速开始

前置要求

  • Node.js 18+
  • npm 或 yarn
  • Garmin Connect 账户

安装依赖

前端React

npm install

后端Python/Flask建议在虚拟环境中安装

cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt

配置环境变量

复制 backend/.env.examplebackend/.env 并填入真实值(.env 已被 .gitignore 忽略,不会进入版本库):

PORT=5000
DB_TYPE=sqlite                       # 本地开发用 sqlite生产切 mariadb
DATABASE_PATH=./data/health.db
JWT_SECRET=your_jwt_secret_here      # 生产务必更换
CORS_ORIGIN=http://localhost:3000,http://localhost:5173

早期 Node 后端的配置方式已弃用,一律使用 backend/.env

数据库SQLite / MariaDB 可插拔

数据层通过 DB_TYPE 环境变量切换后端,业务代码无需改动

  • SQLite默认本地开发:零配置,由 DATABASE_PATH 指定文件位置。
  • MariaDB生产:运行于 NAS192.168.50.64)本地 MariaDB 10.11root 经 socket /run/mysqld/mysqld10.sock(或 TCP 127.0.0.1:3306连接PyMySQL 独立库 garmin_health_lab。完整配置见 backend/.env.example
DB_TYPE=mariadb
MARIADB_SOCKET=/run/mysqld/mysqld10.sock
MARIADB_HOST=127.0.0.1
MARIADB_PORT=3306
MARIADB_USER=root
MARIADB_PASSWORD=your_production_mariadb_password
MARIADB_DATABASE=garmin_health_lab

复制 backend/.env.examplebackend/.env 并填入真实值;.env 已被 .gitignore 忽略,不会进入版本库。

启动开发服务器

后端Flask端口 5000

cd backend
source venv/bin/activate
python app.py
# 或生产方式gunicorn wsgi:app -b 0.0.0.0:5000

前端ReactCRA 开发服务器proxy 到后端 5000

npm install       # 在仓库根目录执行workspaces 装 client
npm run dev       # 同时起后端 5000 + 前端 3000或分别 dev:backend / dev:client

登录走 auth-hub SSO点登录会跳到统一认证页回跳后后端签 JWT。 数据库首次启动自动建表(db.init_db(),幂等)。

冒烟测试

cd backend
python tests/smoke.py

📚 API 文档

认证auth-hub 统一 SSO

  • POST /api/auth/auth-hub/start - 发起 SSO 登录(返回 auth-hub 授权 URL
  • GET /api/auth/callback?code=… - auth-hub 回调(浏览器跳转,签发 JWT
  • POST /api/auth/refresh - 刷新 Token
  • POST /api/auth/logout - 登出

本地邮箱/密码注册登录已移除;账号体系由 auth-hub 统一提供 (见 AUTH_HUB_INTEGRATION.md)。 Garmin 账号的绑定/解绑走 /api/garmin/*(见下),与 Web 登录相互独立。

Garmin 数据同步

  • POST /api/garmin/login - 发起 Garmin OAuth 授权(需验证码时 POST /api/garmin/mfa 提交)
  • GET /api/garmin/auth-status / GET /api/garmin/login-status - 授权状态
  • DELETE /api/garmin/login / POST /api/garmin/disconnect - 退出/解绑 Garmin 账号
  • POST /api/garmin/sync - 同步 Garmin 数据
  • POST /api/garmin/sync-latest - 仅同步最新数据
  • POST /api/garmin/sync-details - 补齐运动详情/全天曲线(后台队列)
  • GET /api/garmin/status / GET /api/garmin/auto-sync - 同步状态
  • GET /api/garmin/activities/<activity_id>/detail - 单次运动详情

健康数据

  • GET /api/health/summary - 获取健康摘要
  • GET /api/health/steps - 获取步数数据
  • GET /api/health/heart-rate - 获取心率数据
  • GET /api/health/sleep - 获取睡眠数据
  • GET /api/health/activities - 获取运动数据
  • GET /api/health/fitness-age - 身体年龄
  • GET /api/health/series - 全天曲线(心率/压力/身体电量/呼吸/血氧)
  • GET /api/health/body-composition / blood-pressure / race-predictions
  • GET /api/health/badges / personal-records / challenges / devices

分析与建议

  • GET /api/analysis/trends - 获取数据趋势
  • GET /api/analysis/recommendations - 规则引擎健康建议
  • GET /api/analysis/models - 可用大模型及其配置状态
  • GET /api/analysis/ai-recommendations - 大模型健康建议(带缓存)

AI 教练

  • GET /api/analysis/briefing - 晨间简报 + 今日运动处方,附计算出的特征上下文
    • 立即返回。若没有匹配当前数据的模型答案,先返回规则版并带上 meta.pending,模型版本在后台生成,再次请求即可取到
    • ?date= 指定日期(默认最新有数据的一天)、?refresh=1 忽略缓存、 ?wait=1 阻塞等待模型(一次生成 2~5 分钟)
  • GET /api/analysis/trend-insight?metric=&startDate=&endDate= - 对选定区间内 单个指标的变化做归因分析(阻塞,未知指标返回 400 并附 supported 列表)
  • POST /api/analysis/copilot - 健康 Copilot 问答SSE 流式返回
    • 请求体 {question, history?, date?, model?}
    • 事件序列 startdelta* → done,失败时为 error

AI 相关接口全部经由自建 ai-gatewayOpenAI 兼容,https://ai.zichuan.xyz/v1AI_GATEWAY_BASE_URL)。 该网关的主上游是大型推理模型,一次生成实测需 2~5 分钟,因此简报走后台生成 + 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎, meta.source 会说明本次由谁作答。

🚀 部署(生产 = NAS

  • 位置NAS 192.168.50.64 /volume1/web/garmin-health-labroot 跑 gunicorn 0.0.0.0:81242 workers / 4 threads / --timeout 300开机自启 DSM 任务调度器执行 deploy/S99garmin.sh
  • 公网NAS frpc → http://129.146.26.249:8124
  • 一键部署:先 npm run build,再 ./deploy/push.sh(同步 backend + deploy + 清空重推 client/build + sudo 重启 + pid/health 双校验)。
  • 部署排障前:先读 PROGRESS.mddeploy/ 脚本确认事实。

🔐 安全说明

  • Garmin 账户密码不入库(令牌授权,约一年有效)
  • 所有 API 请求需要 JWT 认证auth-hub SSO 签发)
  • 敏感数据不在前端存储;backend/.env 含密钥,不提交 git

📝 开发指南

详见 DEVELOPMENT.mdARCHITECTURE.md

📄 许可证

MIT

Description
No description provided
Readme 9.5 MiB
Languages
JavaScript 61%
Python 22.2%
TypeScript 12.1%
CSS 4.1%
Shell 0.6%