审计 57 个接口后,把账号里真有数据却从未入库的部分补上。全部走同步模块, 界面只读本地库。 新增数据 - 体重与身体成分(体脂率/肌肉量/体水分/骨量/内脏脂肪/代谢年龄) - 血压(接口通,账号暂无记录) - 跑步成绩预测(5 公里 / 10 公里 / 半马 / 全马) - 爬坡分、饮水量、出汗量 → health_data 新增七列 - 全天曲线:心率 / 压力 / 身体电量 / 呼吸 / 血氧 - 挑战赛(徽章挑战与好友挑战,与一次性的徽章不同,有周期和进度) - 已配对设备 新增界面 - /body/ 身体成分:体重大数字 + BMI 分级 + 体脂肌肉曲线 + 血压表格 - /race/ 成绩预测:四个距离的预测成绩与配速,以及预测随时间的变化 - /challenges/ 挑战赛:按类型筛选,有目标的显示进度条 - /devices/ 已配对设备 - 每日页新增「全天曲线」,这是存日内采样的主要目的 - 健康页新增「身体成分」分组与「更多」入口,运动页加挑战赛与成绩预测入口 同步开销 - 日内曲线每天五个请求,14 天以内的同步顺带拉,更长的历史交给后台 「补齐详细数据」,否则一年的同步会多出约 1800 个请求 - 原来的「补齐运动详情」扩展为统一的补齐任务,分阶段上报进度 日内采样抽稀到每天 240 点:手机图表分辨不出更多,只会把行撑大。 全量 446 项测试通过。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
136 lines
3.6 KiB
Python
136 lines
3.6 KiB
Python
"""Health data routes: summary / steps / heart-rate / sleep / activities."""
|
|
import datetime
|
|
|
|
from flask import Blueprint, request, g, jsonify
|
|
|
|
from auth import require_auth
|
|
from services import health as health_svc
|
|
from services import settings as settings_svc
|
|
from services import fitness_age
|
|
from services import garmin_extras as extras
|
|
|
|
bp = Blueprint("health", __name__)
|
|
|
|
|
|
def _range():
|
|
return request.args.get("startDate"), request.args.get("endDate")
|
|
|
|
|
|
@bp.route("/summary", methods=["GET"])
|
|
@require_auth
|
|
def summary():
|
|
s, e = _range()
|
|
return jsonify(health_svc.get_summary(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/steps", methods=["GET"])
|
|
@require_auth
|
|
def steps():
|
|
s, e = _range()
|
|
return jsonify(health_svc.get_steps(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/heart-rate", methods=["GET"])
|
|
@require_auth
|
|
def heart_rate():
|
|
s, e = _range()
|
|
return jsonify(health_svc.get_heart_rate(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/sleep", methods=["GET"])
|
|
@require_auth
|
|
def sleep():
|
|
s, e = _range()
|
|
return jsonify(health_svc.get_sleep(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/activities", methods=["GET"])
|
|
@require_auth
|
|
def activities():
|
|
s, e = _range()
|
|
return jsonify(health_svc.get_activities(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/fitness-age", methods=["GET"])
|
|
@require_auth
|
|
def body_age():
|
|
"""身体年龄, computed from the profile plus the most recent readings.
|
|
|
|
VO2max only refreshes after an outdoor run or ride, so the search window
|
|
is wide: the last recorded value is still the current one.
|
|
"""
|
|
profile = settings_svc.get_raw(g.user_id)
|
|
start = (datetime.date.today() - datetime.timedelta(days=180)).isoformat()
|
|
days = health_svc.get_summary(g.user_id, start, None)
|
|
|
|
def latest(key):
|
|
for day in reversed(days):
|
|
if day.get(key):
|
|
return day[key]
|
|
return None
|
|
|
|
return jsonify(fitness_age.estimate(
|
|
age=settings_svc.age_from(profile["birth_date"]),
|
|
sex=profile["sex"],
|
|
vo2max=latest("vo2max"),
|
|
resting_hr=latest("heartRate"),
|
|
bmi=settings_svc.bmi_from(profile["height_cm"], profile["weight_kg"]),
|
|
))
|
|
|
|
|
|
@bp.route("/badges", methods=["GET"])
|
|
@require_auth
|
|
def badges():
|
|
"""Earned badges (奖励), most recent first."""
|
|
return jsonify(health_svc.get_badges(g.user_id))
|
|
|
|
|
|
@bp.route("/personal-records", methods=["GET"])
|
|
@require_auth
|
|
def personal_records():
|
|
return jsonify(health_svc.get_personal_records(g.user_id))
|
|
|
|
|
|
@bp.route("/body-composition", methods=["GET"])
|
|
@require_auth
|
|
def body_composition():
|
|
"""Weight and everything a connected scale reports with it."""
|
|
s, e = _range()
|
|
return jsonify(extras.get_body_composition(g.user_id, s, e))
|
|
|
|
|
|
@bp.route("/blood-pressure", methods=["GET"])
|
|
@require_auth
|
|
def blood_pressure():
|
|
return jsonify(extras.get_blood_pressure(g.user_id))
|
|
|
|
|
|
@bp.route("/race-predictions", methods=["GET"])
|
|
@require_auth
|
|
def race_predictions():
|
|
"""Garmin's predicted 5K / 10K / half / marathon times, in seconds."""
|
|
return jsonify(extras.get_race_predictions(g.user_id))
|
|
|
|
|
|
@bp.route("/series", methods=["GET"])
|
|
@require_auth
|
|
def daily_series():
|
|
"""Within-day curves for one date: heart rate, stress, body battery,
|
|
respiration, SpO2."""
|
|
date = request.args.get("date")
|
|
if not date:
|
|
return jsonify({"error": "缺少 date 参数"}), 400
|
|
return jsonify(extras.get_daily_series(g.user_id, date))
|
|
|
|
|
|
@bp.route("/challenges", methods=["GET"])
|
|
@require_auth
|
|
def challenges():
|
|
return jsonify(extras.get_challenges(g.user_id))
|
|
|
|
|
|
@bp.route("/devices", methods=["GET"])
|
|
@require_auth
|
|
def devices():
|
|
return jsonify(extras.get_devices(g.user_id))
|