From 9506f9edc86decb89f5c3c7632117024c322a4fc Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 15:52:28 +0500 Subject: [PATCH] =?UTF-8?q?perf(migration):=20=D1=80=D0=B5=D0=B7=D1=8E?= =?UTF-8?q?=D0=BC=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=BE=20offset=20(=D0=BD=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=87=D0=B8=D1=82=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=91=D0=BD=D0=BD=D0=BE=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit startOffset у всех документных импортов + эндпоинт ?offset=N: при продолжении пропускаем уже перенесённые через offset API (а не перечитывая сотни страниц). Идемпотентный skip по msId держит границу. Co-Authored-By: Claude Opus 5.5 --- .../Admin/MoySkladImportController.cs | 11 ++++++----- .../MoySklad/MoySkladDocumentImportService.cs | 16 ++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/food-market.api/Controllers/Admin/MoySkladImportController.cs b/src/food-market.api/Controllers/Admin/MoySkladImportController.cs index 8ee19da..d6de7d2 100644 --- a/src/food-market.api/Controllers/Admin/MoySkladImportController.cs +++ b/src/food-market.api/Controllers/Admin/MoySkladImportController.cs @@ -151,6 +151,7 @@ public async Task> ImportSupplies([FromBody] ImportRequest return BadRequest(new { error = "Токен не настроен." }); var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant."); var type = HttpContext.Request.Query["type"].ToString(); // захватываем ДО Task.Run (HttpContext потом недоступен) + int startOffset = int.TryParse(HttpContext.Request.Query["offset"].ToString(), out var off) ? off : 0; var job = _jobs.Create(string.IsNullOrEmpty(type) ? "supplies" : type); job.Stage = $"Перенос документов ({(string.IsNullOrEmpty(type) ? "приёмки" : type)}) с историей…"; _ = Task.Run(async () => @@ -165,11 +166,11 @@ public async Task> ImportSupplies([FromBody] ImportRequest var pr = new foodmarket.Infrastructure.Integrations.MoySklad.MoySkladDocumentImportService.DocImportProgress(); switch (type) { - case "enter": await docs.ImportEntersAsync(token, pr, CancellationToken.None); break; - case "loss": await docs.ImportLossesAsync(token, pr, CancellationToken.None); break; - case "purchasereturn": await docs.ImportSupplierReturnsAsync(token, pr, CancellationToken.None); break; - case "retaildemand": await docs.ImportRetailSalesAsync(token, pr, CancellationToken.None); break; - default: await docs.ImportSuppliesAsync(token, 0, pr, CancellationToken.None); break; + case "enter": await docs.ImportEntersAsync(token, startOffset, pr, CancellationToken.None); break; + case "loss": await docs.ImportLossesAsync(token, startOffset, pr, CancellationToken.None); break; + case "purchasereturn": await docs.ImportSupplierReturnsAsync(token, startOffset, pr, CancellationToken.None); break; + case "retaildemand": await docs.ImportRetailSalesAsync(token, startOffset, pr, CancellationToken.None); break; + default: await docs.ImportSuppliesAsync(token, startOffset, pr, CancellationToken.None); break; } job.Total = pr.Seen; job.Created = pr.Created; job.Skipped = pr.Skipped; job.Message = $"Приёмки: перенесено {pr.Created}, пропущено {pr.Skipped}, ошибок {pr.Errors}. " + diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladDocumentImportService.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladDocumentImportService.cs index 4321a0c..66edf64 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladDocumentImportService.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladDocumentImportService.cs @@ -195,14 +195,14 @@ private async Task LoadMapsAsync(Guid orgId, CancellationToken ct) } // ── Оприходования (enter) ──────────────────────────────────────────────── - public async Task ImportEntersAsync(string token, DocImportProgress progress, CancellationToken ct) + public async Task ImportEntersAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct) { var orgId = _tenant.OrganizationId!.Value; progress.Entity = "enter"; var m = await LoadMapsAsync(orgId, ct); var done = (await _db.Enters.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet(); var used = (await _db.Enters.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase); string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } } - await foreach (var d in _client.StreamDocumentsAsync(token, "entity/enter", "positions,store", 0, null, ct)) + await foreach (var d in _client.StreamDocumentsAsync(token, "entity/enter", "positions,store", startOffset, null, ct)) { progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; } try @@ -224,14 +224,14 @@ await foreach (var d in _client.StreamDocumentsAsync(token, "entity/ } // ── Списания (loss) ────────────────────────────────────────────────────── - public async Task ImportLossesAsync(string token, DocImportProgress progress, CancellationToken ct) + public async Task ImportLossesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct) { var orgId = _tenant.OrganizationId!.Value; progress.Entity = "loss"; var m = await LoadMapsAsync(orgId, ct); var done = (await _db.Losses.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet(); var used = (await _db.Losses.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase); string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } } - await foreach (var d in _client.StreamDocumentsAsync(token, "entity/loss", "positions,store", 0, null, ct)) + await foreach (var d in _client.StreamDocumentsAsync(token, "entity/loss", "positions,store", startOffset, null, ct)) { progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; } try @@ -253,14 +253,14 @@ await foreach (var d in _client.StreamDocumentsAsync(token, "entity/ } // ── Возвраты поставщику (purchasereturn) ───────────────────────────────── - public async Task ImportSupplierReturnsAsync(string token, DocImportProgress progress, CancellationToken ct) + public async Task ImportSupplierReturnsAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct) { var orgId = _tenant.OrganizationId!.Value; progress.Entity = "purchasereturn"; var m = await LoadMapsAsync(orgId, ct); var done = (await _db.SupplierReturns.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet(); var used = (await _db.SupplierReturns.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase); string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } } - await foreach (var d in _client.StreamDocumentsAsync(token, "entity/purchasereturn", "positions,agent,store", 0, null, ct)) + await foreach (var d in _client.StreamDocumentsAsync(token, "entity/purchasereturn", "positions,agent,store", startOffset, null, ct)) { progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; } try @@ -284,14 +284,14 @@ await foreach (var d in _client.StreamDocumentsAsync(token, "entity/ } // ── Розничные продажи (retaildemand) ───────────────────────────────────── - public async Task ImportRetailSalesAsync(string token, DocImportProgress progress, CancellationToken ct) + public async Task ImportRetailSalesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct) { var orgId = _tenant.OrganizationId!.Value; progress.Entity = "retaildemand"; var m = await LoadMapsAsync(orgId, ct); var done = (await _db.RetailSales.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet(); var used = (await _db.RetailSales.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase); string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } } - await foreach (var d in _client.StreamDocumentsAsync(token, "entity/retaildemand", "positions,store", 0, null, ct)) + await foreach (var d in _client.StreamDocumentsAsync(token, "entity/retaildemand", "positions,store", startOffset, null, ct)) { progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; } try