Files
GarminHealthLab/README.md
ericwyuan 9d6ebbe422 ops: 生产从 NAS 整体迁移到甲骨文云主机
NAS 局域网 IP 因重启被 DHCP 换过两次,磁盘/网络稳定性都不如已经跑着好几个
生产服务的甲骨文机器。整体搬迁:应用 + 数据库都搬走,NAS 只保留 Gitea(这个
仓库的源码托管,未动)。

## 迁移过程(已核对无损)

- MariaDB:NAS 导出(10.11 源库,处理了只有新版本才有的 `/*M!999999` 注释)
  → 导入甲骨文 MariaDB 10.3.39,**20 张表逐条精确 COUNT(*) 比对完全一致**
- 冻结 NAS(停服务)后又 dump 一次核对,确认期间零数据差异,才继续删库
- NAS `garmin_health_lab` 已 DROP DATABASE,备份在本地
  `~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`
- 应用部署到 `/opt/garmin-health-lab`,systemd 单元(`ubuntu` 用户,非
  root),和这台机器上的 ai-gateway/auth-hub 同一套约定
- 公网:`https://garmin.zichuan.xyz`,DNS + Caddy 反代 + 自动 TLS,替代原来
  `NAS frpc → 甲骨文:8124` 那条隧道(已从 NAS 的 frpc.toml 精确删除对应段,
  其它转发未动,改完逐条复检过没打断)
- auth-hub 回调地址换成新域名,NAS/旧端口那几条历史回调已清掉
- AI 网关配置改本地回环(网关现在同机了),触发真实生成验证过

## 一个当场拦下来的风险

甲骨文部署完默认开着自动同步。迁移窗口期两边并行跑时,若两边的调度器同时去
刷新 Garmin 令牌,会撞上按账号计算的 SSO 限流(`GarminHealthLab` 仓库
2026-09-03 那次事故的根因,那次修复花了一整天)。确认账号级 auto_sync 设置
本来是关的、这次算侥幸没撞上——不是设计上的保险,所以迁移期间显式在甲骨文这边
加了 `AUTO_SYNC=false`,直接在运行进程里验证过生效,确认 NAS 已冻结、数据无
缺口后才打开。

## 文档 / 脚本同步

CLAUDE.md 明确写过"部署位置会变,排障前先查、不要凭记忆"——这次是第二次踩中
同一类问题(上次是"NAS 有没有生产环境"判断错),所以把 CLAUDE.md / PROGRESS.md
/ README.md / docs/* 里的部署事实全部更新,NAS 时代的 `deploy/` 脚本加废弃
说明保留参考、不删除,新增 `deploy/push_oracle.sh`(当场跑通一次真实部署)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 23:53:15 +08:00

223 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 佳明健康数据分析平台 (Garmin Health Lab)
一个完整的健康数据分析平台用于获取、分析和可视化你的佳明Garmin设备数据。
## 📊 主要功能
- **数据同步**: 通过 Garmin API 自动同步你的健康数据
- **综合分析**: 步数、心率、睡眠、运动、压力等多维度分析
- **数据可视化**: 交互式图表和仪表板展示健康数据趋势
- **智能建议**: 基于数据分析的个性化健康建议
- **数据导出**: 支持数据导出为 CSV/JSON 格式
## 🛠 技术栈
### 前端
- React 18 + TypeScriptCRA / react-scripts 构建)
- Framework7 9`framework7-react`iOS 风格 UI
- Recharts (数据可视化)
- Axios (API 请求JWT 拦截器)
### 后端
- Python 3.10+ + Flask应用工厂
- Gunicorn生产运行甲骨文云主机 :5500Caddy 反代出 `garmin.zichuan.xyz`
- 可插拔数据层SQLite本地开发/ MariaDB生产与应用同机的 10.3.39 库
`garmin_health_lab`,经 PyMySQL/TCP
- 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
```bash
npm install
```
后端Python/Flask建议在虚拟环境中安装
```bash
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
```
### 配置环境变量
复制 `backend/.env.example``backend/.env` 并填入真实值(`.env` 已被
`.gitignore` 忽略,不会进入版本库):
```env
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生产**:运行于甲骨文云主机(`129.146.26.249`)本地 MariaDB
10.3.39,独立账号 `garmin` 经 TCP `127.0.0.1:3306` 连接PyMySQL
独立库 `garmin_health_lab`。完整配置见 `backend/.env.example`
```env
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
```bash
cd backend
source venv/bin/activate
python app.py
# 或生产方式gunicorn wsgi:app -b 0.0.0.0:5000
```
前端ReactCRA 开发服务器proxy 到后端 5000
```bash
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()`,幂等)。
### 冒烟测试
```bash
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](docs/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?}`
- 事件序列 `start``delta`* → `done`,失败时为 `error`
> AI 相关接口全部经由自建 **ai-gateway**OpenAI 兼容,`https://ai.zichuan.xyz/v1`
> 见 `AI_GATEWAY_BASE_URL`)。
> 该网关的主上游是大型推理模型,一次生成实测需 2~5 分钟,因此简报走后台生成 +
> 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎,
> `meta.source` 会说明本次由谁作答。
## 🚀 部署(生产 = 甲骨文云主机2026-09-12 起)
- **位置**`129.146.26.249` `/opt/garmin-health-lab``ubuntu` 用户跑
gunicorn `127.0.0.1:5500`2 workers / 4 threads / --timeout 300
systemd 单元 `garmin-health-lab.service`
- **公网**`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`
- **一键部署**:先 `npm run build`,再 `./deploy/push_oracle.sh`ssh key 同步
backend + 清空重推 `client/build` + pip install + systemctl restart +
PID/health 双校验)。
- **部署排障前**:先读 `PROGRESS.md``deploy/` 脚本确认事实——部署位置换过
一次NAS → 甲骨文),凭记忆断言曾经出过错。
## 🔐 安全说明
- Garmin 账户密码不入库(令牌授权,约一年有效)
- 所有 API 请求需要 JWT 认证auth-hub SSO 签发)
- 敏感数据不在前端存储;`backend/.env` 含密钥,不提交 git
## 📝 开发指南
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md) 与 [ARCHITECTURE.md](./docs/ARCHITECTURE.md)
## 📄 许可证
MIT