Show real 0-100% download progress from transferred bytes and saved rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Magomed 2026-07-18 15:45:43 +03:00
parent 9ee5561f3d
commit 1f6f442b21
3 changed files with 193 additions and 73 deletions

View File

@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes
public bool IsAuthenticated => !string.IsNullOrEmpty(_token); public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null) public async Task<PriceSummaryResponse> GetPriceSummaryAsync(
string supplierId = null,
string regionId = null,
string q = null,
int? limit = null,
int? offset = null,
IProgress<HttpTransferProgress> transferProgress = null)
{ {
EnsureAuthenticated(); EnsureAuthenticated();
@ -130,7 +136,10 @@ namespace Электронная_Фармация.Classes
path += "?" + string.Join("&", queryParams); path += "?" + string.Join("&", queryParams);
} }
var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса"); var responseBody = transferProgress == null
? await SendAuthorizedGetAsync(path, "загрузку прайса")
: await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress);
try try
{ {
var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions); var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions);
@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes
} }
private async Task<string> SendAuthorizedGetAsync(string path, string operationName) private async Task<string> SendAuthorizedGetAsync(string path, string operationName)
{
return await SendAuthorizedGetWithProgressAsync(path, operationName, null);
}
private async Task<string> SendAuthorizedGetWithProgressAsync(
string path,
string operationName,
IProgress<HttpTransferProgress> transferProgress)
{ {
var fullUrl = BuildFullUrl(path); var fullUrl = BuildFullUrl(path);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
@ -185,8 +202,8 @@ namespace Электронная_Фармация.Classes
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl)) using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
{ {
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
var response = await _httpClient.SendAsync(httpRequest); var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
var responseBody = await response.Content.ReadAsStringAsync(); var responseBody = await ReadContentWithProgressAsync(response, transferProgress);
stopwatch.Stop(); stopwatch.Stop();
AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
@ -243,6 +260,29 @@ namespace Электронная_Фармация.Classes
} }
} }
private static async Task<string> ReadContentWithProgressAsync(
HttpResponseMessage response,
IProgress<HttpTransferProgress> 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) private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
{ {
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@ -333,4 +373,29 @@ namespace Электронная_Фармация.Classes
public string InvoiceSum { get; set; } public string InvoiceSum { get; set; }
public string RefuseSum { 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));
}
}
}
} }

View File

@ -52,10 +52,10 @@
// progressDownload // progressDownload
// //
this.progressDownload.Location = new System.Drawing.Point(25, 52); this.progressDownload.Location = new System.Drawing.Point(25, 52);
this.progressDownload.MarqueeAnimationSpeed = 30;
this.progressDownload.Name = "progressDownload"; this.progressDownload.Name = "progressDownload";
this.progressDownload.Size = new System.Drawing.Size(630, 22); 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; this.progressDownload.TabIndex = 6;
// //
// lblProgress // lblProgress

View File

