60ab5be3eb
Закрывает последнюю дыру #2 аудита журналирования. Phase A (dev) — миграция схемы + retention tooling. Phase B (прод-rewrite через SQL под postgres) — отдельным шагом с явным approve. Решения заказчика: * Scope: все 7 таблиц (auth_log, activity_log, tenant_operations_log, webhook_log, balance_transactions, pd_processing_log, saas_admin_audit_log) * FK на webhook_log: W1 — удалить FK от failed_webhook_jobs+rejected_deals_log * Retention defaults: auth:24м, activity:36м, tenant_ops:24м, webhook:3м, balance:84м, pd:36м, saas_admin:84м. Cron Sundays 03:00 МСК * Hash-chain: per-partition (audit_chain_hash трг через TG_TABLE_NAME уже работает per-partition; совместимо с hole #1 per-RLS-scope fix) Phase A: * db/schema.sql v8.30→v8.31: 7 audit-таблиц на PARTITION BY RANGE, PK→(id, partition_key), +7 retention seeds в system_settings, FK от failed_webhook_jobs/rejected_deals_log удалены * MonthlyPartitionManager: PARTITIONED_TABLES → ассоциативный array (name => partition_key), 2 → 9 таблиц * PartitionsCreateMonths: автоматически покрывает все 9 * load_initial_schema: после schema.sql вызывает Artisan partitions:create-months --ahead=2 (без этого первый INSERT падает) * 2026_05_22_000001_tenant_operations_log: idempotency guard * VerifyAuditChains: per-partition scan через pg_inherits; fallback на single-scope для не-партиционированной таблицы; per-RLS-scope partition_clause сохранён внутри каждой партиции * AuditChainBreachMail: +partitionName param (NULL=fallback на tableName) * PartitionsDropExpired (новая): cron Sundays 03:00 МСК, retention из system_settings, dry-run mode, safety guard retention=0 * SchedulerHeartbeatTracker +partitions:drop-expired (10080 мин) Без Laravel-миграции для прода — она оставляла БД пустой при migrate:fresh. Подход: schema.sql декларирует партиционированные + ad-hoc SQL под postgres для прод-rewrite (отдельный commit + ручной деплой + pg_dump backup). Тесты: 1219/1231 (35/35 hole #2 specs, 88 assertions). 3 fail — pre-existing AdminPdSubjectRequestsControllerTest::executeErasure_* (FK actor_admin_user_id после partitioning pd_processing_log, отдельная задача для hole #4 follow-up, не блокирует). cspell +2 слова (партиционировать, дёшева). Pint --fix чистый. Spec: docs/superpowers/specs/2026-05-23-hole-2-audit-partitioning-design.md Plan: docs/superpowers/plans/2026-05-23-hole-2-audit-partitioning-plan.md
65 lines
2.6 KiB
PHP
65 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\MonthlyPartitionManager;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* Создаёт ежемесячные партиции для всех таблиц в MonthlyPartitionManager::PARTITIONED_TABLES
|
|
* на N месяцев вперёд от текущей даты.
|
|
*
|
|
* Hole #2 (23.05.2026): расширен с 2 бизнес-таблиц до 9 (+ 7 audit-таблиц).
|
|
*
|
|
* Замена `pg_partman` на native Windows-стеке (расширение недоступно
|
|
* без сборки из исходников). Запускается ежесуточно через Windows Task
|
|
* Scheduler / cron — идемпотентна (проверяет pg_class перед CREATE).
|
|
*
|
|
* По дефолту 2 месяца вперёд (паритет с инициализацией schema.sql:
|
|
* 6 партиций при `migrate:fresh`, последующие месяцы — этим cron'ом).
|
|
*
|
|
* Источник: MonthlyPartitionManager::PARTITIONED_TABLES (единственный SoT списка таблиц);
|
|
* project_phase1_strategy.md (pg_partman заменён ручным cron'ом).
|
|
*/
|
|
class PartitionsCreateMonths extends Command
|
|
{
|
|
/** @var string */
|
|
protected $signature = 'partitions:create-months {--ahead=2 : Сколько месяцев вперёд создать партиций}';
|
|
|
|
/** @var string */
|
|
protected $description = 'Создаёт ежемесячные партиции для всех партиционированных таблиц на N месяцев вперёд (idempotent)';
|
|
|
|
public function handle(MonthlyPartitionManager $manager): int
|
|
{
|
|
$ahead = max(1, (int) $this->option('ahead'));
|
|
$now = Carbon::now()->startOfMonth();
|
|
|
|
$created = 0;
|
|
$skipped = 0;
|
|
|
|
for ($i = 0; $i <= $ahead; $i++) {
|
|
$monthStart = $now->copy()->addMonths($i);
|
|
|
|
foreach (array_keys(MonthlyPartitionManager::PARTITIONED_TABLES) as $table) {
|
|
$partitionName = $manager->partitionName($table, $monthStart);
|
|
|
|
if ($manager->ensureMonth($table, $monthStart)) {
|
|
$created++;
|
|
$this->info(" create <fg=green>{$partitionName}</>");
|
|
} else {
|
|
$skipped++;
|
|
$this->line(" skip <fg=gray>{$partitionName}</> (already exists)");
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("Done: {$created} created, {$skipped} skipped (ahead={$ahead}).");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|