Pain points:
1. Импорт на ~30k товарах проходит 15-30 мин, nginx рвал на 60s → 504.
2. При импорте/очистке ничего не видно — ни счётчика, ни прогресса.
3. Токен приходилось вводить каждый раз вручную.
Фиксы:
- Async-job pattern: POST /api/admin/other-system/import-products и
/api/admin/cleanup/all/async возвращают jobId, реальная работа
в Task.Run. GET /api/admin/jobs/{id} — статус +
Total/Created/Updated/Skipped/Deleted/Stage/Message.
- ImportJobRegistry (singleton, in-memory) — хранит job-progress.
- OtherSystemImportService обновляет progress по мере пейджинга
(в т.ч. счётчик Created/Updated/Skipped).
- Cleanup разбит на именованные шаги, Stage меняется по мере
"Товары…" → "Группы…" → "Контрагенты…".
- Токен per-organization: Organization.OtherSystemToken + миграция
Phase3_OrganizationOtherSystemToken. Endpoints:
GET/PUT /api/admin/other-system/settings.
- Импорт-endpoints больше не требуют token в теле — берут из org.
- HttpContextTenantContext.UseOverride(orgId) — AsyncLocal-scope
для background tasks (HttpContext там нет, а query-filter'у нужен
orgId — ставим через override).
Nginx (host + web-container) получил 60-минутный timeout на
/api/admin/import/ чтобы старый sync-путь тоже не ронять (на
случай если кто-то вернёт sync call).
Web:
- OtherSystemImportPage переработан: блок "Токен API" (save/test
mask), блок импорта с кнопками без поля токена.
- JobCard с polling каждые 1.5s отображает живые счётчики и stage.
- DangerZone тоже теперь async с live-прогрессом.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
2.2 KiB
C#
48 lines
2.2 KiB
C#
using System.Collections.Concurrent;
|
||
|
||
namespace foodmarket.Infrastructure.Integrations.MoySklad;
|
||
|
||
public enum ImportJobStatus { Running, Succeeded, Failed, Cancelled }
|
||
|
||
public class ImportJobProgress
|
||
{
|
||
public Guid Id { get; init; } = Guid.NewGuid();
|
||
public string Kind { get; init; } = ""; // "products" | "counterparties"
|
||
public DateTime StartedAt { get; init; } = DateTime.UtcNow;
|
||
public DateTime? FinishedAt { get; set; }
|
||
public ImportJobStatus Status { get; set; } = ImportJobStatus.Running;
|
||
public string? Stage { get; set; } // человекочитаемое описание текущего шага
|
||
public int Total { get; set; } // входящих записей от MS (растёт по мере пейджинга)
|
||
public int Created { get; set; }
|
||
public int Updated { get; set; }
|
||
public int Skipped { get; set; }
|
||
public int Deleted { get; set; } // для cleanup
|
||
public int GroupsCreated { get; set; }
|
||
public string? Message { get; set; } // последняя ошибка / финальное сообщение
|
||
public List<string> Errors { get; set; } = [];
|
||
}
|
||
|
||
// Process-memory реестр прогресса фоновых импортов. Один процесс API — одно DI singleton.
|
||
// При рестарте контейнера история импортов теряется — для просмотра «вчерашнего» надо
|
||
// смотреть логи. На MVP достаточно.
|
||
public class ImportJobRegistry
|
||
{
|
||
private readonly ConcurrentDictionary<Guid, ImportJobProgress> _jobs = new();
|
||
|
||
public ImportJobProgress Create(string kind)
|
||
{
|
||
var job = new ImportJobProgress { Kind = kind };
|
||
_jobs[job.Id] = job;
|
||
return job;
|
||
}
|
||
|
||
public ImportJobProgress? Get(Guid id) => _jobs.TryGetValue(id, out var j) ? j : null;
|
||
|
||
public IReadOnlyList<ImportJobProgress> RecentlyFinished(int take = 10) =>
|
||
_jobs.Values
|
||
.Where(j => j.FinishedAt is not null)
|
||
.OrderByDescending(j => j.FinishedAt)
|
||
.Take(take)
|
||
.ToList();
|
||
}
|