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:
ericwyuan
2026-09-02 19:34:32 +08:00
parent 68363957ec
commit 2d9a2be185
12 changed files with 609 additions and 496 deletions

View File

@@ -1,167 +1,160 @@
# 项目架构说明
> 现状2026-09-02后端为 **Python/Flask 单体**,生产部署在 **NAS :8124**。
> 早期 Node.js/Express + `server/` 目录的后端已被整体重写为
> `backend/`Python本文件描述的是当前实现。
## 整体架构
```
┌─────────────────────────────────────────────────────┐
│ 浏览器 (React 前端)
┌────────────┬──────────────┬──────────┬──────────┐
│ 仪表板 │ 数据同步 │ 数据分析 │ 设置
└────────────┴──────────────┴──────────┴──────────┘
────────────────────────────────────────────────────
│ HTTP/REST
┌──────────────────────────────────────────────────────┐
│ Node.js/Express 后端服务器 │
┌──────────────────────────────────────────────┐
│ API 路由层 │
│ │ ├─ /auth - 用户认证
│ │ ├─ /garmin - Garmin 数据同步 │
│ ├─ /health - 健康数据查询 │
│ │ └─ /analysis - 数据分析
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ │ 服务层
│ ├─ GarminService - Garmin API 集成 │
│ │ ├─ HealthService - 健康数据业务逻辑
│ │ ├─ AuthService - 认证授权
│ └─ AnalysisService - 数据分析 │
└──────────────────────────────────────────────┘
└──────────────────────┬───────────────────────────────┘
┌──────────────────────────────┐
│ SQLite 数据库
│ ├─ users
│ ├─ health_data │
│ ├─ activities
└─ sync_status
└──────────────────────────────┘
┌──────────────────────────────┐
│ Garmin Cloud API
│ ├─ 用户认证
│ ├─ 数据获取 │
│ └─ 数据同步 │
└──────────────────────────────┘
┌──────────────────────────────────────────────────────────
浏览器 (React SPA)
Framework7 9 (iOS 主题) + Recharts单端口同源部署
┌──────┬──────┬──────┬──────┬──────────────┐
│ 今日 │ 健康 │ 趋势 │ 运动 │ 设置 / 更多… │
└──────────────────────────────────────┘
└───────────────┬──────────────────────────────────────────┘
│ HTTP/JSON + JWTBearerCopilot 用 SSE
┌──────────────────────────────────────────────────────────┐
Flask 单体gunicorn 2w/4tNAS :8124
┌────────────────────────────────────────────────────┐
│ │ 蓝图层 routes/ /api/auth /api/garmin /api/health │ │
│ │ /api/analysis /api/settings │
├────────────────────────────────────────────────────┤
│ │ 服务层 services/ 业务逻辑(无 Flask 依赖) │ │
│ auth_hub_client garmin garmin_auth garmin_extras │
│ scheduler(定时同步) ai/coach/insights/jobs(AI队列) │
│ │ health analysis fitness_age settings scopes │ │
├────────────────────────────────────────────────────┤
│ │ 横切auth.py(JWT+require_auth) config.py(配置) │ │
│ │ db.py(可插拔数据层+幂等迁移) │ │
└────────────────────────────────────────────────────┘
───────┬──────────────────────────────┬──────────────────┘
│ │
┌──────────────────┐ ┌─────────────────────────────┐
│ SQLite (开发) │ │ MariaDB (生产, NAS 10.11) │
│ data/health.db │ │ garmin_health_lab 库
│ DB_TYPE=sqlite │ root@socket mysqld10.sock
└──────────────────┘ └─────────────────────────────┘
│ 外部系统(经 services/ 调用)│
▼ ▼
┌──────────────────────────────────────────────────────────┐
auth-hub SSO Garmin Cloud API ai-gateway │
129.146.26.249: (OAuth, 账号级限流 (https://ai.zichuan
│ 5300 OIDC 429 → 退避24h) .xyz/v1, NVIDIA推理)
└──────────────────────────────────────────────────────────┘
```
## 核心模块
- 单端口部署Flask 既出 API 又托管 React 构建产物(`STATIC_DIR`),前端 API
请求为**同源 `/api/*`**`client/.env.production` 写死 `REACT_APP_API_URL=/api`)。
- SPA 路由兜底404 handler 对非 `/api/` 路径回退 `index.html`,页面刷新不 404。
### 1. 前端 (Client)
- **框架**: React 18 + TypeScript
- **路由**: React Router
- **UI 组件**: 自定义 + CSS
- **数据可视化**: Recharts
- **API 通信**: Axios
## 后端分层
**主要页面**:
- Dashboard (仪表板)
- DataSync (数据同步)
- Analysis (数据分析)
- Recommendations (健康建议)
- Settings (设置)
### routes/ — 蓝图(只做参数校验与序列化,逻辑在 services
### 2. 后端 (Server)
- **框架**: Express.js
- **语言**: TypeScript
- **数据库**: SQLite3
- **认证**: JWT
| 蓝图 | 前缀 | 主要端点 |
|---|---|---|
| `auth` | `/api/auth` | SSO 登录 `POST /auth-hub/start``GET /callback``POST /logout``/refresh` |
| `garmin` | `/api/garmin` | `POST /sync``/sync-latest``/sync-details``GET /auth-status``/login-status``/status``/auto-sync``/activities/<id>/detail``POST /login``/mfa``/disconnect``DELETE /login` |
| `health` | `/api/health` | `summary` `steps` `heart-rate` `sleep` `activities` `fitness-age` `badges` `personal-records` `body-composition` `blood-pressure` `race-predictions` `series` `challenges` `devices` |
| `analysis` | `/api/analysis` | `trends` `recommendations` `models` `ai-recommendations` `briefing` `trend-insight` `copilot`(SSE) `insight` `insight/queue` |
| `settings` | `/api/settings` | `GET/PUT ""` `GET /options` `GET /rating-basis` |
**主要服务**:
- **AuthService**: 用户认证和授权
- **GarminService**: Garmin API 集成和数据获取
- **HealthService**: 健康数据管理
- **AnalysisService**: 数据分析和建议生成
另有 `GET /api/health/status`健康检查app.py 直接定义)。
### 3. 数据库 (SQLite)
**表结构**:
- `users`: 用户信息和 Garmin 凭证
- `health_data`: 每日健康数据汇总
- `activities`: 运动活动记录
- `sync_status`: 数据同步状态追踪
### services/ — 领域逻辑
- **garmin 同步族**`garmin.py`(核心同步 + 429 限流退避,`rate_limited_until`
持久化到 `sync_status`,以 DB 为准防多 worker 内存不一致)、`garmin_auth.py`
OAuth 登录 + MFA 验证码会话,跨 worker 经 `garmin_mfa_sessions` 表交接)、
`garmin_extras.py`(身体成分/血压/血氧/全天曲线/设备/徽章/挑战等扩展数据)。
- **调度与队列**`scheduler.py`自动同步DB `job_locks` 抢占,多 worker 只
跑一次)、`jobs.py`AI 生产者/消费者队列,同表互斥,含 `refill_backlog`)。
- **AI 教练**`insights.py` 特征工程z 分数 / 趋势斜率 / 活动量对比)→
`coach.py` 三套提示词 + 规则引擎兜底 → `ai.py` 多模型链 + 缓存
`ai_insights`(数据指纹失效)。
- 其余:`analysis.py` / `health.py` / `fitness_age.py`(身体年龄推算)/
`settings.py`(设置吸附校验)/ `auth_hub_client.py`OIDC 客户端)/
`scopes.py`
### 认证链路auth-hub SSO
```
用户点「登录」→ POST /api/auth/auth-hub/start → 302 auth-hub 登录页
→ auth-hub 回调 GET /api/auth/callback?code=… → exchange_code_for_token
→ get_userinfo → find_or_create_user → 后端签 JWT7 天)→ 前端存 localStorage
```
- 本地邮箱/密码注册登录**已移除**;每个账号来自 auth-hub。
- **Garmin 账号授权是独立流程**(与 Web 登录分开):`/api/garmin/login`
起 OAuth 流程,需要验证码时 `/api/garmin/mfa` 提交,令牌存
`garmin_tokens`(约一年有效,密码不入库)。
- 所有数据路由经 `require_auth` 校验 JWT。
### 数据层db.py 可插拔)
- `DB_TYPE=sqlite`(开发默认,`backend/data/health.db`)↔ `DB_TYPE=mariadb`
(生产 NAS`config.py` 读取,业务代码不分叉。
- 20 张表:`users health_data activities sync_status garmin_tokens badges
personal_records job_locks garmin_mfa_sessions user_settings
activity_details body_composition blood_pressure race_predictions
daily_series challenges devices ai_recommendations ai_jobs ai_insights`。
- `init_db()` 幂等(`CREATE TABLE IF NOT EXISTS` + 按列检测的 `ALTER`
多 gunicorn worker 并发调用安全。`id` 为 VARCHAR(64),外键指向 `users`。
## 数据流
### 1. Garmin 数据同步流程
### 1. Garmin 同步(手动 + 自动)
```
用户点击"同步"
POST /api/garmin/sync
GarminService.syncData()
获取 Garmin 授权 Token
调用 Garmin API 获取数据
转换数据格式
存储到本地 SQLite
更新 sync_status
返回同步结果
用户/调度器触发
→ 检查 sync_status.rate_limited_until429 冷却 24hDB 为准)
→ garmin.py 分批拉取health_data 每日指标、activities、daily_series 等)
→ 库内 upsert → 更新 sync_status → prefetch_insightsAI 洞察预取入队)
```
### 2. 数据分析流程
- 同步频率与历史范围history_days读用户设置`0` 表示全部历史 = 730 天上限。
- 全天曲线每天 5 个请求14 天内同步顺带拉取,更长的历史走「补齐详细数据」
后台任务(`POST /api/garmin/sync-details` 触发,队列消费)。
### 2. AI 晨报 / Copilot / 趋势归因
```
GET /api/health/summary (日期范围)
从数据库查询健康数据
计算统计指标
生成趋势分析
返回分析结果
前端绘制图表
AI 作业入队(ai_jobs优先级队列) → jobs.py worker 单飞消费(DB 互斥)
→ services/ai.py 调 ai-gateway(https://ai.zichuan.xyz/v1)
→ 结果写 ai_insights(指纹缓存) → 前端轮询 GET /analysis/insight 取回
Copilot: POST /analysis/copilot 走 SSE(stream_chat失败非流式回退)
```
### 3. 健康建议生成流程
```
GET /api/analysis/recommendations
AnalysisService.generateRecommendations()
分析历史数据
识别异常和趋势
根据规则引擎生成建议
按优先级排序
返回建议列表
```
- 生产 `AI_MODEL_CHAIN=gateway`(单上游,网关内部再 fanout NVIDIA/Gemini/Ollama
- 队列并发 2、间隔 5s共享网关过高会 502晨报一次生成 2~5 分钟,
前端一律「后台生成 + 轮询」而非阻塞等待;`meta.source` 标注 AI 或规则引擎。
## 关键特性
## 部署架构(生产)
### 安全性
- Garmin 密码使用加密存储
- JWT 令牌验证所有请求
- CORS 配置限制来源
- 环境变量管理敏感配置
- **Development 本地**`npm run dev` = Flask 5000`BACKEND_PORT` 固定,避开
`PORT` 被 CRA 抢占)+ CRA dev server 3000proxy → 5000。SQLite。
- **Production NAS**`192.168.50.64` `/volume1/web/garmin-health-lab`root
跑 gunicorn `0.0.0.0:8124`2w/4t--timeout 300 —— AI 生成可长达分钟级),
开机自启 DSM 任务调度器 → `deploy/S99garmin.sh`MariaDB 10.11
root@`/run/mysqld/mysqld10.sock`,库 `garmin_health_lab`。
- **公网**NAS frpc → 甲骨文 `129.146.26.249:8124`frp 隧道,重连需数秒)。
- **一键部署**`deploy/push.sh`tar-over-ssh 同步 backend+deploy、清空重推
`client/build`、sudo 重启、**pid 变化 + health 200 双校验**);前端先
`npm run build`。`deploy/start.sh` / `stop.sh` 供手工与开机脚本调用。
- **auth-hub**:回调注册 LAN `192.168.50.64:8124` 与公网 `129.146.26.249:8124`
两个地址client `996aLPw4T5gl-rYZ`CORS 白名单含两地址 + localhost。
### 扩展性
- 模块化的服务设计
- 易于添加新的分析算法
- 支持数据导出功能
- 可扩展的 API 端点
## 关键设计约束
### 性能
- 数据库索引优化查询
- API 缓存策略
- 异步处理长运行任务
- 增量数据同步支持
## 部署架构
- **Development 本地开发**`npm run dev` 同时启动前后端 —— Vite/React (3000/5173) + Flask (5000, SQLite)。
- **Production 生产环境**Oracle 云服务器 129.146.26.249
- Flask + gunicorn 直绑 `0.0.0.0:8123`systemd `garmin-health-lab.service`,开机自启)
- React SPA 静态包由后端 `STATIC_DIR` 提供
- MariaDB 10.3`127.0.0.1:3306`,专用账号 `garmin`,库 `garmin_health_lab`
- AI 网关 `129.146.26.249:5100/v1`
1. **阈值来自公开参考值AI 只解读不定阈值**;依据页面逐条列出处。
2. **本地日历日期**优先于 UTC`toISOString()` 陷阱见 DEVELOPMENT.md
3. **单点部署、无反向代理**:同端口 API + SPA减少一个故障面。
4. **多 worker 安全**:一切跨进程协调走 DBjob_locks / garmin_mfa_sessions /
ai_jobs / sync_status不做进程内假设。
5. **可插拔数据层**:业务代码不分叉 SQLite/MariaDB。

