Initial commit: Set up Garmin Health Lab project structure

- Initialize monorepo with root workspace configuration
- Set up Express.js backend with TypeScript
- Set up React 18 frontend with TypeScript
- Create database schema with SQLite
- Implement project architecture and documentation
- Add development and deployment guidelines

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-08-23 11:11:29 +08:00
commit d73405decb
34 changed files with 1699 additions and 0 deletions

11
.claude/launch.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000
}
]
}

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
node_modules/
dist/
build/
*.log
npm-debug.log*
.DS_Store
.env
.env.local
.env.*.local
.idea/
.vscode/
*.swp
*.swo
~*
# Database
*.db
*.sqlite
*.sqlite3
# API Keys and secrets
.env.production.local
config/secrets.json
# Build artifacts
server/dist/
client/build/

88
CLAUDE.md Normal file
View File

@@ -0,0 +1,88 @@
# Garmin Health Lab - 项目指南
## 项目简介
Garmin Health Lab 是一个完整的健康数据分析平台,用于:
- 获取和同步佳明Garmin设备数据
- 进行多维度的健康数据分析
- 提供个性化的健康建议
- 可视化健康趋势
## 技术栈
- **前端**: React 18 + TypeScript + Recharts
- **后端**: Node.js + Express + TypeScript
- **数据库**: SQLite3
- **API 集成**: Garmin Connect API
## 快速开始
### 安装
```bash
npm install
```
### 配置
```bash
cp server/.env.example server/.env
# 编辑 server/.env 填入 Garmin 凭证
```
### 开发
```bash
npm run dev
```
- 前端: http://localhost:3000
- 后端: http://localhost:5000
## 项目结构
- `client/` - React 前端应用
- `server/` - Node.js 后端服务
- `docs/` - 文档
详见 [ARCHITECTURE.md](docs/ARCHITECTURE.md) 和 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
## 核心功能
1. **仪表板** - 健康数据概览和最近数据展示
2. **数据同步** - 从 Garmin 同步最新健康数据
3. **数据分析** - 趋势分析和数据可视化
4. **健康建议** - 基于数据的个性化建议
5. **设置** - 用户配置和偏好设置
## 关键特性
- ✅ Garmin API 集成
- ✅ 多维度数据分析(步数、心率、睡眠等)
- ✅ 实时数据同步
- ✅ 交互式数据可视化
- ✅ JWT 认证安全
- ✅ 本地数据存储
## 开发指南
- 详见 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
- API 文档见 README.md
## 常用命令
```bash
npm run dev # 开发模式
npm run build # 构建项目
npm run typecheck # 类型检查
npm start # 生产模式
```
## 下一步任务
- [ ] 实现 Garmin OAuth 认证
- [ ] 完成 Garmin API 数据获取
- [ ] 实现仪表板可视化
- [ ] 添加数据分析算法
- [ ] 部署和优化
## 联系方式
项目维护: ericwyuan.g@gmail.com

121
README.md Normal file
View File

