diff --git a/src/vitals/web/app.py b/src/vitals/web/app.py index 1696230..7631a14 100644 --- a/src/vitals/web/app.py +++ b/src/vitals/web/app.py @@ -1264,6 +1264,119 @@ async def get_bmi_analysis(): } +@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, + } + + @app.get("/api/meals", response_model=list[MealResponse]) async def get_meals( days: int = Query(default=30, ge=1, le=365, description="查询天数"), diff --git a/tests/test_web.py b/tests/test_web.py index 4f3575a..bdefc69 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -317,3 +317,14 @@ def test_bmi_analysis_endpoint(client): assert "bmi_status" in data assert "target_weight" in data assert "estimated_days" in data + + +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