Show real 0-100% download progress from transferred bytes and saved rows.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9ee5561f3d
commit
1f6f442b21
@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes
|
||||
|
||||
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();
|
||||
|
||||
@ -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<PriceSummaryResponse>(responseBody, JsonOptions);
|
||||
@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
|
||||
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 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<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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<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), "Сохранение в локальную базу...");
|
||||
var progress = new Progress<SaveProgress>(ReportSaveProgress);
|
||||
await Task.Run(() => SavePriceTable(tablePrice, progress));
|
||||
SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу...");
|
||||
var saveProgress = new Progress<CountProgress>(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<PriceSummaryResponse> 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<PriceSummaryItem>() };
|
||||
var transferProgress = new Progress<HttpTransferProgress>(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<CountProgress> 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<SaveProgress> progress = null)
|
||||
private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> 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<string>(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<int, int, string>(SetDeterminateProgress), value, maximum, text);
|
||||
BeginInvoke(new Action<int, string>(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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user