[阶段1.1-1.7] 实现完整的认证系统
后端实现: - 创建 AuthService 包含密码加密、JWT 生成和验证 - 创建 authMiddleware 用于 API 路由保护 - 实现 auth 路由 (register, login, logout, /me) 前端实现: - 创建 Login 页面 (登录/注册标签页) - 创建 ProtectedRoute 组件用于路由保护 - 更新 App.tsx 集成路由保护 - 前端 API 客户端已包含认证方法和拦截器 验收标准已满足: - 用户可以注册和登录 - JWT Token 正确生成和验证 - 受保护的路由需要有效 Token - 未认证用户重定向到登录页面 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
153
backend/services/health.py
Normal file
153
backend/services/health.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Health data service: read endpoints + upsert helpers used by the Garmin sync.
|
||||
|
||||
Mirrors the original Node HealthService, including the camelCase JSON mapping.
|
||||
Upserts use backend-specific SQL because SQLite does not support
|
||||
`ON DUPLICATE KEY UPDATE` (it uses `ON CONFLICT ... DO UPDATE`).
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from db import execute, query_one, query_all
|
||||
from config import DB_TYPE
|
||||
|
||||
|
||||
def _range_sql(user_id, start=None, end=None):
|
||||
params = [user_id]
|
||||
sql = "WHERE user_id = ?"
|
||||
if start:
|
||||
sql += " AND date >= ?"
|
||||
params.append(start)
|
||||
if end:
|
||||
sql += " AND date <= ?"
|
||||
params.append(end)
|
||||
return sql, params
|
||||
|
||||
|
||||
def get_summary(user_id, start=None, end=None):
|
||||
sql, params = _range_sql(user_id, start, end)
|
||||
rows = query_all(
|
||||
"SELECT date, steps, heart_rate, heart_rate_variability, "
|
||||
"sleep_duration, sleep_quality, stress, calories_burned "
|
||||
f"FROM health_data {sql} ORDER BY date ASC",
|
||||
params,
|
||||
)
|
||||
return [
|
||||
{
|
||||
"date": r["date"],
|
||||
"steps": r["steps"],
|
||||
"heartRate": r["heart_rate"],
|
||||
"heartRateVariability": r["heart_rate_variability"],
|
||||
"sleep": (
|
||||
{"duration": r["sleep_duration"], "quality": r["sleep_quality"]}
|
||||
if r["sleep_duration"] is not None
|
||||
else None
|
||||
),
|
||||
"stress": r["stress"],
|
||||
"caloriesBurned": r["calories_burned"],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def get_steps(user_id, start=None, end=None):
|
||||
sql, params = _range_sql(user_id, start, end)
|
||||
rows = query_all(
|
||||
f"SELECT date, steps FROM health_data {sql} AND steps IS NOT NULL ORDER BY date ASC",
|
||||
params,
|
||||
)
|
||||
return [{"date": r["date"], "steps": r["steps"]} for r in rows]
|
||||
|
||||
|
||||
def get_heart_rate(user_id, start=None, end=None):
|
||||
sql, params = _range_sql(user_id, start, end)
|
||||
rows = query_all(
|
||||
f"SELECT date, heart_rate, heart_rate_variability FROM health_data {sql} "
|
||||
"AND heart_rate IS NOT NULL ORDER BY date ASC",
|
||||
params,
|
||||
)
|
||||
return [
|
||||
{
|
||||
"date": r["date"],
|
||||
"heartRate": r["heart_rate"],
|
||||
"heartRateVariability": r["heart_rate_variability"],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def get_sleep(user_id, start=None, end=None):
|
||||
sql, params = _range_sql(user_id, start, end)
|
||||
rows = query_all(
|
||||
f"SELECT date, sleep_duration, sleep_quality FROM health_data {sql} "
|
||||
"AND sleep_duration IS NOT NULL ORDER BY date ASC",
|
||||
params,
|
||||
)
|
||||
return [
|
||||
{"date": r["date"], "duration": r["sleep_duration"], "quality": r["sleep_quality"]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def get_activities(user_id, start=None, end=None):
|
||||
sql, params = _range_sql(user_id, start, end)
|
||||
rows = query_all(
|
||||
"SELECT id, activity_type, start_time, end_time, duration, distance, "
|
||||
"calories, heart_rate_average, heart_rate_max "
|
||||
f"FROM activities {sql} ORDER BY start_time DESC",
|
||||
params,
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def upsert_health_daily(user_id, record):
|
||||
hid = f"{user_id}-{record['date']}"
|
||||
cols = [
|
||||
"id", "user_id", "date", "steps", "heart_rate",
|
||||
"heart_rate_variability", "blood_pressure_systolic",
|
||||
"blood_pressure_diastolic", "sleep_duration", "sleep_quality",
|
||||
"stress", "calories_burned",
|
||||
]
|
||||
placeholders = ", ".join(["?"] * len(cols))
|
||||
vals = [
|
||||
hid, user_id, record.get("date"), record.get("steps"),
|
||||
record.get("heartRate"), record.get("heartRateVariability"),
|
||||
record.get("bloodPressureSystolic"), record.get("bloodPressureDiastolic"),
|
||||
record.get("sleepDuration"), record.get("sleepQuality"),
|
||||
record.get("stress"), record.get("caloriesBurned"),
|
||||
]
|
||||
if DB_TYPE == "mariadb":
|
||||
update_cols = [c for c in cols if c not in ("id", "user_id")]
|
||||
updates = ", ".join([f"{c}=VALUES({c})" for c in update_cols])
|
||||
sql = (
|
||||
f"INSERT INTO health_data ({', '.join(cols)}) VALUES ({placeholders}) "
|
||||
f"ON DUPLICATE KEY UPDATE {updates}, updated_at=CURRENT_TIMESTAMP"
|
||||
)
|
||||
else:
|
||||
update_cols = [c for c in cols if c not in ("id", "user_id")]
|
||||
updates = ", ".join([f"{c}=excluded.{c}" for c in update_cols])
|
||||
sql = (
|
||||
f"INSERT INTO health_data ({', '.join(cols)}) VALUES ({placeholders}) "
|
||||
f"ON CONFLICT(user_id, date) DO UPDATE SET {updates}, updated_at=CURRENT_TIMESTAMP"
|
||||
)
|
||||
execute(sql, vals)
|
||||
return hid
|
||||
|
||||
|
||||
def insert_activity(user_id, activity):
|
||||
aid = str(uuid.uuid4())
|
||||
cols = [
|
||||
"id", "user_id", "activity_type", "start_time", "end_time",
|
||||
"duration", "distance", "calories", "heart_rate_average", "heart_rate_max",
|
||||
]
|
||||
placeholders = ", ".join(["?"] * len(cols))
|
||||
vals = [
|
||||
aid, user_id, activity.get("activityType"), activity.get("startTime"),
|
||||
activity.get("endTime"), activity.get("duration"), activity.get("distance"),
|
||||
activity.get("calories"), activity.get("heartRateAverage"),
|
||||
activity.get("heartRateMax"),
|
||||
]
|
||||
execute(
|
||||
f"INSERT INTO activities ({', '.join(cols)}) VALUES ({placeholders})",
|
||||
vals,
|
||||
)
|
||||
return aid
|
||||
Reference in New Issue
Block a user