diff --git a/backend/CookifyAPI/CookifyAPI/Controllers/Imports/ImagesMigrationController.cs b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/ImagesMigrationController.cs new file mode 100644 index 0000000..24a042c --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/ImagesMigrationController.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CookifyAPI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace CookifyAPI.Controllers.Imports; + +[ApiController] +[Route("api/[controller]")] +public class ImagesMigrationController : ControllerBase +{ + private readonly ImageMigrationService _migrationService; + + public ImagesMigrationController(ImageMigrationService migrationService) + { + _migrationService = migrationService; + } + + [HttpPost("process-json")] + public async Task ProcessJson(IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest("File is empty"); + + string json; + + using (var reader = new StreamReader(file.OpenReadStream())) + { + json = await reader.ReadToEndAsync(); + } + + var processed = await _migrationService.ProcessAllAsync(json); + + var resultJson = JsonSerializer.Serialize(processed, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + return Content(resultJson, "application/json"); + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Controllers/Imports/IngredientImportController.cs b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/IngredientImportController.cs new file mode 100644 index 0000000..b6776c0 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/IngredientImportController.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CookifyAPI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace CookifyAPI.Controllers.Imports; + +[ApiController] +[Route("api/[controller]")] +public class IngredientImportController : ControllerBase +{ + private readonly IngredientImportService _importService; + + public IngredientImportController(IngredientImportService importService) + { + _importService = importService; + } + + [HttpPost("import")] + public async Task Import(IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest("Файл отсутствует"); + + if (!file.FileName.EndsWith(".json")) + return BadRequest("Требуется JSON файл"); + + using var reader = new StreamReader(file.OpenReadStream()); + var json = await reader.ReadToEndAsync(); + + await _importService.ImportAsync(json); + + return Ok(new { message = "Импорт завершён" }); + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Controllers/Imports/TagImportController.cs b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/TagImportController.cs new file mode 100644 index 0000000..f45e57a --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Controllers/Imports/TagImportController.cs @@ -0,0 +1,33 @@ +using CookifyAPI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace CookifyAPI.Controllers.Imports; + +[ApiController] +[Route("api/[controller]")] +public class TagImportController : ControllerBase +{ + private readonly TagImportService _service; + + public TagImportController(TagImportService service) + { + _service = service; + } + + [HttpPost("import")] + public async Task Import(IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest("Файл отсутствует"); + + if (!file.FileName.EndsWith(".json")) + return BadRequest("Требуется JSON файл"); + + using var reader = new StreamReader(file.OpenReadStream()); + var json = await reader.ReadToEndAsync(); + + await _service.ImportAsync(json); + + return Ok(new { message = "Импорт завершён" }); + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Controllers/RecipesController.cs b/backend/CookifyAPI/CookifyAPI/Controllers/RecipesController.cs index 834b5b2..6a66850 100644 --- a/backend/CookifyAPI/CookifyAPI/Controllers/RecipesController.cs +++ b/backend/CookifyAPI/CookifyAPI/Controllers/RecipesController.cs @@ -12,10 +12,12 @@ namespace CookifyAPI.Controllers; public class RecipesController : ControllerBase { private readonly IRecipeService _service; + private readonly RecipeImportService _importService; - public RecipesController(IRecipeService service) + public RecipesController(IRecipeService service, RecipeImportService importService) { _service = service; + _importService = importService; } // GET: api/recipes @@ -37,6 +39,20 @@ public async Task> GetRecipe(int id) return Ok(recipe); } + [HttpPost("import")] + [RequestSizeLimit(50_000_000)] + public async Task Import(IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest("Файл пуст"); + + using var stream = file.OpenReadStream(); + + await _importService.ImportAsync(stream); + + return Ok(); + } + // [HttpPost] // public async Task Create(Recipe recipe) diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/IngredientDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/IngredientDto.cs similarity index 89% rename from backend/CookifyAPI/CookifyAPI/DTOs/IngredientDto.cs rename to backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/IngredientDto.cs index 4f9ef1e..354171d 100644 --- a/backend/CookifyAPI/CookifyAPI/DTOs/IngredientDto.cs +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/IngredientDto.cs @@ -1,4 +1,4 @@ -namespace CookifyAPI.DTOs; +namespace CookifyAPI.DTOs.Ingredients; public class IngredientDto { diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/JsonIngredientDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/JsonIngredientDto.cs new file mode 100644 index 0000000..a8146ca --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Ingredients/JsonIngredientDto.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace CookifyAPI.DTOs.Ingredients; + +public class JsonIngredientDto +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("calories_100g")] + public float? Calories100g { get; set; } + + [JsonPropertyName("protein_100g")] + public float? Protein100g { get; set; } + + [JsonPropertyName("fat_100g")] + public float? Fat100g { get; set; } + + [JsonPropertyName("carb_100g")] + public float? Carb100g { get; set; } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonIngredientInRecipeDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonIngredientInRecipeDto.cs new file mode 100644 index 0000000..809a104 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonIngredientInRecipeDto.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace CookifyAPI.DTOs.Recipes; + +public class JsonIngredientInRecipeDto +{ + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("amount")] + public float Amount { get; set; } + [JsonPropertyName("unit")] + public string Unit { get; set; } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonRecipeDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonRecipeDto.cs new file mode 100644 index 0000000..3a2d080 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/JsonRecipeDto.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; +using CookifyAPI.DTOs.Steps; + +namespace CookifyAPI.DTOs.Recipes; + +public class JsonRecipeDto +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("cooking_time_min")] + public int CookingTimeMin { get; set; } + + [JsonPropertyName("servings")] + public int Servings { get; set; } + + [JsonPropertyName("difficulty_text")] + public string DifficultyText { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("calories")] + public float Calories { get; set; } + + [JsonPropertyName("protein")] + public float Protein { get; set; } + + [JsonPropertyName("fat")] + public float Fat { get; set; } + + [JsonPropertyName("carb")] + public float Carb { get; set; } + + [JsonPropertyName("ingredients")] + public List Ingredients { get; set; } + + [JsonPropertyName("tags")] + public List Tags { get; set; } + + [JsonPropertyName("steps")] + public List Steps { get; set; } + + [JsonPropertyName("images")] + public List Images { get; set; } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeDetailDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeDetailDto.cs index 7622db0..a977f26 100644 --- a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeDetailDto.cs +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeDetailDto.cs @@ -1,4 +1,6 @@ -using CookifyAPI.DTOs; +using CookifyAPI.DTOs.Ingredients; +using CookifyAPI.DTOs.Steps; + namespace CookifyAPI.DTOs.Recipes; public class RecipeDetailDto @@ -19,7 +21,7 @@ public class RecipeDetailDto // Связанные данные public List Images { get; set; } = new(); - public List Steps { get; set; } = new(); + public List Steps { get; set; } = new(); public List Tags { get; set; } = new(); public List Ingredients { get; set; } } \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Steps/JsonStepDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Steps/JsonStepDto.cs new file mode 100644 index 0000000..15b2abe --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Steps/JsonStepDto.cs @@ -0,0 +1,20 @@ + + +using System.Text.Json.Serialization; + +namespace CookifyAPI.DTOs.Steps; + +public class JsonStepDto +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("step_number")] + public int StepNumber { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("image_url")] + public string ImageUrl { get; set; } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeStepDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Steps/StepDto.cs similarity index 76% rename from backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeStepDto.cs rename to backend/CookifyAPI/CookifyAPI/DTOs/Steps/StepDto.cs index 05a2fae..6a95ad6 100644 --- a/backend/CookifyAPI/CookifyAPI/DTOs/Recipes/RecipeStepDto.cs +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Steps/StepDto.cs @@ -1,6 +1,6 @@ -namespace CookifyAPI.DTOs.Recipes; +namespace CookifyAPI.DTOs.Steps; -public class RecipeStepDto +public class StepDto { public int Id { get; set; } public string Title {get; set;} diff --git a/backend/CookifyAPI/CookifyAPI/DTOs/Tags/JsonTagDto.cs b/backend/CookifyAPI/CookifyAPI/DTOs/Tags/JsonTagDto.cs new file mode 100644 index 0000000..7642547 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/DTOs/Tags/JsonTagDto.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace CookifyAPI.DTOs.Tags; + +public class JsonTagDto +{ + [JsonPropertyName("name")] + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Data/DbSeeder.cs b/backend/CookifyAPI/CookifyAPI/Data/DbSeeder.cs index d218c5e..b77ff4e 100644 --- a/backend/CookifyAPI/CookifyAPI/Data/DbSeeder.cs +++ b/backend/CookifyAPI/CookifyAPI/Data/DbSeeder.cs @@ -13,126 +13,129 @@ public static async Task SeedAsync(AppDbContext context) var user = new User { - Email = "my@gmail.com", - Username = "hi_there", - PasswordHash = "0" - }; - - var tags = new List - { - new Tag { Name = "Суп" }, - new Tag { Name = "Быстро" }, - new Tag { Name = "Завтрак" }, - new Tag { Name = "Выпечка" } - }; - - var ingredients = new List - { - new Ingredient { Name = "Курица бройлерная" }, - new Ingredient { Name = "Рис" }, - new Ingredient { Name = "Чеснок" }, - new Ingredient { Name = "Масло сливочное" }, - new Ingredient { Name = "Лук репчатый" }, - new Ingredient { Name = "Морковь" }, - new Ingredient { Name = "Томатная паста" }, - new Ingredient { Name = "Зелень" }, - new Ingredient { Name = "Соль" }, - new Ingredient { Name = "Вода" }, - new Ingredient { Name = "Картофель" }, - new Ingredient { Name = "Сметана" }, - new Ingredient { Name = "Мука" }, + Email = "@gmail.com", + Username = "admin", + PasswordHash = "admin" }; context.Users.Add(user); - context.Tags.AddRange(tags); - context.Ingredients.AddRange(ingredients); - - await context.SaveChangesAsync(); // Сохраняем пользователя, чтобы EF сгенерировал ID - - var recipe = new Recipe - { - Title = "Картофельные драники", - Description = "Картофельные драники - очень быстро, очень просто, очень вкусно!", - AuthorId = user.Id, - CookingTimeMin = 30, - Servings = 4, - Difficulty = 1 - }; - - context.Recipes.Add(recipe); await context.SaveChangesAsync(); - - context.RecipeImages.AddRange( - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3618.jpg", Order = 0 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3619.jpg", Order = 1 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3620.jpg", Order = 2 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3621.jpg", Order = 3 } - ); - context.RecipeSteps.AddRange( - new RecipeStep { RecipeId = recipe.Id, StepNumber = 1, Title = "1 шак", Description = "Картофель натереть на крупной тёрке, отжать, добавить муку, сметану, соль, перемешать.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3619.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 2, Title = "title нужен?", Description = "Выкладывать столовой ложкой на очень хорошо разогретую сковороду в растопленное масло.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3620.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 3, Description = "Обжаривать драники с обеих сторон до золотистой корочки. Подавать горячими со сметаной.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3621.jpg" } - ); - - await context.SaveChangesAsync(); - - recipe = new Recipe - { - Title = "Суп «Харчо»", - Description = "Суп харчо - вкусный, ароматный, острый. Традиционно харчо варят из говядины, но по этому рецепту готовится суп с курицей.", - AuthorId = user.Id, - CookingTimeMin = 60, - Servings = 6, - Difficulty = 2 - }; - - context.Recipes.Add(recipe); - await context.SaveChangesAsync(); - - // Images - context.RecipeImages.AddRange( - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30806.jpg", Order = 0 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30807.jpg", Order = 1 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30822.jpg", Order = 2 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30808.jpg", Order = 3 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30809.jpg", Order = 4 }, - new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30823.jpg", Order = 5 } - ); - - // Steps - context.RecipeSteps.AddRange( - new RecipeStep { RecipeId = recipe.Id, StepNumber = 1, Description = "Все ингредиенты подготовить.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30807.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 2, Description = "Курицу нарезать.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30822.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 3, Description = "Варить курицу.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30808.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 4, Description = "Добавить рис.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30809.jpg" }, - new RecipeStep { RecipeId = recipe.Id, StepNumber = 5, Description = "Добавить овощи.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30823.jpg" } - ); - - var recipeTags = new List - { - new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Быстро").Id }, - new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Суп").Id }, - new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Завтрак").Id } - }; - context.AddRange(recipeTags); - var recipeIngredients = new List - { - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Курица бройлерная").Id, Amount = 1, Unit = "шт."}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Рис").Id, Amount = 0.5f, Unit = "стакана"}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Чеснок").Id, Amount = 1, Unit = "головка"}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Масло сливочное").Id, Amount = 50, Unit = "г"}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Лук репчатый").Id, Amount = 1, Unit = "шт."}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Морковь").Id, Amount = 1, Unit = "шт."}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Томатная паста").Id, Amount = 2, Unit = "ст. ложки"}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Зелень").Id, Amount = 55, Unit = "г"}, // Среднее значение от 50-60 - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Соль").Id, Amount = 1, Unit = "ст. ложка"}, - new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Вода").Id, Amount = 2.5f, Unit = "л"} - - }; - context.AddRange(recipeIngredients); - - await context.SaveChangesAsync(); + // var tags = new List + // { + // new Tag { Name = "Суп" }, + // new Tag { Name = "Быстро" }, + // new Tag { Name = "Завтрак" }, + // new Tag { Name = "Выпечка" } + // }; + // + // var ingredients = new List + // { + // new Ingredient { Name = "Курица бройлерная" }, + // new Ingredient { Name = "Рис" }, + // new Ingredient { Name = "Чеснок" }, + // new Ingredient { Name = "Масло сливочное" }, + // new Ingredient { Name = "Лук репчатый" }, + // new Ingredient { Name = "Морковь" }, + // new Ingredient { Name = "Томатная паста" }, + // new Ingredient { Name = "Зелень" }, + // new Ingredient { Name = "Соль" }, + // new Ingredient { Name = "Вода" }, + // new Ingredient { Name = "Картофель" }, + // new Ingredient { Name = "Сметана" }, + // new Ingredient { Name = "Мука" }, + // }; + // + // context.Users.Add(user); + // context.Tags.AddRange(tags); + // context.Ingredients.AddRange(ingredients); + // + // await context.SaveChangesAsync(); // Сохраняем пользователя, чтобы EF сгенерировал ID + // + // var recipe = new Recipe + // { + // Title = "Картофельные драники", + // Description = "Картофельные драники - очень быстро, очень просто, очень вкусно!", + // AuthorId = user.Id, + // CookingTimeMin = 30, + // Servings = 4, + // Difficulty = 1 + // }; + // + // context.Recipes.Add(recipe); + // await context.SaveChangesAsync(); + // + // context.RecipeImages.AddRange( + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3618.jpg", Order = 0 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3619.jpg", Order = 1 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3620.jpg", Order = 2 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/4/big_3621.jpg", Order = 3 } + // ); + // + // context.RecipeSteps.AddRange( + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 1, Title = "1 шак", Description = "Картофель натереть на крупной тёрке, отжать, добавить муку, сметану, соль, перемешать.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3619.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 2, Title = "title нужен?", Description = "Выкладывать столовой ложкой на очень хорошо разогретую сковороду в растопленное масло.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3620.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 3, Description = "Обжаривать драники с обеих сторон до золотистой корочки. Подавать горячими со сметаной.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/4/big_3621.jpg" } + // ); + // + // await context.SaveChangesAsync(); + // + // recipe = new Recipe + // { + // Title = "Суп «Харчо»", + // Description = "Суп харчо - вкусный, ароматный, острый. Традиционно харчо варят из говядины, но по этому рецепту готовится суп с курицей.", + // AuthorId = user.Id, + // CookingTimeMin = 60, + // Servings = 6, + // Difficulty = 2 + // }; + // + // context.Recipes.Add(recipe); + // await context.SaveChangesAsync(); + // + // // Images + // context.RecipeImages.AddRange( + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30806.jpg", Order = 0 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30807.jpg", Order = 1 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30822.jpg", Order = 2 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30808.jpg", Order = 3 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30809.jpg", Order = 4 }, + // new RecipeImage { RecipeId = recipe.Id, Url = "https://www.russianfood.com/dycontent/images_upl/31/big_30823.jpg", Order = 5 } + // ); + // + // // Steps + // context.RecipeSteps.AddRange( + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 1, Description = "Все ингредиенты подготовить.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30807.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 2, Description = "Курицу нарезать.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30822.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 3, Description = "Варить курицу.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30808.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 4, Description = "Добавить рис.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30809.jpg" }, + // new RecipeStep { RecipeId = recipe.Id, StepNumber = 5, Description = "Добавить овощи.", ImageUrl = "https://www.russianfood.com/dycontent/images_upl/31/big_30823.jpg" } + // ); + // + // var recipeTags = new List + // { + // new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Быстро").Id }, + // new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Суп").Id }, + // new M2MRecipeTag { RecipeId = recipe.Id, TagId = tags.First(t => t.Name == "Завтрак").Id } + // }; + // context.AddRange(recipeTags); + // var recipeIngredients = new List + // { + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Курица бройлерная").Id, Amount = 1, Unit = "шт."}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Рис").Id, Amount = 0.5f, Unit = "стакана"}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Чеснок").Id, Amount = 1, Unit = "головка"}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Масло сливочное").Id, Amount = 50, Unit = "г"}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Лук репчатый").Id, Amount = 1, Unit = "шт."}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Морковь").Id, Amount = 1, Unit = "шт."}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Томатная паста").Id, Amount = 2, Unit = "ст. ложки"}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Зелень").Id, Amount = 55, Unit = "г"}, // Среднее значение от 50-60 + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Соль").Id, Amount = 1, Unit = "ст. ложка"}, + // new M2MRecipeIngredient { RecipeId = recipe.Id, IngredientId = ingredients.First(t => t.Name == "Вода").Id, Amount = 2.5f, Unit = "л"} + // + // }; + // context.AddRange(recipeIngredients); + // + // await context.SaveChangesAsync(); } } diff --git a/backend/CookifyAPI/CookifyAPI/Program.cs b/backend/CookifyAPI/CookifyAPI/Program.cs index 8bc436a..9b84416 100644 --- a/backend/CookifyAPI/CookifyAPI/Program.cs +++ b/backend/CookifyAPI/CookifyAPI/Program.cs @@ -36,6 +36,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); + builder.Services.AddControllers() .AddJsonOptions(options => { diff --git a/backend/CookifyAPI/CookifyAPI/Services/CloudinaryImageService.cs b/backend/CookifyAPI/CookifyAPI/Services/CloudinaryImageService.cs index 3baa912..f1f006f 100644 --- a/backend/CookifyAPI/CookifyAPI/Services/CloudinaryImageService.cs +++ b/backend/CookifyAPI/CookifyAPI/Services/CloudinaryImageService.cs @@ -13,6 +13,7 @@ public interface IImageService /// Максимальный размер файла в МБ /// Публичный URL картинки Task UploadAsync(IFormFile file, string folder, int maxFileSizeMb = 10); + Task UploadAsync(Stream stream, string fileName, string folder, int maxFileSizeMb = 10); } public class CloudinaryImageService : IImageService @@ -64,6 +65,39 @@ public async Task UploadAsync(IFormFile file, string folder, int maxFile return result.SecureUrl.ToString(); } + + public async Task UploadAsync(Stream stream, string fileName, string folder, int maxFileSizeMb = 10) + { + // if (file == null || file.Length == 0) + // throw new ArgumentException("File is empty"); + // + // // Проверка размера + // var maxBytes = maxFileSizeMb * 1024 * 1024; + // if (file.Length > maxBytes) + // throw new PayloadTooLargeException($"File size exceeds {maxFileSizeMb} MB"); + // + // // Проверка расширения + // var ext = Path.GetExtension(file.FileName); + // if (!_allowedExtensions.Contains(ext)) + // throw new InvalidOperationException("Only jpg, png, webp images are allowed"); + + //await using var stream = file.OpenReadStream(); + + var uploadParams = new ImageUploadParams + { + File = new FileDescription(fileName, stream), + Folder = folder, + Overwrite = true, + UseFilename = true + }; + + var result = await _cloudinary.UploadAsync(uploadParams); + + if (result.StatusCode != System.Net.HttpStatusCode.OK) + throw new Exception(result.Error?.Message ?? "Failed to upload image"); + + return result.SecureUrl.ToString(); + } } public class PayloadTooLargeException : Exception diff --git a/backend/CookifyAPI/CookifyAPI/Services/Imports/ImageMigrationService.cs b/backend/CookifyAPI/CookifyAPI/Services/Imports/ImageMigrationService.cs new file mode 100644 index 0000000..5910270 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Services/Imports/ImageMigrationService.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using CookifyAPI.DTOs.Recipes; + +namespace CookifyAPI.Services; + +public class ImageMigrationService +{ + private readonly HttpClient _httpClient; + private readonly IImageService _imageService; + //private readonly Dictionary _cache = new(); + + public ImageMigrationService( + HttpClient httpClient, + IImageService imageService) + { + _httpClient = httpClient; + _imageService = imageService; + } + + public async Task ProcessImageAsync(string url, string folder) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + // if (_cache.TryGetValue(url, out var cached)) + // return cached; + + try + { + using var stream = await _httpClient.GetStreamAsync(url); + + var uploadedUrl = await _imageService.UploadAsync(stream, "image.jpg", folder); + + //_cache[url] = uploadedUrl; + + return uploadedUrl; + } + catch + { + // ошибка сети или cloudinary + return null; + } + } + + public async Task ProcessRecipeAsync(JsonRecipeDto recipe) + { + // 1. Основные изображения + if (recipe.Images != null) + { + for (int i = 0; i < recipe.Images.Count; i++) + { + var newUrl = await ProcessImageAsync(recipe.Images[i], "CookifyApp/recipes"); + if (newUrl != null) + recipe.Images[i] = newUrl; + } + } + + // 2. Шаги + if (recipe.Steps != null) + { + foreach (var step in recipe.Steps) + { + var newUrl = await ProcessImageAsync(step.ImageUrl, "CookifyApp/steps"); + if (newUrl != null) + step.ImageUrl = newUrl; + } + } + } + + public async Task> ProcessAllAsync(string json) + { + var recipes = JsonSerializer.Deserialize>(json); + + foreach (var recipe in recipes) + { + await ProcessRecipeAsync(recipe); + } + + return recipes; + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Services/Imports/IngredientImportService.cs b/backend/CookifyAPI/CookifyAPI/Services/Imports/IngredientImportService.cs new file mode 100644 index 0000000..053da4d --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Services/Imports/IngredientImportService.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using CookifyAPI.Data; +using CookifyAPI.Models; +using CookifyAPI.DTOs.Ingredients; +using Microsoft.EntityFrameworkCore; + +namespace CookifyAPI.Services; + +public class IngredientImportService +{ + private readonly AppDbContext _context; + + public IngredientImportService(AppDbContext context) + { + _context = context; + } + + public async Task ImportAsync(string json) + { + if (string.IsNullOrWhiteSpace(json)) + throw new ArgumentException("JSON пустой"); + + var items = JsonSerializer.Deserialize>(json); + if (items == null || !items.Any()) + return; + + // 1. Нормализованные имена для поиска существующих + var normalizedNames = items + .Select(i => Normalize(i.Name)) + .ToList(); + + var existing = await _context.Ingredients + .Where(i => normalizedNames.Contains(i.Name)) + .ToListAsync(); + + var existingDict = existing.ToDictionary(i => Normalize(i.Name)); + + var toInsert = new List(); + + foreach (var item in items) + { + var normalized = Normalize(item.Name); + + if (existingDict.TryGetValue(normalized, out var existingItem)) + { + // обновление (если нужно) + existingItem.Calories100g = item.Calories100g; + existingItem.Protein100g = item.Protein100g; + existingItem.Fat100g = item.Fat100g; + existingItem.Carb100g = item.Carb100g; + } + else + { + toInsert.Add(new Ingredient + { + Name = normalized, + Calories100g = item.Calories100g, + Protein100g = item.Protein100g, + Fat100g = item.Fat100g, + Carb100g = item.Carb100g + }); + } + } + + _context.Ingredients.AddRange(toInsert); + + // 4. Сохраняем изменения апдейтов + await _context.SaveChangesAsync(); + } + + private string Normalize(string name) + { + return name?.Trim().ToLowerInvariant() ?? string.Empty; + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Services/Imports/RecipeImportService.cs b/backend/CookifyAPI/CookifyAPI/Services/Imports/RecipeImportService.cs new file mode 100644 index 0000000..ccf502f --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Services/Imports/RecipeImportService.cs @@ -0,0 +1,144 @@ +using System.Text.Json; +using CookifyAPI.Data; +using CookifyAPI.DTOs.Recipes; +using CookifyAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace CookifyAPI.Services; + +public class RecipeImportService +{ + private readonly AppDbContext _context; + + public RecipeImportService(AppDbContext context) + { + _context = context; + } + + public async Task ImportAsync(Stream stream) + { + var recipes = await JsonSerializer.DeserializeAsync>(stream); + + if (recipes == null || recipes.Count == 0) + return; + + // --- 1. PRELOAD --- + + var ingredientNames = recipes + .SelectMany(r => r.Ingredients) + .Select(i => Normalize(i.Name)) + .Distinct() + .ToList(); + + var tagNames = recipes + .SelectMany(r => r.Tags) + .Select(Normalize) + .Distinct() + .ToList(); + + var ingredients = await _context.Ingredients + .Where(i => ingredientNames.Contains(i.Name)) + .ToListAsync(); + + var tags = await _context.Tags + .Where(t => tagNames.Contains(t.Name)) + .ToListAsync(); + + var ingredientDict = ingredients.ToDictionary(i => i.Name); + var tagDict = tags.ToDictionary(t => t.Name); + + var adminId = await _context.Users + .Where(u => u.Username == "admin") + .Select(u => u.Id) + .FirstOrDefaultAsync(); + + if (adminId == 0) + throw new Exception("Admin not found"); + + // --- 2. ИМПОРТ --- + + foreach (var dto in recipes) + { + var recipe = new Recipe + { + AuthorId = adminId, + Title = dto.Title, + Description = dto.Description, + CookingTimeMin = dto.CookingTimeMin, + Servings = dto.Servings, + Difficulty = 0,//dto.DifficultyText, + + Calories100g = dto.Calories, + Protein100g = dto.Protein, + Fat100g = dto.Fat, + Carb100g = dto.Carb, + + Ingredients = new List(), + Tags = new List(), + Steps = new List(), + Images = new List() + }; + + // --- INGREDIENTS --- + foreach (var ing in dto.Ingredients + .GroupBy(i => Normalize(i.Name)) + .Select(g => g.First())) + { + var normalized = Normalize(ing.Name); + + if (!ingredientDict.TryGetValue(normalized, out var ingredient)) + continue; // или логировать + + recipe.Ingredients.Add(new M2MRecipeIngredient + { + IngredientId = ingredient.Id, + Amount = ing.Amount, + Unit = ing.Unit + }); + } + + // --- TAGS --- + foreach (var tagName in dto.Tags + .Select(Normalize) + .Distinct()) + { + if (!tagDict.TryGetValue(tagName, out var tag)) + continue; + + recipe.Tags.Add(new M2MRecipeTag + { + TagId = tag.Id + }); + } + + // --- STEPS --- + recipe.Steps = dto.Steps + .OrderBy(s => s.StepNumber) + .Select(s => new RecipeStep + { + Title = s.Title, + StepNumber = s.StepNumber, + Description = s.Description, + ImageUrl = s.ImageUrl + }) + .ToList(); + + // --- IMAGES --- + recipe.Images = dto.Images + .Select(url => new RecipeImage + { + Url = url //несоответствие, отсутствует очередность + }) + .ToList(); + + _context.Recipes.Add(recipe); + } + + await _context.SaveChangesAsync(); + } + + private string Normalize(string name) + { + return name?.Trim().ToLowerInvariant() ?? string.Empty; + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Services/Imports/TagImportService.cs b/backend/CookifyAPI/CookifyAPI/Services/Imports/TagImportService.cs new file mode 100644 index 0000000..320b2ae --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Services/Imports/TagImportService.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using CookifyAPI.Data; +using CookifyAPI.DTOs.Tags; +using CookifyAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace CookifyAPI.Services; + +public class TagImportService +{ + private readonly AppDbContext _context; + + public TagImportService(AppDbContext context) + { + _context = context; + } + + public async Task ImportAsync(string json) + { + if (string.IsNullOrWhiteSpace(json)) + throw new ArgumentException("JSON пустой"); + + var items = JsonSerializer.Deserialize>(json); + if (items == null || !items.Any()) + return; + + // 1. Нормализованные имена для поиска существующих + var normalizedNames = items + .Select(i => Normalize(i.Name)) + .ToList(); + + var existing = await _context.Tags + .Where(i => normalizedNames.Contains(i.Name)) + .ToListAsync(); + + var existingDict = existing.ToDictionary(i => Normalize(i.Name)); + + var toInsert = new List(); + + foreach (var item in items) + { + var normalized = Normalize(item.Name); + + if (existingDict.TryGetValue(normalized, out Tag existingItem)) + { + // обновление (если нужно) + existingItem.Name = item.Name; + } + else + { + toInsert.Add(new Tag + { + Name = normalized + }); + } + } + + _context.Tags.AddRange(toInsert); + + // 4. Сохраняем изменения апдейтов + await _context.SaveChangesAsync(); + } + + private string Normalize(string name) + { + return name?.Trim().ToLowerInvariant() ?? string.Empty; + } +} \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Services/RecipeService.cs b/backend/CookifyAPI/CookifyAPI/Services/RecipeService.cs index d689719..c057baf 100644 --- a/backend/CookifyAPI/CookifyAPI/Services/RecipeService.cs +++ b/backend/CookifyAPI/CookifyAPI/Services/RecipeService.cs @@ -1,7 +1,8 @@ using CookifyAPI.Data; -using CookifyAPI.DTOs; +using CookifyAPI.DTOs.Ingredients; using CookifyAPI.DTOs.Pagination; using CookifyAPI.DTOs.Recipes; +using CookifyAPI.DTOs.Steps; using CookifyAPI.Models; using Microsoft.EntityFrameworkCore; @@ -71,7 +72,7 @@ public async Task> GetRecipesListAsync() Steps = r.Steps .OrderBy(s => s.StepNumber) - .Select(s => new RecipeStepDto + .Select(s => new StepDto { Id = s.Id, Title = s.Title,