feat(api): 添加营养建议端点 /api/nutrition/recommendations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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])
|
@app.get("/api/meals", response_model=list[MealResponse])
|
||||||
async def get_meals(
|
async def get_meals(
|
||||||
days: int = Query(default=30, ge=1, le=365, description="查询天数"),
|
days: int = Query(default=30, ge=1, le=365, description="查询天数"),
|
||||||
|
|||||||
@@ -317,3 +317,14 @@ def test_bmi_analysis_endpoint(client):
|
|||||||
assert "bmi_status" in data
|
assert "bmi_status" in data
|
||||||
assert "target_weight" in data
|
assert "target_weight" in data
|
||||||
assert "estimated_days" 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
|
||||||
|
|||||||
Reference in New Issue
Block a user