上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS IP、8124 端口还是旧的,补上。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
169 lines
11 KiB
Markdown
169 lines
11 KiB
Markdown
# 项目架构说明
|
||
|
||
> 现状(2026-09-12):后端为 **Python/Flask 单体**,生产部署在**甲骨文云主机**
|
||
> (`garmin.zichuan.xyz`,此前是 NAS :8124,已整体迁移)。
|
||
> 早期 Node.js/Express + `server/` 目录的后端已被整体重写为
|
||
> `backend/`(Python),本文件描述的是当前实现。
|
||
|
||
## 整体架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ 浏览器 (React SPA) │
|
||
│ Framework7 9 (iOS 主题) + Recharts,单端口同源部署 │
|
||
│ ┌──────┬──────┬──────┬──────┬──────────────┐ │
|
||
│ │ 今日 │ 健康 │ 趋势 │ 运动 │ 设置 / 更多… │ │
|
||
│ └──────┴──────┴──────┴──────┴──────────────┘ │
|
||
└───────────────┬──────────────────────────────────────────┘
|
||
│ HTTP/JSON + JWT(Bearer);Copilot 用 SSE
|
||
▼
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ Flask 单体(gunicorn 2w/4t,甲骨文云主机 :5500) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 蓝图层 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 → 后端签 JWT(7 天)→ 前端存 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` 读取,业务代码不分叉。
|
||
- 21 张表:`users health_data activities sync_status sync_history 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`
|
||
(`sync_history` 追加式记录每次自动/手动/立即同步结果,见数据流 §1)。
|
||
- `init_db()` 幂等(`CREATE TABLE IF NOT EXISTS` + 按列检测的 `ALTER`),
|
||
多 gunicorn worker 并发调用安全。`id` 为 VARCHAR(64),外键指向 `users`。
|
||
|
||
## 数据流
|
||
|
||
### 1. Garmin 同步(手动 + 自动)
|
||
|
||
```
|
||
用户/调度器触发(trigger_kind: auto/manual/quick)
|
||
→ 检查 sync_status.rate_limited_until(429 冷却 24h,DB 为准)
|
||
→ garmin.py 分批拉取(health_data 每日指标、activities、daily_series 等)
|
||
→ 库内 upsert → 更新 sync_status → prefetch_insights(AI 洞察预取入队)
|
||
→ 每次尝试(含被限流拒绝)都向 sync_history 追加一行(只读查询
|
||
GET /api/garmin/sync-history,前端「同步记录」页展示)
|
||
```
|
||
|
||
- 同步频率与历史范围(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 3000(proxy → 5000)。SQLite。
|
||
- **Production**(甲骨文云主机,`129.146.26.249`):`/opt/garmin-health-lab`,
|
||
`ubuntu` 用户跑 gunicorn `127.0.0.1:5500`(2w/4t,--timeout 300 —— AI 生成
|
||
可长达分钟级),systemd 单元 `garmin-health-lab.service`(`enabled`,
|
||
`Restart=always`);MariaDB 10.3.39 本机 TCP、独立账号 `garmin`,库
|
||
`garmin_health_lab`。和这台机器上的 ai-gateway/auth-hub/fam-edge 同一套
|
||
部署约定。此前(2026-09-01~12)部署在 NAS,已整体迁移并下线。
|
||
- **公网**:Caddy 反代 `https://garmin.zichuan.xyz` → `127.0.0.1:5500`,
|
||
TLS 自动签发。
|
||
- **一键部署**:`deploy/push_oracle.sh`(ssh key 认证同步 backend、清空重推
|
||
`client/build`、pip install、systemctl restart、**PID 变化 + health 200
|
||
双校验**);前端先 `npm run build`。NAS 时代的 `deploy/push.sh` /
|
||
`start.sh` / `stop.sh` / `S99garmin.sh` 已废弃,保留仅供参考。
|
||
- **auth-hub**:回调只注册 `https://garmin.zichuan.xyz/auth/callback`
|
||
一条(client `996aLPw4T5gl-rYZ`);CORS 白名单含该地址 + localhost。
|
||
|
||
## 关键设计约束
|
||
|
||
1. **阈值来自公开参考值,AI 只解读不定阈值**;依据页面逐条列出处。
|
||
2. **本地日历日期**优先于 UTC(`toISOString()` 陷阱见 DEVELOPMENT.md)。
|
||
3. **单点部署、无反向代理**:同端口 API + SPA,减少一个故障面。
|
||
4. **多 worker 安全**:一切跨进程协调走 DB(job_locks / garmin_mfa_sessions /
|
||
ai_jobs / sync_status),不做进程内假设。
|
||
5. **可插拔数据层**:业务代码不分叉 SQLite/MariaDB。
|