Files
chat/services/nutrition_service/main.py
Andrew K. Choi 537e7b363f
All checks were successful
continuous-integration/drone/push Build is passing
main commit
2025-10-16 16:30:25 +09:00

462 lines
19 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.

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