# 项目架构说明 > 现状(2026-09-02):后端为 **Python/Flask 单体**,生产部署在 **NAS :8124**。 > 早期 Node.js/Express + `server/` 目录的后端已被整体重写为 > `backend/`(Python),本文件描述的是当前实现。 ## 整体架构 ``` ┌──────────────────────────────────────────────────────────┐ │ 浏览器 (React SPA) │ │ Framework7 9 (iOS 主题) + Recharts,单端口同源部署 │ │ ┌──────┬──────┬──────┬──────┬──────────────┐ │ │ │ 今日 │ 健康 │ 趋势 │ 运动 │ 设置 / 更多… │ │ │ └──────┴──────┴──────┴──────┴──────────────┘ │ └───────────────┬──────────────────────────────────────────┘ │ HTTP/JSON + JWT(Bearer);Copilot 用 SSE ▼ ┌──────────────────────────────────────────────────────────┐ │ Flask 单体(gunicorn 2w/4t,NAS :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//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` 读取,业务代码不分叉。 - 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_until(429 冷却 24h,DB 为准) → garmin.py 分批拉取(health_data 每日指标、activities、daily_series 等) → 库内 upsert → 更新 sync_status → prefetch_insights(AI 洞察预取入队) ``` - 同步频率与历史范围(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 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 安全**:一切跨进程协调走 DB(job_locks / garmin_mfa_sessions / ai_jobs / sync_status),不做进程内假设。 5. **可插拔数据层**:业务代码不分叉 SQLite/MariaDB。