fix(s30): QuickLoginDemoSeeder — свежий scope на каждый sub-step
Разбил монолитный SeedAsync на 4 метода, каждый со своим DI-scope и DbContext'ом: 1) EnsureDemoOrgAsync — org Демо-магазин; 2) EnsureTenantReferencesAsync — единицы/магазин/роли через DevDataSeeder.SeedTenantReferencesAsync; 3) TryFillCatalogAsync — DemoTenantSeeder (товары/приёмки/продажи); 4) EnsureDemoUsersAsync — трио демо-юзеров. Корень проблемы на стенде: DemoTenantSeeder падает с «Tenant not set» (при старте нет HTTP-контекста → ITenantContext.OrganizationId пуст → StockService.ApplyMovementAsync бросает). Если переиспользовать один scope, следующий UserManager.CreateAsync возвращает DbUpdateConcurrencyException — DbContext транзакционно сломан. Теперь: catalog-step изолирован и его падение не отравляет создание юзеров. Catalog нужен только для показательного UI — он опциональный, с явным логом «ОК — создать можно через POST /api/admin/seed-demo после логина». Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e3a8db3206
commit
f817abc8c7
|
|
@ -70,43 +70,65 @@ public async Task StartAsync(CancellationToken ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SeedAsync(CancellationToken ct)
|
private async Task SeedAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Каждый sub-step использует СВОЙ scope (и, значит, свой DbContext).
|
||||||
|
// Это важно: DemoTenantSeeder может упасть с «Tenant not set»
|
||||||
|
// (нет HTTP-контекста при старте приложения), оставив DbContext в
|
||||||
|
// транзакционно-битом состоянии. Если бы мы переиспользовали один
|
||||||
|
// scope, следующие UserManager.CreateAsync возвращали бы
|
||||||
|
// DbUpdateConcurrencyException. Свежий scope = свежая транзакция.
|
||||||
|
var demoOrgId = await EnsureDemoOrgAsync(ct);
|
||||||
|
|
||||||
|
await EnsureTenantReferencesAsync(demoOrgId, ct);
|
||||||
|
|
||||||
|
await TryFillCatalogAsync(demoOrgId, ct);
|
||||||
|
|
||||||
|
await EnsureDemoUsersAsync(demoOrgId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Guid> EnsureDemoOrgAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var scope = _services.CreateScope();
|
using var scope = _services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
|
||||||
|
|
||||||
// 1. «Демо-магазин» (orgEntity). Отдельная org, в отличие от «Demo Market»
|
|
||||||
// из DevDataSeeder — чтобы не трогать e2e, которые завязаны на Demo Market.
|
|
||||||
var demoOrg = await db.Organizations.IgnoreQueryFilters()
|
var demoOrg = await db.Organizations.IgnoreQueryFilters()
|
||||||
.FirstOrDefaultAsync(o => o.Name == OrgName, ct);
|
.FirstOrDefaultAsync(o => o.Name == OrgName, ct);
|
||||||
|
if (demoOrg is not null) return demoOrg.Id;
|
||||||
|
|
||||||
var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
|
var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
|
||||||
if (demoOrg is null)
|
demoOrg = new Organization
|
||||||
{
|
{
|
||||||
demoOrg = new Organization
|
Name = OrgName,
|
||||||
{
|
CountryCode = "KZ",
|
||||||
Name = OrgName,
|
Bin = "000000000001",
|
||||||
CountryCode = "KZ",
|
Address = "Алматы, ул. Демо-стенд 1",
|
||||||
Bin = "000000000001",
|
Phone = "+7 (727) 000-00-01",
|
||||||
Address = "Алматы, ул. Демо-стенд 1",
|
Email = "demo@food-market.local",
|
||||||
Phone = "+7 (727) 000-00-01",
|
DefaultCurrencyId = kzt?.Id,
|
||||||
Email = "demo@food-market.local",
|
};
|
||||||
DefaultCurrencyId = kzt?.Id,
|
db.Organizations.Add(demoOrg);
|
||||||
};
|
await db.SaveChangesAsync(ct);
|
||||||
db.Organizations.Add(demoOrg);
|
_log.LogInformation("QuickLoginDemoSeeder: создана организация {OrgName}", OrgName);
|
||||||
await db.SaveChangesAsync(ct);
|
return demoOrg.Id;
|
||||||
_log.LogInformation("QuickLoginDemoSeeder: создана организация {OrgName}", OrgName);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Tenant references (единицы, магазин, PriceType, роли).
|
private async Task EnsureTenantReferencesAsync(Guid orgId, CancellationToken ct)
|
||||||
// SeedTenantReferencesAsync идемпотентен сам по себе.
|
{
|
||||||
await DevDataSeeder.SeedTenantReferencesAsync(db, demoOrg.Id, ct);
|
using var scope = _services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
await DevDataSeeder.SeedTenantReferencesAsync(db, orgId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Каталог демо-товаров (50 товаров, 10 контрагентов, ...).
|
private async Task TryFillCatalogAsync(Guid orgId, CancellationToken ct)
|
||||||
// Отдельный DemoTenantSeeder — scoped-сервис, уже зарегистрирован в DI.
|
{
|
||||||
var catalogSeeder = scope.ServiceProvider.GetRequiredService<DemoTenantSeeder>();
|
// DemoTenantSeeder требует ITenantContext с orgId — при старте без
|
||||||
|
// HTTP-запроса этот контекст пуст, StockService.ApplyMovementAsync
|
||||||
|
// падает с «Tenant not set». Поэтому ловим и игнорируем: катало́г —
|
||||||
|
// nice-to-have, демо-trio юзеров всё равно создастся отдельно.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var summary = await catalogSeeder.SeedAsync(demoOrg.Id, ct);
|
using var scope = _services.CreateScope();
|
||||||
|
var catalogSeeder = scope.ServiceProvider.GetRequiredService<DemoTenantSeeder>();
|
||||||
|
var summary = await catalogSeeder.SeedAsync(orgId, ct);
|
||||||
if (!summary.AlreadySeeded)
|
if (!summary.AlreadySeeded)
|
||||||
{
|
{
|
||||||
_log.LogInformation("QuickLoginDemoSeeder: каталог демо наполнен — товаров {Products}, продаж {Sales}",
|
_log.LogInformation("QuickLoginDemoSeeder: каталог демо наполнен — товаров {Products}, продаж {Sales}",
|
||||||
|
|
@ -115,25 +137,29 @@ private async Task SeedAsync(CancellationToken ct)
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Не фатально: демо-данные для стенда — ценные, но не критичные.
|
_log.LogWarning(ex, "QuickLoginDemoSeeder: демо-каталог не наполнился " +
|
||||||
// Если упал seed — всё равно продолжаем создавать юзеров.
|
"(это ОК — создать его можно из UI через POST /api/admin/seed-demo после логина Admin'ом).");
|
||||||
_log.LogWarning(ex, "QuickLoginDemoSeeder: демо-каталог не наполнился");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureDemoUsersAsync(Guid orgId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var scope = _services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
||||||
|
|
||||||
// 4. Трио сотрудников. Пароль у них — обычный Admin12345! для совместимости,
|
|
||||||
// но мастер-пароль 123456 всё равно пустит (см. AuthorizationController).
|
|
||||||
var adminRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
var adminRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
||||||
.FirstAsync(r => r.OrganizationId == demoOrg.Id && r.IsSystem && r.Name == "Администратор", ct);
|
.FirstAsync(r => r.OrganizationId == orgId && r.IsSystem && r.Name == "Администратор", ct);
|
||||||
var cashierRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
var cashierRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
||||||
.FirstAsync(r => r.OrganizationId == demoOrg.Id && r.IsSystem && r.Name == "Кассир", ct);
|
.FirstAsync(r => r.OrganizationId == orgId && r.IsSystem && r.Name == "Кассир", ct);
|
||||||
var keeperRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
var keeperRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
||||||
.FirstAsync(r => r.OrganizationId == demoOrg.Id && r.IsSystem && r.Name == "Кладовщик", ct);
|
.FirstAsync(r => r.OrganizationId == orgId && r.IsSystem && r.Name == "Кладовщик", ct);
|
||||||
|
|
||||||
await EnsureUserAsync(db, userMgr, demoOrg.Id, DemoAdminEmail, "Демо Админ Магазина",
|
await EnsureUserAsync(db, userMgr, orgId, DemoAdminEmail, "Демо Админ Магазина",
|
||||||
SystemRoles.Admin, adminRole.Id, "Администратор", ct);
|
SystemRoles.Admin, adminRole.Id, "Администратор", ct);
|
||||||
await EnsureUserAsync(db, userMgr, demoOrg.Id, DemoCashierEmail, "Демо Кассир",
|
await EnsureUserAsync(db, userMgr, orgId, DemoCashierEmail, "Демо Кассир",
|
||||||
SystemRoles.Cashier, cashierRole.Id, "Кассир", ct);
|
SystemRoles.Cashier, cashierRole.Id, "Кассир", ct);
|
||||||
await EnsureUserAsync(db, userMgr, demoOrg.Id, DemoKeeperEmail, "Демо Кладовщик",
|
await EnsureUserAsync(db, userMgr, orgId, DemoKeeperEmail, "Демо Кладовщик",
|
||||||
SystemRoles.Storekeeper, keeperRole.Id, "Кладовщик", ct);
|
SystemRoles.Storekeeper, keeperRole.Id, "Кладовщик", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue