背景:文档停在 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)
11 KiB
11 KiB
项目架构说明
现状(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/<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读取,业务代码不分叉。- 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 跑 gunicorn0.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两个地址(client996aLPw4T5gl-rYZ);CORS 白名单含两地址 + localhost。
关键设计约束
- 阈值来自公开参考值,AI 只解读不定阈值;依据页面逐条列出处。
- 本地日历日期优先于 UTC(
toISOString()陷阱见 DEVELOPMENT.md)。 - 单点部署、无反向代理:同端口 API + SPA,减少一个故障面。
- 多 worker 安全:一切跨进程协调走 DB(job_locks / garmin_mfa_sessions / ai_jobs / sync_status),不做进程内假设。
- 可插拔数据层:业务代码不分叉 SQLite/MariaDB。