用户问:限流了,重新输账号密码验证码换个新令牌行不行。
不行,而且是最糟的一种试法。`garth.login()` 和 `refresh_oauth2()` 打的是
同一个 SSO 端点,流程还更重;限流按**账号**计(不是按 IP、按 UA),换设备
换网络都绕不开;而窗口内每次尝试都会把窗口往后推。
而这正是被卡住时第一个会去试的操作,代码里却只有 `_connect` 的刷新有闸门,
重新绑定那条路照发不误。
- start_login 在 sso 冷却窗口内直接拒绝,不建会话行、不碰网络
- 错误信息说清三件事:为什么现在不试、什么时候恢复、换设备没用
- 路由返 429(请求本身没毛病,是该晚点再来)并带 retryAfterSeconds
- 数据端点的 429 不参与拦截,force 可以推翻
前端补上 UI:报错文案早先承诺了「同步页选择强制重试」,但那个按钮不存在。
现在只在被冷却拒绝之后才出现,样式刻意做得不像第二个「开始同步」——它是给
估算失准时的出口,不是随手可点的第二选择。
顺带修一个正要被我引入的 bug:`onClick={syncHistory}` 会把 MouseEvent 当成
force 传进去,等于每次点开始同步都跳过冷却。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
佳明健康数据分析平台 (Garmin Health Lab)
一个完整的健康数据分析平台,用于获取、分析和可视化你的佳明(Garmin)设备数据。
📊 主要功能
- 数据同步: 通过 Garmin API 自动同步你的健康数据
- 综合分析: 步数、心率、睡眠、运动、压力等多维度分析
- 数据可视化: 交互式图表和仪表板展示健康数据趋势
- 智能建议: 基于数据分析的个性化健康建议
- 数据导出: 支持数据导出为 CSV/JSON 格式
🛠 技术栈
前端
- React 18 + TypeScript(CRA / react-scripts 构建)
- Framework7 9(
framework7-react,iOS 风格 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 统一 SSO(OAuth2/OIDC,本地密码登录已移除)
- garminconnect(Garmin 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.example 为 backend/.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(生产):运行于 NAS(192.168.50.64)本地 MariaDB 10.11,root 经
socket
/run/mysqld/mysqld10.sock(或 TCP127.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.example为backend/.env并填入真实值;.env已被.gitignore忽略,不会进入版本库。
启动开发服务器
后端(Flask,端口 5000):
cd backend
source venv/bin/activate
python app.py
# 或生产方式:gunicorn wsgi:app -b 0.0.0.0:5000
前端(React,CRA 开发服务器,proxy 到后端 5000):
npm install # 在仓库根目录执行(workspaces 装 client)
npm run dev # 同时起后端 5000 + 前端 3000(或分别 dev:backend / dev:client)
- 前端: http://localhost:3000
- 后端 API: http://localhost:5000/api
登录走 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- 刷新 TokenPOST /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-predictionsGET /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?} - 事件序列
start→delta* →done,失败时为error
- 请求体
AI 相关接口全部经由自建 ai-gateway(OpenAI 兼容,
https://ai.zichuan.xyz/v1, 见AI_GATEWAY_BASE_URL)。 该网关的主上游是大型推理模型,一次生成实测需 2~5 分钟,因此简报走后台生成 + 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎,meta.source会说明本次由谁作答。
🚀 部署(生产 = NAS)
- 位置:NAS
192.168.50.64/volume1/web/garmin-health-lab,root 跑 gunicorn0.0.0.0:8124(2 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.md与deploy/脚本确认事实。
🔐 安全说明
- Garmin 账户密码不入库(令牌授权,约一年有效)
- 所有 API 请求需要 JWT 认证(auth-hub SSO 签发)
- 敏感数据不在前端存储;
backend/.env含密钥,不提交 git
📝 开发指南
详见 DEVELOPMENT.md 与 ARCHITECTURE.md
📄 许可证
MIT