feat(s30): PruneAutoTestEntities + одноразовая зачистка стейджа (шаг 7/9)
HousekeepingJobs.PruneAutoTestEntitiesAsync — Hangfire-recurring (02:45 UTC
daily, Cron:PruneAutoTestEntities). Удаляет:
- orgs по regex autotest-паттерна в имени (quality-, soak-, UI-a11y-,
SmokeOrgA/B-, PerfBaseline-, Reg-, RT-ratetest-, Probe Org, cat3*,
visual-, factory-smoke-, mt6*, doc4*, auth2*, rep5*, rt8*, wiz9*,
i18n*, twofa*, iso*, mob1*, axdet-, load-, s19-test- + любое имя
с [0-9]{10,}) старше 3 дней + все их таблицы с OrganizationId;
- orphan autotest-юзеров (без удалённой org) по regex в email и доменам
@e.kz / @test-fm.local.
Защищены: SuperAdmin (OrganizationId IS NULL), admin@food-market.local,
admin@demo-market.local, demo-*@food-market.local, market@email.com,
«Demo Market» и «Демо-магазин» — явные исключения.
deploy/stage-prune-autotest.sql — ровно та же логика как SQL-скрипт для
ручного запуска. Прогнал на stage: было 8653 user'ов / 8634 orgs →
стало 300 users / 305 orgs. SuperAdmin/demo/market остались.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5728b729a8
commit
c69114f8ab
174
deploy/stage-prune-autotest.sql
Normal file
174
deploy/stage-prune-autotest.sql
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
-- deploy/stage-prune-autotest.sql
|
||||
-- Одноразовая зачистка стейджа от автотестового мусора. Пишется как idempotent
|
||||
-- SQL-скрипт — его же логика живёт в HousekeepingJobs.PruneAutoTestEntitiesAsync
|
||||
-- (Hangfire 02:45 UTC daily). Этот файл пригоден для ручного запуска
|
||||
-- (например, на prod-vm через `docker exec food-market-stage-postgres-1 psql
|
||||
-- -U food_market -d food_market -f /tmp/stage-prune-autotest.sql`).
|
||||
--
|
||||
-- Что НЕ трогает:
|
||||
-- * admin@food-market.local (SuperAdmin — OrganizationId IS NULL)
|
||||
-- * admin@demo-market.local
|
||||
-- * market@email.com
|
||||
-- * demo-*@food-market.local (quick-login demo-trio)
|
||||
-- * «Demo Market» и «Демо-магазин» (seeded orgs)
|
||||
--
|
||||
-- Условие удаления: имя орги подходит под регулярку autotest ИЛИ email юзера
|
||||
-- подходит под autotest-патéрн / служебный домен. Для свежих прогонов —
|
||||
-- threshold 3 дня (меняется через psql -v).
|
||||
|
||||
\set threshold_days 3
|
||||
|
||||
DO $do$
|
||||
DECLARE
|
||||
threshold_ts timestamp with time zone := NOW() - INTERVAL '3 days';
|
||||
org_ids uuid[];
|
||||
tbl text;
|
||||
pass int := 0;
|
||||
remaining int;
|
||||
autotest_org_rx text := '[0-9]{10,}' ||
|
||||
'|^quality-|^soak-|^(ui-)?a11y-|^smokeorg[ab]?-|^perfbaseline-|^reg-' ||
|
||||
'|^rt-ratetest-|^probe\s|^cat3[0-9]-|^visual-|^factory-smoke-|^mt6[0-9]-' ||
|
||||
'|^doc4[0-9]-|^auth2[0-9]-|^rep5[0-9]-|^rt8[0-9]-|^wiz9[0-9]-' ||
|
||||
'|^i18n[0-9]+-|^twofa[0-9]+-|^iso[a-z]*[0-9]*-|^mob1[0-9]-|^axdet-' ||
|
||||
'|^load-sales-|^load-|^s19-test-';
|
||||
autotest_user_rx text := '^(cat3[0-9]|visual|mt6[0-9]|factory|doc4[0-9]|auth2[0-9]|rep5[0-9]|rt8[0-9]|wiz9[0-9]|quality|i18n[0-9]+|twofa[0-9]+|a11y|mob1[0-9]|iso|axdet|smoke|ratetest|perf|probe|load|s19-test|admin-[0-9]|cashier-[0-9]|keeper-[0-9]|reg-mt)';
|
||||
autotest_domain_rx text := '(@test-fm\.local|@e\.kz)$';
|
||||
deleted_orgs int;
|
||||
deleted_users int;
|
||||
BEGIN
|
||||
SELECT array_agg("Id") INTO org_ids FROM public.organizations
|
||||
WHERE "Name" <> 'Demo Market'
|
||||
AND "Name" <> 'Демо-магазин'
|
||||
AND "Name" ~* autotest_org_rx
|
||||
AND "CreatedAt" < threshold_ts;
|
||||
|
||||
IF array_length(org_ids, 1) IS NOT NULL THEN
|
||||
RAISE NOTICE 'Нашли % autotest-органов для удаления', array_length(org_ids, 1);
|
||||
|
||||
FOR pass IN 1..10 LOOP
|
||||
remaining := 0;
|
||||
FOR tbl IN
|
||||
SELECT c.table_name
|
||||
FROM information_schema.columns c
|
||||
JOIN information_schema.tables t
|
||||
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
||||
WHERE c.table_schema = 'public'
|
||||
AND c.column_name = 'OrganizationId'
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
AND c.table_name NOT IN ('organizations', 'users')
|
||||
LOOP
|
||||
BEGIN
|
||||
EXECUTE format('DELETE FROM public.%I WHERE "OrganizationId" = ANY($1)', tbl) USING org_ids;
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
remaining := remaining + 1;
|
||||
END;
|
||||
END LOOP;
|
||||
EXIT WHEN remaining = 0;
|
||||
END LOOP;
|
||||
|
||||
DELETE FROM "AspNetUserRoles" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM "AspNetUserTokens" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM "AspNetUserClaims" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM "AspNetUserLogins" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM "OpenIddictTokens" WHERE "Subject" IN (
|
||||
SELECT u."Id"::text FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM "OpenIddictAuthorizations" WHERE "Subject" IN (
|
||||
SELECT u."Id"::text FROM public.users u WHERE u."OrganizationId" = ANY(org_ids)
|
||||
);
|
||||
|
||||
GET DIAGNOSTICS deleted_users = ROW_COUNT;
|
||||
DELETE FROM public.users WHERE "OrganizationId" = ANY(org_ids);
|
||||
DELETE FROM public.organizations WHERE "Id" = ANY(org_ids);
|
||||
GET DIAGNOSTICS deleted_orgs = ROW_COUNT;
|
||||
RAISE NOTICE 'Удалено orgs=%, users с org=%', deleted_orgs, deleted_users;
|
||||
ELSE
|
||||
RAISE NOTICE 'Нет autotest-органов старше %', threshold_ts;
|
||||
END IF;
|
||||
|
||||
-- Orphan autotest-юзеры (без tagged org). Исключения — такие же как в
|
||||
-- Hangfire-методе: SuperAdmin, demo-trio, admin@demo-market.local, market@.
|
||||
DELETE FROM "AspNetUserRoles" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM "AspNetUserTokens" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM "AspNetUserClaims" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM "AspNetUserLogins" WHERE "UserId" IN (
|
||||
SELECT u."Id" FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM "OpenIddictTokens" WHERE "Subject" IN (
|
||||
SELECT u."Id"::text FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM "OpenIddictAuthorizations" WHERE "Subject" IN (
|
||||
SELECT u."Id"::text FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts
|
||||
);
|
||||
DELETE FROM public.users u
|
||||
WHERE (u."Email" ~* autotest_user_rx OR u."Email" ~* autotest_domain_rx OR u."Email" ~ '[0-9]{10,}')
|
||||
AND u."Email" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u."Email" <> 'admin@food-market.local'
|
||||
AND u."Email" <> 'admin@demo-market.local'
|
||||
AND u."Email" <> 'market@email.com'
|
||||
AND u."OrganizationId" IS NOT NULL
|
||||
AND u."CreatedAt" < threshold_ts;
|
||||
GET DIAGNOSTICS deleted_users = ROW_COUNT;
|
||||
RAISE NOTICE 'Удалено orphan autotest-юзеров: %', deleted_users;
|
||||
END $do$;
|
||||
|
||||
-- Итог
|
||||
SELECT 'users_remaining' AS metric, COUNT(*) AS value FROM users
|
||||
UNION ALL
|
||||
SELECT 'orgs_remaining', COUNT(*) FROM organizations;
|
||||
|
|
@ -33,7 +33,9 @@
|
|||
Клик — моментальный логин через мастер-пароль. На проде блока нет.
|
||||
- [x] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса
|
||||
сверху + `<meta robots="noindex,nofollow">` в index.html стенда.
|
||||
- [ ] 7. **Чистка автотестового мусора** — SQL-скрипт для стейджа + расширение
|
||||
- [x] 7. **Чистка автотестового мусора** — 8653 → 300 user'ов, 8634 → 305 org
|
||||
(стейдж, 2026-10-04). Протестирован на существующей БД: SuperAdmin / admin@
|
||||
demo-market.local / market@email.com остались, свежие прогоны (<3д) сохранены. SQL-скрипт для стейджа + расширение
|
||||
`HousekeepingJobs.PruneQualityTestOrgsAsync` на новые autotest-патéрны,
|
||||
чтобы cron 02:30 UTC гасил новые юзеры сам. Не трогать: SuperAdmin,
|
||||
admin@demo-market.local, market@email.com, demo-* аккаунты.
|
||||
|
|
|
|||
|
|
@ -72,6 +72,16 @@ public Task StartAsync(CancellationToken ct)
|
|||
cronExpression: cronQualityOrgs,
|
||||
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
||||
|
||||
// Sprint 30: широкий cleanup всех autotest-органа/юзеров (e2e, perf,
|
||||
// smoke, a11y, ratetest, mt6*, cat3*, ...). Default 02:45 UTC
|
||||
// (между prune-quality 02:30 и prune-stock 03:30).
|
||||
var cronAutoTest = _cfg["Hangfire:Cron:PruneAutoTestEntities"] ?? "45 2 * * *";
|
||||
_jobs.AddOrUpdate<HousekeepingJobs>(
|
||||
recurringJobId: "prune-autotest-entities",
|
||||
methodCall: j => j.PruneAutoTestEntitiesAsync(CancellationToken.None),
|
||||
cronExpression: cronAutoTest,
|
||||
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
||||
|
||||
// Sprint 20: weekly VACUUM ANALYZE топ-5 таблиц + ежечасный disk-monitor.
|
||||
var cronVacuum = _cfg["Hangfire:Cron:VacuumTopTables"] ?? "0 4 * * 0"; // Воскресенье 04:00 UTC
|
||||
var cronDisk = _cfg["Hangfire:Cron:DiskMonitor"] ?? "0 * * * *"; // каждый час
|
||||
|
|
|
|||
|
|
@ -254,4 +254,228 @@ SELECT c.table_name
|
|||
ids.Count, minHours);
|
||||
return ids.Count;
|
||||
}
|
||||
|
||||
/// <summary>Sprint 30: расширение cleanup'а на ВСЕ автотестовые орги/юзеры
|
||||
/// (не только <c>quality-*</c>): e2e, perf, smoke, a11y, ratetest, mt6*,
|
||||
/// cat3*, visual-*, factory-smoke-*, auth2*, doc4*, rep5*, rt8*, wiz9*,
|
||||
/// i18n*, twofa*, iso*, mob14*, mob15*, axdet-*, probe-*, load-*,
|
||||
/// s19-test-* и всё с суффиксом из ≥10 цифр.
|
||||
///
|
||||
/// Запускается тем же механизмом (Hangfire recurring) что
|
||||
/// <see cref="PruneQualityTestOrgsAsync"/>. Порог давности
|
||||
/// <c>Cleanup:AutoTestOrgDays</c> (default 3) — чтобы живые e2e-прогоны
|
||||
/// успевали создать-использовать org и завершиться.
|
||||
///
|
||||
/// Защищены от удаления:
|
||||
/// <list type="bullet">
|
||||
/// <item>Demo Market и Демо-магазин (seeded) + любые орги с именем
|
||||
/// ровно <c>Demo Market</c> / <c>Демо-магазин</c>;</item>
|
||||
/// <item>demo-admin@/demo-cashier@/demo-keeper@ юзеры;</item>
|
||||
/// <item>SuperAdmin (без OrganizationId) — не удаляется ни при каких
|
||||
/// условиях (условие OrganizationId IS NOT NULL в юзер-DELETE);</item>
|
||||
/// <item>market@email.com — явное исключение по email.</item>
|
||||
/// </list></summary>
|
||||
public async Task<int> PruneAutoTestEntitiesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var minDays = _cfg.GetValue("Cleanup:AutoTestOrgDays", 3);
|
||||
var threshold = DateTime.UtcNow.AddDays(-minDays);
|
||||
|
||||
// Regex в PG: '~*' = case-insensitive match. Собираем выражение по
|
||||
// «автотестовым» префиксам/признакам, аналогично QuickLoginController.
|
||||
// Условие на имя орги. ВАЖНО: даже «просто цифры в конце» ловит почти
|
||||
// любой e2e-прогон, который суффиксит имя timestamp'ом.
|
||||
const string autoTestOrgRx =
|
||||
@"[0-9]{10,}" + // любой длинный цифровой суффикс
|
||||
@"|^quality-" +
|
||||
@"|^soak-" +
|
||||
@"|^(ui-)?a11y-" +
|
||||
@"|^smokeorg[ab]?-" +
|
||||
@"|^perfbaseline-" +
|
||||
@"|^reg-" +
|
||||
@"|^rt-ratetest-" +
|
||||
@"|^probe\s" + // "Probe Org"
|
||||
@"|^cat3[0-9]-" +
|
||||
@"|^visual-" +
|
||||
@"|^factory-smoke-" +
|
||||
@"|^mt6[0-9]-" +
|
||||
@"|^doc4[0-9]-" +
|
||||
@"|^auth2[0-9]-" +
|
||||
@"|^rep5[0-9]-" +
|
||||
@"|^rt8[0-9]-" +
|
||||
@"|^wiz9[0-9]-" +
|
||||
@"|^i18n[0-9]+-" +
|
||||
@"|^twofa[0-9]+-" +
|
||||
@"|^iso[a-z]*[0-9]*-" +
|
||||
@"|^mob1[0-9]-" +
|
||||
@"|^axdet-" +
|
||||
@"|^load-sales-" +
|
||||
@"|^load-" +
|
||||
@"|^s19-test-";
|
||||
|
||||
// SqlQueryRaw<Guid> — удобный способ вытащить только Id без mapping'а
|
||||
// к entity. Параметры через FormattableString небезопасно соединять
|
||||
// (regex из конфига → потенциальный inject), поэтому используем Npgsql-
|
||||
// параметры напрямую.
|
||||
var orgIdSql = @"
|
||||
SELECT ""Id"" FROM public.organizations
|
||||
WHERE ""Name"" <> 'Demo Market'
|
||||
AND ""Name"" <> 'Демо-магазин'
|
||||
AND ""Name"" ~* @rx
|
||||
AND ""CreatedAt"" < @thr
|
||||
LIMIT 20000";
|
||||
var ids = await _db.Database.SqlQueryRaw<Guid>(orgIdSql,
|
||||
new Npgsql.NpgsqlParameter("@rx", autoTestOrgRx),
|
||||
new Npgsql.NpgsqlParameter("@thr", threshold))
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
_log.LogInformation("Hangfire/PruneAutoTestEntities: нет org-кандидатов старше {Days}д", minDays);
|
||||
// Юзеры без org (orphan-autotest) всё равно могут остаться — обрабатываем ниже.
|
||||
}
|
||||
|
||||
// Тот же трюк с итеративным DELETE по information_schema, что в
|
||||
// PruneQualityTestOrgsAsync. Юзеры дополнительно вычищаются по
|
||||
// регулярке email'а (широкий set autotest-паттернов).
|
||||
var sql = $@"
|
||||
DO $do$
|
||||
DECLARE
|
||||
org_ids uuid[] := ARRAY[{(ids.Count == 0 ? "NULL::uuid" : string.Join(",", ids.Select(i => $"'{i}'::uuid")))}];
|
||||
tbl text;
|
||||
pass int := 0;
|
||||
remaining int;
|
||||
autotest_user_rx text := '^(cat3[0-9]|visual|mt6[0-9]|factory|doc4[0-9]|auth2[0-9]|rep5[0-9]|rt8[0-9]|wiz9[0-9]|quality|i18n[0-9]+|twofa[0-9]+|a11y|mob1[0-9]|iso|axdet|smoke|ratetest|perf|probe|load|s19-test|admin-[0-9]|cashier-[0-9]|keeper-[0-9]|reg-mt)';
|
||||
autotest_domain_rx text := '(@test-fm\.local|@e\.kz)$';
|
||||
threshold_ts timestamp with time zone := '{threshold:O}'::timestamp with time zone;
|
||||
BEGIN
|
||||
IF array_length(org_ids, 1) IS NOT NULL AND org_ids[1] IS NOT NULL THEN
|
||||
-- 1. Итеративная очистка tenant-таблиц.
|
||||
FOR pass IN 1..10 LOOP
|
||||
remaining := 0;
|
||||
FOR tbl IN
|
||||
SELECT c.table_name
|
||||
FROM information_schema.columns c
|
||||
JOIN information_schema.tables t
|
||||
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
||||
WHERE c.table_schema = 'public'
|
||||
AND c.column_name = 'OrganizationId'
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
AND c.table_name NOT IN ('organizations', 'users')
|
||||
LOOP
|
||||
BEGIN
|
||||
EXECUTE format('DELETE FROM public.%I WHERE ""OrganizationId"" = ANY($1)', tbl) USING org_ids;
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
remaining := remaining + 1;
|
||||
END;
|
||||
END LOOP;
|
||||
EXIT WHEN remaining = 0;
|
||||
END LOOP;
|
||||
|
||||
-- 2. AspNet* / OpenIddict для юзеров этих org.
|
||||
DELETE FROM ""AspNetUserRoles"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM ""AspNetUserTokens"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM ""AspNetUserClaims"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM ""AspNetUserLogins"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM ""OpenIddictTokens"" WHERE ""Subject"" IN (
|
||||
SELECT u.""Id""::text FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
DELETE FROM ""OpenIddictAuthorizations"" WHERE ""Subject"" IN (
|
||||
SELECT u.""Id""::text FROM public.users u WHERE u.""OrganizationId"" = ANY(org_ids)
|
||||
);
|
||||
|
||||
-- 3. Юзеры этих org.
|
||||
DELETE FROM public.users WHERE ""OrganizationId"" = ANY(org_ids);
|
||||
|
||||
-- 4. Сами organizations.
|
||||
DELETE FROM public.organizations WHERE ""Id"" = ANY(org_ids);
|
||||
END IF;
|
||||
|
||||
-- 5. Orphan-юзеры: autotest-паттерн email + не SuperAdmin (OrganizationId
|
||||
-- IS NOT NULL) + не demo-* + не market@email.com + создан раньше threshold.
|
||||
-- Эти юзеры могут остаться когда org уже удалена, но AspNetUserRoles —
|
||||
-- нет (если был cascade NOT), либо если тест не создал org вообще.
|
||||
DELETE FROM ""AspNetUserRoles"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM ""AspNetUserTokens"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM ""AspNetUserClaims"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM ""AspNetUserLogins"" WHERE ""UserId"" IN (
|
||||
SELECT u.""Id"" FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM ""OpenIddictTokens"" WHERE ""Subject"" IN (
|
||||
SELECT u.""Id""::text FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM ""OpenIddictAuthorizations"" WHERE ""Subject"" IN (
|
||||
SELECT u.""Id""::text FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts
|
||||
);
|
||||
DELETE FROM public.users u
|
||||
WHERE (u.""Email"" ~* autotest_user_rx OR u.""Email"" ~* autotest_domain_rx OR u.""Email"" ~ '[0-9]{{10,}}')
|
||||
AND u.""Email"" NOT LIKE 'demo-%@food-market.local'
|
||||
AND u.""Email"" <> 'admin@food-market.local'
|
||||
AND u.""Email"" <> 'admin@demo-market.local'
|
||||
AND u.""Email"" <> 'market@email.com'
|
||||
AND u.""OrganizationId"" IS NOT NULL
|
||||
AND u.""CreatedAt"" < threshold_ts;
|
||||
END $do$;
|
||||
";
|
||||
|
||||
await _db.Database.ExecuteSqlRawAsync(sql, ct);
|
||||
_log.LogInformation("Hangfire/PruneAutoTestEntities: удалено {Count} org (старше {Days}д) + orphan autotest-юзеры",
|
||||
ids.Count, minDays);
|
||||
return ids.Count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue