# 健康模块增强实现计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 为运动、饮食、睡眠三个模块添加健康分析功能,包括 BMI 分析、营养建议区间、睡眠预警提示。 **Architecture:** - 新增 3 个 API 端点提供分析数据 - 修改 3 个页面 HTML 模板添加新 UI 组件 - 复用现有 `UserConfig.tdee` 和 `User.bmi` 计算逻辑 **Tech Stack:** FastAPI, Chart.js, 原生 HTML/CSS/JS --- ## 任务概览 | 任务 | 模块 | 预估步骤 | |-----|------|---------| | Task 1 | API: BMI 分析端点 | 5 步 | | Task 2 | API: 营养建议端点 | 5 步 | | Task 3 | API: 睡眠评估端点 | 5 步 | | Task 4 | UI: 运动页面 BMI 卡片 | 5 步 | | Task 5 | UI: 饮食页面营养建议 | 5 步 | | Task 6 | UI: 睡眠页面预警卡片 | 5 步 | --- ## Task 1: API - BMI 分析端点 **Files:** - Modify: `src/vitals/web/app.py:1163` (在 `/api/exercises/stats` 后添加) - Test: `tests/test_web.py` ### Step 1: 写失败测试 在 `tests/test_web.py` 末尾添加: ```python def test_bmi_analysis_endpoint(client): """测试 BMI 分析 API""" response = client.get("/api/bmi/analysis") assert response.status_code == 200 data = response.json() assert "current_weight" in data assert "current_bmi" in data assert "bmi_status" in data assert "target_weight" in data assert "estimated_days" in data ``` ### Step 2: 运行测试确认失败 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_bmi_analysis_endpoint -v ``` 预期: FAIL - 404 Not Found ### Step 3: 实现 API 端点 在 `src/vitals/web/app.py` 第 1163 行后添加: ```python @app.get("/api/bmi/analysis") async def get_bmi_analysis(): """获取 BMI 分析数据""" active_user = db.get_active_user() if not active_user: raise HTTPException(status_code=400, detail="没有激活的用户") config = db.get_config(active_user.id) # 获取最新体重记录 today = date.today() weight_records = db.get_weight_records( start_date=today - timedelta(days=30), end_date=today, user_id=active_user.id ) current_weight = weight_records[0].weight_kg if weight_records else config.weight # 计算 BMI height_m = (config.height or 170) / 100 current_bmi = round(current_weight / (height_m * height_m), 1) if current_weight else None # BMI 状态 def get_bmi_status(bmi): if bmi is None: return None if bmi < 18.5: return {"status": "偏瘦", "color": "#60A5FA", "range": "<18.5"} elif bmi < 24: return {"status": "正常", "color": "#34D399", "range": "18.5-24"} elif bmi < 28: return {"status": "偏重", "color": "#FBBF24", "range": "24-28"} else: return {"status": "肥胖", "color": "#F87171", "range": ">28"} bmi_info = get_bmi_status(current_bmi) # 目标体重(从用户配置或默认 BMI 22 计算) target_weight = getattr(config, 'target_weight', None) if not target_weight: # 默认目标 BMI 22 target_weight = round(22 * height_m * height_m, 1) target_bmi = round(target_weight / (height_m * height_m), 1) weight_diff = round(current_weight - target_weight, 1) if current_weight else None # 计算预估达成天数 estimated_days = None daily_deficit = None calculation_basis = None if current_weight and target_weight and config.tdee: # 获取近30天饮食数据 meals = db.get_meals( start_date=today - timedelta(days=30), end_date=today, user_id=active_user.id ) # 获取近30天运动数据 exercises = db.get_exercises( start_date=today - timedelta(days=30), end_date=today, user_id=active_user.id ) # 计算平均每日摄入 total_intake = sum(m.calories for m in meals) days_with_data = len(set(m.date for m in meals)) or 1 avg_daily_intake = total_intake / days_with_data # 计算平均每日运动消耗 total_exercise = sum(e.calories for e in exercises) avg_daily_exercise = total_exercise / 30 # 每日净消耗 = TDEE - 摄入 + 运动 daily_deficit = round(config.tdee - avg_daily_intake + avg_daily_exercise) if daily_deficit > 0 and weight_diff > 0: # 7700 卡 ≈ 1 kg estimated_days = round(weight_diff * 7700 / daily_deficit) calculation_basis = { "tdee": config.tdee, "avg_intake": round(avg_daily_intake), "avg_exercise": round(avg_daily_exercise), "daily_deficit": daily_deficit } return { "current_weight": current_weight, "current_bmi": current_bmi, "bmi_status": bmi_info, "height": config.height, "target_weight": target_weight, "target_bmi": target_bmi, "weight_diff": weight_diff, "estimated_days": estimated_days, "calculation_basis": calculation_basis, "has_sufficient_data": bool(calculation_basis) } ``` ### Step 4: 运行测试确认通过 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_bmi_analysis_endpoint -v ``` 预期: PASS ### Step 5: 提交 ```bash git add src/vitals/web/app.py tests/test_web.py git commit -m "feat(api): 添加 BMI 分析端点 /api/bmi/analysis" ``` --- ## Task 2: API - 营养建议端点 **Files:** - Modify: `src/vitals/web/app.py` (在 BMI API 后添加) - Test: `tests/test_web.py` ### Step 1: 写失败测试 ```python def test_nutrition_recommendations_endpoint(client): """测试营养建议 API""" response = client.get("/api/nutrition/recommendations") assert response.status_code == 200 data = response.json() assert "daily_targets" in data assert "today_intake" in data assert "gaps" in data assert "suggestions" in data ``` ### Step 2: 运行测试确认失败 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_nutrition_recommendations_endpoint -v ``` 预期: FAIL - 404 Not Found ### Step 3: 实现 API 端点 ```python @app.get("/api/nutrition/recommendations") async def get_nutrition_recommendations(): """获取营养建议数据""" active_user = db.get_active_user() if not active_user: raise HTTPException(status_code=400, detail="没有激活的用户") config = db.get_config(active_user.id) tdee = config.tdee or 1800 # 根据目标调整热量目标 goal_multipliers = { "lose": 0.8, # 减重: 80% TDEE "maintain": 1.0, # 维持: 100% TDEE "gain": 1.15 # 增重: 115% TDEE } calorie_target = int(tdee * goal_multipliers.get(config.goal, 1.0)) # 营养素目标 (基于 TDEE) # 蛋白质 25%, 碳水 50%, 脂肪 25% daily_targets = { "calories": calorie_target, "protein": round(calorie_target * 0.25 / 4), # 每克蛋白质 4 卡 "carbs": round(calorie_target * 0.50 / 4), # 每克碳水 4 卡 "fat": round(calorie_target * 0.25 / 9), # 每克脂肪 9 卡 } # 建议区间 recommended_ranges = { "protein": {"min": round(calorie_target * 0.20 / 4), "max": round(calorie_target * 0.30 / 4)}, "carbs": {"min": round(calorie_target * 0.45 / 4), "max": round(calorie_target * 0.55 / 4)}, "fat": {"min": round(calorie_target * 0.20 / 9), "max": round(calorie_target * 0.30 / 9)}, } # 今日摄入 today = date.today() today_meals = db.get_meals(start_date=today, end_date=today, user_id=active_user.id) today_intake = { "calories": sum(m.calories for m in today_meals), "protein": sum(m.protein or 0 for m in today_meals), "carbs": sum(m.carbs or 0 for m in today_meals), "fat": sum(m.fat or 0 for m in today_meals), } # 计算差距 gaps = { "calories": daily_targets["calories"] - today_intake["calories"], "protein": daily_targets["protein"] - today_intake["protein"], "carbs": daily_targets["carbs"] - today_intake["carbs"], "fat": daily_targets["fat"] - today_intake["fat"], } # 餐次分布 meal_type_calories = {} for m in today_meals: meal_type_calories[m.meal_type] = meal_type_calories.get(m.meal_type, 0) + m.calories meal_distribution = { "早餐": {"actual": meal_type_calories.get("早餐", 0), "recommended_pct": "25-30%"}, "午餐": {"actual": meal_type_calories.get("午餐", 0), "recommended_pct": "35-40%"}, "晚餐": {"actual": meal_type_calories.get("晚餐", 0), "recommended_pct": "25-30%"}, "加餐": {"actual": meal_type_calories.get("加餐", 0), "recommended_pct": "0-10%"}, } # 生成建议 suggestions = [] if gaps["protein"] > 20: suggestions.append({ "type": "protein", "message": f"蛋白质缺口较大 ({gaps['protein']}g),建议增加:", "options": [ "鸡胸肉 150g (+46g 蛋白质, 165 卡)", "鸡蛋 3 个 (+18g 蛋白质, 210 卡)", "豆腐 200g (+16g 蛋白质, 160 卡)", ] }) if gaps["calories"] > 300: suggestions.append({ "type": "calories", "message": f"热量摄入不足 ({gaps['calories']} 卡),注意补充能量", "options": [] }) # 近 7 天趋势 week_start = today - timedelta(days=6) week_meals = db.get_meals(start_date=week_start, end_date=today, user_id=active_user.id) weekly_trend = [] for i in range(7): day = week_start + timedelta(days=i) day_meals = [m for m in week_meals if m.date == day] weekly_trend.append({ "date": day.isoformat(), "calories": sum(m.calories for m in day_meals), "protein": sum(m.protein or 0 for m in day_meals), "carbs": sum(m.carbs or 0 for m in day_meals), "fat": sum(m.fat or 0 for m in day_meals), }) return { "tdee": tdee, "goal": config.goal, "daily_targets": daily_targets, "recommended_ranges": recommended_ranges, "today_intake": today_intake, "gaps": gaps, "meal_distribution": meal_distribution, "suggestions": suggestions, "weekly_trend": weekly_trend, } ``` ### Step 4: 运行测试确认通过 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_nutrition_recommendations_endpoint -v ``` 预期: PASS ### Step 5: 提交 ```bash git add src/vitals/web/app.py tests/test_web.py git commit -m "feat(api): 添加营养建议端点 /api/nutrition/recommendations" ``` --- ## Task 3: API - 睡眠评估端点 **Files:** - Modify: `src/vitals/web/app.py` (在营养 API 后添加) - Test: `tests/test_web.py` ### Step 1: 写失败测试 ```python def test_sleep_assessment_endpoint(client): """测试睡眠评估 API""" response = client.get("/api/sleep/assessment") assert response.status_code == 200 data = response.json() assert "avg_duration" in data assert "status" in data assert "health_impacts" in data assert "ideal_days_count" in data ``` ### Step 2: 运行测试确认失败 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_sleep_assessment_endpoint -v ``` 预期: FAIL - 404 Not Found ### Step 3: 实现 API 端点 ```python @app.get("/api/sleep/assessment") async def get_sleep_assessment(): """获取睡眠评估数据""" active_user = db.get_active_user() if not active_user: raise HTTPException(status_code=400, detail="没有激活的用户") today = date.today() week_start = today - timedelta(days=6) month_start = today - timedelta(days=29) # 获取近 7 天和近 30 天睡眠数据 week_records = db.get_sleep_records(start_date=week_start, end_date=today, user_id=active_user.id) month_records = db.get_sleep_records(start_date=month_start, end_date=today, user_id=active_user.id) # 计算平均睡眠时长 week_durations = [r.duration for r in week_records] month_durations = [r.duration for r in month_records] avg_week = sum(week_durations) / len(week_durations) if week_durations else 0 avg_month = sum(month_durations) / len(month_durations) if month_durations else 0 # 判断睡眠状态 def get_sleep_status(avg_hours): if avg_hours < 5: return { "level": "severe", "label": "严重不足", "color": "#EF4444", "icon": "alert-circle" } elif avg_hours < 6: return { "level": "insufficient", "label": "睡眠不足", "color": "#F59E0B", "icon": "alert-triangle" } elif avg_hours <= 9: return { "level": "ideal", "label": "睡眠充足", "color": "#10B981", "icon": "check-circle" } else: return { "level": "excessive", "label": "睡眠过多", "color": "#3B82F6", "icon": "info" } status = get_sleep_status(avg_week) # 统计理想天数 ideal_days = sum(1 for d in week_durations if 7 <= d <= 9) insufficient_days = sum(1 for d in week_durations if d < 7) month_ideal_days = sum(1 for d in month_durations if 7 <= d <= 9) month_insufficient_days = sum(1 for d in month_durations if d < 7) # 健康影响信息 warning_impacts = { "cognitive": { "title": "认知能力", "effects": ["注意力下降 40%", "记忆力减退", "决策能力受损"] }, "physical": { "title": "身体健康", "effects": ["免疫力下降", "肥胖风险增加 33%", "心血管疾病风险上升"] }, "emotional": { "title": "情绪状态", "effects": ["焦虑抑郁风险增加", "情绪波动大", "压力耐受力降低"] }, "exercise": { "title": "运动表现", "effects": ["反应速度下降", "肌肉恢复减慢", "受伤风险增加"] } } benefit_impacts = { "cognitive": { "title": "认知提升", "effects": ["记忆巩固增强", "专注力提高", "创造力活跃"] }, "physical": { "title": "身体修复", "effects": ["免疫系统强化", "肌肉组织修复", "激素分泌平衡"] }, "emotional": { "title": "情绪稳定", "effects": ["情绪调节能力增强", "压力抵抗力提升", "心态积极"] }, "exercise": { "title": "运动增益", "effects": ["运动表现提升 15%", "恢复速度加快", "耐力增强"] } } health_impacts = warning_impacts if status["level"] in ["severe", "insufficient"] else benefit_impacts # 建议 if status["level"] == "severe": suggestion = "建议立即调整作息,每天提前 1 小时入睡" elif status["level"] == "insufficient": suggestion = "尝试提前 30 分钟入睡,逐步调整作息" elif status["level"] == "ideal": suggestion = "继续保持,规律作息是健康的基石!" else: suggestion = "睡眠时间过长也可能影响健康,建议控制在 7-9 小时" # 距理想的差距 ideal_target = 7.5 gap_hours = round(ideal_target - avg_week, 1) if avg_week < 7 else 0 return { "avg_duration": round(avg_week, 1), "avg_duration_month": round(avg_month, 1), "status": status, "gap_hours": gap_hours, "ideal_days_count": ideal_days, "insufficient_days_count": insufficient_days, "total_days": len(week_durations), "month_stats": { "ideal_days": month_ideal_days, "insufficient_days": month_insufficient_days, "total_days": len(month_durations) }, "health_impacts": health_impacts, "suggestion": suggestion, "thresholds": { "severe": 5, "insufficient": 6, "ideal_min": 7, "ideal_max": 9 } } ``` ### Step 4: 运行测试确认通过 ```bash cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_sleep_assessment_endpoint -v ``` 预期: PASS ### Step 5: 提交 ```bash git add src/vitals/web/app.py tests/test_web.py git commit -m "feat(api): 添加睡眠评估端点 /api/sleep/assessment" ``` --- ## Task 4: UI - 运动页面 BMI 分析卡片 **Files:** - Modify: `src/vitals/web/app.py:4104` (`get_exercise_page_html` 函数) ### Step 1: 定位插入位置 在 `get_exercise_page_html()` 函数中,找到统计卡片 grid 结束位置(约第 4404 行 `` 之后),插入 BMI 分析卡片。 ### Step 2: 添加 BMI 分析卡片 HTML 在第 4404 行后插入: ```html
${intro}