[阶段2.1-2.2] 实现 Garmin 数据同步和健康数据查询路由
后端实现: - 实现 garmin 路由 - POST /api/garmin/sync 触发 Garmin 数据同步 - GET /api/garmin/status 获取同步状态 - 实现 health 路由(所有端点都需要认证) - GET /api/health/summary 获取健康数据摘要 - GET /api/health/steps 获取步数数据 - GET /api/health/heart-rate 获取心率数据 - GET /api/health/sleep 获取睡眠数据 - GET /api/health/activities 获取运动数据 依赖: - 安装 garmin-connect 用于 Garmin API 集成 - 所有健康数据查询都通过认证中间件保护 验收标准已满足: - 所有路由返回正确的格式 - 认证检查已应用 - 日期范围过滤支持 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,8 @@ python tests/smoke.py
|
||||
## 📚 API 文档
|
||||
|
||||
### 认证
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `POST /api/auth/register` - 用户注册(email, garminEmail, garminPassword)
|
||||
- `POST /api/auth/login` - 用户登录(email, password)
|
||||
- `POST /api/auth/logout` - 用户登出
|
||||
- `POST /api/auth/refresh` - 刷新 Token
|
||||
|
||||
|
||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -5690,6 +5690,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/app-root-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
|
||||
"integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
@@ -7429,6 +7438,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
@@ -10346,6 +10362,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/garmin-connect": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/garmin-connect/-/garmin-connect-1.6.2.tgz",
|
||||
"integrity": "sha512-8QGeB3ZQTfaG1UZe1hd3CEOsaAozC1quG+CjX8yV0P6x+AyyWz4jqgIJ3MEK2NhDsxlXsF4jBOdpjUQubzmdFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"app-root-path": "^3.1.0",
|
||||
"axios": "^1.5.1",
|
||||
"crypto": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.3",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"qs": "^6.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/garmin-health-lab-client": {
|
||||
"resolved": "client",
|
||||
"link": true
|
||||
@@ -13665,6 +13697,15 @@
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
@@ -14452,6 +14493,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth-1.0a": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz",
|
||||
"integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -21599,6 +21646,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"garmin-connect": "^1.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.6.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"garmin-connect": "^1.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.6.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
|
||||
@@ -1,14 +1,76 @@
|
||||
import express from 'express';
|
||||
import { syncData, getSyncStatus } from '../services/GarminService';
|
||||
import { getUserById } from '../services/AuthService';
|
||||
import { authMiddleware, AuthRequest } from '../middleware/authMiddleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement Garmin API integration
|
||||
router.post('/sync', (req, res) => {
|
||||
res.json({ message: 'Garmin sync endpoint' });
|
||||
/**
|
||||
* POST /api/garmin/sync
|
||||
* Trigger Garmin data synchronization (requires authentication)
|
||||
*/
|
||||
router.post('/sync', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
status: 404,
|
||||
message: 'User not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Sync Garmin data using stored credentials
|
||||
const result = await syncData(userId, {
|
||||
garminEmail: user.garminEmail,
|
||||
garminPassword: req.body.garminPassword || '',
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: result.status === 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/status', (req, res) => {
|
||||
res.json({ message: 'Garmin sync status endpoint' });
|
||||
/**
|
||||
* GET /api/garmin/status
|
||||
* Get Garmin synchronization status (requires authentication)
|
||||
*/
|
||||
router.get('/status', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const status = await getSyncStatus(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: status,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,26 +1,170 @@
|
||||
import express from 'express';
|
||||
import { getSummary, getSteps, getHeartRate, getSleep, getActivities } from '../services/HealthService';
|
||||
import { authMiddleware, AuthRequest } from '../middleware/authMiddleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement health data endpoints
|
||||
router.get('/summary', (req, res) => {
|
||||
res.json({ message: 'Health summary endpoint' });
|
||||
// Apply authentication middleware to all health routes
|
||||
router.use(authMiddleware);
|
||||
|
||||
/**
|
||||
* GET /api/health/summary
|
||||
* Get health data summary for a date range
|
||||
* Query params: startDate, endDate (ISO 8601 format, e.g., 2024-08-23)
|
||||
*/
|
||||
router.get('/summary', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await getSummary(userId, {
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/steps', (req, res) => {
|
||||
res.json({ message: 'Steps data endpoint' });
|
||||
/**
|
||||
* GET /api/health/steps
|
||||
* Get steps data for a date range
|
||||
* Query params: startDate, endDate
|
||||
*/
|
||||
router.get('/steps', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await getSteps(userId, {
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/heart-rate', (req, res) => {
|
||||
res.json({ message: 'Heart rate data endpoint' });
|
||||
/**
|
||||
* GET /api/health/heart-rate
|
||||
* Get heart rate data for a date range
|
||||
* Query params: startDate, endDate
|
||||
*/
|
||||
router.get('/heart-rate', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await getHeartRate(userId, {
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/sleep', (req, res) => {
|
||||
res.json({ message: 'Sleep data endpoint' });
|
||||
/**
|
||||
* GET /api/health/sleep
|
||||
* Get sleep data for a date range
|
||||
* Query params: startDate, endDate
|
||||
*/
|
||||
router.get('/sleep', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await getSleep(userId, {
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/activities', (req, res) => {
|
||||
res.json({ message: 'Activities endpoint' });
|
||||
/**
|
||||
* GET /api/health/activities
|
||||
* Get activities for a date range
|
||||
* Query params: startDate, endDate
|
||||
*/
|
||||
router.get('/activities', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await getActivities(userId, {
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user