View File

@@ -0,0 +1,33 @@
# auth-hub 集成说明
本应用不自己管账号——登录由 **auth-hub**(统一 SSO / OIDC 提供方)承接,
本仓库只做 OAuth2 client 侧的事。
## 依赖的配置backend/.env
| 变量 | 说明 |
|---|---|
| `AUTH_HUB_BASE_URL` | auth-hub 服务地址(生产 `http://129.146.26.249:5300` |
| `AUTH_HUB_CLIENT_ID` | 本应用在 auth-hub 注册的 client |
| `AUTH_HUB_CLIENT_SECRET` | 对应 secret**只放 .env不入库** |
| `AUTH_HUB_REDIRECT_URI` | 回调地址,必须与 auth-hub 侧注册值完全一致 |
## 生产 clientNAS :8124
- client_id`996aLPw4T5gl-rYZ`
- 在 auth-hub 注册的回调:
- LAN`http://192.168.50.64:8124/auth/callback`
- 公网frp`http://129.146.26.249:8124/auth/callback`
- 注意 auth-hub 的 dev 与 prod 是**两套独立数据库、各自注册 client**
在 dev auth-hub 上创建的 client 不能用于 prod反之亦然。
## 流程
1. 前端请求 `POST /api/auth/auth-hub/start` → 拿到 auth-hub 授权 URL浏览器跳转
2. auth-hub 登录后回调 `GET /api/auth/callback?code=…`
3. 后端 `services/auth_hub_client.py``exchange_code_for_token`(换 token
`get_userinfo``find_or_create_user`(无则建号)→ 本应用签 JWT 返回前端。
本地邮箱/密码注册登录(曾经的 `/api/auth/register|login`**已移除**。
Garmin 账号授权是另一条独立流程(见 routes/garmin.py 的 `/api/garmin/login`
与 Web 登录无关。

View File

@@ -1,94 +1,101 @@
# 开发指南
> 当前技术栈:**React 18 + TypeScript + Framework7 9CRA** 前端 +
> **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_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
# 开发模式(同时运行前后端)
npm run dev
# 后端测试pytest446+ 项)
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 客户端。
- 前端界面回归:本地起后端 + CRAChrome 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_*` | — | 生产 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` | 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.logroot 所有,用 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**:账号级限流,退避 24hDB 持久化)。密码/验证码正确
仍 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 遵循现有风格(函数式 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 已覆盖)。

View File

@@ -4,7 +4,7 @@
状态:`✅ 已完成` · `🚧 进行中` · `📋 待做` · `⏸ 已搁置`
最后更新2026-08-25
最后更新2026-09-02
---
@@ -13,10 +13,12 @@
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 1.1 | 新建项目分析佳明海外账号健康数据 | Gitea 自建仓库Web 应用 | ✅ |
| 1.2 | 部署到 Oracle 云服务器 (129.146.26.249),用 MariaDB | Flask + gunicorn + MariaDB(127.0.0.1:3306)SQLite 供开发 | ✅ |
| 1.3 | 公网可访问 | gunicorn 直绑 Oracle:8123frp 隧道已移除) | ✅ |
| 1.2 | 部署到 NAS192.168.50.64,用 MariaDB | Flask + gunicorn(root, :8124) + NAS MariaDB 10.11(库 `garmin_health_lab`SQLite 供开发 | ✅ |
| 1.3 | 公网可访问 | NAS frpc 隧道 → 甲骨文公网 `129.146.26.249:8124` | ✅ |
| 1.4 | 单元测试 / 界面自测 / 性能测试 | 446 项 pytest界面用浏览器实测接口逐个计时 | ✅ |
| 1.5 | 一任务一 commit完成即推送 | 已成为固定流程 | ✅ |
| 1.6 | auth-hub 统一登录SSO | OAuth2/OIDC回调注册 LAN + 公网两地址;本地邮箱密码登录已移除 | ✅ |
| 1.7 | 一键部署脚本 | `deploy/push.sh`tar-over-ssh 同步 + 清空重推前端 + sudo 重启 + pid/health 双校验 | ✅ |
## 二、数据同步
@@ -97,6 +99,10 @@
| 6.1 | 多个大上下文文本模型,可切换 | gateway / gemini-flash / llama-70b / nemotron-49b / mistral-large | ✅ |
| 6.2 | 暂时隐藏 AI 模块 | `FEATURES.ai = false` | ✅ |
| 6.3 | AI 用来解释,不用来定阈值 | 阈值全部来自公开参考值AI 只负责解读 | ✅ 已确认为设计原则 |
| 6.4 | AI 教练:晨报 / 趋势归因 / Copilot2026-09-01 | `services/insights` 特征工程 → `coach.py` 三套提示词 + 规则兜底 → ai-gateway晨报后台生成 + 轮询Copilot SSE | ✅ |
| 6.5 | AI 生成走后台队列,界面不阻塞 | `services/jobs.py` 生产者/消费者DB 互斥);每日/运动详情持续排队不限量(`refill_backlog` | ✅ |
| 6.6 | 队列状态可见、可重试 | 设置 → AI 生成队列页 + `/analysis/insight/queue/retry` | ✅ |
| 6.7 | AI 模型不可用时降级不报错 | 规则引擎兜底,`meta.source` 标注谁作答;流式失败自动退回非流式 | ✅ |
---
@@ -111,6 +117,9 @@
| 心率区间百分比偏高区间1 显示 90%,手表是 52% | 分母用了「落在区间内的总时长」,应为整次运动时长 | 改用运动时长低于区间1 的时间不再被挤掉 |
| 打开运动详情超时 60s | 每个请求都重新认证 Garmin`_connect` 单次约 11 秒 | 按进程缓存已认证会话15 分钟 TTL冷启 16s → 热 7s → 命中缓存 0.8s |
| 主要收益显示 UNKNOWN | Garmin 用 UNKNOWN 表示「没有结论」 | 映射为中文UNKNOWN 直接不显示该区块 |
| Copilot 浮窗布局错乱(✕ 错位、输入框缩成 21px | Framework7 全局 `button { width: 100% }` 压过自定义类 | `.copilot-*` 按钮显式 `width: auto`2026-09-02 已修并部署 NAS :8124 |
| Garmin 429 限流反复触发(同步死循环) | 退避太短 + 守卫被多 worker 内存卡死 + RetryError 里的 429 未识别 | 退避 24h + `rate_limited_until` 以 DB 为准 + 沿异常 `__cause__` 链识别 429 |
| 文档与实现脱节导致误判部署位置 | 文档停在 Node.js / 甲骨文 8123 时代 | docs/* 与 CLAUDE.md/README 已按 NAS :8124 + Flask 现状重写2026-09-02 |
## 测试与性能2026-08-24