diff --git a/RhSolutions.Api/Controllers/ProductsController.cs b/RhSolutions.Api/Controllers/ProductsController.cs index 21bd8e2..34ed05b 100644 --- a/RhSolutions.Api/Controllers/ProductsController.cs +++ b/RhSolutions.Api/Controllers/ProductsController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; -using RhSolutions.Api.Models; +using RhSolutions.Models; using RhSolutions.Api.Services; +using System.Linq; namespace RhSolutions.Api.Controllers { @@ -31,17 +32,25 @@ namespace RhSolutions.Api.Controllers } [HttpPost] - public async Task PostProductsFromXls() + public IActionResult PostProductsFromXls() { try { - var products = parser.GetProducts(HttpContext); - await foreach (var p in products) - { - using (p) + var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku) + .Select(g => new Product(g.Key) { - dbContext.Add(p); - } + Name = g.First().Name, + DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).ToList(), + ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(), + IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true), + ProductMeasure = g.First().ProductMeasure, + DeliveryMakeUp = g.First().DeliveryMakeUp, + Price = g.First().Price + }); + + foreach (var p in products) + { + dbContext.Add(p); } dbContext.SaveChanges(); diff --git a/RhSolutions.Api/Controllers/SearchController.cs b/RhSolutions.Api/Controllers/SearchController.cs index a2ed194..92d93d0 100644 --- a/RhSolutions.Api/Controllers/SearchController.cs +++ b/RhSolutions.Api/Controllers/SearchController.cs @@ -1,28 +1,27 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using RhSolutions.Api.Models; +using RhSolutions.Models; namespace RhSolutions.Api.Controllers { - [Route("api/[controller]")] - public class SearchController : ControllerBase - { - private RhSolutionsContext context; + [Route("api/[controller]")] + public class SearchController : ControllerBase + { + private RhSolutionsContext context; - public SearchController(RhSolutionsContext context) - { - this.context = context; - } + public SearchController(RhSolutionsContext context) + { + this.context = context; + } - [HttpGet] - public IAsyncEnumerable SearchProducts([FromQuery] string query) - { - return context.Products - .Where(p => EF.Functions.ToTsVector( - "russian", string.Join(' ', - new [] {p.ProductLine ?? string.Empty, p.Name ?? string.Empty })) - .Matches(EF.Functions.WebSearchToTsQuery("russian", query))) - .AsAsyncEnumerable(); - } - } + [HttpGet] + public IAsyncEnumerable SearchProducts([FromQuery] string query) + { + return context.Products + .Where(p => EF.Functions.ToTsVector( + "russian", string.Join(' ', new[] { p.Name, string.Join(' ', p.ProductLines)})) + .Matches(EF.Functions.WebSearchToTsQuery("russian", query))) + .AsAsyncEnumerable(); + } + } } \ No newline at end of file diff --git a/RhSolutions.Api/Deploy/Database/init-database.sql b/RhSolutions.Api/Deploy/Database/init-database.sql index ec6ac7d..91e9f95 100644 --- a/RhSolutions.Api/Deploy/Database/init-database.sql +++ b/RhSolutions.Api/Deploy/Database/init-database.sql @@ -7,19 +7,19 @@ CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( START TRANSACTION; CREATE TABLE "Products" ( - "Id" integer GENERATED BY DEFAULT AS IDENTITY, - "ProductSku" text NULL, - "DeprecatedSkus" text[] NOT NULL, + "Id" text NOT NULL, "Name" text NOT NULL, - "ProductLine" text NULL, - "IsOnWarehouse" boolean NULL, + "ProductSku" text NOT NULL, + "DeprecatedSkus" text[] NOT NULL, + "ProductLines" text[] NOT NULL, + "IsOnWarehouse" boolean NOT NULL, "ProductMeasure" integer NOT NULL, "DeliveryMakeUp" double precision NULL, - "Price" numeric(8,2) NOT NULL, + "Price" numeric NOT NULL, CONSTRAINT "PK_Products" PRIMARY KEY ("Id") ); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20221201071323_Init', '7.0.0'); +VALUES ('20230511043408_Init', '7.0.5'); COMMIT; \ No newline at end of file diff --git a/RhSolutions.Api/Migrations/20221201071323_Init.Designer.cs b/RhSolutions.Api/Migrations/20230511043408_Init.Designer.cs similarity index 73% rename from RhSolutions.Api/Migrations/20221201071323_Init.Designer.cs rename to RhSolutions.Api/Migrations/20230511043408_Init.Designer.cs index c62450e..887bd5a 100644 --- a/RhSolutions.Api/Migrations/20221201071323_Init.Designer.cs +++ b/RhSolutions.Api/Migrations/20230511043408_Init.Designer.cs @@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RhSolutions.Api.Models; +using RhSolutions.Models; #nullable disable namespace RhSolutions.Api.Migrations { [DbContext(typeof(RhSolutionsContext))] - [Migration("20221201071323_Init")] + [Migration("20230511043408_Init")] partial class Init { /// @@ -21,18 +21,15 @@ namespace RhSolutions.Api.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("ProductVersion", "7.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("RhSolutions.Api.Models.Product", b => + modelBuilder.Entity("RhSolutions.Models.Product", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Id") + .HasColumnType("text"); b.Property("DeliveryMakeUp") .HasColumnType("double precision"); @@ -41,7 +38,7 @@ namespace RhSolutions.Api.Migrations .IsRequired() .HasColumnType("text[]"); - b.Property("IsOnWarehouse") + b.Property("IsOnWarehouse") .HasColumnType("boolean"); b.Property("Name") @@ -49,15 +46,17 @@ namespace RhSolutions.Api.Migrations .HasColumnType("text"); b.Property("Price") - .HasColumnType("decimal(8,2)"); + .HasColumnType("numeric"); - b.Property("ProductLine") - .HasColumnType("text"); + b.Property>("ProductLines") + .IsRequired() + .HasColumnType("text[]"); b.Property("ProductMeasure") .HasColumnType("integer"); b.Property("ProductSku") + .IsRequired() .HasColumnType("text"); b.HasKey("Id"); diff --git a/RhSolutions.Api/Migrations/20221201071323_Init.cs b/RhSolutions.Api/Migrations/20230511043408_Init.cs similarity index 77% rename from RhSolutions.Api/Migrations/20221201071323_Init.cs rename to RhSolutions.Api/Migrations/20230511043408_Init.cs index 2ac6ad0..7f9427f 100644 --- a/RhSolutions.Api/Migrations/20221201071323_Init.cs +++ b/RhSolutions.Api/Migrations/20230511043408_Init.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable @@ -16,16 +15,15 @@ namespace RhSolutions.Api.Migrations name: "Products", columns: table => new { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProductSku = table.Column(type: "text", nullable: true), - DeprecatedSkus = table.Column>(type: "text[]", nullable: false), + Id = table.Column(type: "text", nullable: false), Name = table.Column(type: "text", nullable: false), - ProductLine = table.Column(type: "text", nullable: true), - IsOnWarehouse = table.Column(type: "boolean", nullable: true), + ProductSku = table.Column(type: "text", nullable: false), + DeprecatedSkus = table.Column>(type: "text[]", nullable: false), + ProductLines = table.Column>(type: "text[]", nullable: false), + IsOnWarehouse = table.Column(type: "boolean", nullable: false), ProductMeasure = table.Column(type: "integer", nullable: false), DeliveryMakeUp = table.Column(type: "double precision", nullable: true), - Price = table.Column(type: "numeric(8,2)", nullable: false) + Price = table.Column(type: "numeric", nullable: false) }, constraints: table => { diff --git a/RhSolutions.Api/Migrations/RhSolutionsContextModelSnapshot.cs b/RhSolutions.Api/Migrations/RhSolutionsContextModelSnapshot.cs index 60c89e0..0052799 100644 --- a/RhSolutions.Api/Migrations/RhSolutionsContextModelSnapshot.cs +++ b/RhSolutions.Api/Migrations/RhSolutionsContextModelSnapshot.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RhSolutions.Api.Models; +using RhSolutions.Models; #nullable disable @@ -18,18 +18,15 @@ namespace RhSolutions.Api.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("ProductVersion", "7.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("RhSolutions.Api.Models.Product", b => + modelBuilder.Entity("RhSolutions.Models.Product", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Id") + .HasColumnType("text"); b.Property("DeliveryMakeUp") .HasColumnType("double precision"); @@ -38,7 +35,7 @@ namespace RhSolutions.Api.Migrations .IsRequired() .HasColumnType("text[]"); - b.Property("IsOnWarehouse") + b.Property("IsOnWarehouse") .HasColumnType("boolean"); b.Property("Name") @@ -46,15 +43,17 @@ namespace RhSolutions.Api.Migrations .HasColumnType("text"); b.Property("Price") - .HasColumnType("decimal(8,2)"); + .HasColumnType("numeric"); - b.Property("ProductLine") - .HasColumnType("text"); + b.Property>("ProductLines") + .IsRequired() + .HasColumnType("text[]"); b.Property("ProductMeasure") .HasColumnType("integer"); b.Property("ProductSku") + .IsRequired() .HasColumnType("text"); b.HasKey("Id"); diff --git a/RhSolutions.Api/Models/Measure.cs b/RhSolutions.Api/Models/Measure.cs deleted file mode 100644 index 9fbd20c..0000000 --- a/RhSolutions.Api/Models/Measure.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace RhSolutions.Api.Models -{ - public enum Measure { Kg, M, M2, P } -} \ No newline at end of file diff --git a/RhSolutions.Api/Models/Product.cs b/RhSolutions.Api/Models/Product.cs deleted file mode 100644 index a8d1ec5..0000000 --- a/RhSolutions.Api/Models/Product.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace RhSolutions.Api.Models -{ - public partial class Product : IDisposable - { - [Key] - [JsonIgnore] - public int Id { get; set; } - public string ProductSku { get; set; } = string.Empty; - public List DeprecatedSkus { get; set; } = new(); - public string Name { get; set; } = string.Empty; - public string ProductLine { get; set; } = string.Empty; - public bool? IsOnWarehouse { get; set; } - public Measure ProductMeasure { get; set; } - public double? DeliveryMakeUp { get; set; } - - [Column(TypeName = "decimal(8,2)")] - public decimal Price { get; set; } - - public void Dispose() - { - Debug.WriteLine($"{this} disposed"); - } - - public override string ToString() - { - return $"({ProductSku}) {Name}"; - } - } -} \ No newline at end of file diff --git a/RhSolutions.Api/Models/RhsolutionsContext.cs b/RhSolutions.Api/Models/RhsolutionsContext.cs index 2da0783..9d9169f 100644 --- a/RhSolutions.Api/Models/RhsolutionsContext.cs +++ b/RhSolutions.Api/Models/RhsolutionsContext.cs @@ -1,11 +1,21 @@ using Microsoft.EntityFrameworkCore; -namespace RhSolutions.Api.Models; +namespace RhSolutions.Models; public class RhSolutionsContext : DbContext { public RhSolutionsContext(DbContextOptions options) - : base(options) { } - + : base(options) { } + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .Property(e => e.ProductSku) + .HasConversion(v => v.ToString(), v => new ProductSku(v)); + builder.Entity() + .Property(e => e.DeprecatedSkus) + .HasPostgresArrayConversion(v => v.ToString(), v => new ProductSku(v)); + } } diff --git a/RhSolutions.Api/Models/Sku.cs b/RhSolutions.Api/Models/Sku.cs deleted file mode 100644 index c1a4349..0000000 --- a/RhSolutions.Api/Models/Sku.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; - -namespace RhSolutions.Api.Models -{ - public class Sku - { - private const string matchPattern = @"([1\D]|\b)(?
\d{6})([1\s-]|)(?\d{3})\b"; - private string? _article; - private string? _variant; - - public Sku(string article, string variant) - { - Article = article; - Variant = variant; - } - - [Key] - public string Id - { - get - { - return $"1{Article}1{Variant}"; - } - set - { - if (TryParse(value, out IEnumerable skus)) - { - if (skus.Count() > 1) - { - throw new ArgumentException($"More than one valid sku detected: {value}"); - } - else - { - this.Article = skus.First().Article; - this.Variant = skus.First().Variant; - } - } - else - { - throw new ArgumentException($"Invalid sku input: {value}"); - } - } - } - public string? Article - { - get - { - return _article; - } - set - { - if (value == null || value.Length != 6 || value.Where(c => char.IsDigit(c)).Count() != 6) - { - throw new ArgumentException($"Wrong Article: {Article}"); - } - else - { - _article = value; - } - } - } - public string? Variant - { - get - { - return _variant; - } - set - { - if (value == null || value.Length != 3 || value.Where(c => char.IsDigit(c)).Count() != 3) - { - throw new ArgumentException($"Wrong Variant: {Variant}"); - } - else _variant = value; - } - } - public static IEnumerable GetValidSkus(string line) - { - MatchCollection matches = Regex.Matches(line, matchPattern); - if (matches.Count == 0) - { - yield break; - } - else - { - foreach (Match m in matches) - { - yield return new Sku(m.Groups["Article"].Value, m.Groups["Variant"].Value); - } - } - } - - public static bool TryParse(string line, out IEnumerable skus) - { - MatchCollection matches = Regex.Matches(line, matchPattern); - if (matches.Count == 0) - { - skus = Enumerable.Empty(); - return false; - } - - else - { - skus = GetValidSkus(line); - return true; - } - } - - public override bool Equals(object? obj) - { - return obj is Sku sku && - Article!.Equals(sku.Article) && - Variant!.Equals(sku.Variant); - } - - public override int GetHashCode() - { - return HashCode.Combine(Article, Variant); - } - - public override string ToString() - { - return $"1{Article}1{Variant}"; - } - } -} \ No newline at end of file diff --git a/RhSolutions.Api/Program.cs b/RhSolutions.Api/Program.cs index 1defdda..00a8c84 100644 --- a/RhSolutions.Api/Program.cs +++ b/RhSolutions.Api/Program.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using RhSolutions.Api.Models; +using RhSolutions.Models; using RhSolutions.Api.Services; var builder = WebApplication.CreateBuilder(args); diff --git a/RhSolutions.Api/RhSolutions.Api.csproj b/RhSolutions.Api/RhSolutions.Api.csproj index 1e6dd7f..a5ab8f8 100644 --- a/RhSolutions.Api/RhSolutions.Api.csproj +++ b/RhSolutions.Api/RhSolutions.Api.csproj @@ -8,13 +8,14 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/RhSolutions.Api/Services/ClosedXMLParser.cs b/RhSolutions.Api/Services/ClosedXMLParser.cs index 2af9afb..5c15a2f 100644 --- a/RhSolutions.Api/Services/ClosedXMLParser.cs +++ b/RhSolutions.Api/Services/ClosedXMLParser.cs @@ -1,35 +1,36 @@ using ClosedXML.Excel; -using RhSolutions.Api.Models; +using RhSolutions.Models; namespace RhSolutions.Api.Services { public class ClosedXMLParser : IPricelistParser { - public async IAsyncEnumerable GetProducts(HttpContext context) + public List GetProducts(HttpContext context) { using (var memoryStream = new MemoryStream()) { if (context == null) { - yield break; + return new List(); } - - await context.Request.Body.CopyToAsync(memoryStream); + + context.Request.Body.CopyToAsync(memoryStream).GetAwaiter().GetResult(); + List products = new(); using (var wb = new XLWorkbook(memoryStream)) { + Dictionary collected = new(); var table = GetTable(wb); var rows = table.DataRange.Rows(); - var enumerator = rows.GetEnumerator(); - while (enumerator.MoveNext()) + foreach (var row in rows) { - if (Sku.TryParse(enumerator.Current.Field("Актуальный материал") - .GetString(), out IEnumerable skus)) + if (ProductSku.TryParse(row.Field("Актуальный материал") + .GetString(), out _)) { - yield return ParseRow(enumerator.Current); + products.Add(ParseRow(row)); } - } - yield break; + } } + return products; } } private IXLTable GetTable(XLWorkbook wb) @@ -60,10 +61,10 @@ namespace RhSolutions.Api.Services .GetString() .Split('\n') .First(); - Sku.TryParse(row.Field("Актуальный материал") - .GetString(), out IEnumerable productSkus); - Sku.TryParse(row.Field("Прежний материал") - .GetString(), out IEnumerable deprecatedSkus); + ProductSku.TryParse(row.Field("Актуальный материал") + .GetString(), out IEnumerable productSkus); + ProductSku.TryParse(row.Field("Прежний материал") + .GetString(), out IEnumerable deprecatedSkus); string measureField = new string(row.Field("Ед. изм.") .GetString() @@ -100,7 +101,7 @@ namespace RhSolutions.Api.Services string onWarehouseField = row.Field("Складская программа") .GetString(); - bool? IsOnWarehouse; + bool IsOnWarehouse; switch (onWarehouseField) { @@ -111,7 +112,7 @@ namespace RhSolutions.Api.Services IsOnWarehouse = false; break; default: - IsOnWarehouse = null; + IsOnWarehouse = false; break; } @@ -123,12 +124,11 @@ namespace RhSolutions.Api.Services price = 0.0M; } - return new Product + return new Product(productSkus.First()) { - ProductLine = productLine, + ProductLines = new List() { productLine }, Name = productName, - ProductSku = productSkus.First().Id, - DeprecatedSkus = deprecatedSkus.Select(s => s.Id).ToList(), + DeprecatedSkus = deprecatedSkus.ToList(), ProductMeasure = productMeasure, DeliveryMakeUp = productWarehouseCount, IsOnWarehouse = IsOnWarehouse, diff --git a/RhSolutions.Api/Services/IPricelistParser.cs b/RhSolutions.Api/Services/IPricelistParser.cs index eed1dee..ee00f53 100644 --- a/RhSolutions.Api/Services/IPricelistParser.cs +++ b/RhSolutions.Api/Services/IPricelistParser.cs @@ -1,9 +1,9 @@ -using RhSolutions.Api.Models; +using RhSolutions.Models; namespace RhSolutions.Api.Services { public interface IPricelistParser { - public IAsyncEnumerable GetProducts(HttpContext context); + public List GetProducts(HttpContext context); } } \ No newline at end of file diff --git a/RhSolutions.Api/global.json b/RhSolutions.Api/global.json index 2948868..5031d31 100644 --- a/RhSolutions.Api/global.json +++ b/RhSolutions.Api/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.402" + "version": "6.0.311" } } \ No newline at end of file