fix(visitors): партиции нарезает только штатный механизм + сторож схемы на 80 таблиц
Полный прогон вскрыл реальный баг: миграция создавала партицию site_events_2026_07 своими границами (в местном времени), а MonthlyPartitionManager — site_events_y2026_m07 в UTC. Партиции перекрывались (42P17) и роняли 19 ЧУЖИХ тестов. Теперь партиции нарезает только менеджер (ensureRange), как у всех остальных таблиц. Плюс сторож схемы обновлён под +2 таблицы учёта (80 таблиц, 141 индекс) и записаны готовые ссылки с метками для смс/hh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Services\MonthlyPartitionManager;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -60,13 +61,12 @@ return new class extends Migration
|
||||
$c->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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 не делает
|
||||
из текста кликабельные ссылки). Такой заход придёт без метки — как прямой. Поэтому в
|
||||
вакансии лучше писать адрес с меткой прямо текстом: человек скопирует его целиком.
|
||||
Reference in New Issue
Block a user