Files
GarminHealthLab/backend/routes/settings.py
ericwyuan c70e7ced80 feat(api): 个人资料/单位/同步偏好 + 运动详情 + 身体年龄
设置 (services/settings.py, routes/settings.py)
- user_settings 表:身高/体重/出生日期/性别/单位/自动同步开关/同步频率/历史范围
- GET|PUT /api/settings,GET /api/settings/options(取值由后端给,前端不臆造)
- GET /api/settings/rating-basis:把每条参考区间的来源公开出来。
  一个把数字标成「偏低」的区间是在下判断,用户有权看到依据。

运动详情 (services/garmin.py)
- GET /api/garmin/activities/<id>/detail:概览/分段/心率区间/天气/装备/采样曲线
- 首次打开回源 Garmin 并落库,之后走缓存;?refresh=1 强制刷新
- 采样点在写入时抽稀到 300,手机图表画不了更多,也免得整行撑大

身体年龄 (services/fitness_age.py)
- 0.2.8 版 garminconnect 没有 fitnessage 接口,改为本地按公开常模推算:
  VO₂max 对应年龄为基准,静息心率与 BMI 做有上限的修正
- 返回每一步的中间值,界面照实展示,不做成一个不可追溯的分数
- 高于参考表最年轻一档时按 20 岁计——那里外推会得到「11 岁」这种结果

调度器改为每 5 分钟 tick,是否该同步按各账号自己的频率判断

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-24 00:26:26 +08:00

86 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Profile, units and sync preferences."""
from flask import Blueprint, request, g, jsonify
from auth import require_auth
from services import settings as settings_svc
from services import fitness_age
bp = Blueprint("settings", __name__)
@bp.route("", methods=["GET"])
@bp.route("/", methods=["GET"])
@require_auth
def get_settings():
return jsonify(settings_svc.get_settings(g.user_id))
@bp.route("", methods=["PUT"])
@bp.route("/", methods=["PUT"])
@require_auth
def put_settings():
try:
return jsonify(settings_svc.save_settings(g.user_id, request.json or {}))
except settings_svc.InvalidSetting as e:
return jsonify({"error": str(e)}), 400
@bp.route("/options", methods=["GET"])
@require_auth
def options():
"""The values the pickers may offer, so the UI never invents one the
backend would reject."""
return jsonify({
"sexes": list(settings_svc.SEXES),
"units": list(settings_svc.UNITS),
"autoSyncMinutes": list(settings_svc.INTERVALS),
"historyDays": list(settings_svc.HISTORY),
})
@bp.route("/rating-basis", methods=["GET"])
@require_auth
def rating_basis():
"""Where every rating in the app comes from.
Surfaced in 设置 because a band that colours a number 偏低 is making a
claim, and the user is entitled to see what that claim rests on.
"""
return jsonify({
"fitnessAge": fitness_age.BASIS,
"bands": BAND_SOURCES,
"note": (
"参考区间用于给数字一个位置感,不是诊断标准。"
"AI 只负责解读这些结果,不参与设定任何阈值。"
),
})
# Kept beside the UI's RANGES table (client/src/lib/ranges.ts). Each entry says
# where that metric's band edges came from — the honest answer for several of
# them is "general-population orientation figures", and it says so.
BAND_SOURCES = [
{"metric": "步数", "bands": "<5k 偏低 · 58k 一般 · 812k 达标 · >12k 优秀",
"source": "步数与死亡率的队列研究(约 8000 步起获益明显12000 步后趋平)"},
{"metric": "静息心率", "bands": "<50 很低 · 5065 正常 · 6575 偏高 · >75 较高",
"source": "健康成人静息心率 60100 bpm 为正常范围,规律运动者常低于 60"},
{"metric": "心率变异性", "bands": "<25 偏低 · 2540 一般 · 4070 良好 · >70 很好",
"source": "夜间 RMSSD 的一般人群分布;个体差异极大,趋势比绝对值更有意义"},
{"metric": "睡眠时长", "bands": "<6h 不足 · 67h 偏少 · 79h 充足 · >9h 偏多",
"source": "美国睡眠医学会 / 睡眠研究会成人 79 小时建议"},
{"metric": "压力", "bands": "025 休息 · 2650 偏低 · 5175 中等 · >75 偏高",
"source": "Garmin 官方压力分级,与手表显示一致"},
{"metric": "身体电量", "bands": "025 很低 · 2650 偏低 · 5175 良好 · >75 充足",
"source": "Garmin 官方 Body Battery 分级"},
{"metric": "血氧", "bands": "<90 偏低 · 9094 略低 · ≥95 正常",
"source": "静息血氧饱和度常用临床参考;腕表光学测量误差较大,仅供趋势参考"},
{"metric": "呼吸频率", "bands": "<12 偏低 · 1220 正常 · >20 偏高",
"source": "成人静息呼吸频率 1220 次/分"},
{"metric": "强度分钟", "bands": "<10 偏少 · 1021 一般 · ≥21 达标",
"source": "WHO 每周 150 分钟中等强度活动,折合每天约 21 分钟"},
{"metric": "训练准备度", "bands": "025 很低 · 2650 偏低 · 5175 就绪 · >75 很好",
"source": "Garmin 官方 Training Readiness 分级"},
{"metric": "爬楼", "bands": "<5 偏少 · 510 达标 · >10 优秀",
"source": "一般性活动量参考,无权威标准"},
]