This commit is contained in:
462
services/nutrition_service/main.py
Normal file
462
services/nutrition_service/main.py
Normal file
@@ -0,0 +1,462 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query, Path, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import and_, desc, select, func, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from services.nutrition_service.models import (
|
||||
FoodItem, UserNutritionEntry, WaterIntake,
|
||||
UserActivityEntry, NutritionGoal
|
||||
)
|
||||
from services.nutrition_service.schemas import (
|
||||
FoodItemCreate, FoodItemResponse, UserNutritionEntryCreate,
|
||||
UserNutritionEntryResponse, WaterIntakeCreate, WaterIntakeResponse,
|
||||
UserActivityEntryCreate, UserActivityEntryResponse,
|
||||
NutritionGoalCreate, NutritionGoalResponse,
|
||||
FoodSearchQuery, FoodDetailsQuery, DailyNutritionSummary
|
||||
)
|
||||
from services.nutrition_service.fatsecret_client import FatSecretClient
|
||||
from shared.auth import get_current_user_from_token
|
||||
from shared.config import settings
|
||||
from shared.database import get_db
|
||||
|
||||
app = FastAPI(title="Nutrition Service", version="1.0.0")
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Создаем клиент FatSecret
|
||||
fatsecret_client = FatSecretClient()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "service": "nutrition_service"}
|
||||
|
||||
|
||||
# Эндпоинты для работы с API FatSecret
|
||||
@app.post("/api/v1/nutrition/search", response_model=List[FoodItemResponse])
|
||||
async def search_foods(
|
||||
search_query: FoodSearchQuery,
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Поиск продуктов питания по запросу в FatSecret API"""
|
||||
try:
|
||||
# Вызов API FatSecret для поиска продуктов
|
||||
search_results = await fatsecret_client.search_foods(
|
||||
search_query.query,
|
||||
search_query.page_number,
|
||||
search_query.max_results
|
||||
)
|
||||
|
||||
# Обработка результатов поиска
|
||||
foods = []
|
||||
if 'foods' in search_results and 'food' in search_results['foods']:
|
||||
food_list = search_results['foods']['food']
|
||||
# Если результат всего один, API возвращает словарь вместо списка
|
||||
if isinstance(food_list, dict):
|
||||
food_list = [food_list]
|
||||
|
||||
for food in food_list:
|
||||
# Получение деталей о продукте
|
||||
food_details = await fatsecret_client.get_food_details(food['food_id'])
|
||||
parsed_food = await fatsecret_client.parse_food_data(food_details)
|
||||
|
||||
# Проверяем, существует ли продукт в базе данных
|
||||
query = select(FoodItem).where(FoodItem.fatsecret_id == parsed_food['fatsecret_id'])
|
||||
result = await db.execute(query)
|
||||
db_food = result.scalars().first()
|
||||
|
||||
# Если продукт не существует, сохраняем его
|
||||
if not db_food:
|
||||
db_food = FoodItem(**parsed_food)
|
||||
db.add(db_food)
|
||||
await db.commit()
|
||||
await db.refresh(db_food)
|
||||
|
||||
foods.append(FoodItemResponse.model_validate(db_food))
|
||||
|
||||
return foods
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error searching foods: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/food/{food_id}", response_model=FoodItemResponse)
|
||||
async def get_food_details(
|
||||
food_id: int = Path(..., description="ID продукта в базе данных"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение детальной информации о продукте по ID из базы данных"""
|
||||
query = select(FoodItem).where(FoodItem.id == food_id)
|
||||
result = await db.execute(query)
|
||||
food = result.scalars().first()
|
||||
|
||||
if not food:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Food item not found"
|
||||
)
|
||||
|
||||
return FoodItemResponse.model_validate(food)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/fatsecret/{fatsecret_id}", response_model=FoodItemResponse)
|
||||
async def get_food_by_fatsecret_id(
|
||||
fatsecret_id: str = Path(..., description="ID продукта в FatSecret"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение детальной информации о продукте по FatSecret ID"""
|
||||
# Проверяем, есть ли продукт в нашей базе данных
|
||||
query = select(FoodItem).where(FoodItem.fatsecret_id == fatsecret_id)
|
||||
result = await db.execute(query)
|
||||
food = result.scalars().first()
|
||||
|
||||
# Если продукт не найден в базе, запрашиваем его с FatSecret API
|
||||
if not food:
|
||||
try:
|
||||
food_details = await fatsecret_client.get_food_details(fatsecret_id)
|
||||
parsed_food = await fatsecret_client.parse_food_data(food_details)
|
||||
|
||||
food = FoodItem(**parsed_food)
|
||||
db.add(food)
|
||||
await db.commit()
|
||||
await db.refresh(food)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching food details: {str(e)}"
|
||||
)
|
||||
|
||||
return FoodItemResponse.model_validate(food)
|
||||
|
||||
|
||||
# Эндпоинты для работы с записями питания пользователя
|
||||
@app.post("/api/v1/nutrition/entries", response_model=UserNutritionEntryResponse)
|
||||
async def create_nutrition_entry(
|
||||
entry_data: UserNutritionEntryCreate,
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Создание новой записи о питании пользователя"""
|
||||
# Получаем ID пользователя из токена
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
# Если указан ID продукта, проверяем его наличие
|
||||
food_item = None
|
||||
if entry_data.food_item_id:
|
||||
query = select(FoodItem).where(FoodItem.id == entry_data.food_item_id)
|
||||
result = await db.execute(query)
|
||||
food_item = result.scalars().first()
|
||||
|
||||
if not food_item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Food item not found"
|
||||
)
|
||||
|
||||
# Создаем данные для записи
|
||||
nutrition_data = entry_data.model_dump(exclude={"food_item_id"})
|
||||
nutrition_entry = UserNutritionEntry(**nutrition_data, user_id=user_id)
|
||||
|
||||
if food_item:
|
||||
nutrition_entry.food_item_id = food_item.id
|
||||
|
||||
# Если питательные данные не указаны, рассчитываем их на основе продукта
|
||||
if not entry_data.calories and food_item.calories:
|
||||
nutrition_entry.calories = food_item.calories * entry_data.quantity
|
||||
if not entry_data.protein_grams and food_item.protein_grams:
|
||||
nutrition_entry.protein_grams = food_item.protein_grams * entry_data.quantity
|
||||
if not entry_data.fat_grams and food_item.fat_grams:
|
||||
nutrition_entry.fat_grams = food_item.fat_grams * entry_data.quantity
|
||||
if not entry_data.carbs_grams and food_item.carbs_grams:
|
||||
nutrition_entry.carbs_grams = food_item.carbs_grams * entry_data.quantity
|
||||
|
||||
db.add(nutrition_entry)
|
||||
await db.commit()
|
||||
await db.refresh(nutrition_entry)
|
||||
|
||||
return UserNutritionEntryResponse.model_validate(nutrition_entry)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/entries", response_model=List[UserNutritionEntryResponse])
|
||||
async def get_user_nutrition_entries(
|
||||
start_date: date = Query(..., description="Начальная дата для выборки"),
|
||||
end_date: date = Query(..., description="Конечная дата для выборки"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение записей о питании пользователя за указанный период"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
query = (
|
||||
select(UserNutritionEntry)
|
||||
.where(
|
||||
and_(
|
||||
UserNutritionEntry.user_id == user_id,
|
||||
UserNutritionEntry.entry_date >= start_date,
|
||||
UserNutritionEntry.entry_date <= end_date
|
||||
)
|
||||
)
|
||||
.order_by(UserNutritionEntry.entry_date, UserNutritionEntry.meal_type)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
entries = result.scalars().all()
|
||||
|
||||
return [UserNutritionEntryResponse.model_validate(entry) for entry in entries]
|
||||
|
||||
|
||||
# Эндпоинты для работы с записями о потреблении воды
|
||||
@app.post("/api/v1/nutrition/water", response_model=WaterIntakeResponse)
|
||||
async def create_water_intake(
|
||||
intake_data: WaterIntakeCreate,
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Создание новой записи о потреблении воды"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
water_intake = WaterIntake(**intake_data.model_dump(), user_id=user_id)
|
||||
db.add(water_intake)
|
||||
await db.commit()
|
||||
await db.refresh(water_intake)
|
||||
|
||||
return WaterIntakeResponse.model_validate(water_intake)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/water", response_model=List[WaterIntakeResponse])
|
||||
async def get_user_water_intake(
|
||||
start_date: date = Query(..., description="Начальная дата для выборки"),
|
||||
end_date: date = Query(..., description="Конечная дата для выборки"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение записей о потреблении воды за указанный период"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
query = (
|
||||
select(WaterIntake)
|
||||
.where(
|
||||
and_(
|
||||
WaterIntake.user_id == user_id,
|
||||
WaterIntake.entry_date >= start_date,
|
||||
WaterIntake.entry_date <= end_date
|
||||
)
|
||||
)
|
||||
.order_by(WaterIntake.entry_date, WaterIntake.entry_time)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
entries = result.scalars().all()
|
||||
|
||||
return [WaterIntakeResponse.model_validate(entry) for entry in entries]
|
||||
|
||||
|
||||
# Эндпоинты для работы с записями о физической активности
|
||||
@app.post("/api/v1/nutrition/activity", response_model=UserActivityEntryResponse)
|
||||
async def create_activity_entry(
|
||||
activity_data: UserActivityEntryCreate,
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Создание новой записи о физической активности"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
# Если не указаны сожженные калории, рассчитываем примерно
|
||||
if not activity_data.calories_burned:
|
||||
# Простой расчет на основе типа активности и продолжительности
|
||||
# Точный расчет требует больше параметров (вес, рост, возраст, пол)
|
||||
activity_intensity = {
|
||||
"walking": 5, # ккал/мин
|
||||
"running": 10,
|
||||
"cycling": 8,
|
||||
"swimming": 9,
|
||||
"yoga": 4,
|
||||
"weight_training": 6,
|
||||
"hiit": 12,
|
||||
"pilates": 5,
|
||||
}
|
||||
|
||||
activity_type = activity_data.activity_type.lower()
|
||||
intensity = activity_intensity.get(activity_type, 5) # По умолчанию 5 ккал/мин
|
||||
|
||||
# Увеличиваем интенсивность в зависимости от указанной интенсивности
|
||||
if activity_data.intensity == "high":
|
||||
intensity *= 1.5
|
||||
elif activity_data.intensity == "low":
|
||||
intensity *= 0.8
|
||||
|
||||
calories_burned = intensity * activity_data.duration_minutes
|
||||
activity_data.calories_burned = round(calories_burned, 1)
|
||||
|
||||
activity_entry = UserActivityEntry(**activity_data.model_dump(), user_id=user_id)
|
||||
db.add(activity_entry)
|
||||
await db.commit()
|
||||
await db.refresh(activity_entry)
|
||||
|
||||
return UserActivityEntryResponse.model_validate(activity_entry)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/activity", response_model=List[UserActivityEntryResponse])
|
||||
async def get_user_activities(
|
||||
start_date: date = Query(..., description="Начальная дата для выборки"),
|
||||
end_date: date = Query(..., description="Конечная дата для выборки"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение записей о физической активности за указанный период"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
query = (
|
||||
select(UserActivityEntry)
|
||||
.where(
|
||||
and_(
|
||||
UserActivityEntry.user_id == user_id,
|
||||
UserActivityEntry.entry_date >= start_date,
|
||||
UserActivityEntry.entry_date <= end_date
|
||||
)
|
||||
)
|
||||
.order_by(UserActivityEntry.entry_date, UserActivityEntry.created_at)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
entries = result.scalars().all()
|
||||
|
||||
return [UserActivityEntryResponse.model_validate(entry) for entry in entries]
|
||||
|
||||
|
||||
# Эндпоинты для работы с целями питания
|
||||
@app.post("/api/v1/nutrition/goals", response_model=NutritionGoalResponse)
|
||||
async def create_or_update_nutrition_goals(
|
||||
goal_data: NutritionGoalCreate,
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Создание или обновление целей по питанию и активности"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
# Проверяем, существуют ли уже цели для пользователя
|
||||
query = select(NutritionGoal).where(NutritionGoal.user_id == user_id)
|
||||
result = await db.execute(query)
|
||||
existing_goal = result.scalars().first()
|
||||
|
||||
if existing_goal:
|
||||
# Обновляем существующую цель
|
||||
for key, value in goal_data.model_dump(exclude_unset=True).items():
|
||||
setattr(existing_goal, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(existing_goal)
|
||||
return NutritionGoalResponse.model_validate(existing_goal)
|
||||
else:
|
||||
# Создаем новую цель
|
||||
new_goal = NutritionGoal(**goal_data.model_dump(), user_id=user_id)
|
||||
db.add(new_goal)
|
||||
await db.commit()
|
||||
await db.refresh(new_goal)
|
||||
return NutritionGoalResponse.model_validate(new_goal)
|
||||
|
||||
|
||||
@app.get("/api/v1/nutrition/goals", response_model=NutritionGoalResponse)
|
||||
async def get_nutrition_goals(
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение целей пользователя по питанию и активности"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
query = select(NutritionGoal).where(NutritionGoal.user_id == user_id)
|
||||
result = await db.execute(query)
|
||||
goal = result.scalars().first()
|
||||
|
||||
if not goal:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Nutrition goals not found for this user"
|
||||
)
|
||||
|
||||
return NutritionGoalResponse.model_validate(goal)
|
||||
|
||||
|
||||
# Сводные отчеты
|
||||
@app.get("/api/v1/nutrition/summary/daily", response_model=DailyNutritionSummary)
|
||||
async def get_daily_nutrition_summary(
|
||||
target_date: date = Query(..., description="Дата для получения сводки"),
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Получение дневной сводки по питанию, потреблению воды и физической активности"""
|
||||
user_id = user_data["user_id"]
|
||||
|
||||
# Запрос записей о питании
|
||||
meals_query = select(UserNutritionEntry).where(
|
||||
and_(
|
||||
UserNutritionEntry.user_id == user_id,
|
||||
UserNutritionEntry.entry_date == target_date
|
||||
)
|
||||
).order_by(UserNutritionEntry.meal_type)
|
||||
|
||||
meals_result = await db.execute(meals_query)
|
||||
meals = meals_result.scalars().all()
|
||||
|
||||
# Запрос записей о воде
|
||||
water_query = select(WaterIntake).where(
|
||||
and_(
|
||||
WaterIntake.user_id == user_id,
|
||||
WaterIntake.entry_date == target_date
|
||||
)
|
||||
).order_by(WaterIntake.entry_time)
|
||||
|
||||
water_result = await db.execute(water_query)
|
||||
water_entries = water_result.scalars().all()
|
||||
|
||||
# Запрос записей об активности
|
||||
activity_query = select(UserActivityEntry).where(
|
||||
and_(
|
||||
UserActivityEntry.user_id == user_id,
|
||||
UserActivityEntry.entry_date == target_date
|
||||
)
|
||||
).order_by(UserActivityEntry.created_at)
|
||||
|
||||
activity_result = await db.execute(activity_query)
|
||||
activity_entries = activity_result.scalars().all()
|
||||
|
||||
# Расчет суммарных значений
|
||||
total_calories = sum(meal.calories or 0 for meal in meals)
|
||||
total_protein = sum(meal.protein_grams or 0 for meal in meals)
|
||||
total_fat = sum(meal.fat_grams or 0 for meal in meals)
|
||||
total_carbs = sum(meal.carbs_grams or 0 for meal in meals)
|
||||
total_water = sum(water.amount_ml for water in water_entries)
|
||||
total_activity = sum(activity.duration_minutes for activity in activity_entries)
|
||||
calories_burned = sum(activity.calories_burned or 0 for activity in activity_entries)
|
||||
|
||||
# Формирование ответа
|
||||
summary = DailyNutritionSummary(
|
||||
date=target_date,
|
||||
total_calories=total_calories,
|
||||
total_protein_grams=total_protein,
|
||||
total_fat_grams=total_fat,
|
||||
total_carbs_grams=total_carbs,
|
||||
total_water_ml=total_water,
|
||||
total_activity_minutes=total_activity,
|
||||
estimated_calories_burned=calories_burned,
|
||||
meals=[UserNutritionEntryResponse.model_validate(meal) for meal in meals],
|
||||
water_entries=[WaterIntakeResponse.model_validate(water) for water in water_entries],
|
||||
activity_entries=[UserActivityEntryResponse.model_validate(activity) for activity in activity_entries]
|
||||
)
|
||||
|
||||
return summary
|
||||
Reference in New Issue
Block a user