0
0

Update products class

This commit is contained in:
Sergey Chebotar 2023-05-11 07:55:26 +03:00
parent 49381b06a7
commit 3ab838e27f
15 changed files with 115 additions and 265 deletions

View File

@ -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<IActionResult> 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<Product>(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<Product>(p);
}
dbContext.SaveChanges();

View File

@ -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<Product> 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<Product> 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();
}
}
}

View File

@ -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;

View File

@ -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
{
/// <inheritdoc />
@ -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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Id")
.HasColumnType("text");
b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision");
@ -41,7 +38,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired()
.HasColumnType("text[]");
b.Property<bool?>("IsOnWarehouse")
b.Property<bool>("IsOnWarehouse")
.HasColumnType("boolean");
b.Property<string>("Name")
@ -49,15 +46,17 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text");
b.Property<decimal>("Price")
.HasColumnType("decimal(8,2)");
.HasColumnType("numeric");
b.Property<string>("ProductLine")
.HasColumnType("text");
b.Property<List<string>>("ProductLines")
.IsRequired()
.HasColumnType("text[]");
b.Property<int>("ProductMeasure")
.HasColumnType("integer");
b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");

View File

@ -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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProductSku = table.Column<string>(type: "text", nullable: true),
DeprecatedSkus = table.Column<List<string>>(type: "text[]", nullable: false),
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
ProductLine = table.Column<string>(type: "text", nullable: true),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: true),
ProductSku = table.Column<string>(type: "text", nullable: false),
DeprecatedSkus = table.Column<List<string>>(type: "text[]", nullable: false),
ProductLines = table.Column<List<string>>(type: "text[]", nullable: false),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: false),
ProductMeasure = table.Column<int>(type: "integer", nullable: false),
DeliveryMakeUp = table.Column<double>(type: "double precision", nullable: true),
Price = table.Column<decimal>(type: "numeric(8,2)", nullable: false)
Price = table.Column<decimal>(type: "numeric", nullable: false)
},
constraints: table =>
{

View File

@ -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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Id")
.HasColumnType("text");
b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision");
@ -38,7 +35,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired()
.HasColumnType("text[]");
b.Property<bool?>("IsOnWarehouse")
b.Property<bool>("IsOnWarehouse")
.HasColumnType("boolean");
b.Property<string>("Name")
@ -46,15 +43,17 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text");
b.Property<decimal>("Price")
.HasColumnType("decimal(8,2)");
.HasColumnType("numeric");
b.Property<string>("ProductLine")
.HasColumnType("text");
b.Property<List<string>>("ProductLines")
.IsRequired()
.HasColumnType("text[]");
b.Property<int>("ProductMeasure")
.HasColumnType("integer");
b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");

View File

@ -1,4 +0,0 @@
namespace RhSolutions.Api.Models
{
public enum Measure { Kg, M, M2, P }
}

View File

@ -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<string> 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}";
}
}
}

View File

@ -1,11 +1,21 @@
using Microsoft.EntityFrameworkCore;
namespace RhSolutions.Api.Models;
namespace RhSolutions.Models;
public class RhSolutionsContext : DbContext
{
public RhSolutionsContext(DbContextOptions<RhSolutionsContext> options)
: base(options) { }
: base(options) { }
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Product>()
.Property(e => e.ProductSku)
.HasConversion(v => v.ToString(), v => new ProductSku(v));
builder.Entity<Product>()
.Property(e => e.DeprecatedSkus)
.HasPostgresArrayConversion<ProductSku, string>(v => v.ToString(), v => new ProductSku(v));
}
}

View File

@ -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)(?<Article>\d{6})([1\s-]|)(?<Variant>\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<Sku> 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<Sku> 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<Sku> skus)
{
MatchCollection matches = Regex.Matches(line, matchPattern);
if (matches.Count == 0)
{
skus = Enumerable.Empty<Sku>();
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}";
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore;
using RhSolutions.Api.Models;
using RhSolutions.Models;
using RhSolutions.Api.Services;
var builder = WebApplication.CreateBuilder(args);

View File

@ -8,13 +8,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.97.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<PackageReference Include="ClosedXML" Version="0.101.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Rhsolutions.ProductSku" Version="0.2.13" />
</ItemGroup>
</Project>

View File

@ -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<Product> GetProducts(HttpContext context)
public List<Product> GetProducts(HttpContext context)
{
using (var memoryStream = new MemoryStream())
{
if (context == null)
{
yield break;
return new List<Product>();
}
await context.Request.Body.CopyToAsync(memoryStream);
context.Request.Body.CopyToAsync(memoryStream).GetAwaiter().GetResult();
List<Product> products = new();
using (var wb = new XLWorkbook(memoryStream))
{
Dictionary<ProductSku, Product> 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<Sku> 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<Sku> productSkus);
Sku.TryParse(row.Field("Прежний материал")
.GetString(), out IEnumerable<Sku> deprecatedSkus);
ProductSku.TryParse(row.Field("Актуальный материал")
.GetString(), out IEnumerable<ProductSku> productSkus);
ProductSku.TryParse(row.Field("Прежний материал")
.GetString(), out IEnumerable<ProductSku> 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<string>() { productLine },
Name = productName,
ProductSku = productSkus.First().Id,
DeprecatedSkus = deprecatedSkus.Select(s => s.Id).ToList(),
DeprecatedSkus = deprecatedSkus.ToList(),
ProductMeasure = productMeasure,
DeliveryMakeUp = productWarehouseCount,
IsOnWarehouse = IsOnWarehouse,

View File

@ -1,9 +1,9 @@
using RhSolutions.Api.Models;
using RhSolutions.Models;
namespace RhSolutions.Api.Services
{
public interface IPricelistParser
{
public IAsyncEnumerable<Product> GetProducts(HttpContext context);
public List<Product> GetProducts(HttpContext context);
}
}

View File

@ -1,5 +1,5 @@
{
"sdk": {
"version": "6.0.402"
"version": "6.0.311"
}
}