@@ -0,0 +1,121 @@
# 佳明健康数据分析平台 (Garmin Health Lab)
一个完整的健康数据分析平台用于获取、分析和可视化你的佳明Garmin设备数据。
## 📊 主要功能
- **数据同步**: 通过 Garmin API 自动同步你的健康数据
- **综合分析**: 步数、心率、睡眠、运动、压力等多维度分析
- **数据可视化**: 交互式图表和仪表板展示健康数据趋势
- **智能建议**: 基于数据分析的个性化健康建议
- **数据导出**: 支持数据导出为 CSV/JSON 格式
## 🛠 技术栈
### 前端
- React 18 + TypeScript
- Recharts (数据可视化)
- Tailwind CSS (样式)
- Axios (API 请求)
### 后端
- Node.js + Express
- TypeScript
- SQLite3 (本地数据存储)
- garmin-connect (Garmin API 集成)
## 📁 项目结构
```
GarminHealthLab/
├── client/ # 前端应用
│ ├── src/
│ │ ├── components/ # React 组件
│ │ ├── pages/ # 页面
│ │ ├── services/ # API 服务
│ │ └── types/ # TypeScript 类型定义
│ └── package.json
├── server/ # 后端应用
│ ├── src/
│ │ ├── routes/ # API 路由
│ │ ├── services/ # 业务逻辑
│ │ ├── models/ # 数据模型
│ │ ├── middleware/ # 中间件
│ │ └── utils/ # 工具函数
│ └── package.json
├── docs/ # 文档
├── package.json # 工作空间根配置
└── README.md
```
## 🚀 快速开始
### 前置要求
- Node.js 18+
- npm 或 yarn
- Garmin Connect 账户
### 安装依赖
```bash
npm install
```
### 配置环境变量
创建 `server/.env` 文件:
```env
PORT=5000
NODE_ENV=development
GARMIN_CONNECT_USER=your_garmin_email
GARMIN_CONNECT_PASSWORD=your_garmin_password
DATABASE_PATH=./data/health.db
JWT_SECRET=your_jwt_secret_here
CORS_ORIGIN=http://localhost:3000
```
### 启动开发服务器
```bash
npm run dev
```
- 前端: http://localhost:3000
- 后端: http://localhost:5000
## 📚 API 文档
### 认证
- `POST /api/auth/login` - 用户登录
- `POST /api/auth/logout` - 用户登出
- `POST /api/auth/refresh` - 刷新 Token
### Garmin 数据同步
- `POST /api/garmin/sync` - 同步 Garmin 数据
- `GET /api/garmin/status` - 获取同步状态
### 健康数据
- `GET /api/health/summary` - 获取健康摘要
- `GET /api/health/steps` - 获取步数数据
- `GET /api/health/heart-rate` - 获取心率数据
- `GET /api/health/sleep` - 获取睡眠数据
- `GET /api/health/activities` - 获取运动数据
### 分析与建议
- `GET /api/analysis/trends` - 获取数据趋势
- `GET /api/analysis/recommendations` - 获取健康建议
## 🔐 安全说明
- Garmin 账户密码使用加密存储
- 所有 API 请求需要 JWT 认证
- 敏感数据不在前端存储
## 📝 开发指南
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md)
## 📄 许可证
MIT

44
client/package.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "garmin-health-lab-client",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"axios": "^1.5.0",
"recharts": "^2.8.0",
"tailwindcss": "^3.3.2",
"date-fns": "^2.30.0"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"typescript": "^5.1.3",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

12
client/public/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="佳明健康数据分析平台">
<title>Garmin Health Lab - 佳明健康数据分析</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

26
client/src/App.tsx Normal file
View File

@@ -0,0 +1,26 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Dashboard from './pages/Dashboard';
import DataSync from './pages/DataSync';
import Analysis from './pages/Analysis';
import Recommendations from './pages/Recommendations';
import Settings from './pages/Settings';
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/sync" element={<DataSync />} />
<Route path="/analysis" element={<Analysis />} />
<Route path="/recommendations" element={<Recommendations />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Layout>
</Router>
);
}
export default App;

View File

@@ -0,0 +1,129 @@
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
}
.logo {
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.tagline {
font-size: 0.9rem;
opacity: 0.9;
}
.container {
display: flex;
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
gap: 2rem;
padding: 2rem;
}
.sidebar {
width: 250px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: fit-content;
position: sticky;
top: 2rem;
}
.nav-list {
list-style: none;
padding: 0;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
text-decoration: none;
color: #333;
border-left: 3px solid transparent;
transition: all 0.3s ease;
}
.nav-link:hover {
background-color: #f5f5f5;
border-left-color: #667eea;
}
.nav-link.active {
background-color: #f0f0ff;
border-left-color: #667eea;
color: #667eea;
font-weight: 600;
}
.icon {
font-size: 1.2rem;
}
.label {
flex: 1;
}
.content {
flex: 1;
background: white;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
padding: 2rem;
color: #666;
font-size: 0.9rem;
border-top: 1px solid #eee;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
padding: 1rem;
}
.sidebar {
width: 100%;
position: static;
}
.nav-list {
display: flex;
gap: 0.5rem;
overflow-x: auto;
}
.nav-link {
flex: 1;
min-width: 100px;
font-size: 0.9rem;
}
.logo {
font-size: 1.4rem;
}
}

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import './Layout.css';
interface LayoutProps {
children: React.ReactNode;
}
function Layout({ children }: LayoutProps) {
const location = useLocation();
const navigationItems = [
{ path: '/', label: '仪表板', icon: '📊' },
{ path: '/sync', label: '数据同步', icon: '🔄' },
{ path: '/analysis', label: '数据分析', icon: '📈' },
{ path: '/recommendations', label: '健康建议', icon: '💡' },
{ path: '/settings', label: '设置', icon: '⚙️' },
];
return (
<div className="layout">
<header className="header">
<div className="header-content">
<h1 className="logo">🏃 Garmin Health Lab</h1>
<p className="tagline"></p>
</div>
</header>
<div className="container">
<nav className="sidebar">
<ul className="nav-list">
{navigationItems.map(item => (
<li key={item.path}>
<Link
to={item.path}
className={`nav-link ${location.pathname === item.path ? 'active' : ''}`}
>
<span className="icon">{item.icon}</span>
<span className="label">{item.label}</span>
</Link>
</li>
))}
</ul>
</nav>
<main className="content">
{children}
</main>
</div>
<footer className="footer">
<p>© 2024 Garmin Health Lab. All rights reserved.</p>
</footer>
</div>
);
}
export default Layout;

