上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS IP、8124 端口还是旧的,补上。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
10 KiB
开发指南
当前技术栈:React 18 + TypeScript + Framework7 9(CRA) 前端 + Python 3.10+ / Flask 后端 + SQLite(开发)/ MariaDB(生产)。 仓库根
package.json是 npm workspaces(含client),后端依赖装进backend/.venv。
环境设置
前置要求
- Python 3.10+(本地用 3.13 亦可;NAS 是 3.10)
- Node.js 18+ / npm
- Git
- Garmin Connect 账户(用于数据同步联调)
- 联调 auth-hub 登录需能访问 auth-hub(内网
http://129.146.26.249:5300, 或配置你自己的 auth-hub)
安装步骤
cd ~/Desktop/Work/GarminHealthLab
# 1) 后端虚拟环境 + 依赖(含 pytest 等 dev 依赖)
npm run setup:backend
# 等价于: cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
# 2) 前端依赖(workspace)
npm install
# 3) 配置环境变量
cp backend/.env.example backend/.env
# 编辑 backend/.env:至少设置 JWT_SECRET;AI_GATEWAY_* 见下;DB_TYPE 默认 sqlite
启动开发服务器
npm run dev # 同时起后端(5000) + 前端 CRA(3000, proxy→5000)
npm run dev:backend # 只起后端: backend/.venv/bin/python app.py(BACKEND_PORT=5000 固定)
npm run dev:client # 只起前端: react-scripts start
- 前端: http://localhost:3000(登录走 auth-hub SSO 跳转)
- 后端 API: http://localhost:5000/api
app.py顶部会db.init_db()自动建表;开发库是backend/data/health.db。
为什么不用
PORT:很多工具会给前端进程注入PORT,若 Flask 直接读PORT会抢走 CRA 的端口。后端一律用BACKEND_PORT(见config.py)。
连接 NAS 生产 MariaDB(可选)
本地开发默认 SQLite。要连 NAS 生产库联调,在 backend/.env 覆写:
DB_TYPE=mariadb
MARIADB_SOCKET=/run/mysqld/mysqld10.sock
MARIADB_HOST=127.0.0.1
MARIADB_PORT=3306
MARIADB_USER=root
MARIADB_PASSWORD=<nas-mariadb-root密码>
MARIADB_DATABASE=garmin_health_lab
(本地 macOS 没有该 socket;通常经 ssh 端口转发或直接在 NAS 上跑联调。)
项目命令
# 后端测试(pytest,446+ 项)
npm run test # = cd backend && .venv/bin/python -m pytest
cd backend && .venv/bin/python tests/smoke.py # 冒烟
# 前端
npm run typecheck # tsc --noEmit
npm run build # client 构建 → client/build/
代码结构
后端 (backend/)
backend/
├── app.py # create_app(): CORS、init_db、scheduler/jobs 启动、蓝图装配、SPA 静态托管
├── wsgi.py # gunicorn 入口(wsgi:app)
├── config.py # 从 backend/.env 读配置(DB/AUTH/AI/端口/静态目录)
├── auth.py # sign_token / decode_token / require_auth
├── db.py # 连接与执行、SCHEMA(20表)、幂等 init_db()
├── routes/ # 蓝图:auth garmin health analysis settings
├── services/ # 业务逻辑:garmin garmin_auth garmin_extras scheduler
│ # ai coach insights jobs analysis health
│ # fitness_age settings auth_hub_client scopes
├── tests/ # conftest.py + 各模块测试 + smoke.py
├── data/ # SQLite(本地,git 忽略)
├── static/ # 前端构建产物(部署用;见「部署」)
├── .env.example
├── requirements.txt # 运行依赖
└── requirements-dev.txt # 运行 + pytest
前端 (client/)
client/
├── src/
│ ├── index.tsx / App.tsx # 入口与路由框架(F7 App)
│ ├── pages/ # TodayPage HealthPage TrendsPage ExercisePage
│ │ # SettingsPage DailyPage SleepPage BodyPage RacePage
│ │ # ChallengesPage DevicesPage MetricDetailPage
│ │ # ActivityDetailPage BodyAgePage RatingBasisPage
│ │ # AiQueuePage SyncPage Recommendations LoginPage
│ ├── components/ # AiBriefing AiPanel Copilot(浮窗) TrendInsight Screen
│ │ # Skeleton + charts/
│ ├── lib/ # day.ts(本地日历日期) metrics.ts(指标注册表) …
│ ├── services/api.ts # axios 客户端 + JWT 拦截器
│ ├── features.ts # FEATURES.ai 功能开关
│ ├── f7theme.css theme.css index.css
│ └── routes.ts types/
├── public/
└── .env.production # REACT_APP_API_URL=/api(同源;缺了会 Network Error)
开发工作流
添加新的 API 端点
- 写服务函数(
backend/services/your_feature.py,纯逻辑,可单测)def compute_something(user_id, payload): ... - 挂到蓝图(
backend/routes/xxx.py,用require_auth拿g.user_id)@bp.route("/something", methods=["POST"]) @require_auth def something(): from services.your_feature import compute_something return jsonify(compute_something(g.user_id, request.get_json() or {})) - 前端调用(
client/src/services/api.ts加方法) - 加测试(
backend/tests/test_your_feature.py) - 跑
npm run test+npm run typecheck
数据库变更(必须幂等)
db.py 的 SCHEMA 是 CREATE TABLE IF NOT EXISTS;新增列走迁移列表
(db.py 底部 _migrations / 列检测逻辑),先查 INFORMATION_SCHEMA 再
ALTER,保证多 worker 并发 init_db() 不炸。改完对 SQLite 与 MariaDB 各跑
一遍测试(迁移两套方言都要过)。
前端构建与同源部署
npm run build
# 产物在 client/build/ —— 不要手动拷到 backend/static/,走 deploy/push.sh
部署到 NAS(生产)
cd ~/Desktop/Work/GarminHealthLab
npm run build # 1. 先出前端产物
./deploy/push.sh # 2. 推送 + 重启(会问 ssh 与 sudo 密码,可设 NAS_PASSWORD)
push.sh 做了什么(见脚本注释,务必理解再执行):
- ssh master 连接(ControlMaster,密码只输一次);
- 定位远端 app 目录(
/volume1/web/garmin-health-lab); - tar 同步
backend/(排除 .venv/.env/pycache/db/tests)+deploy/; - 清空远端 static 并重推
client/build(防旧 chunk 堆积); - sudo 重启(服务以 root 跑,DSM 开机启动,日志 root 所有)——
stop.sh→ 等端口释放 →start.sh; - 校验:health 200 + gunicorn pid 必须变化(pid 没变=旧进程还在服务, 改动未生效,脚本会报错退出)。
老脚本
deploy/deploy.sh内置密码且不校验 pid,仅应急用。生产部署一律push.sh。公网生效比 NAS 本地晚几秒(frp 重连)。
测试
- pytest:
npm run test(446+ 项)。后端服务层为纯函数设计,测试不依赖 Flask 实例(conftest.py建临时 SQLite)。 - 覆盖重点:设置吸附/校验、身体年龄方向性、运动详情列存解析、Garmin 同步 入库 + 「只读本地」保证、AI 缓存/队列互斥、调度器、auth-hub 客户端。
- 前端界面回归:本地起后端 + CRA,Chrome headless + CDP 截图比对
(参见
.workbuddy/memory/日志里的做法)。 - 生产自测入口:甲骨文云主机
curl http://127.0.0.1:5500/api/health/status(200),或公网curl https://garmin.zichuan.xyz/api/health/status。
环境变量速查(backend/.env)
| 变量 | 默认 | 说明 |
|---|---|---|
BACKEND_PORT |
5000 | 后端监听端口(优先于 PORT) |
DB_TYPE |
sqlite | sqlite / mariadb |
DATABASE_PATH |
./data/health.db | SQLite 文件位置 |
MARIADB_* |
— | 生产 MariaDB(NAS 10.11,socket mysqld10.sock) |
JWT_SECRET |
dev_secret_change_me | 生产必换 |
JWT_EXPIRY_DAYS |
7 | token 有效期 |
AUTH_HUB_BASE_URL |
http://129.146.26.249:5300 | SSO 提供方 |
AUTH_HUB_CLIENT_ID / _SECRET |
— | SSO 客户端(生产 996a…) |
AUTH_HUB_REDIRECT_URI |
https://garmin.zichuan.xyz/auth/callback | 回调地址 |
STATIC_DIR |
backend/static | 前端产物目录(生产 ./static) |
AI_GATEWAY_BASE_URL |
https://ai.zichuan.xyz/v1 | OpenAI 兼容网关 |
AI_GATEWAY_TOKEN |
— | 网关 Bearer token |
AI_MODEL_CHAIN |
gateway,gemini-flash,llama-70b | 模型回退链(生产仅 gateway) |
AI_TIMEOUT_SECONDS |
300 | 网关超时(推理模型可达分钟级) |
AI_JOB_CONCURRENCY / AI_JOB_GAP_SECONDS |
2 / 5 | AI 队列并发与间隔(共享网关勿调高) |
AI_JOBS |
true | false 则停消费(页面只显示计算值) |
调试
- 后端日志:开发直接看终端;NAS 看
/volume1/web/garmin-health-lab/logs/(error.log / access.log / stdout.log,root 所有,用 sudo)。 refill_backlog1064 SQL 报错是已知未修 bug(AI 回填 SQL 与 MariaDB 语法不兼容),见 CLAUDE.md「已知问题」。
常见问题
- 前端 Network Error(部署后):构建时没带
.env.production的REACT_APP_API_URL=/api,打进了 localhost:5000。检查 bundle 或重跑npm run build。 - 凌晨打开是昨天:用了
toISOString()(UTC)。改用lib/day.ts。 - 按钮/输入框布局怪异:Framework7
button { width: 100% }。自定义组件 按钮显式width: auto(见 Copilot.css 修复注释)。 - Garmin 同步 429:账号级限流,退避 24h(DB 持久化)。密码/验证码正确
仍 429 就是撞限流,等冷却;
rate_limited_until以 DB 为准。 - 运动详情慢/超时:会话按进程缓存(15 分钟 TTL);冷启约 16s。
- 测试连库:测试用临时 SQLite,不要连生产 MariaDB。
代码规范与提交
- TypeScript 严格模式;Python 遵循现有风格(函数式 services,docstring)。
- 提交信息约定(见 PROGRESS.md / REQUIREMENTS.md):
feat:fix:docs:refactor:test:前缀,一任务一 commit,完成即git push(origin 即 NAS Giteahttp://192.168.50.64:3000/ericwyuan/GarminHealthLab.git)。 - 绝不提交:
.env、密钥、client/build/、.venv、*.db(.gitignore 已覆盖)。