diff --git a/app/database/migrations/2026_07_13_100000_create_site_visitors_and_events.php b/app/database/migrations/2026_07_13_100000_create_site_visitors_and_events.php index 7b87d92f..ffc62c99 100644 --- a/app/database/migrations/2026_07_13_100000_create_site_visitors_and_events.php +++ b/app/database/migrations/2026_07_13_100000_create_site_visitors_and_events.php @@ -1,6 +1,7 @@ statement('CREATE INDEX IF NOT EXISTS idx_site_events_visitor ON site_events (visitor_id, occurred_at DESC)'); $c->statement('CREATE INDEX IF NOT EXISTS idx_site_events_event ON site_events (event, occurred_at DESC)'); - // Партиция текущего месяца — чтобы INSERT работал сразу после миграции. - // Остальные нарезает partitions:create-months (--behind=1 закрывает прошлый месяц). - $start = now()->startOfMonth()->format('Y-m-d'); - $end = now()->startOfMonth()->addMonth()->format('Y-m-d'); - $name = 'site_events_'.now()->format('Y_m'); - $c->statement("CREATE TABLE IF NOT EXISTS {$name} PARTITION OF site_events - FOR VALUES FROM ('{$start}') TO ('{$end}')"); + // Партиции нарезает ТОЛЬКО MonthlyPartitionManager — у него UTC-границы и своё + // именование (site_events_yYYYY_mMM). Своя «ручная» партиция здесь дала бы + // перекрытие с ним (42P17) и уронила бы чужие тесты. Прошлый и текущий месяц — + // сразу, чтобы INSERT работал с первой секунды после миграции. + $partitions = app(MonthlyPartitionManager::class); + $partitions->ensureRange('site_events', now()->startOfMonth()->subMonth(), now()->startOfMonth()->addMonths(2)); // Пишет supplier-worker, читают админ-роли. crm_app_user — НЕТ. foreach (['crm_supplier_worker'] as $role) { diff --git a/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php b/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php index 43d212e4..96223793 100644 --- a/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php +++ b/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php @@ -59,7 +59,7 @@ it('supplier_csv_reconcile_log table exists with required columns and status CHE ]))->toThrow(QueryException::class); }); -it('schema.sql v8.62 has correct metrics — 78 base tables, 137 indexes, 48 RLS policies', function () { +it('schema.sql v8.64 has correct metrics — 80 base tables, 141 indexes, 48 RLS policies', function () { // Замена destructive `migrate:fresh` (cross-test coupling: после DROP CASCADE остальные // Feature-тесты в той же сессии видели пустую БД). Static parse `db/schema.sql` — // источник истины метрик. @@ -81,6 +81,10 @@ it('schema.sql v8.62 has correct metrics — 78 base tables, 137 indexes, 48 RLS // (autopodbor_runs/competitors/sources/merge_events) + 9 индексов (вкл. 2 UNIQUE dedup) + // 4 RLS-политики tenant_isolation. Итог статического парса db/schema.sql (v8.62): // 78 base tables, 137 индексов, 48 RLS-политик. + // v8.64 (учёт посетителей, spec 2026-07-13): +2 base таблицы (site_visitors, site_events) + // + 4 индекса (2 на гостей: first_seen/channel; 2 на события: visitor/event) + 0 RLS-политик + // (системные таблицы без tenant_id — доступ ролями, как supplier_order_checks). + // Итог статического парса db/schema.sql (v8.64): 80 base tables, 141 индекс, 48 RLS-политик. // NB: бегущий счётчик в ШАПКЕ schema.sql несёт исторический дрейф — отдельный canon-sync, // не предмет этого теста; тест сверяет фактический парс ФАЙЛА. $schemaPath = dirname(base_path()).DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql'; @@ -92,10 +96,10 @@ it('schema.sql v8.62 has correct metrics — 78 base tables, 137 indexes, 48 RLS $createTables = preg_match_all('/^CREATE TABLE\b/m', $schema); $partitionOf = preg_match_all('/CREATE TABLE\s+\w+\s+PARTITION OF\b/m', $schema); $baseTables = $createTables - $partitionOf; - expect($baseTables)->toBe(78); // v8.62: +4 autopodbor таблицы к v8.55 (74) + expect($baseTables)->toBe(80); // v8.64: +2 таблицы учёта посетителей к v8.62 (78) $createIndexes = preg_match_all('/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/m', $schema); - expect($createIndexes)->toBe(137); // v8.62: +9 autopodbor индексов к v8.55 (128) + expect($createIndexes)->toBe(141); // v8.64: +4 индекса учёта посетителей к v8.62 (137) $createPolicies = preg_match_all('/^CREATE\s+POLICY\b/m', $schema); expect($createPolicies)->toBe(48); // v8.62: +4 autopodbor RLS-политики к v8.52 (44) diff --git a/docs/superpowers/notes/2026-07-13-ssylki-s-metkami.md b/docs/superpowers/notes/2026-07-13-ssylki-s-metkami.md new file mode 100644 index 00000000..1ca4972b --- /dev/null +++ b/docs/superpowers/notes/2026-07-13-ssylki-s-metkami.md @@ -0,0 +1,31 @@ +# Ссылки с метками для рассылок и объявлений + +Дата: 13.07.2026. Относится к учёту посетителей (spec `2026-07-13-visitors-analytics-design.md`). + +Метка в ссылке — единственный способ точно отделить смс от прямых заходов: переход +из смски браузер отдаёт **без источника**, и без метки он неотличим от «набрал адрес руками». + +## Готовые ссылки + +| Куда | Ссылка | +|---|---| +| Смс-рассылка | `https://liderra.ru/?utm_source=sms&utm_medium=sms&utm_campaign=<дата>` | +| Вакансия на hh.ru | `https://liderra.ru/?utm_source=hh&utm_medium=vacancy` | +| Телеграм | `https://liderra.ru/?utm_source=tg` | +| Визитки / QR | `https://liderra.ru/?utm_source=qr` | + +`<дата>` в кампании — чтобы отличать рассылки друг от друга, например `rassylka-14-07`. + +## Как это выглядит в отчёте + +В админке → «Посетители» такие гости попадут в строки «Смс-рассылка», «Вакансия на hh.ru» +и т. д. Без метки они все свалятся в «Прямые заходы». + +## Оговорки + +- В смс ссылка длиннее на ~30 символов. Если это критично для стоимости — можно завести + короткий адрес-перенаправление (`liderra.ru/s`), который сам подставит метку. Это отдельная + небольшая задача, пока не сделана. +- Ссылку в вакансии на hh.ru соискатели чаще **копируют руками**, а не жмут (hh не делает + из текста кликабельные ссылки). Такой заход придёт без метки — как прямой. Поэтому в + вакансии лучше писать адрес с меткой прямо текстом: человек скопирует его целиком.