Упрощён клиент
This commit is contained in:
@@ -39,7 +39,7 @@ internal sealed class SmsApiService(IMenuStore store) : ISmsApiService
|
|||||||
return FailSendOrder("CommandParameters не указаны или имеют неверный формат.");
|
return FailSendOrder("CommandParameters не указаны или имеют неверный формат.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = (parameters.MenuItems ?? [])
|
var items = parameters.MenuItems
|
||||||
.Select(item => (item.Id, item.Quantity))
|
.Select(item => (item.Id, item.Quantity))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
|
||||||
<PackageReference Include="Npgsql" Version="10.0.3" />
|
<PackageReference Include="Npgsql" Version="10.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ internal sealed class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbCon
|
|||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
|
var connectionString = configuration.GetConnectionString("Default")
|
||||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString("Default"))
|
?? throw new InvalidOperationException("Connection string 'Default' не задана.");
|
||||||
.UseSnakeCaseNamingConvention();
|
|
||||||
return new AppDbContext(optionsBuilder.Options);
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseNpgsql(connectionString)
|
||||||
|
.UseSnakeCaseNamingConvention()
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new AppDbContext(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Npgsql;
|
|
||||||
|
|
||||||
namespace ConsoleApp.Data;
|
|
||||||
|
|
||||||
internal sealed class DatabaseInitializer(AppDbContext db, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var connectionString = configuration.GetConnectionString("Default")
|
|
||||||
?? throw new InvalidOperationException("Connection string 'Default' не задана.");
|
|
||||||
|
|
||||||
await EnsureDatabaseExistsAsync(connectionString, cancellationToken);
|
|
||||||
await db.Database.MigrateAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task EnsureDatabaseExistsAsync(string connectionString, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
|
||||||
var databaseName = builder.Database
|
|
||||||
?? throw new InvalidOperationException("Имя базы данных не указано в строке подключения.");
|
|
||||||
|
|
||||||
builder.Database = "postgres";
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(builder.ConnectionString);
|
|
||||||
await connection.OpenAsync(cancellationToken);
|
|
||||||
|
|
||||||
await using var checkCommand = connection.CreateCommand();
|
|
||||||
checkCommand.CommandText = "SELECT 1 FROM pg_database WHERE datname = @name";
|
|
||||||
checkCommand.Parameters.AddWithValue("name", databaseName);
|
|
||||||
|
|
||||||
var exists = await checkCommand.ExecuteScalarAsync(cancellationToken) is not null;
|
|
||||||
if (exists)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var createCommand = connection.CreateCommand();
|
|
||||||
createCommand.CommandText = $"CREATE DATABASE \"{databaseName.Replace("\"", "\"\"")}\"";
|
|
||||||
await createCommand.ExecuteNonQueryAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace ConsoleApp.Data;
|
|
||||||
|
|
||||||
internal sealed class DishRepository(AppDbContext db)
|
|
||||||
{
|
|
||||||
public async Task SaveAsync(IReadOnlyList<Dish> dishes, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await db.Dishes.ExecuteDeleteAsync(cancellationToken);
|
|
||||||
db.Dishes.AddRange(dishes.Select(ToEntity));
|
|
||||||
await db.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MenuDish ToEntity(Dish dish) => new()
|
|
||||||
{
|
|
||||||
Id = dish.Id,
|
|
||||||
Article = dish.Article,
|
|
||||||
Name = dish.Name,
|
|
||||||
Price = dish.Price,
|
|
||||||
IsWeighted = dish.IsWeighted,
|
|
||||||
FullPath = dish.FullPath,
|
|
||||||
Barcodes = dish.Barcodes.ToList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ApiClient;
|
|
||||||
using ApiClient.Grpc;
|
|
||||||
using ApiClient.Http;
|
|
||||||
using ConsoleApp.Data;
|
|
||||||
using ConsoleApp.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace ConsoleApp;
|
|
||||||
|
|
||||||
public static class DependencyInjection
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddConsoleApp(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddDbContext<AppDbContext>(options =>
|
|
||||||
options.UseNpgsql(configuration.GetConnectionString("Default"))
|
|
||||||
.UseSnakeCaseNamingConvention());
|
|
||||||
|
|
||||||
services.AddScoped<DatabaseInitializer>();
|
|
||||||
services.AddScoped<DishRepository>();
|
|
||||||
services.AddScoped<ConsoleAppRunner>();
|
|
||||||
|
|
||||||
services.AddSingleton<ISmsClient>(sp =>
|
|
||||||
{
|
|
||||||
var backend = configuration["ApiClient:Backend"] ?? "Http";
|
|
||||||
return backend.Equals("Grpc", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? SmsClientFactory.CreateGrpc(configuration.GetSection("Grpc").Get<GrpcSmsClientOptions>() ?? new())
|
|
||||||
: SmsClientFactory.CreateHttp(configuration.GetSection("Http").Get<HttpSmsClientOptions>() ?? new());
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,57 @@
|
|||||||
namespace ConsoleApp.Logging;
|
namespace ConsoleApp.Logging;
|
||||||
|
|
||||||
internal sealed class TeeTextWriter(TextWriter consoleWriter, TextWriter logWriter) : TextWriter
|
public sealed class ConsoleLog : IDisposable
|
||||||
{
|
{
|
||||||
public override System.Text.Encoding Encoding => consoleWriter.Encoding;
|
private readonly StreamWriter _file;
|
||||||
|
|
||||||
public override void Write(char value)
|
private ConsoleLog(string filePath, DateTime startedAt)
|
||||||
{
|
{
|
||||||
consoleWriter.Write(value);
|
FilePath = filePath;
|
||||||
logWriter.Write(value);
|
StartedAt = startedAt;
|
||||||
|
_file = new StreamWriter(filePath, append: true) { AutoFlush = true };
|
||||||
|
_file.WriteLine($"Запуск: {startedAt:yyyy-MM-dd HH:mm:ss}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(string? value)
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
public DateTime StartedAt { get; }
|
||||||
|
|
||||||
|
public static ConsoleLog Open(string? fileName = null)
|
||||||
{
|
{
|
||||||
consoleWriter.Write(value);
|
var startedAt = TruncateToSeconds(DateTime.Now);
|
||||||
logWriter.Write(value);
|
fileName ??= $"test-sms-console-app-{startedAt:yyyyMMdd_HHmmss}.log";
|
||||||
|
return new ConsoleLog(Path.GetFullPath(fileName), startedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteLine(string? value)
|
private static DateTime TruncateToSeconds(DateTime value) =>
|
||||||
|
new(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Kind);
|
||||||
|
|
||||||
|
public void Write(string? text)
|
||||||
{
|
{
|
||||||
consoleWriter.WriteLine(value);
|
Console.Write(text);
|
||||||
logWriter.WriteLine(value);
|
_file.Write(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Flush()
|
public void WriteLine(string? text = null)
|
||||||
{
|
{
|
||||||
consoleWriter.Flush();
|
if (text is null)
|
||||||
logWriter.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
{
|
||||||
logWriter.Dispose();
|
Console.WriteLine();
|
||||||
|
_file.WriteLine();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
Console.WriteLine(text);
|
||||||
|
_file.WriteLine(text);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal static class ConsoleLog
|
public string? ReadLine(string prompt)
|
||||||
{
|
|
||||||
public static IDisposable Configure()
|
|
||||||
{
|
{
|
||||||
var logPath = $"test-sms-console-app-{DateTime.Now:yyyyMMdd}.log";
|
Write(prompt);
|
||||||
var logWriter = new StreamWriter(logPath, append: true) { AutoFlush = true };
|
var line = Console.ReadLine();
|
||||||
var tee = new TeeTextWriter(global::System.Console.Out, logWriter);
|
_file.WriteLine(line ?? "");
|
||||||
global::System.Console.SetOut(tee);
|
return line;
|
||||||
return logWriter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _file.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
using ConsoleApp;
|
using System.Globalization;
|
||||||
|
using ApiClient;
|
||||||
|
using ApiClient.Grpc;
|
||||||
|
using ApiClient.Http;
|
||||||
|
using ConsoleApp.Data;
|
||||||
using ConsoleApp.Logging;
|
using ConsoleApp.Logging;
|
||||||
using ConsoleApp.Services;
|
using Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Npgsql;
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
@@ -10,12 +15,222 @@ var configuration = new ConfigurationBuilder()
|
|||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
using var _ = ConsoleLog.Configure();
|
var connectionString = configuration.GetConnectionString("Default")
|
||||||
|
?? throw new InvalidOperationException("Connection string 'Default' не задана.");
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var httpOptions = configuration.GetSection("Http").Get<HttpSmsClientOptions>() ?? new();
|
||||||
services.AddSingleton<IConfiguration>(configuration);
|
var grpcOptions = configuration.GetSection("Grpc").Get<GrpcSmsClientOptions>() ?? new();
|
||||||
services.AddConsoleApp(configuration);
|
|
||||||
|
|
||||||
await using var provider = services.BuildServiceProvider();
|
using var log = ConsoleLog.Open();
|
||||||
await using var scope = provider.CreateAsyncScope();
|
|
||||||
await scope.ServiceProvider.GetRequiredService<ConsoleAppRunner>().RunAsync(CancellationToken.None);
|
var smsClient = AskSmsClient(log, httpOptions, grpcOptions);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.WriteLine("Инициализация базы данных...");
|
||||||
|
await using var db = CreateDbContext(connectionString);
|
||||||
|
await EnsureDatabaseAsync(connectionString);
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
log.WriteLine("База данных готова.");
|
||||||
|
|
||||||
|
log.WriteLine("Запрос меню с сервера...");
|
||||||
|
var menuResponse = await smsClient.GetMenuAsync(withPrice: true);
|
||||||
|
|
||||||
|
if (!menuResponse.Success)
|
||||||
|
{
|
||||||
|
log.WriteLine(menuResponse.ToString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dishes = menuResponse.Data?.MenuItems ?? [];
|
||||||
|
await SaveDishesAsync(db, dishes);
|
||||||
|
|
||||||
|
log.WriteLine("Меню:");
|
||||||
|
foreach (var dish in dishes)
|
||||||
|
{
|
||||||
|
log.WriteLine(dish.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var order = ReadOrder(log, dishes);
|
||||||
|
if (order is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WriteLine(order.ToString());
|
||||||
|
log.WriteLine("Отправка заказа на сервер...");
|
||||||
|
var sendResponse = await smsClient.SendOrderAsync(order);
|
||||||
|
|
||||||
|
if (sendResponse.Success)
|
||||||
|
{
|
||||||
|
log.WriteLine("УСПЕХ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WriteLine(sendResponse.ToString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(smsClient as IDisposable)?.Dispose();
|
||||||
|
log.WriteLine($"Файл лога (запуск {log.StartedAt:yyyy-MM-dd HH:mm:ss}): {log.FilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static ISmsClient AskSmsClient(ConsoleLog log, HttpSmsClientOptions http, GrpcSmsClientOptions grpc)
|
||||||
|
{
|
||||||
|
log.WriteLine("Выберите протокол: 1 — HTTP, 2 — gRPC");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var input = log.ReadLine("> ")?.Trim();
|
||||||
|
|
||||||
|
if (input is "1" or "http" or "Http" or "HTTP")
|
||||||
|
{
|
||||||
|
return SmsClientFactory.CreateHttp(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input is "2" or "grpc" or "Grpc" or "gRPC")
|
||||||
|
{
|
||||||
|
return SmsClientFactory.CreateGrpc(grpc);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WriteLine("Введите 1 (HTTP) или 2 (gRPC).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AppDbContext CreateDbContext(string connectionString)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseNpgsql(connectionString)
|
||||||
|
.UseSnakeCaseNamingConvention()
|
||||||
|
.Options;
|
||||||
|
return new AppDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task EnsureDatabaseAsync(string connectionString)
|
||||||
|
{
|
||||||
|
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||||
|
var databaseName = builder.Database
|
||||||
|
?? throw new InvalidOperationException("Имя базы данных не указано в строке подключения.");
|
||||||
|
|
||||||
|
builder.Database = "postgres";
|
||||||
|
|
||||||
|
await using var connection = new NpgsqlConnection(builder.ConnectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
await using var checkCommand = connection.CreateCommand();
|
||||||
|
checkCommand.CommandText = "SELECT 1 FROM pg_database WHERE datname = @name";
|
||||||
|
checkCommand.Parameters.AddWithValue("name", databaseName);
|
||||||
|
|
||||||
|
if (await checkCommand.ExecuteScalarAsync() is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var createCommand = connection.CreateCommand();
|
||||||
|
createCommand.CommandText = $"CREATE DATABASE \"{databaseName.Replace("\"", "\"\"")}\"";
|
||||||
|
await createCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SaveDishesAsync(AppDbContext db, IReadOnlyList<Dish> dishes)
|
||||||
|
{
|
||||||
|
await db.Dishes.ExecuteDeleteAsync();
|
||||||
|
db.Dishes.AddRange(dishes.Select(d => new MenuDish
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
Article = d.Article,
|
||||||
|
Name = d.Name,
|
||||||
|
Price = d.Price,
|
||||||
|
IsWeighted = d.IsWeighted,
|
||||||
|
FullPath = d.FullPath,
|
||||||
|
Barcodes = d.Barcodes.ToList(),
|
||||||
|
}));
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Order? ReadOrder(ConsoleLog log, IReadOnlyList<Dish> dishes)
|
||||||
|
{
|
||||||
|
var dishesByArticle = dishes.ToDictionary(d => d.Article, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
log.WriteLine();
|
||||||
|
log.WriteLine("Введите позиции заказа в формате Код:Количество;Код:Количество;...");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var input = log.ReadLine("> ");
|
||||||
|
|
||||||
|
if (!TryParseOrderInput(input, out var lines, out var parseError))
|
||||||
|
{
|
||||||
|
log.WriteLine(parseError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unknownArticles = lines
|
||||||
|
.Select(line => line.Article)
|
||||||
|
.Where(article => !dishesByArticle.ContainsKey(article))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (unknownArticles.Count > 0)
|
||||||
|
{
|
||||||
|
log.WriteLine($"Неизвестные коды: {string.Join(", ", unknownArticles)}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Order order = new();
|
||||||
|
foreach (var (article, quantity) in lines)
|
||||||
|
{
|
||||||
|
order = order.AddItem(dishesByArticle[article].Id, quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryParseOrderInput(
|
||||||
|
string? input,
|
||||||
|
out List<(string Article, decimal Quantity)> items,
|
||||||
|
out string errorMessage)
|
||||||
|
{
|
||||||
|
items = [];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
errorMessage = "Строка заказа не может быть пустой.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = input.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
var pair = part.Split(':', 2);
|
||||||
|
if (pair.Length != 2)
|
||||||
|
{
|
||||||
|
errorMessage = $"Некорректный формат позиции: '{part}'. Ожидается Код:Количество.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var article = pair[0].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(article))
|
||||||
|
{
|
||||||
|
errorMessage = "Артикул не может быть пустым.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decimal.TryParse(pair[1].Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var quantity)
|
||||||
|
&& !decimal.TryParse(pair[1].Trim(), NumberStyles.Number, CultureInfo.CurrentCulture, out quantity))
|
||||||
|
{
|
||||||
|
errorMessage = $"Некорректное количество для артикула '{article}'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity <= 0)
|
||||||
|
{
|
||||||
|
errorMessage = $"Количество для артикула '{article}' должно быть больше нуля.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add((article, quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
using ApiClient;
|
|
||||||
using ConsoleApp.Data;
|
|
||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace ConsoleApp.Services;
|
|
||||||
|
|
||||||
internal sealed class ConsoleAppRunner(
|
|
||||||
DatabaseInitializer databaseInitializer,
|
|
||||||
DishRepository dishRepository,
|
|
||||||
ISmsClient smsClient)
|
|
||||||
{
|
|
||||||
public async Task RunAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Инициализация базы данных...");
|
|
||||||
await databaseInitializer.InitializeAsync(cancellationToken);
|
|
||||||
Console.WriteLine("База данных готова.");
|
|
||||||
|
|
||||||
Console.WriteLine("Запрос меню с сервера...");
|
|
||||||
var menuResponse = await smsClient.GetMenuAsync(withPrice: true, cancellationToken);
|
|
||||||
|
|
||||||
if (!menuResponse.Success)
|
|
||||||
{
|
|
||||||
Console.WriteLine(menuResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dishes = menuResponse.Data?.MenuItems ?? [];
|
|
||||||
await dishRepository.SaveAsync(dishes, cancellationToken);
|
|
||||||
|
|
||||||
Console.WriteLine("Меню:");
|
|
||||||
foreach (var dish in dishes)
|
|
||||||
{
|
|
||||||
Console.WriteLine(dish);
|
|
||||||
}
|
|
||||||
|
|
||||||
var order = await ReadOrderAsync(dishes, cancellationToken);
|
|
||||||
if (order is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine(order);
|
|
||||||
Console.WriteLine("Отправка заказа на сервер...");
|
|
||||||
var sendResponse = await smsClient.SendOrderAsync(order, cancellationToken);
|
|
||||||
|
|
||||||
if (sendResponse.Success)
|
|
||||||
{
|
|
||||||
Console.WriteLine("УСПЕХ");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine(sendResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<Order?> ReadOrderAsync(IReadOnlyList<Dish> dishes, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var dishesByArticle = dishes.ToDictionary(d => d.Article, StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Введите позиции заказа в формате Код:Количество;Код:Количество;...");
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Console.Write("> ");
|
|
||||||
var input = Console.ReadLine();
|
|
||||||
|
|
||||||
if (!OrderInputParser.TryParse(input, out var lines, out var parseError))
|
|
||||||
{
|
|
||||||
Console.WriteLine(parseError);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var unknownArticles = lines
|
|
||||||
.Select(line => line.Article)
|
|
||||||
.Where(article => !dishesByArticle.ContainsKey(article))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (unknownArticles.Count > 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Неизвестные коды: {string.Join(", ", unknownArticles)}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Order order = new();
|
|
||||||
foreach (var (article, quantity) in lines)
|
|
||||||
{
|
|
||||||
order = order.AddItem(dishesByArticle[article].Id, quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult<Order?>(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult<Order?>(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace ConsoleApp.Services;
|
|
||||||
|
|
||||||
internal static class OrderInputParser
|
|
||||||
{
|
|
||||||
public static bool TryParse(
|
|
||||||
string? input,
|
|
||||||
out List<(string Article, decimal Quantity)> items,
|
|
||||||
out string errorMessage)
|
|
||||||
{
|
|
||||||
items = [];
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
{
|
|
||||||
errorMessage = "Строка заказа не может быть пустой.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = input.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
foreach (var part in parts)
|
|
||||||
{
|
|
||||||
var pair = part.Split(':', 2);
|
|
||||||
if (pair.Length != 2)
|
|
||||||
{
|
|
||||||
errorMessage = $"Некорректный формат позиции: '{part}'. Ожидается Код:Количество.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var article = pair[0].Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(article))
|
|
||||||
{
|
|
||||||
errorMessage = "Артикул не может быть пустым.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decimal.TryParse(pair[1].Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var quantity)
|
|
||||||
&& !decimal.TryParse(pair[1].Trim(), NumberStyles.Number, CultureInfo.CurrentCulture, out quantity))
|
|
||||||
{
|
|
||||||
errorMessage = $"Некорректное количество для артикула '{article}'.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quantity <= 0)
|
|
||||||
{
|
|
||||||
errorMessage = $"Количество для артикула '{article}' должно быть больше нуля.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.Add((article, quantity));
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage = "";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Host=localhost;Port=5432;Database=sms_task;Username=sms;Password=sms"
|
"Default": "Host=localhost;Port=5432;Database=sms_task;Username=sms;Password=sms"
|
||||||
},
|
},
|
||||||
"ApiClient": {
|
|
||||||
"Backend": "Http"
|
|
||||||
},
|
|
||||||
"Http": {
|
"Http": {
|
||||||
"BaseUrl": "http://localhost:5053",
|
"BaseUrl": "http://localhost:5053",
|
||||||
"Username": "user",
|
"Username": "user",
|
||||||
|
|||||||
Reference in New Issue
Block a user