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>
42 lines
1.3 KiB
C#
42 lines
1.3 KiB
C#
using foodmarket.Infrastructure.Integrations.MoySklad;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace foodmarket.Api.Controllers.Admin;
|
|
|
|
[ApiController]
|
|
[Authorize(Policy = "AdminAccess")]
|
|
[Route("api/admin/jobs")]
|
|
public class AdminJobsController : ControllerBase
|
|
{
|
|
private readonly ImportJobRegistry _jobs;
|
|
public AdminJobsController(ImportJobRegistry jobs) => _jobs = jobs;
|
|
|
|
public record JobView(
|
|
Guid Id,
|
|
string Kind,
|
|
string Status,
|
|
string? Stage,
|
|
DateTime StartedAt,
|
|
DateTime? FinishedAt,
|
|
int Total, int Created, int Updated, int Skipped, int Deleted, int GroupsCreated,
|
|
string? Message,
|
|
IReadOnlyList<string> Errors);
|
|
|
|
private static JobView Project(ImportJobProgress j) => new(
|
|
j.Id, j.Kind, j.Status.ToString(), j.Stage, j.StartedAt, j.FinishedAt,
|
|
j.Total, j.Created, j.Updated, j.Skipped, j.Deleted, j.GroupsCreated,
|
|
j.Message, j.Errors.TakeLast(20).ToList());
|
|
|
|
[HttpGet("{id:guid}")]
|
|
public ActionResult<JobView> Get(Guid id)
|
|
{
|
|
var j = _jobs.Get(id);
|
|
return j is null ? NotFound() : Project(j);
|
|
}
|
|
|
|
[HttpGet("recent")]
|
|
public IReadOnlyList<JobView> Recent([FromQuery] int take = 10)
|
|
=> _jobs.RecentlyFinished(take).Select(Project).ToList();
|
|
}
|