docs: 文档/配置/测试同步 NAS :8124 生产现状,清除 Node.js 与甲骨文残留
背景:文档停在 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)
This commit is contained in:
@@ -1,94 +1,101 @@
|
||||
# 开发指南
|
||||
|
||||
> 当前技术栈:**React 18 + TypeScript + Framework7 9(CRA)** 前端 +
|
||||
> **Python 3.10+ / Flask** 后端 + SQLite(开发)/ MariaDB(生产)。
|
||||
> 仓库根 `package.json` 是 npm workspaces(含 `client`),后端依赖装进
|
||||
> `backend/.venv`。
|
||||
|
||||
## 环境设置
|
||||
|
||||
### 前置要求
|
||||
- Node.js 18+
|
||||
- npm 或 yarn
|
||||
- Python 3.10+(本地用 3.13 亦可;NAS 是 3.10)
|
||||
- Node.js 18+ / npm
|
||||
- Git
|
||||
- Garmin Connect 账户
|
||||
- Garmin Connect 账户(用于数据同步联调)
|
||||
- 联调 auth-hub 登录需能访问 auth-hub(内网 `http://129.146.26.249:5300`,
|
||||
或配置你自己的 auth-hub)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
cd ~/Desktop/Work
|
||||
git clone <repository-url> GarminHealthLab
|
||||
cd GarminHealthLab
|
||||
```
|
||||
```bash
|
||||
cd ~/Desktop/Work/GarminHealthLab
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
# 1) 后端虚拟环境 + 依赖(含 pytest 等 dev 依赖)
|
||||
npm run setup:backend
|
||||
# 等价于: cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
|
||||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
# 复制示例文件
|
||||
cp server/.env.example server/.env
|
||||
|
||||
# 编辑 server/.env,填入你的 Garmin 凭证
|
||||
# GARMIN_CONNECT_USER=your_email@example.com
|
||||
# GARMIN_CONNECT_PASSWORD=your_password
|
||||
# JWT_SECRET=generate_a_random_string
|
||||
```
|
||||
# 2) 前端依赖(workspace)
|
||||
npm install
|
||||
|
||||
4. **启动开发服务器**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 前端: http://localhost:3000
|
||||
- 后端 API: http://localhost:5000/api
|
||||
# 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=<nas-mariadb-root密码>
|
||||
MARIADB_DATABASE=garmin_health_lab
|
||||
```
|
||||
|
||||
(本地 macOS 没有该 socket;通常经 ssh 端口转发或直接在 NAS 上跑联调。)
|
||||
|
||||
## 项目命令
|
||||
|
||||
```bash
|
||||
# 开发模式(同时运行前后端)
|
||||
npm run dev
|
||||
# 后端测试(pytest,446+ 项)
|
||||
npm run test # = cd backend && .venv/bin/python -m pytest
|
||||
cd backend && .venv/bin/python tests/smoke.py # 冒烟
|
||||
|
||||
# 仅运行后端服务器
|
||||
npm run dev:server
|
||||
|
||||
# 仅运行前端应用
|
||||
npm run dev:client
|
||||
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
# 生产模式启动
|
||||
npm start
|
||||
|
||||
# 类型检查
|
||||
npm run typecheck
|
||||
# 前端
|
||||
npm run typecheck # tsc --noEmit
|
||||
npm run build # client 构建 → client/build/
|
||||
```
|
||||
|
||||
## 代码结构
|
||||
|
||||
### 后端 (server/)
|
||||
### 后端 (backend/)
|
||||
|
||||
```
|
||||
server/
|
||||
├── src/
|
||||
│ ├── index.ts # 入口文件
|
||||
│ ├── routes/ # API 路由
|
||||
│ │ ├── auth.ts
|
||||
│ │ ├── garmin.ts
|
||||
│ │ ├── health.ts
|
||||
│ │ └── analysis.ts
|
||||
│ ├── services/ # 业务逻辑服务
|
||||
│ │ ├── AuthService.ts
|
||||
│ │ ├── GarminService.ts
|
||||
│ │ ├── HealthService.ts
|
||||
│ │ └── AnalysisService.ts
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ └── database.ts
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
├── dist/ # 编译输出
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
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/)
|
||||
@@ -96,183 +103,134 @@ server/
|
||||
```
|
||||
client/
|
||||
├── src/
|
||||
│ ├── index.tsx # 入口文件
|
||||
│ ├── App.tsx # 主组件
|
||||
│ ├── components/ # 可复用组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Dashboard.tsx
|
||||
│ │ ├── DataSync.tsx
|
||||
│ │ ├── Analysis.tsx
|
||||
│ │ ├── Recommendations.tsx
|
||||
│ │ └── Settings.tsx
|
||||
│ ├── services/ # API 服务
|
||||
│ │ └── api.ts
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ └── index.css # 全局样式
|
||||
├── public/ # 静态资源
|
||||
└── package.json
|
||||
│ ├── 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. **创建路由处理器** (`server/src/routes/yourFeature.ts`)
|
||||
```typescript
|
||||
import express from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/endpoint', (req, res) => {
|
||||
// 业务逻辑
|
||||
});
|
||||
|
||||
export default router;
|
||||
1. **写服务函数**(`backend/services/your_feature.py`,纯逻辑,可单测)
|
||||
```python
|
||||
def compute_something(user_id, payload):
|
||||
...
|
||||
```
|
||||
|
||||
2. **在主文件中注册路由** (`server/src/index.ts`)
|
||||
```typescript
|
||||
import yourFeatureRoutes from './routes/yourFeature';
|
||||
app.use('/api/yourfeature', yourFeatureRoutes);
|
||||
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`
|
||||
|
||||
3. **创建 API 客户端方法** (`client/src/services/api.ts`)
|
||||
```typescript
|
||||
getYourEndpoint() {
|
||||
return this.client.get('/yourfeature/endpoint');
|
||||
}
|
||||
```
|
||||
### 数据库变更(必须幂等)
|
||||
|
||||
### 添加新页面
|
||||
`db.py` 的 `SCHEMA` 是 `CREATE TABLE IF NOT EXISTS`;新增列走迁移列表
|
||||
(`db.py` 底部 `_migrations` / 列检测逻辑),**先查 INFORMATION_SCHEMA 再
|
||||
ALTER**,保证多 worker 并发 `init_db()` 不炸。改完对 SQLite 与 MariaDB 各跑
|
||||
一遍测试(迁移两套方言都要过)。
|
||||
|
||||
1. **创建页面组件** (`client/src/pages/YourPage.tsx`)
|
||||
```typescript
|
||||
import React from 'react';
|
||||
|
||||
function YourPage() {
|
||||
return <div>Page content</div>;
|
||||
}
|
||||
|
||||
export default YourPage;
|
||||
```
|
||||
### 前端构建与同源部署
|
||||
|
||||
2. **在 App.tsx 中添加路由**
|
||||
```typescript
|
||||
<Route path="/your-path" element={<YourPage />} />
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
|
||||
使用数据库工具函数简化操作:
|
||||
|
||||
```typescript
|
||||
import { runAsync, getAsync, allAsync } from '../utils/database';
|
||||
|
||||
// 插入数据
|
||||
await runAsync('INSERT INTO table (col1, col2) VALUES (?, ?)', [val1, val2]);
|
||||
|
||||
// 查询单行
|
||||
const row = await getAsync('SELECT * FROM table WHERE id = ?', [id]);
|
||||
|
||||
// 查询多行
|
||||
const rows = await allAsync('SELECT * FROM table WHERE status = ?', ['active']);
|
||||
```
|
||||
|
||||
## 调试
|
||||
|
||||
### 后端调试
|
||||
```bash
|
||||
# 启用详细日志
|
||||
DEBUG=* npm run dev:server
|
||||
|
||||
# 使用 Node 调试器
|
||||
node --inspect server/dist/index.js
|
||||
npm run build
|
||||
# 产物在 client/build/ —— 不要手动拷到 backend/static/,走 deploy/push.sh
|
||||
```
|
||||
|
||||
### 前端调试
|
||||
- 使用 React DevTools 浏览器扩展
|
||||
- 使用浏览器开发者工具 (F12)
|
||||
- 检查网络请求和响应
|
||||
## 部署到 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 重连)。
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试后端 API
|
||||
- pytest:`npm run test`(446+ 项)。后端服务层为纯函数设计,测试不依赖 Flask
|
||||
实例(`conftest.py` 建临时 SQLite)。
|
||||
- 覆盖重点:设置吸附/校验、身体年龄方向性、运动详情列存解析、Garmin 同步
|
||||
入库 + 「只读本地」保证、AI 缓存/队列互斥、调度器、auth-hub 客户端。
|
||||
- 前端界面回归:本地起后端 + CRA,Chrome headless + CDP 截图比对
|
||||
(参见 `.workbuddy/memory/` 日志里的做法)。
|
||||
- 生产自测入口:NAS `curl http://127.0.0.1:8124/api/health/status`(200)。
|
||||
|
||||
使用 curl 或 Postman:
|
||||
## 环境变量速查(backend/.env)
|
||||
|
||||
```bash
|
||||
# 获取健康摘要
|
||||
curl http://localhost:5000/api/health/summary
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `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` | http://129.146.26.249:8124/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 则停消费(页面只显示计算值) |
|
||||
|
||||
# 触发 Garmin 同步
|
||||
curl -X POST http://localhost:5000/api/garmin/sync
|
||||
## 调试
|
||||
|
||||
# 获取健康建议
|
||||
curl http://localhost:5000/api/analysis/recommendations
|
||||
```
|
||||
- 后端日志:开发直接看终端;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「已知问题」。
|
||||
|
||||
## 常见问题
|
||||
### 常见问题
|
||||
|
||||
### 数据库连接失败
|
||||
```bash
|
||||
# 检查数据库文件
|
||||
ls -la ./data/health.db
|
||||
- **前端 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。
|
||||
|
||||
# 重置数据库
|
||||
rm ./data/health.db
|
||||
npm run dev
|
||||
```
|
||||
## 代码规范与提交
|
||||
|
||||
### 前端无法连接后端
|
||||
- 检查 CORS 配置
|
||||
- 确保后端运行在 5000 端口
|
||||
- 检查防火墙设置
|
||||
|
||||
### Garmin 认证失败
|
||||
- 验证 Garmin 邮箱和密码
|
||||
- 检查网络连接
|
||||
- 查看后端日志
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **数据库索引**
|
||||
- 在频繁查询的字段上添加索引
|
||||
- 定期分析查询性能
|
||||
|
||||
2. **缓存策略**
|
||||
- 实现 API 响应缓存
|
||||
- 使用浏览器缓存
|
||||
|
||||
3. **代码分割**
|
||||
- React 懒加载路由
|
||||
- 按需加载 JavaScript
|
||||
|
||||
## 代码规范
|
||||
|
||||
### TypeScript
|
||||
- 使用严格模式
|
||||
- 为所有函数参数添加类型
|
||||
- 使用接口定义复杂对象
|
||||
|
||||
### CSS
|
||||
- 使用 BEM 命名规范
|
||||
- 响应式设计优先
|
||||
- 避免内联样式
|
||||
|
||||
### 提交信息
|
||||
```
|
||||
feat: 添加新功能描述
|
||||
fix: 修复 bug 描述
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
```
|
||||
|
||||
## 资源链接
|
||||
|
||||
- [Express.js 文档](https://expressjs.com/)
|
||||
- [React 文档](https://react.dev/)
|
||||
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||
- [SQLite 文档](https://www.sqlite.org/)
|
||||
- [Garmin API](https://developer.garmin.com/)
|
||||
- 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 已覆盖)。
|
||||
|
||||
Reference in New Issue
Block a user