24
client/src/index.css Normal file
View File

@@ -0,0 +1,24 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
html, body, #root {
width: 100%;
height: 100%;
}

10
client/src/index.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import './Pages.css';
function Analysis() {
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
</div>
);
}
export default Analysis;

View File

@@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react';
import { apiClient } from '../services/api';
import './Pages.css';
function Dashboard() {
const [loading, setLoading] = useState(true);
const [summary, setSummary] = useState<any>(null);
useEffect(() => {
const loadData = async () => {
try {
// TODO: Fetch health summary data
setLoading(false);
} catch (error) {
console.error('Failed to load summary:', error);
setLoading(false);
}
};
loadData();
}, []);
if (loading) {
return <div className="page-loading">...</div>;
}
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
</div>
);
}
export default Dashboard;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import './Pages.css';
function DataSync() {
const handleSync = async () => {
// TODO: Implement data sync
};
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
<button onClick={handleSync} className="btn btn-primary">
Garmin
</button>
</div>
);
}
export default DataSync;

View File

@@ -0,0 +1,59 @@
.page {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.page h2 {
color: #333;
margin-bottom: 1.5rem;
font-size: 1.8rem;
}
.page-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
font-size: 1.2rem;
color: #666;
}
.placeholder {
color: #999;
font-size: 1.1rem;
padding: 2rem;
background-color: #f9f9f9;
border-radius: 8px;
text-align: center;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem;
}
.btn-primary {
background-color: #667eea;
color: white;
}
.btn-primary:hover {
background-color: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import './Pages.css';
function Recommendations() {
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
</div>
);
}
export default Recommendations;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import './Pages.css';
function Settings() {
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
</div>
);
}
export default Settings;

View File

@@ -0,0 +1,78 @@
import axios, { AxiosInstance } from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
}
// Auth endpoints
login(email: string, password: string) {
return this.client.post('/auth/login', { email, password });
}
logout() {
return this.client.post('/auth/logout');
}
// Garmin endpoints
syncGarminData() {
return this.client.post('/garmin/sync');
}
getGarminSyncStatus() {
return this.client.get('/garmin/status');
}
// Health endpoints
getHealthSummary(startDate?: string, endDate?: string) {
return this.client.get('/health/summary', {
params: { startDate, endDate }
});
}
getStepsData(startDate?: string, endDate?: string) {
return this.client.get('/health/steps', {
params: { startDate, endDate }
});
}
getHeartRateData(startDate?: string, endDate?: string) {
return this.client.get('/health/heart-rate', {
params: { startDate, endDate }
});
}
getSleepData(startDate?: string, endDate?: string) {
return this.client.get('/health/sleep', {
params: { startDate, endDate }
});
}
getActivities(startDate?: string, endDate?: string) {
return this.client.get('/health/activities', {
params: { startDate, endDate }
});
}
// Analysis endpoints
getTrends(metricType?: string) {
return this.client.get('/analysis/trends', {
params: { metricType }
});
}
getRecommendations() {
return this.client.get('/analysis/recommendations');
}
}
export const apiClient = new ApiClient();

37
client/src/types/index.ts Normal file
View File

