上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS IP、8124 端口还是旧的,补上。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
11 KiB
项目架构说明
现状(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(自动同步,DBjob_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用户跑 gunicorn127.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一条(client996aLPw4T5gl-rYZ);CORS 白名单含该地址 + localhost。
关键设计约束
- 阈值来自公开参考值,AI 只解读不定阈值;依据页面逐条列出处。
- 本地日历日期优先于 UTC(
toISOString()陷阱见 DEVELOPMENT.md)。 - 单点部署、无反向代理:同端口 API + SPA,减少一个故障面。
- 多 worker 安全:一切跨进程协调走 DB(job_locks / garmin_mfa_sessions / ai_jobs / sync_status),不做进程内假设。
- 可插拔数据层:业务代码不分叉 SQLite/MariaDB。