Files
GarminHealthLab/docs/ARCHITECTURE.md
ericwyuan 2d9a2be185 docs: 文档/配置/测试同步 NAS :8124 生产现状,清除 Node.js 与甲骨文残留
背景:文档停在 Node.js 时代或甲骨文 8123 部署,与生产(NAS :8124 + Flask +
auth-hub + ai-gateway)严重脱节,曾导致凭旧记忆误判'无线上环境'。

- CLAUDE.md 重写:技术栈/结构/命令/部署事实/关键坑(F7 button、UTC 日期、
  429 退避以 DB 为准、迁移幂等、AI 生成耗时)
- docs/ARCHITECTURE.md 重写为 Flask 蓝图+services+可插拔数据层 + NAS 部署
- docs/DEVELOPMENT.md 重写为 Flask/CRA 开发指南 + push.sh 部署流程
- docs/REQUIREMENTS.md:部署条目改 NAS 8124;补 auth-hub/AI 教练/新修复
- docs/AUTH_HUB_INTEGRATION.md 新增(补 .env.example 悬空引用)
- README.md:技术栈/DB/auth-hub/API 清单/部署节修正
- backend/config.py 与 .env.example:AUTH_HUB_REDIRECT_URI 默认 8123→8124,
  MariaDB 注释 Oracle→NAS
- tests:GatewayCourtesy 并发测试对齐 MAX_CONCURRENT(AI_JOB_CONCURRENCY=2);
  conftest 禁用 create_app 后台队列线程,修整库测试 flaky(585 passed)
2026-09-02 19:34:32 +08:00

