Files
GarminHealthLab/docs/DEVELOPMENT.md
ericwyuan 4331a07462 docs: docs/ 目录同步甲骨文部署事实(上一提交漏加)
上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS
IP、8124 端口还是旧的,补上。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 23:53:26 +08:00

10 KiB
Raw Permalink Blame History

开发指南

当前技术栈:React 18 + TypeScript + Framework7 9CRA 前端 + Python 3.10+ / Flask 后端 + SQLite开发/ MariaDB生产。 仓库根 package.json 是 npm workspacesclient),后端依赖装进 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_SECRETAI_GATEWAY_* 见下DB_TYPE 默认 sqlite

启动开发服务器

npm run dev            # 同时起后端(5000) + 前端 CRA(3000, proxy→5000)
npm run dev:backend    # 只起后端: backend/.venv/bin/python app.pyBACKEND_PORT=5000 固定)
npm run dev:client     # 只起前端: react-scripts start

为什么不用 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 上跑联调。)

项目命令

# 后端测试pytest446+ 项)
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 端点

  1. 写服务函数backend/services/your_feature.py,纯逻辑,可单测)
    def compute_something(user_id, payload):
        ...
    
  2. 挂到蓝图backend/routes/xxx.py,用 require_authg.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 {}))
    
  3. 前端调用client/src/services/api.ts 加方法)
  4. 加测试backend/tests/test_your_feature.py
  5. npm run test + npm run typecheck

数据库变更(必须幂等)

db.pySCHEMACREATE 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 做了什么(见脚本注释,务必理解再执行):

  1. ssh master 连接ControlMaster密码只输一次
  2. 定位远端 app 目录(/volume1/web/garmin-health-lab
  3. tar 同步 backend/(排除 .venv/.env/pycache/db/tests+ deploy/
  4. 清空远端 static 并重推 client/build(防旧 chunk 堆积);
  5. sudo 重启(服务以 root 跑DSM 开机启动,日志 root 所有)—— stop.sh → 等端口释放 → start.sh
  6. 校验health 200 + gunicorn pid 必须变化pid 没变=旧进程还在服务, 改动未生效,脚本会报错退出)。

老脚本 deploy/deploy.sh 内置密码且不校验 pid仅应急用。生产部署一律 push.sh。公网生效比 NAS 本地晚几秒frp 重连)。

测试

  • pytestnpm run test446+ 项)。后端服务层为纯函数设计,测试不依赖 Flask 实例(conftest.py 建临时 SQLite
  • 覆盖重点:设置吸附/校验、身体年龄方向性、运动详情列存解析、Garmin 同步 入库 + 「只读本地」保证、AI 缓存/队列互斥、调度器、auth-hub 客户端。
  • 前端界面回归:本地起后端 + CRAChrome 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_* 生产 MariaDBNAS 10.11socket 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.logroot 所有,用 sudo
  • refill_backlog 1064 SQL 报错是已知未修 bugAI 回填 SQL 与 MariaDB 语法不兼容),见 CLAUDE.md「已知问题」。

常见问题

  • 前端 Network Error部署后:构建时没带 .env.productionREACT_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:账号级限流,退避 24hDB 持久化)。密码/验证码正确 仍 429 就是撞限流,等冷却;rate_limited_until 以 DB 为准。
  • 运动详情慢/超时会话按进程缓存15 分钟 TTL冷启约 16s。
  • 测试连库:测试用临时 SQLite不要连生产 MariaDB。

代码规范与提交

  • TypeScript 严格模式Python 遵循现有风格(函数式 servicesdocstring
  • 提交信息约定(见 PROGRESS.md / REQUIREMENTS.mdfeat: fix: docs: refactor: test: 前缀,一任务一 commit完成即 git pushorigin 即 NAS Gitea http://192.168.50.64:3000/ericwyuan/GarminHealthLab.git)。
  • 绝不提交.env、密钥、client/build/.venv*.db.gitignore 已覆盖)。