using System; using System.Collections.Generic; using System.Data; using System.Data.SQLite; using System.Drawing; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Электронная_Фармация.Classes; using Электронная_Фармация.Properties; namespace Электронная_Фармация.HelpForms { public partial class HF_DownloadDataFromServer : Form { // Доли общего прогресса 0..100. private const int DownloadPhaseEnd = 45; private const int BuildPhaseEnd = 55; private const int SavePhaseEnd = 100; public HF_DownloadDataFromServer() { InitializeComponent(); } // Чтобы при обновлении прайса не терять корзины других аптек, // чистим TempOrderItems только для активного грузополучателя. public string ActiveConsigneeName { get; set; } = string.Empty; private readonly string _login = Settings.Default.stringLogin ?? string.Empty; private readonly string _password = Settings.Default.stringPassword ?? string.Empty; private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString; private bool _isDownloading; private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e) { UiThemeHelper.ApplyToControlTree(this); ConfigureLogView(); btnCloseThis.Enabled = false; dataGridView1.Visible = false; rtxtDebug.Visible = true; rtxtDebug.ReadOnly = true; _isDownloading = true; SetOverallProgress(0, "Подключение к серверу..."); var client = new ApiClient(); AppendStatus($"Сервер: {client.BaseUrl}"); AppendStatus($"Лог: {AppDebugLog.CurrentLogFilePath}"); AppDebugLog.Info("Download", $"Старт загрузки прайса. Сервер={client.BaseUrl}"); try { SetOverallProgress(1, "Авторизация..."); await EnsureAuthenticatedAsync(client); SetOverallProgress(3, "Загрузка прайса с сервера..."); AppendStatus("Загружаю сводный прайс..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); int totalItems = allSuppliersSummary.Summary?.Count ?? 0; // Наценки прайса/региона/покупателя считает сервер. // Десктоп сохраняет цену из API как есть, без локального пересчёта. AppendStatus("Цена берётся с сервера без локального пересчёта наценок"); AppendStatus($"Получено {totalItems} позиций"); SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)"); SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы..."); var buildProgress = new Progress(p => { int phasePercent = MapPhasePercent(p.Current, p.Total, DownloadPhaseEnd, BuildPhaseEnd); SetOverallProgress( phasePercent, $"Обработка: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)"); }); var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, buildProgress)); SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу..."); var saveProgress = new Progress(p => { int phasePercent = MapPhasePercent(p.Current, p.Total, BuildPhaseEnd, SavePhaseEnd); SetOverallProgress( phasePercent, $"Сохранение: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)"); }); await Task.Run(() => SavePriceTable(tablePrice, saveProgress)); SetOverallProgress(100, $"Готово: {tablePrice.Rows.Count} / {tablePrice.Rows.Count} (100%)"); DialogResult = DialogResult.OK; AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); Settings.Default.LastPriceUpdateUtc = DateTime.UtcNow; Settings.Default.LastPriceReminderUtc = DateTime.MinValue; Settings.Default.Save(); } catch (UnauthorizedAccessException ex) { SetOverallProgress(0, "Ошибка авторизации"); AppendStatus($"Ошибка авторизации: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); } catch (HttpRequestException ex) { SetOverallProgress(0, "Ошибка сети"); AppendStatus($"Ошибка сети: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); } catch (Exception ex) { SetOverallProgress(0, "Ошибка загрузки"); AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); } finally { _isDownloading = false; btnCloseThis.Enabled = true; } } private async Task EnsureAuthenticatedAsync(ApiClient client) { if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken)) { client.SetToken(Settings.Default.stringToken); AppendStatus("Используется сохранённый токен..."); return; } await LoginAndSaveTokenAsync(client); } private async Task LoginAndSaveTokenAsync(ApiClient client) { if (string.IsNullOrWhiteSpace(_login) || string.IsNullOrWhiteSpace(_password)) { throw new UnauthorizedAccessException( "Не заданы логин и пароль. Выполните авторизацию в меню «Регистрация»."); } AppendStatus("Выполняю авторизацию..."); var token = await client.LoginAsync(_login, _password); Settings.Default.stringToken = token; Settings.Default.Save(); AppendStatus("Авторизация успешно пройдена"); } private async Task GetFullPriceSummaryPagedAsync(ApiClient client) { var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId; const int supplierPageLimit = 100000; const int discoveryLimit = 50000; var combined = new PriceSummaryResponse { Summary = new List() }; var transferProgress = new Progress(ReportTransferProgress); var supplierIds = new List(ConsigneeHelper.GetKnownSupplierIds()); if (supplierIds.Count == 0) { AppendStatus("Первая загрузка: определяю список поставщиков..."); SetOverallProgress(3, "Определение поставщиков..."); var discovery = await GetPriceSummaryWithAuthRetryAsync( client, supplierId: null, regionId: regionId, limit: discoveryLimit, offset: 0, transferProgress: transferProgress); if (discovery?.MarkupPercent.HasValue == true) { combined.MarkupPercent = discovery.MarkupPercent; } var discovered = new HashSet(StringComparer.OrdinalIgnoreCase); if (discovery?.Summary != null) { foreach (var item in discovery.Summary) { if (!string.IsNullOrWhiteSpace(item.SupplierId)) { discovered.Add(item.SupplierId.Trim()); } } } supplierIds.AddRange(discovered); AppendStatus($"Найдено поставщиков: {supplierIds.Count}"); } if (supplierIds.Count == 0) { AppendStatus("Поставщики не найдены, пробую полный запрос..."); var fallback = await GetPriceSummaryWithAuthRetryAsync( client, supplierId: null, regionId: regionId, limit: 200000, offset: 0, transferProgress: transferProgress); if (fallback?.MarkupPercent.HasValue == true) { combined.MarkupPercent = fallback.MarkupPercent; } if (fallback?.Summary != null) { combined.Summary.AddRange(fallback.Summary); } return combined; } int totalSuppliers = supplierIds.Count; for (int i = 0; i < totalSuppliers; i++) { var supplierId = supplierIds[i]; int supplierProgressStart = 3 + (int)((DownloadPhaseEnd - 3) * ((double)i / totalSuppliers)); int supplierProgressEnd = 3 + (int)((DownloadPhaseEnd - 3) * ((double)(i + 1) / totalSuppliers)); SetOverallProgress( supplierProgressStart, $"Поставщик {i + 1}/{totalSuppliers}..."); AppendStatus($"Загрузка поставщика {i + 1}/{totalSuppliers}: {supplierId}"); PriceSummaryResponse chunk; try { chunk = await GetPriceSummaryWithAuthRetryAsync( client, supplierId: supplierId, regionId: regionId, limit: supplierPageLimit, offset: 0, transferProgress: transferProgress); } catch (Exception ex) { AppendStatus($"Ошибка поставщика {supplierId}: {ex.Message}"); AppDebugLog.Error("Download", $"Ошибка загрузки прайса поставщика {supplierId}", ex); continue; } if (i == 0 && chunk?.MarkupPercent.HasValue == true && !combined.MarkupPercent.HasValue) { combined.MarkupPercent = chunk.MarkupPercent; } int got = chunk?.Summary?.Count ?? 0; if (got > 0) { combined.Summary.AddRange(chunk.Summary); } SetOverallProgress( Math.Max(supplierProgressStart, supplierProgressEnd - 1), $"Поставщик {i + 1}/{totalSuppliers}: +{got}, всего {combined.Summary.Count}"); AppendStatus($"Поставщик {i + 1}/{totalSuppliers}: получено {got}, всего {combined.Summary.Count}"); } // Лёгкий discovery новых поставщиков, которых ещё не было в локальной базе. try { AppendStatus("Проверка новых поставщиков..."); var probe = await GetPriceSummaryWithAuthRetryAsync( client, supplierId: null, regionId: regionId, limit: 5000, offset: 0, transferProgress: null); var known = new HashSet(supplierIds, StringComparer.OrdinalIgnoreCase); var newcomers = new List(); if (probe?.Summary != null) { foreach (var item in probe.Summary) { if (!string.IsNullOrWhiteSpace(item.SupplierId) && known.Add(item.SupplierId.Trim())) { newcomers.Add(item.SupplierId.Trim()); } } } for (int i = 0; i < newcomers.Count; i++) { var supplierId = newcomers[i]; AppendStatus($"Новый поставщик {i + 1}/{newcomers.Count}: {supplierId}"); var chunk = await GetPriceSummaryWithAuthRetryAsync( client, supplierId: supplierId, regionId: regionId, limit: supplierPageLimit, offset: 0, transferProgress: transferProgress); int got = chunk?.Summary?.Count ?? 0; if (got > 0) { combined.Summary.AddRange(chunk.Summary); } AppendStatus($"Новый поставщик: +{got}, всего {combined.Summary.Count}"); } } catch (Exception ex) { AppendStatus($"Проверка новых поставщиков пропущена: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка discovery новых поставщиков", ex); } return combined; } private async Task GetPriceSummaryWithAuthRetryAsync( ApiClient client, string supplierId, string regionId, int limit, int offset, IProgress transferProgress) { try { return await client.GetPriceSummaryAsync( supplierId: supplierId, regionId: regionId, limit: limit, offset: offset, transferProgress: transferProgress); } catch (UnauthorizedAccessException) { AppendStatus("Токен недействителен, повторная авторизация..."); SetOverallProgress(2, "Повторная авторизация..."); Settings.Default.stringToken = string.Empty; Settings.Default.Save(); client.SetToken(null); await LoginAndSaveTokenAsync(client); return await client.GetPriceSummaryAsync( supplierId: supplierId, regionId: regionId, limit: limit, offset: offset, transferProgress: transferProgress); } } private void ReportTransferProgress(HttpTransferProgress transfer) { if (transfer.TotalBytes.HasValue && transfer.TotalBytes.Value > 0) { int phasePercent = MapPhasePercent(transfer.BytesReceived, transfer.TotalBytes.Value, 3, DownloadPhaseEnd); SetOverallProgress( phasePercent, $"Скачивание: {FormatBytes(transfer.BytesReceived)} / {FormatBytes(transfer.TotalBytes.Value)} ({transfer.Percent}%)"); } else { SetOverallProgress( Math.Min(DownloadPhaseEnd - 1, 5), $"Скачивание: получено {FormatBytes(transfer.BytesReceived)}..."); } } private static DataTable BuildPriceTable( PriceSummaryResponse allSuppliersSummary, IProgress progress = null) { var tablePrice = new DataTable(); tablePrice.Columns.Add("supplier_price_id"); tablePrice.Columns.Add("guid_es"); tablePrice.Columns.Add("es_code"); tablePrice.Columns.Add("supplier_id"); tablePrice.Columns.Add("DrugName"); tablePrice.Columns.Add("SupplierName"); tablePrice.Columns.Add("Price"); tablePrice.Columns.Add("Quantity"); tablePrice.Columns.Add(new DataColumn { ColumnName = "ExpiryPeriod", DataType = typeof(string), AllowDBNull = true }); tablePrice.Columns.Add(new DataColumn { ColumnName = "Description", DataType = typeof(string), AllowDBNull = true }); tablePrice.Columns.Add("Zakaz"); tablePrice.Columns.Add("SummaZakaza"); tablePrice.Columns.Add(new DataColumn { ColumnName = "TradeName", DataType = typeof(string), AllowDBNull = true }); tablePrice.Columns.Add(new DataColumn { ColumnName = "Dosage", DataType = typeof(string), AllowDBNull = true }); tablePrice.Columns.Add(new DataColumn { ColumnName = "MarkupPercent", DataType = typeof(decimal), AllowDBNull = true }); if (allSuppliersSummary?.Summary == null) { return tablePrice; } int total = allSuppliersSummary.Summary.Count; int current = 0; foreach (var item in allSuppliersSummary.Summary) { var row = tablePrice.NewRow(); row["supplier_price_id"] = item.SupplierPriceID ?? string.Empty; row["guid_es"] = item.GuidEs ?? string.Empty; row["es_code"] = item.EsCode?.ToString() ?? "0"; row["supplier_id"] = item.SupplierId ?? string.Empty; row["DrugName"] = item.DrugName ?? string.Empty; row["SupplierName"] = item.SupplierName ?? string.Empty; // Цена уже итоговая с сервера — локально не пересчитываем. var serverPrice = item.Price ?? 0m; row["Price"] = serverPrice.ToString(System.Globalization.CultureInfo.InvariantCulture); row["Quantity"] = item.Quantity?.ToString() ?? string.Empty; row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod; row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description; row["Zakaz"] = string.Empty; row["SummaZakaza"] = string.Empty; row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim(); row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim(); row["MarkupPercent"] = item.MarkupPercent.HasValue && item.MarkupPercent.Value > 0m ? item.MarkupPercent.Value : 0m; tablePrice.Rows.Add(row); current++; if (progress != null && (current % 500 == 0 || current == total)) { progress.Report(new CountProgress(current, total)); } } return tablePrice; } private void SavePriceTable(DataTable tablePrice, IProgress progress = null) { const string commandToDelete = "delete from [PriceList]"; var commandToDeleteTempOrder = string.IsNullOrWhiteSpace(ActiveConsigneeName) ? "delete from [TempOrderItems]" : "delete from [TempOrderItems] where [ConsigneeName] = @consignee"; const string commandToInsert = @"INSERT INTO [PriceList] ( [guid_es], [es_code], [supplier_id], [DrugName], [SupplierName], [Price], [Quantity], [ExpiryPeriod], [Description], [Zakaz], [SummaZakaza], [supplier_price_id], [TradeName], [Dosage], [MarkupPercent] ) VALUES ( @guid_es, @es_code, @supplier_id, @DrugName, @SupplierName, @Price, @Quantity, @ExpiryPeriod, @Description, @Zakaz, @SummaZakaza, @supplier_price_id, @TradeName, @Dosage, @MarkupPercent )"; int total = tablePrice.Rows.Count; using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb)) { sqliteCon.Open(); using (var cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon)) using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon)) { cmdDeleteBeforeInsert.ExecuteNonQuery(); if (!string.IsNullOrWhiteSpace(ActiveConsigneeName) && commandToDeleteTempOrder.Contains("@consignee")) { cmdDeleteTempOrder.Parameters.AddWithValue("@consignee", ActiveConsigneeName.Trim()); } cmdDeleteTempOrder.ExecuteNonQuery(); } using (var transaction = sqliteCon.BeginTransaction()) using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) { var pGuidEs = cmd.Parameters.Add("@guid_es", DbType.String); var pEsCode = cmd.Parameters.Add("@es_code", DbType.String); var pSupplierId = cmd.Parameters.Add("@supplier_id", DbType.String); var pDrugName = cmd.Parameters.Add("@DrugName", DbType.String); var pSupplierName = cmd.Parameters.Add("@SupplierName", DbType.String); var pPrice = cmd.Parameters.Add("@Price", DbType.String); var pQuantity = cmd.Parameters.Add("@Quantity", DbType.String); var pExpiry = cmd.Parameters.Add("@ExpiryPeriod", DbType.String); var pDescription = cmd.Parameters.Add("@Description", DbType.String); var pZakaz = cmd.Parameters.Add("@Zakaz", DbType.String); var pSumma = cmd.Parameters.Add("@SummaZakaza", DbType.String); var pSupplierPriceId = cmd.Parameters.Add("@supplier_price_id", DbType.String); var pTradeName = cmd.Parameters.Add("@TradeName", DbType.String); var pDosage = cmd.Parameters.Add("@Dosage", DbType.String); var pMarkup = cmd.Parameters.Add("@MarkupPercent", DbType.Object); int saved = 0; bool hasTradeName = tablePrice.Columns.Contains("TradeName"); bool hasDosage = tablePrice.Columns.Contains("Dosage"); bool hasMarkup = tablePrice.Columns.Contains("MarkupPercent"); foreach (DataRow row in tablePrice.Rows) { pGuidEs.Value = row["guid_es"] ?? DBNull.Value; pEsCode.Value = row["es_code"] ?? DBNull.Value; pSupplierId.Value = row["supplier_id"] ?? DBNull.Value; pDrugName.Value = row["DrugName"] ?? DBNull.Value; pSupplierName.Value = row["SupplierName"] ?? DBNull.Value; pPrice.Value = row["Price"] ?? DBNull.Value; pQuantity.Value = row["Quantity"] ?? DBNull.Value; pExpiry.Value = row["ExpiryPeriod"] ?? DBNull.Value; pDescription.Value = row["Description"] ?? DBNull.Value; pZakaz.Value = row["Zakaz"] ?? DBNull.Value; pSumma.Value = row["SummaZakaza"] ?? DBNull.Value; pSupplierPriceId.Value = row["supplier_price_id"] ?? DBNull.Value; pTradeName.Value = hasTradeName ? (row["TradeName"] ?? DBNull.Value) : DBNull.Value; pDosage.Value = hasDosage ? (row["Dosage"] ?? DBNull.Value) : DBNull.Value; pMarkup.Value = hasMarkup ? (row["MarkupPercent"] ?? DBNull.Value) : DBNull.Value; cmd.ExecuteNonQuery(); saved++; if (progress != null && (saved % 250 == 0 || saved == total)) { progress.Report(new CountProgress(saved, total)); } } transaction.Commit(); } } } private static int MapPhasePercent(long current, long total, int phaseStart, int phaseEnd) { if (total <= 0) { return phaseStart; } double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total)); return phaseStart + (int)((phaseEnd - phaseStart) * ratio); } private static int PercentOf(long current, long total) { if (total <= 0) { return 0; } return (int)Math.Max(0, Math.Min(100, (100.0 * current) / total)); } private static string FormatBytes(long bytes) { if (bytes < 1024) { return $"{bytes} Б"; } if (bytes < 1024 * 1024) { return $"{bytes / 1024.0:0.0} КБ"; } return $"{bytes / (1024.0 * 1024.0):0.00} МБ"; } private void SetOverallProgress(int percent, string text) { if (IsDisposed) { return; } if (InvokeRequired) { BeginInvoke(new Action(SetOverallProgress), percent, text); return; } if (progressDownload.Style != ProgressBarStyle.Continuous) { progressDownload.Style = ProgressBarStyle.Continuous; } progressDownload.Minimum = 0; progressDownload.Maximum = 100; progressDownload.Value = Math.Max(0, Math.Min(100, percent)); lblProgress.Text = FormatProgressText(percent, text); lblProgress.Refresh(); progressDownload.Refresh(); } private void ConfigureLogView() { rtxtDebug.ReadOnly = true; rtxtDebug.WordWrap = true; rtxtDebug.ScrollBars = RichTextBoxScrollBars.Vertical; rtxtDebug.HideSelection = false; rtxtDebug.DetectUrls = false; rtxtDebug.BorderStyle = BorderStyle.FixedSingle; if (rtxtDebug.Font.Name != "Consolas") { rtxtDebug.Font = new Font("Consolas", 9f); } lblProgress.AutoEllipsis = false; lblProgress.UseMnemonic = false; } private static string FormatProgressText(int percent, string text) { return WrapLongText($"{Math.Max(0, Math.Min(100, percent))}% — {text}", 78, 2); } private static string WrapLongText(string text, int maxLineLength, int maxLines) { if (string.IsNullOrEmpty(text) || text.Length <= maxLineLength) { return text ?? string.Empty; } var lines = new List(); var remaining = text.Trim(); while (!string.IsNullOrEmpty(remaining) && lines.Count < maxLines) { if (remaining.Length <= maxLineLength) { lines.Add(remaining); break; } var breakIndex = remaining.LastIndexOf(' ', Math.Min(maxLineLength, remaining.Length - 1)); if (breakIndex <= 0) { breakIndex = maxLineLength; } lines.Add(remaining.Substring(0, breakIndex).TrimEnd()); remaining = remaining.Substring(breakIndex).TrimStart(); } if (!string.IsNullOrEmpty(remaining) && lines.Count >= maxLines) { var lastLine = lines[lines.Count - 1]; if (lastLine.Length > maxLineLength - 3) { lastLine = lastLine.Substring(0, Math.Max(0, maxLineLength - 3)).TrimEnd(); } lines[lines.Count - 1] = lastLine + "..."; } return string.Join(Environment.NewLine, lines); } private void AppendStatus(string message) { if (InvokeRequired) { BeginInvoke(new Action(AppendStatus), message); return; } var wrapped = WrapLongText(message ?? string.Empty, 96, int.MaxValue); rtxtDebug.AppendText(wrapped + Environment.NewLine); rtxtDebug.SelectionStart = rtxtDebug.TextLength; rtxtDebug.SelectionLength = 0; rtxtDebug.ScrollToCaret(); rtxtDebug.Refresh(); AppDebugLog.Info("Download", message); } private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e) { if (_isDownloading) { e.Cancel = true; } } private void btnCloseThis_Click(object sender, EventArgs e) { Close(); } private sealed class CountProgress { public CountProgress(int current, int total) { Current = current; Total = total; } public int Current { get; } public int Total { get; } } } }