@@ -0,0 +1,37 @@
export interface HealthSummary {
date: string;
steps: number;
heartRate?: number;
sleep?: {
duration: number;
quality: number;
};
caloriesBurned?: number;
stress?: number;
}
export interface Activity {
id: string;
activityType: string;
startTime: string;
endTime: string;
duration: number;
distance?: number;
calories?: number;
heartRateAverage?: number;
heartRateMax?: number;
}
export interface Recommendation {
id: string;
category: string;
recommendation: string;
priority: 'high' | 'medium' | 'low';
basedOn: string[];
}
export interface TrendData {
date: string;
value: number;
[key: string]: any;
}

21
client/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules"]
}

175
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,175 @@
# 项目架构说明
## 整体架构
```
┌─────────────────────────────────────────────────────┐
│ 浏览器 (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 │
│ ├─ 用户认证 │
│ ├─ 数据获取 │
│ └─ 数据同步 │
└──────────────────────────────┘
```
## 核心模块
### 1. 前端 (Client)
- **框架**: React 18 + TypeScript
- **路由**: React Router
- **UI 组件**: 自定义 + CSS
- **数据可视化**: Recharts
- **API 通信**: Axios
**主要页面**:
- Dashboard (仪表板)
- DataSync (数据同步)
- Analysis (数据分析)
- Recommendations (健康建议)
- Settings (设置)
### 2. 后端 (Server)
- **框架**: Express.js
- **语言**: TypeScript
- **数据库**: SQLite3
- **认证**: JWT
**主要服务**:
- **AuthService**: 用户认证和授权
- **GarminService**: Garmin API 集成和数据获取
- **HealthService**: 健康数据管理
- **AnalysisService**: 数据分析和建议生成
### 3. 数据库 (SQLite)
**表结构**:
- `users`: 用户信息和 Garmin 凭证
- `health_data`: 每日健康数据汇总
- `activities`: 运动活动记录
- `sync_status`: 数据同步状态追踪
## 数据流
### 1. Garmin 数据同步流程
```
用户点击"同步"
POST /api/garmin/sync
GarminService.syncData()
获取 Garmin 授权 Token
调用 Garmin API 获取数据
转换数据格式
存储到本地 SQLite
更新 sync_status
返回同步结果
```
### 2. 数据分析流程
```
GET /api/health/summary (日期范围)
从数据库查询健康数据
计算统计指标
生成趋势分析
返回分析结果
前端绘制图表
```
### 3. 健康建议生成流程
```
GET /api/analysis/recommendations
AnalysisService.generateRecommendations()
分析历史数据
识别异常和趋势
根据规则引擎生成建议
按优先级排序
返回建议列表
```
## 关键特性
### 安全性
- Garmin 密码使用加密存储
- JWT 令牌验证所有请求
- CORS 配置限制来源
- 环境变量管理敏感配置
### 扩展性
- 模块化的服务设计
- 易于添加新的分析算法
- 支持数据导出功能
- 可扩展的 API 端点
### 性能
- 数据库索引优化查询
- API 缓存策略
- 异步处理长运行任务
- 增量数据同步支持
## 部署架构
```
┌─────────────────────────────────┐
│ Development 本地开发 │
│ └─ npm run dev (同时运行前后端) │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Production 生产环境 │
│ ├─ Docker 容器化 │
│ ├─ Nginx 反向代理 │
│ ├─ Node.js 后端服务 │
│ └─ SQLite 数据持久化 │
└─────────────────────────────────┘
```

