# 开发指南 > 当前技术栈:**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) ### 安装步骤 ```bash 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 ``` ### 启动开发服务器 ```bash 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` 覆写: ```env DB_TYPE=mariadb MARIADB_SOCKET=/run/mysqld/mysqld10.sock MARIADB_HOST=127.0.0.1 MARIADB_PORT=3306 MARIADB_USER=root MARIADB_PASSWORD= MARIADB_DATABASE=garmin_health_lab ``` (本地 macOS 没有该 socket;通常经 ssh 端口转发或直接在 NAS 上跑联调。) ## 项目命令 ```bash # 后端测试(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 端点 1. **写服务函数**(`backend/services/your_feature.py`,纯逻辑,可单测) ```python def compute_something(user_id, payload): ... ``` 2. **挂到蓝图**(`backend/routes/xxx.py`,用 `require_auth` 拿 `g.user_id`) ```python @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.py` 的 `SCHEMA` 是 `CREATE TABLE IF NOT EXISTS`;新增列走迁移列表 (`db.py` 底部 `_migrations` / 列检测逻辑),**先查 INFORMATION_SCHEMA 再 ALTER**,保证多 worker 并发 `init_db()` 不炸。改完对 SQLite 与 MariaDB 各跑 一遍测试(迁移两套方言都要过)。 ### 前端构建与同源部署 ```bash npm run build # 产物在 client/build/ —— 不要手动拷到 backend/static/,走 deploy/push.sh ``` ## 部署到 NAS(生产) ```bash 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 重连)。 ## 测试 - 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_backlog` 1064 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 Gitea `http://192.168.50.64:3000/ericwyuan/GarminHealthLab.git`)。 - **绝不提交**:`.env`、密钥、`client/build/`、`.venv`、`*.db`(.gitignore 已覆盖)。