161 lines
11 KiB
Markdown
Raw 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.
# 项目架构说明
> 现状2026-09-02后端为 **Python/Flask 单体**,生产部署在 **NAS :8124**。
> 早期 Node.js/Express + `server/` 目录的后端已被整体重写为
> `backend/`Python本文件描述的是当前实现。
## 整体架构
```
┌──────────────────────────────────────────────────────────┐
│ 浏览器 (React SPA) │
│ Framework7 9 (iOS 主题) + Recharts单端口同源部署 │
│ ┌──────┬──────┬──────┬──────┬──────────────┐ │
│ │ 今日 │ 健康 │ 趋势 │ 运动 │ 设置 / 更多… │ │
│ └──────┴──────┴──────┴──────┴──────────────┘ │
└───────────────┬──────────────────────────────────────────┘
│ HTTP/JSON + JWTBearerCopilot 用 SSE
┌──────────────────────────────────────────────────────────┐
│ Flask 单体gunicorn 2w/4tNAS :8124
│ ┌────────────────────────────────────────────────────┐ │
│ │ 蓝图层 routes/ /api/auth /api/garmin /api/health │ │
│ │ /api/analysis /api/settings │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ 服务层 services/ 业务逻辑(无 Flask 依赖) │ │
│ │ auth_hub_client garmin garmin_auth garmin_extras │ │
│ │ scheduler(定时同步) ai/coach/insights/jobs(AI队列) │ │
│ │ health analysis fitness_age settings scopes │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ 横切auth.py(JWT+require_auth) config.py(配置) │ │
│ │ db.py(可插拔数据层+幂等迁移) │ │
│ └────────────────────────────────────────────────────┘ │
└───────┬──────────────────────────────┬──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────────────────┐
│ SQLite (开发) │ │ MariaDB (生产, NAS 10.11) │
│ data/health.db │ │ garmin_health_lab 库 │
│ DB_TYPE=sqlite │ │ root@socket mysqld10.sock │
└──────────────────┘ └─────────────────────────────┘
▲ ▲
│ │
│ 外部系统(经 services/ 调用)│
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ auth-hub SSO Garmin Cloud API ai-gateway │
│ 129.146.26.249: (OAuth, 账号级限流 (https://ai.zichuan │
│ 5300 OIDC 429 → 退避24h) .xyz/v1, NVIDIA推理)│
└──────────────────────────────────────────────────────────┘
```
- 单端口部署Flask 既出 API 又托管 React 构建产物(`STATIC_DIR`),前端 API
请求为**同源 `/api/*`**`client/.env.production` 写死 `REACT_APP_API_URL=/api`)。
- SPA 路由兜底404 handler 对非 `/api/` 路径回退 `index.html`,页面刷新不 404。
## 后端分层
### routes/ — 蓝图(只做参数校验与序列化,逻辑在 services
| 蓝图 | 前缀 | 主要端点 |
|---|---|---|
| `auth` | `/api/auth` | SSO 登录 `POST /auth-hub/start``GET /callback``POST /logout``/refresh` |
| `garmin` | `/api/garmin` | `POST /sync``/sync-latest``/sync-details``GET /auth-status``/login-status``/status``/auto-sync``/activities/<id>/detail``POST /login``/mfa``/disconnect``DELETE /login` |
| `health` | `/api/health` | `summary` `steps` `heart-rate` `sleep` `activities` `fitness-age` `badges` `personal-records` `body-composition` `blood-pressure` `race-predictions` `series` `challenges` `devices` |
| `analysis` | `/api/analysis` | `trends` `recommendations` `models` `ai-recommendations` `briefing` `trend-insight` `copilot`(SSE) `insight` `insight/queue` |
| `settings` | `/api/settings` | `GET/PUT ""` `GET /options` `GET /rating-basis` |
另有 `GET /api/health/status`健康检查app.py 直接定义)。
### services/ — 领域逻辑
- **garmin 同步族**`garmin.py`(核心同步 + 429 限流退避,`rate_limited_until`
持久化到 `sync_status`,以 DB 为准防多 worker 内存不一致)、`garmin_auth.py`
OAuth 登录 + MFA 验证码会话,跨 worker 经 `garmin_mfa_sessions` 表交接)、
`garmin_extras.py`(身体成分/血压/血氧/全天曲线/设备/徽章/挑战等扩展数据)。
- **调度与队列**`scheduler.py`自动同步DB `job_locks` 抢占,多 worker 只
跑一次)、`jobs.py`AI 生产者/消费者队列,同表互斥,含 `refill_backlog`)。
- **AI 教练**`insights.py` 特征工程z 分数 / 趋势斜率 / 活动量对比)→
`coach.py` 三套提示词 + 规则引擎兜底 → `ai.py` 多模型链 + 缓存
`ai_insights`(数据指纹失效)。
- 其余:`analysis.py` / `health.py` / `fitness_age.py`(身体年龄推算)/
`settings.py`(设置吸附校验)/ `auth_hub_client.py`OIDC 客户端)/
`scopes.py`
### 认证链路auth-hub SSO
```
用户点「登录」→ POST /api/auth/auth-hub/start → 302 auth-hub 登录页
→ auth-hub 回调 GET /api/auth/callback?code=… → exchange_code_for_token
→ get_userinfo → find_or_create_user → 后端签 JWT7 天)→ 前端存 localStorage
```
- 本地邮箱/密码注册登录**已移除**;每个账号来自 auth-hub。
- **Garmin 账号授权是独立流程**(与 Web 登录分开):`/api/garmin/login`
起 OAuth 流程,需要验证码时 `/api/garmin/mfa` 提交,令牌存
`garmin_tokens`(约一年有效,密码不入库)。
- 所有数据路由经 `require_auth` 校验 JWT。
### 数据层db.py 可插拔)
- `DB_TYPE=sqlite`(开发默认,`backend/data/health.db`)↔ `DB_TYPE=mariadb`
(生产 NAS`config.py` 读取,业务代码不分叉。
- 20 张表:`users health_data activities sync_status garmin_tokens badges
personal_records job_locks garmin_mfa_sessions user_settings
activity_details body_composition blood_pressure race_predictions
daily_series challenges devices ai_recommendations ai_jobs ai_insights`。
- `init_db()` 幂等(`CREATE TABLE IF NOT EXISTS` + 按列检测的 `ALTER`
多 gunicorn worker 并发调用安全。`id` 为 VARCHAR(64),外键指向 `users`。
## 数据流
### 1. Garmin 同步(手动 + 自动)
```
用户/调度器触发
→ 检查 sync_status.rate_limited_until429 冷却 24hDB 为准)
→ garmin.py 分批拉取health_data 每日指标、activities、daily_series 等)
→ 库内 upsert → 更新 sync_status → prefetch_insightsAI 洞察预取入队)
```
- 同步频率与历史范围history_days读用户设置`0` 表示全部历史 = 730 天上限。
- 全天曲线每天 5 个请求14 天内同步顺带拉取,更长的历史走「补齐详细数据」
后台任务(`POST /api/garmin/sync-details` 触发,队列消费)。
### 2. AI 晨报 / Copilot / 趋势归因
```
AI 作业入队(ai_jobs优先级队列) → jobs.py worker 单飞消费(DB 互斥)
→ services/ai.py 调 ai-gateway(https://ai.zichuan.xyz/v1)
→ 结果写 ai_insights(指纹缓存) → 前端轮询 GET /analysis/insight 取回
Copilot: POST /analysis/copilot 走 SSE(stream_chat失败非流式回退)
```
- 生产 `AI_MODEL_CHAIN=gateway`(单上游,网关内部再 fanout NVIDIA/Gemini/Ollama
- 队列并发 2、间隔 5s共享网关过高会 502晨报一次生成 2~5 分钟,
前端一律「后台生成 + 轮询」而非阻塞等待;`meta.source` 标注 AI 或规则引擎。
## 部署架构(生产)
- **Development 本地**`npm run dev` = Flask 5000`BACKEND_PORT` 固定,避开
`PORT` 被 CRA 抢占)+ CRA dev server 3000proxy → 5000。SQLite。
- **Production NAS**`192.168.50.64` `/volume1/web/garmin-health-lab`root
跑 gunicorn `0.0.0.0:8124`2w/4t--timeout 300 —— AI 生成可长达分钟级),
开机自启 DSM 任务调度器 → `deploy/S99garmin.sh`MariaDB 10.11
root@`/run/mysqld/mysqld10.sock`,库 `garmin_health_lab`。
- **公网**NAS frpc → 甲骨文 `129.146.26.249:8124`frp 隧道,重连需数秒)。
- **一键部署**`deploy/push.sh`tar-over-ssh 同步 backend+deploy、清空重推
`client/build`、sudo 重启、**pid 变化 + health 200 双校验**);前端先
`npm run build`。`deploy/start.sh` / `stop.sh` 供手工与开机脚本调用。
- **auth-hub**:回调注册 LAN `192.168.50.64:8124` 与公网 `129.146.26.249:8124`
两个地址client `996aLPw4T5gl-rYZ`CORS 白名单含两地址 + localhost。
## 关键设计约束
1. **阈值来自公开参考值AI 只解读不定阈值**;依据页面逐条列出处。
2. **本地日历日期**优先于 UTC`toISOString()` 陷阱见 DEVELOPMENT.md
3. **单点部署、无反向代理**:同端口 API + SPA减少一个故障面。
4. **多 worker 安全**:一切跨进程协调走 DBjob_locks / garmin_mfa_sessions /
ai_jobs / sync_status不做进程内假设。
5. **可插拔数据层**:业务代码不分叉 SQLite/MariaDB。