278
docs/DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,278 @@
# 开发指南
## 环境设置
### 前置要求
- Node.js 18+
- npm 或 yarn
- Git
- Garmin Connect 账户
### 安装步骤
1. **克隆项目**
```bash
cd ~/Desktop/Work
git clone <repository-url> GarminHealthLab
cd GarminHealthLab
```
2. **安装依赖**
```bash
npm install
```
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
```
4. **启动开发服务器**
```bash
npm run dev
```
- 前端: http://localhost:3000
- 后端 API: http://localhost:5000/api
## 项目命令
```bash
# 开发模式(同时运行前后端)
npm run dev
# 仅运行后端服务器
npm run dev:server
# 仅运行前端应用
npm run dev:client
# 构建项目
npm run build
# 生产模式启动
npm start
# 类型检查
npm run typecheck
```
## 代码结构
### 后端 (server/)
```
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
```
### 前端 (client/)
```
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
```
## 开发工作流
### 添加新的 API 端点
1. **创建路由处理器** (`server/src/routes/yourFeature.ts`)
```typescript
import express from 'express';
const router = express.Router();
router.get('/endpoint', (req, res) => {
// 业务逻辑
});
export default router;
```
2. **在主文件中注册路由** (`server/src/index.ts`)
```typescript
import yourFeatureRoutes from './routes/yourFeature';
app.use('/api/yourfeature', yourFeatureRoutes);
```
3. **创建 API 客户端方法** (`client/src/services/api.ts`)
```typescript
getYourEndpoint() {
return this.client.get('/yourfeature/endpoint');
}
```
### 添加新页面
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
```
### 前端调试
- 使用 React DevTools 浏览器扩展
- 使用浏览器开发者工具 (F12)
- 检查网络请求和响应
## 测试
### 测试后端 API
使用 curl 或 Postman
```bash
# 获取健康摘要
curl http://localhost:5000/api/health/summary
# 触发 Garmin 同步
curl -X POST http://localhost:5000/api/garmin/sync
# 获取健康建议
curl http://localhost:5000/api/analysis/recommendations
```
## 常见问题
### 数据库连接失败
```bash
# 检查数据库文件
ls -la ./data/health.db
# 重置数据库
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/)

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "garmin-health-lab",
"version": "0.1.0",
"description": "佳明健康数据分析平台",
"private": true,
"scripts": {
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:server": "cd server && npm run dev",
"dev:client": "cd client && npm start",
"build": "npm run build:server && npm run build:client",
"build:server": "cd server && npm run build",
"build:client": "cd client && npm run build",
"start": "node server/dist/index.js"
},
"workspaces": [
"server",
"client"
],
"devDependencies": {
"concurrently": "^8.2.0"
}
}

20
server/.env.example Normal file
View File

@@ -0,0 +1,20 @@
# Server Configuration
PORT=5000
NODE_ENV=development
# Database
DATABASE_PATH=./data/health.db
# JWT
JWT_SECRET=your_jwt_secret_key_here_change_in_production
# CORS
CORS_ORIGIN=http://localhost:3000
# Garmin Configuration
# Note: Consider using OAuth instead of storing credentials
GARMIN_CONNECT_USER=your_garmin_email@example.com
GARMIN_CONNECT_PASSWORD=your_password_here
# Data Sync
SYNC_INTERVAL_HOURS=1

29
server/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "garmin-health-lab-server",
"version": "0.1.0",
"description": "Garmin Health Lab Backend",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"sqlite3": "^5.1.6",
"jsonwebtoken": "^9.0.2",
"axios": "^1.5.0",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.3.1",
"@types/jsonwebtoken": "^9.0.2",
"typescript": "^5.1.3",
"tsx": "^3.12.7"
}
}

42
server/src/index.ts Normal file
View File

@@ -0,0 +1,42 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { initializeDatabase } from './utils/database';
import authRoutes from './routes/auth';
import garminRoutes from './routes/garmin';
import healthRoutes from './routes/health';
import analysisRoutes from './routes/analysis';
import { errorHandler } from './middleware/errorHandler';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(express.json());
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
credentials: true
}));
// Initialize database
initializeDatabase();
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/garmin', garminRoutes);
app.use('/api/health', healthRoutes);
app.use('/api/analysis', analysisRoutes);
// Health check
app.get('/api/health/status', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handling
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,31 @@
import { Request, Response, NextFunction } from 'express';
export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
) {
console.error('Error:', err);
const status = err.status || 500;
const message = err.message || 'Internal server error';
res.status(status).json({
error: {
status,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
}
export class AppError extends Error {
constructor(
public status: number,
public message: string
) {
super(message);
this.name = this.constructor.name;
}
}

View File

@@ -0,0 +1,14 @@
import express from 'express';
const router = express.Router();
// TODO: Implement analysis endpoints
router.get('/trends', (req, res) => {
res.json({ message: 'Trends analysis endpoint' });
});
router.get('/recommendations', (req, res) => {
res.json({ message: 'Recommendations endpoint' });
});
export default router;

18
server/src/routes/auth.ts Normal file
View File

@@ -0,0 +1,18 @@
import express from 'express';
const router = express.Router();
// TODO: Implement authentication routes
router.post('/login', (req, res) => {
res.json({ message: 'Login endpoint' });
});
router.post('/logout', (req, res) => {
res.json({ message: 'Logout endpoint' });
});
router.post('/refresh', (req, res) => {
res.json({ message: 'Refresh token endpoint' });
});
export default router;

View File

@@ -0,0 +1,14 @@
import express from 'express';
const router = express.Router();
// TODO: Implement Garmin API integration
router.post('/sync', (req, res) => {
res.json({ message: 'Garmin sync endpoint' });
});
router.get('/status', (req, res) => {
res.json({ message: 'Garmin sync status endpoint' });
});
export default router;

View File

@@ -0,0 +1,26 @@
import express from 'express';
const router = express.Router();
// TODO: Implement health data endpoints
router.get('/summary', (req, res) => {
res.json({ message: 'Health summary endpoint' });
});
router.get('/steps', (req, res) => {
res.json({ message: 'Steps data endpoint' });
});
router.get('/heart-rate', (req, res) => {
res.json({ message: 'Heart rate data endpoint' });
});
router.get('/sleep', (req, res) => {
res.json({ message: 'Sleep data endpoint' });
});
router.get('/activities', (req, res) => {
res.json({ message: 'Activities endpoint' });
});
export default router;

59
server/src/types/index.ts Normal file
View File

@@ -0,0 +1,59 @@
export interface User {
id: string;
email: string;
garminEmail: string;
createdAt: Date;
updatedAt: Date;
}
export interface HealthData {
id: string;
userId: string;
date: Date;
steps: number;
heartRate?: number;
heartRateVariability?: number;
bloodPressure?: {
systolic: number;
diastolic: number;
};
sleep?: {
duration: number;
quality: number;
};
stress?: number;
caloriesBurned?: number;
createdAt: Date;
updatedAt: Date;
}
export interface Activity {
id: string;
userId: string;
activityType: string;
startTime: Date;
endTime: Date;
duration: number;
distance?: number;
calories?: number;
heartRateAverage?: number;
heartRateMax?: number;
createdAt: Date;
}
export interface HealthRecommendation {
id: string;
userId: string;
category: string;
recommendation: string;
priority: 'high' | 'medium' | 'low';
basedOn: string[];
createdAt: Date;
}
export interface SyncStatus {
lastSyncTime: Date;
status: 'idle' | 'syncing' | 'error';
lastError?: string;
recordsSynced: number;
}

View File

@@ -0,0 +1,112 @@
import sqlite3 from 'sqlite3';
import path from 'path';
import fs from 'fs';
const dbPath = process.env.DATABASE_PATH || './data/health.db';
// Ensure data directory exists
const dataDir = path.dirname(dbPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
export const db = new sqlite3.Database(dbPath);
export function initializeDatabase() {
db.serialize(() => {
// Users table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
garmin_email TEXT NOT NULL,
garmin_password_hash TEXT NOT NULL,
jwt_token TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Health data table
db.run(`
CREATE TABLE IF NOT EXISTS health_data (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
date DATE NOT NULL,
steps INTEGER,
heart_rate INTEGER,
heart_rate_variability REAL,
blood_pressure_systolic INTEGER,
blood_pressure_diastolic INTEGER,
sleep_duration INTEGER,
sleep_quality REAL,
stress INTEGER,
calories_burned REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, date)
)
`);
// Activities table
db.run(`
CREATE TABLE IF NOT EXISTS activities (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
activity_type TEXT NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
duration INTEGER,
distance REAL,
calories REAL,
heart_rate_average INTEGER,
heart_rate_max INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
// Sync status table
db.run(`
CREATE TABLE IF NOT EXISTS sync_status (
user_id TEXT PRIMARY KEY,
last_sync_time DATETIME,
status TEXT DEFAULT 'idle',
last_error TEXT,
records_synced INTEGER DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
console.log('Database initialized successfully');
});
}
export function runAsync(sql: string, params: any[] = []): Promise<any> {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve({ id: this.lastID, changes: this.changes });
});
});
}
export function getAsync(sql: string, params: any[] = []): Promise<any> {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
export function allAsync(sql: string, params: any[] = []): Promise<any[]> {
return new Promise((resolve, reject) => {
db.all(sql, params, (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
}

20
server/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}