From 1f6f442b2122ebfeca1271f8785a36c58f68af4b Mon Sep 17 00:00:00 2001 From: Magomed Date: Sat, 18 Jul 2026 15:45:43 +0300 Subject: [PATCH] Show real 0-100% download progress from transferred bytes and saved rows. Co-authored-by: Cursor --- src/ElectronicPharmacy/Classes/ApiClient.cs | 73 ++++++- .../HF_DownloadDataFromServer.Designer.cs | 4 +- .../HelpForms/HF_DownloadDataFromServer.cs | 189 +++++++++++------- 3 files changed, 193 insertions(+), 73 deletions(-) diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index 567852f..1706e0b 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes public bool IsAuthenticated => !string.IsNullOrEmpty(_token); - public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null) + public async Task GetPriceSummaryAsync( + string supplierId = null, + string regionId = null, + string q = null, + int? limit = null, + int? offset = null, + IProgress transferProgress = null) { EnsureAuthenticated(); @@ -130,7 +136,10 @@ namespace Электронная_Фармация.Classes path += "?" + string.Join("&", queryParams); } - var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса"); + var responseBody = transferProgress == null + ? await SendAuthorizedGetAsync(path, "загрузку прайса") + : await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress); + try { var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); @@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes } private async Task SendAuthorizedGetAsync(string path, string operationName) + { + return await SendAuthorizedGetWithProgressAsync(path, operationName, null); + } + + private async Task SendAuthorizedGetWithProgressAsync( + string path, + string operationName, + IProgress transferProgress) { var fullUrl = BuildFullUrl(path); var stopwatch = Stopwatch.StartNew(); @@ -185,8 +202,8 @@ namespace Электронная_Фармация.Classes using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl)) { httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); - var response = await _httpClient.SendAsync(httpRequest); - var responseBody = await response.Content.ReadAsStringAsync(); + var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); + var responseBody = await ReadContentWithProgressAsync(response, transferProgress); stopwatch.Stop(); AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); @@ -243,6 +260,29 @@ namespace Электронная_Фармация.Classes } } + private static async Task ReadContentWithProgressAsync( + HttpResponseMessage response, + IProgress transferProgress) + { + var totalBytes = response.Content.Headers.ContentLength; + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var memory = new System.IO.MemoryStream()) + { + var buffer = new byte[81920]; + long received = 0; + int read; + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) + { + memory.Write(buffer, 0, read); + received += read; + transferProgress?.Report(new HttpTransferProgress(received, totalBytes)); + } + + transferProgress?.Report(new HttpTransferProgress(received, totalBytes ?? received)); + return Encoding.UTF8.GetString(memory.ToArray()); + } + } + private string ParseLoginResponse(HttpResponseMessage response, string responseBody) { if (response.IsSuccessStatusCode) @@ -333,4 +373,29 @@ namespace Электронная_Фармация.Classes public string InvoiceSum { get; set; } public string RefuseSum { get; set; } } + + public sealed class HttpTransferProgress + { + public HttpTransferProgress(long bytesReceived, long? totalBytes) + { + BytesReceived = bytesReceived; + TotalBytes = totalBytes; + } + + public long BytesReceived { get; } + public long? TotalBytes { get; } + + public int Percent + { + get + { + if (!TotalBytes.HasValue || TotalBytes.Value <= 0) + { + return 0; + } + + return (int)Math.Max(0, Math.Min(100, (100.0 * BytesReceived) / TotalBytes.Value)); + } + } + } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs index d47487a..e9c9758 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs @@ -52,10 +52,10 @@ // progressDownload // this.progressDownload.Location = new System.Drawing.Point(25, 52); - this.progressDownload.MarqueeAnimationSpeed = 30; this.progressDownload.Name = "progressDownload"; this.progressDownload.Size = new System.Drawing.Size(630, 22); - this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.progressDownload.Maximum = 100; this.progressDownload.TabIndex = 6; // // lblProgress diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 95b4dae..3d8b23b 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -12,6 +12,11 @@ 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(); @@ -30,7 +35,7 @@ namespace Электронная_Фармация.HelpForms rtxtDebug.Visible = true; rtxtDebug.ReadOnly = true; _isDownloading = true; - SetMarqueeProgress("Подключение к серверу..."); + SetOverallProgress(0, "Подключение к серверу..."); var client = new ApiClient(); AppendStatus($"Сервер: {client.BaseUrl}"); @@ -39,43 +44,58 @@ namespace Электронная_Фармация.HelpForms try { - SetMarqueeProgress("Авторизация..."); + SetOverallProgress(1, "Авторизация..."); await EnsureAuthenticatedAsync(client); - SetMarqueeProgress("Загрузка прайса с сервера..."); - AppendStatus("Загружаю сводный прайс (постранично, весь)..."); + SetOverallProgress(3, "Загрузка прайса с сервера..."); + AppendStatus("Загружаю сводный прайс..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); - AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций"); + int totalItems = allSuppliersSummary.Summary?.Count ?? 0; + AppendStatus($"Получено {totalItems} позиций"); + SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)"); - SetMarqueeProgress("Подготовка таблицы..."); - var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary)); + 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)); - SetDeterminateProgress(0, Math.Max(tablePrice.Rows.Count, 1), "Сохранение в локальную базу..."); - var progress = new Progress(ReportSaveProgress); - await Task.Run(() => SavePriceTable(tablePrice, progress)); + 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)); - SetDeterminateProgress(100, 100, "Готово"); + SetOverallProgress(100, $"Готово: {tablePrice.Rows.Count} / {tablePrice.Rows.Count} (100%)"); DialogResult = DialogResult.OK; AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); } catch (UnauthorizedAccessException ex) { - SetMarqueeProgress("Ошибка авторизации"); + SetOverallProgress(0, "Ошибка авторизации"); AppendStatus($"Ошибка авторизации: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); } catch (HttpRequestException ex) { - SetMarqueeProgress("Ошибка сети"); + SetOverallProgress(0, "Ошибка сети"); AppendStatus($"Ошибка сети: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); } catch (Exception ex) { - SetMarqueeProgress("Ошибка загрузки"); + SetOverallProgress(0, "Ошибка загрузки"); AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); @@ -114,45 +134,51 @@ namespace Электронная_Фармация.HelpForms AppendStatus("Авторизация успешно пройдена"); } - // Тянем весь прайс постранично: сервер отдаёт максимум 5000 за запрос, - // поэтому идём offset'ом, пока страница не окажется неполной. private async Task GetFullPriceSummaryPagedAsync(ApiClient client) { var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId; - // Тянем весь прайс одним запросом: постраничный OFFSET на тяжёлом - // сводном запросе валит сервер на глубоких страницах. Один запрос = - // один расчёт выборки. Страховка-цикл остаётся на случай, если позиций - // окажется больше pageSize. + // Один крупный запрос (OFFSET на больших страницах валит сервер). const int pageSize = 200000; const int maxPages = 100; var combined = new PriceSummaryResponse { Summary = new List() }; + var transferProgress = new Progress(ReportTransferProgress); for (int page = 0; page < maxPages; page++) { int offset = page * pageSize; - SetMarqueeProgress(page == 0 - ? "Ожидание ответа сервера..." - : $"Загрузка страницы {page + 1}..."); + SetOverallProgress( + Math.Min(DownloadPhaseEnd - 1, 3 + page), + page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}..."); PriceSummaryResponse chunk; try { - chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset); + chunk = await client.GetPriceSummaryAsync( + supplierId: null, + regionId: regionId, + limit: pageSize, + offset: offset, + transferProgress: transferProgress); } catch (UnauthorizedAccessException) { AppendStatus("Токен недействителен, повторная авторизация..."); - SetMarqueeProgress("Повторная авторизация..."); + SetOverallProgress(2, "Повторная авторизация..."); Settings.Default.stringToken = string.Empty; Settings.Default.Save(); client.SetToken(null); await LoginAndSaveTokenAsync(client); - SetMarqueeProgress("Повторная загрузка прайса..."); - chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset); + SetOverallProgress(3, "Повторная загрузка прайса..."); + chunk = await client.GetPriceSummaryAsync( + supplierId: null, + regionId: regionId, + limit: pageSize, + offset: offset, + transferProgress: transferProgress); } int got = chunk?.Summary?.Count ?? 0; @@ -160,19 +186,37 @@ namespace Электронная_Фармация.HelpForms { combined.Summary.AddRange(chunk.Summary); AppendStatus($"Загружено {combined.Summary.Count} позиций..."); - SetMarqueeProgress($"Получено {combined.Summary.Count} позиций..."); } if (got < pageSize) { - break; // последняя (неполная) страница — дальше данных нет + break; } } return combined; } - private static DataTable BuildPriceTable(PriceSummaryResponse allSuppliersSummary) + 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"); @@ -215,6 +259,8 @@ namespace Электронная_Фармация.HelpForms return tablePrice; } + int total = allSuppliersSummary.Summary.Count; + int current = 0; foreach (var item in allSuppliersSummary.Summary) { var row = tablePrice.NewRow(); @@ -233,12 +279,18 @@ namespace Электронная_Фармация.HelpForms row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim(); row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim(); 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) + private void SavePriceTable(DataTable tablePrice, IProgress progress = null) { const string commandToDelete = "delete from [PriceList]"; const string commandToDeleteTempOrder = "delete from [TempOrderItems]"; @@ -315,7 +367,7 @@ VALUES saved++; if (progress != null && (saved % 250 == 0 || saved == total)) { - progress.Report(new SaveProgress(saved, total)); + progress.Report(new CountProgress(saved, total)); } } @@ -324,21 +376,43 @@ VALUES } } - private void ReportSaveProgress(SaveProgress state) + private static int MapPhasePercent(long current, long total, int phaseStart, int phaseEnd) { - if (IsDisposed) + if (total <= 0) { - return; + return phaseStart; } - int percent = state.Total <= 0 ? 0 : (int)(100.0 * state.Current / state.Total); - SetDeterminateProgress( - percent, - 100, - $"Сохранение в базу: {state.Current} / {state.Total} ({percent}%)"); + double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total)); + return phaseStart + (int)((phaseEnd - phaseStart) * ratio); } - private void SetMarqueeProgress(string text) + 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) { @@ -347,27 +421,7 @@ VALUES if (InvokeRequired) { - BeginInvoke(new Action(SetMarqueeProgress), text); - return; - } - - progressDownload.Style = ProgressBarStyle.Marquee; - progressDownload.MarqueeAnimationSpeed = 30; - lblProgress.Text = text ?? string.Empty; - lblProgress.Refresh(); - progressDownload.Refresh(); - } - - private void SetDeterminateProgress(int value, int maximum, string text) - { - if (IsDisposed) - { - return; - } - - if (InvokeRequired) - { - BeginInvoke(new Action(SetDeterminateProgress), value, maximum, text); + BeginInvoke(new Action(SetOverallProgress), percent, text); return; } @@ -376,9 +430,10 @@ VALUES progressDownload.Style = ProgressBarStyle.Continuous; } - progressDownload.Maximum = Math.Max(maximum, 1); - progressDownload.Value = Math.Max(0, Math.Min(value, progressDownload.Maximum)); - lblProgress.Text = text ?? string.Empty; + progressDownload.Minimum = 0; + progressDownload.Maximum = 100; + progressDownload.Value = Math.Max(0, Math.Min(100, percent)); + lblProgress.Text = $"{Math.Max(0, Math.Min(100, percent))}% — {text}"; lblProgress.Refresh(); progressDownload.Refresh(); } @@ -411,9 +466,9 @@ VALUES Close(); } - private sealed class SaveProgress + private sealed class CountProgress { - public SaveProgress(int current, int total) + public CountProgress(int current, int total) { Current = current; Total = total;