diff options
Diffstat (limited to 'RhSolutions.SkuParser.Api')
-rw-r--r-- | RhSolutions.SkuParser.Api/Controllers/ProductsController.cs | 48 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Models/Product.cs | 65 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Models/ProductQuantity.cs | 30 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Program.cs | 14 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Properties/launchSettings.json | 15 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj | 5 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Services/CsvParser.cs | 24 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Services/ExcelParser.cs | 76 | ||||
-rw-r--r-- | RhSolutions.SkuParser.Api/Services/ISkuParser.cs | 7 |
9 files changed, 268 insertions, 16 deletions
diff --git a/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs new file mode 100644 index 0000000..d85b01b --- /dev/null +++ b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using RhSolutions.SkuParser.Models; +using RhSolutions.SkuParser.Services; + +namespace RhSolutions.SkuParser.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class ProductsController : ControllerBase +{ + private IServiceProvider _provider; + private Dictionary<Product, double> _result; + public ProductsController(IServiceProvider provider) + { + _provider = provider; + _result = new(); + } + + [HttpPost] + public IActionResult PostFiles() + { + IFormFileCollection files = Request.Form.Files; + try + { + foreach (var file in files) + { + ISkuParser parser = _provider.GetRequiredKeyedService<ISkuParser>(file.ContentType); + IEnumerable<ProductQuantity> productQuantities = parser.ParseProducts(file); + foreach (ProductQuantity pq in productQuantities) + { + if (_result.ContainsKey(pq.Product)) + { + _result[pq.Product] += pq.Quantity; + } + else + { + _result.Add(pq.Product, pq.Quantity); + } + } + } + } + catch (Exception ex) + { + return BadRequest(error: $"{ex.Message}\n\n{ex.Source}\n{ex.StackTrace}"); + } + return new JsonResult(_result.Select(x => new { Sku = x.Key.ToString(), Quantity = x.Value })); + } +}
\ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/Product.cs b/RhSolutions.SkuParser.Api/Models/Product.cs new file mode 100644 index 0000000..fd9ea45 --- /dev/null +++ b/RhSolutions.SkuParser.Api/Models/Product.cs @@ -0,0 +1,65 @@ +using System.Text.RegularExpressions; + +namespace RhSolutions.SkuParser.Models; + +public record Product +{ + /// <summary> + /// Артикул РЕХАУ в заданном формате + /// </summary> + public required string Sku + { + get => _sku; + set + { + _sku = IsValudSku(value) + ? value + : throw new ArgumentException("$Неверный артикул: {value}"); + } + } + private string _sku = string.Empty; + private const string _parsePattern = @"(?<Lead>[1\s]|^|\b)(?<Article>\d{6})(?<Delimiter>[\s13-])(?<Variant>\d{3})(\b|$)"; + private const string _validnessPattern = @"^1\d{6}[1|3]\d{3}$"; + + private static bool IsValudSku(string value) + { + return Regex.IsMatch(value.Trim(), _validnessPattern); + } + private static string GetSku(Match match) + { + string lead = match.Groups["Lead"].Value; + string article = match.Groups["Article"].Value; + string delimiter = match.Groups["Delimiter"].Value; + string variant = match.Groups["Variant"].Value; + + if (lead != "1" && delimiter == "-") + { + return $"1{article}1{variant}"; + } + else + { + return $"{lead}{article}{delimiter}{variant}"; + } + } + + /// <summary> + /// Проверка строки на наличие в ней артикула РЕХАУ + /// </summary> + /// <param name="value">Входная строка для проверки</param> + /// <param name="product">Артикул, если найден. null - если нет</param> + /// <returns>Если артикул в строке есть возвращает true, Если нет - false</returns> + public static bool TryParse(string value, out Product? product) + { + product = null; + MatchCollection matches = Regex.Matches(value, _parsePattern); + if (matches.Count == 0) + { + return false; + } + string sku = GetSku(matches.First()); + product = new Product() { Sku = sku }; + return true; + } + public override int GetHashCode() => Sku.GetHashCode(); + public override string ToString() => Sku; +}
\ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs new file mode 100644 index 0000000..b7b154d --- /dev/null +++ b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs @@ -0,0 +1,30 @@ +using CsvHelper.Configuration.Attributes; + +namespace RhSolutions.SkuParser.Models; + +public class ProductQuantity +{ + [Index(0)] + public required Product Product { get; set; } + [Index(1)] + public required double Quantity { get; set; } + + public override bool Equals(object? obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + ProductQuantity other = (ProductQuantity)obj; + return Product == other.Product && + Quantity == other.Quantity; + } + + public override int GetHashCode() + { + HashCode hash = new(); + hash.Add(Product); + hash.Add(Quantity); + return hash.ToHashCode(); + } +} diff --git a/RhSolutions.SkuParser.Api/Program.cs b/RhSolutions.SkuParser.Api/Program.cs index 1760df1..44c642a 100644 --- a/RhSolutions.SkuParser.Api/Program.cs +++ b/RhSolutions.SkuParser.Api/Program.cs @@ -1,6 +1,12 @@ -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); +using RhSolutions.SkuParser.Services; -app.MapGet("/", () => "Hello World!"); +var builder = WebApplication.CreateBuilder(args); +builder.Services + .AddKeyedScoped<ISkuParser, CsvParser>("text/csv") + .AddKeyedScoped<ISkuParser, ExcelParser>("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .AddKeyedScoped<ISkuParser, ExcelParser>("application/vnd.ms-excel.sheet.macroenabled.12"); +builder.Services.AddControllers(); -app.Run(); +var app = builder.Build(); +app.MapControllers(); +app.Run();
\ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Properties/launchSettings.json b/RhSolutions.SkuParser.Api/Properties/launchSettings.json index 06b56be..09f4eae 100644 --- a/RhSolutions.SkuParser.Api/Properties/launchSettings.json +++ b/RhSolutions.SkuParser.Api/Properties/launchSettings.json @@ -4,7 +4,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:35100", + "applicationUrl": "http://localhost:8080", "sslPort": 44355 } }, @@ -12,17 +12,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5087", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7266;http://localhost:5087", + "launchBrowser": false, + "applicationUrl": "http://localhost:8080", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj index 1b28a01..d6e06a1 100644 --- a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj +++ b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj @@ -6,4 +6,9 @@ <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> + <ItemGroup> + <PackageReference Include="ClosedXML" Version="0.102.3" /> + <PackageReference Include="CsvHelper" Version="33.0.1" /> + </ItemGroup> + </Project> diff --git a/RhSolutions.SkuParser.Api/Services/CsvParser.cs b/RhSolutions.SkuParser.Api/Services/CsvParser.cs new file mode 100644 index 0000000..436a949 --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/CsvParser.cs @@ -0,0 +1,24 @@ +using System.Globalization; +using CsvHelper; +using CsvHelper.Configuration; +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +/// <summary> +/// Парсер артикулов и их количества из файлов *.csv +/// </summary> +public class CsvParser : ISkuParser +{ + public IEnumerable<ProductQuantity> ParseProducts(IFormFile file) + { + using StreamReader reader = new(file.OpenReadStream()); + var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU")) + { + HasHeaderRecord = false, + }; + using CsvReader csvReader = new(reader, config); + + return csvReader.GetRecords<ProductQuantity>().ToList(); + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ExcelParser.cs b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs new file mode 100644 index 0000000..27b10bd --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs @@ -0,0 +1,76 @@ +using ClosedXML.Excel; +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +public class ExcelParser : ISkuParser +{ + public IEnumerable<ProductQuantity> ParseProducts(IFormFile file) + { + using XLWorkbook workbook = new(file.OpenReadStream()); + IXLWorksheet ws = workbook.Worksheet(1); + + var leftTop = ws.FirstCellUsed()?.Address; + var rightBottom = ws.LastCellUsed()?.Address; + if (new object?[] { leftTop, rightBottom }.Any(x => x == null)) + { + throw new ArgumentException($"Таблица пуста: {file.FileName}"); + } + + var lookupRange = ws.Range(leftTop, rightBottom).RangeUsed(); + var columns = lookupRange.Columns(); + + var skuColumnQuantity = columns + .Select(column => new + { + Column = column, + Products = column.CellsUsed() + .Select(cell => !cell.HasFormula && Product.TryParse(cell.Value.ToString(), out Product? p) ? p : null) + }) + .Select(c => new { c.Column, SkuCount = c.Products.Count(p => p != null) }) + .Aggregate((l, r) => l.SkuCount > r.SkuCount ? l : r); + var skuColumn = skuColumnQuantity.SkuCount > 0 ? skuColumnQuantity.Column : null; + + if (skuColumn == null) + { + throw new ArgumentException($"Столбец с артикулом не определен: {file.FileName}"); + } + + var quantityColumn = lookupRange.Columns().Skip(skuColumn.ColumnNumber()) + .Select(column => new + { + Column = column, + IsColumnWithNumbers = column.CellsUsed() + .Count(cell => cell.Value.IsNumber == true) > column.CellsUsed().Count() / 4 + }) + .First(x => x.IsColumnWithNumbers) + .Column; + + if (quantityColumn == null) + { + throw new ArgumentException($"Столбец с количеством не определен: {file.FileName}"); + } + + List<ProductQuantity> result = new(); + var rows = quantityColumn.CellsUsed().Select(x => x.Address.RowNumber); + + foreach (var row in rows) + { + var quantity = quantityColumn.Cell(row).Value; + var sku = skuColumn.Cell(row).Value; + + if (quantity.IsNumber + && Product.TryParse(sku.ToString(), out Product? p)) + { + ProductQuantity pq = new() + { + Product = p!, + Quantity = quantity.GetNumber() + }; + result.Add(pq); + } + } + + return result; + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ISkuParser.cs b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs new file mode 100644 index 0000000..98b9d9c --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs @@ -0,0 +1,7 @@ +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; +public interface ISkuParser +{ + public IEnumerable<ProductQuantity> ParseProducts(IFormFile file); +} |