后端实现: - 创建 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>
154 lines
5.1 KiB
Python
154 lines
5.1 KiB
Python
"""
|
|
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
|