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

238 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发指南
> 当前技术栈:**React 18 + TypeScript + Framework7 9CRA** 前端 +
> **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_SECRETAI_GATEWAY_* 见下DB_TYPE 默认 sqlite
```
### 启动开发服务器
```bash
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
```
- 前端: 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=<nas-mariadb-root密码>
MARIADB_DATABASE=garmin_health_lab
```
(本地 macOS 没有该 socket通常经 ssh 端口转发或直接在 NAS 上跑联调。)
## 项目命令
```bash
# 后端测试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`,纯逻辑,可单测)
```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 客户端。
- 前端界面回归:本地起后端 + 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 报错是**已知未修 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**:账号级限流,退避 24hDB 持久化)。密码/验证码正确
仍 429 就是撞限流,等冷却;`rate_limited_until` 以 DB 为准。
- **运动详情慢/超时**会话按进程缓存15 分钟 TTL冷启约 16s。
- **测试连库**:测试用临时 SQLite不要连生产 MariaDB。
## 代码规范与提交
- TypeScript 严格模式Python 遵循现有风格(函数式 servicesdocstring
- 提交信息约定(见 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 已覆盖)。