@ -12,6 +12,11 @@ namespace Электронная_Фармация.HelpForms
{ {
public partial class HF_DownloadDataFromServer : Form 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() public HF_DownloadDataFromServer()
{ {
InitializeComponent(); InitializeComponent();
@ -30,7 +35,7 @@ namespace Электронная_Фармация.HelpForms
rtxtDebug.Visible = true; rtxtDebug.Visible = true;
rtxtDebug.ReadOnly = true; rtxtDebug.ReadOnly = true;
_isDownloading = true; _isDownloading = true;
SetMarqueeProgress("Подключение к серверу..."); SetOverallProgress(0, "Подключение к серверу...");
var client = new ApiClient(); var client = new ApiClient();
AppendStatus($"Сервер: {client.BaseUrl}"); AppendStatus($"Сервер: {client.BaseUrl}");
@ -39,43 +44,58 @@ namespace Электронная_Фармация.HelpForms
try try
{ {
SetMarqueeProgress("Авторизация..."); SetOverallProgress(1, "Авторизация...");
await EnsureAuthenticatedAsync(client); await EnsureAuthenticatedAsync(client);
SetMarqueeProgress("Загрузка прайса с сервера..."); SetOverallProgress(3, "Загрузка прайса с сервера...");
AppendStatus("Загружаю сводный прайс (постранично, весь)..."); AppendStatus("Загружаю сводный прайс...");
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций"); int totalItems = allSuppliersSummary.Summary?.Count ?? 0;
AppendStatus($"Получено {totalItems} позиций");
SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)");
SetMarqueeProgress("Подготовка таблицы..."); SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы...");
var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary)); var buildProgress = new Progress<CountProgress>(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), "Сохранение в локальную базу..."); SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу...");
var progress = new Progress<SaveProgress>(ReportSaveProgress); var saveProgress = new Progress<CountProgress>(p =>
await Task.Run(() => SavePriceTable(tablePrice, progress)); {
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; DialogResult = DialogResult.OK;
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
SetMarqueeProgress("Ошибка авторизации"); SetOverallProgress(0, "Ошибка авторизации");
AppendStatus($"Ошибка авторизации: {ex.Message}"); AppendStatus($"Ошибка авторизации: {ex.Message}");
AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
SetMarqueeProgress("Ошибка сети"); SetOverallProgress(0, "Ошибка сети");
AppendStatus($"Ошибка сети: {ex.Message}"); AppendStatus($"Ошибка сети: {ex.Message}");
AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
} }
catch (Exception ex) catch (Exception ex)
{ {
SetMarqueeProgress("Ошибка загрузки"); SetOverallProgress(0, "Ошибка загрузки");
AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
@ -114,45 +134,51 @@ namespace Электронная_Фармация.HelpForms
AppendStatus("Авторизация успешно пройдена"); AppendStatus("Авторизация успешно пройдена");
} }
// Тянем весь прайс постранично: сервер отдаёт максимум 5000 за запрос,
// поэтому идём offset'ом, пока страница не окажется неполной.
private async Task<PriceSummaryResponse> GetFullPriceSummaryPagedAsync(ApiClient client) private async Task<PriceSummaryResponse> GetFullPriceSummaryPagedAsync(ApiClient client)
{ {
var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId) var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
? null ? null
: Settings.Default.RegionId; : Settings.Default.RegionId;
// Тянем весь прайс одним запросом: постраничный OFFSET на тяжёлом // Один крупный запрос (OFFSET на больших страницах валит сервер).
// сводном запросе валит сервер на глубоких страницах. Один запрос =
// один расчёт выборки. Страховка-цикл остаётся на случай, если позиций
// окажется больше pageSize.
const int pageSize = 200000; const int pageSize = 200000;
const int maxPages = 100; const int maxPages = 100;
var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() }; var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() };
var transferProgress = new Progress<HttpTransferProgress>(ReportTransferProgress);
for (int page = 0; page < maxPages; page++) for (int page = 0; page < maxPages; page++)
{ {
int offset = page * pageSize; int offset = page * pageSize;
SetMarqueeProgress(page == 0 SetOverallProgress(
? "Ожидание ответа сервера..." Math.Min(DownloadPhaseEnd - 1, 3 + page),
: $"Загрузка страницы {page + 1}..."); page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}...");
PriceSummaryResponse chunk; PriceSummaryResponse chunk;
try 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) catch (UnauthorizedAccessException)
{ {
AppendStatus("Токен недействителен, повторная авторизация..."); AppendStatus("Токен недействителен, повторная авторизация...");
SetMarqueeProgress("Повторная авторизация..."); SetOverallProgress(2, "Повторная авторизация...");
Settings.Default.stringToken = string.Empty; Settings.Default.stringToken = string.Empty;
Settings.Default.Save(); Settings.Default.Save();
client.SetToken(null); client.SetToken(null);
await LoginAndSaveTokenAsync(client); await LoginAndSaveTokenAsync(client);
SetMarqueeProgress("Повторная загрузка прайса..."); SetOverallProgress(3, "Повторная загрузка прайса...");
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);
} }
int got = chunk?.Summary?.Count ?? 0; int got = chunk?.Summary?.Count ?? 0;
@ -160,19 +186,37 @@ namespace Электронная_Фармация.HelpForms
{ {
combined.Summary.AddRange(chunk.Summary); combined.Summary.AddRange(chunk.Summary);
AppendStatus($"Загружено {combined.Summary.Count} позиций..."); AppendStatus($"Загружено {combined.Summary.Count} позиций...");
SetMarqueeProgress($"Получено {combined.Summary.Count} позиций...");
} }
if (got < pageSize) if (got < pageSize)
{ {
break; // последняя (неполная) страница — дальше данных нет break;
} }
} }
return combined; 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<CountProgress> progress = null)
{ {
var tablePrice = new DataTable(); var tablePrice = new DataTable();
tablePrice.Columns.Add("supplier_price_id"); tablePrice.Columns.Add("supplier_price_id");
@ -215,6 +259,8 @@ namespace Электронная_Фармация.HelpForms
return tablePrice; return tablePrice;
} }
int total = allSuppliersSummary.Summary.Count;
int current = 0;
foreach (var item in allSuppliersSummary.Summary) foreach (var item in allSuppliersSummary.Summary)
{ {
var row = tablePrice.NewRow(); var row = tablePrice.NewRow();
@ -233,12 +279,18 @@ namespace Электронная_Фармация.HelpForms
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim(); 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["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
tablePrice.Rows.Add(row); tablePrice.Rows.Add(row);
current++;
if (progress != null && (current % 500 == 0 || current == total))
{
progress.Report(new CountProgress(current, total));
}
} }
return tablePrice; return tablePrice;
} }
private void SavePriceTable(DataTable tablePrice, IProgress<SaveProgress> progress = null) private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null)
{ {
const string commandToDelete = "delete from [PriceList]"; const string commandToDelete = "delete from [PriceList]";
const string commandToDeleteTempOrder = "delete from [TempOrderItems]"; const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
@ -315,7 +367,7 @@ VALUES
saved++; saved++;
if (progress != null && (saved % 250 == 0 || saved == total)) 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); double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total));
SetDeterminateProgress( return phaseStart + (int)((phaseEnd - phaseStart) * ratio);
percent,
100,
$"Сохранение в базу: {state.Current} / {state.Total} ({percent}%)");
} }
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) if (IsDisposed)
{ {
@ -347,27 +421,7 @@ VALUES
if (InvokeRequired) if (InvokeRequired)
{ {
BeginInvoke(new Action<string>(SetMarqueeProgress), text); BeginInvoke(new Action<int, string>(SetOverallProgress), percent, 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<int, int, string>(SetDeterminateProgress), value, maximum, text);
return; return;
} }
@ -376,9 +430,10 @@ VALUES
progressDownload.Style = ProgressBarStyle.Continuous; progressDownload.Style = ProgressBarStyle.Continuous;
} }
progressDownload.Maximum = Math.Max(maximum, 1); progressDownload.Minimum = 0;
progressDownload.Value = Math.Max(0, Math.Min(value, progressDownload.Maximum)); progressDownload.Maximum = 100;
lblProgress.Text = text ?? string.Empty; progressDownload.Value = Math.Max(0, Math.Min(100, percent));
lblProgress.Text = $"{Math.Max(0, Math.Min(100, percent))}% — {text}";
lblProgress.Refresh(); lblProgress.Refresh();
progressDownload.Refresh(); progressDownload.Refresh();
} }
@ -411,9 +466,9 @@ VALUES
Close(); Close();
} }
private sealed class SaveProgress private sealed class CountProgress
{ {
public SaveProgress(int current, int total) public CountProgress(int current, int total)
{ {
Current = current; Current = current;
Total = total; Total = total;