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>
223 lines
8.9 KiB
Markdown
223 lines
8.9 KiB
Markdown
# 佳明健康数据分析平台 (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(生产运行,甲骨文云主机 :5500,Caddy 反代出 `garmin.zichuan.xyz`)
|
||
- 可插拔数据层:SQLite(本地开发)/ MariaDB(生产,与应用同机的 10.3.39 库
|
||
`garmin_health_lab`,经 PyMySQL/TCP)
|
||
- 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):
|
||
```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
|
||
```
|
||
|
||
前端(React,CRA 开发服务器,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
|