A megelőzés után a már felhalmozódott adat takarítása: 70 duplikált névcsoport, 146 rekord, ebből 76 beolvasztandó. Két parancs, mert a kivezetés három környezeten megy (local -> d2d -> éles), és minden környezetnek SAJÁT duplikátum-készlete van - a döntési lapot ezért ott kell újragenerálni, nem beégetett ID-listával dolgozunk. - producers:dedupe-report — döntési xlsx a megrendelőnek. A megtartandó a legtöbb TERMÉKKEL rendelkező rekord (döntetlennél több rendelési tétel, majd régebbi rekord); a végleges nevet a megrendelő hagyja jóvá, mert 48 csoport csak kis/nagybetűben tér el, és az írásmód üzleti döntés. - producers:dedupe — alapból csak kimutatás, --apply hajt végre. A lapot FEJLÉCNÉV alapján olvassa, nem oszlopbetű szerint, így a megrendelő beszúrhat oszlopot vagy átrendezheti a lapot anélkül, hogy eltörne. Hiányos csoportot és ismeretlen azonosítót visszautasít. - Visszafordíthatóság: a jelentés SORONKÉNT tárolja a régi producer_id-t, mert a fordított leképezés azokat a sorokat is átírná, amelyek eredetileg is a megtartott rekordra mutattak. --rollback ebből állít vissza. - Tartós nyom a jelentésfájltól függetlenül: a beolvasztott rekord archive + canSee=0 lesz, és a note-jába kerül, hova olvadt be. - A két nagy táblán nincs index a producer_id-n, ezért táblánként EGY UPDATE fut CASE leképezéssel - 76 külön WHERE 76 teljes scant jelentene. Mért eredmény a d2d másolaton: 27 mp alatt 314 termék + 27 272 archív + 115 árlista-sor átírva; a vizsgált árlistán a "Módosult" sorok 126 -> 23, a gyártó-diffek 114 -> 2. Visszagörgetés után minden szám visszaállt, és az újraszámolt diff ismét 126 - az ok-okozat mindkét irányban igazolt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
255 lines
8.7 KiB
PHP
255 lines
8.7 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\ProducerDeduplicator;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
|
|
class ProducersDedupe extends Command
|
|
{
|
|
protected $signature = 'producers:dedupe
|
|
{file? : a kitöltött döntési lap (producers:dedupe-report kimenete)}
|
|
{--apply : ténylegesen végrehajtja az összevonást (alapértelmezés: csak kimutatás)}
|
|
{--report= : hova írja a jelentést (ez a visszagörgetés bemenete)}
|
|
{--rollback= : egy korábbi jelentés visszagörgetése}
|
|
{--force : megerősítő kérdések nélkül fut (szkriptelt futtatáshoz)}';
|
|
|
|
protected $description = 'Duplikált gyártó rekordok összevonása a kitöltött döntési lap alapján';
|
|
|
|
public function handle(ProducerDeduplicator $deduplicator): int
|
|
{
|
|
if ($rollbackPath = $this->option('rollback')) {
|
|
return $this->rollback($deduplicator, $rollbackPath);
|
|
}
|
|
|
|
$file = $this->argument('file');
|
|
|
|
if (! $file || ! is_file($file)) {
|
|
$this->error('Add meg a kitöltött döntési lapot. Ha még nincs: php artisan producers:dedupe-report');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$decisions = $this->readDecisions($file);
|
|
|
|
if ($decisions === []) {
|
|
$this->error('A lapon nem találtam feldolgozható sort. Megvan az "'
|
|
. ProducersDedupeReport::COLUMN_ID . '" oszlop a fejlécben?');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
['merges' => $merges, 'errors' => $errors] = $deduplicator->buildPlan($decisions);
|
|
|
|
foreach ($errors as $error) {
|
|
$this->warn(' ! ' . $error);
|
|
}
|
|
|
|
if ($merges === []) {
|
|
$this->error('Nincs végrehajtható összevonás.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->summarize($merges);
|
|
|
|
if (! $this->option('apply')) {
|
|
$this->newLine();
|
|
$this->info('Ez csak kimutatás volt. A végrehajtáshoz: --apply');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
if ($errors !== [] && ! $this->confirmed('Voltak figyelmeztetések. Biztosan folytatod?')) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$database = DB::connection()->getDatabaseName();
|
|
if (! $this->confirmed("Végrehajtás a(z) '{$database}' adatbázison. Folytatod?")) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$report = $deduplicator->apply($merges);
|
|
$reportPath = $this->writeReport($report);
|
|
|
|
$this->newLine();
|
|
$this->info('Az összevonás megtörtént.');
|
|
foreach ($report['totals'] as $table => $count) {
|
|
$this->line(' ' . str_pad($table, 24) . number_format($count) . ' sor átírva');
|
|
}
|
|
$this->line(' Jelentés: ' . $reportPath);
|
|
$this->newLine();
|
|
$this->comment('Visszagörgetés: php artisan producers:dedupe --rollback="' . $reportPath . '"');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Megerősítés, --force esetén kérdés nélkül.
|
|
*/
|
|
private function confirmed(string $question): bool
|
|
{
|
|
return $this->option('force') || $this->confirm($question, false);
|
|
}
|
|
|
|
/**
|
|
* A lap fejlécnév alapján olvasódik, nem oszlopbetű szerint: a megrendelő
|
|
* beszúrhat oszlopot vagy átrendezheti a lapot anélkül, hogy ez eltörne.
|
|
*
|
|
* @return array<int, array{group: mixed, id: int, final_name: ?string}>
|
|
*/
|
|
private function readDecisions(string $file): array
|
|
{
|
|
$sheet = IOFactory::load($file)->getActiveSheet();
|
|
$highestRow = $sheet->getHighestDataRow();
|
|
$highestColumn = $sheet->getHighestDataColumn();
|
|
$columnCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
|
|
|
|
$headerRow = null;
|
|
$columns = [];
|
|
|
|
for ($row = 1; $row <= min($highestRow, 30); $row++) {
|
|
for ($column = 1; $column <= $columnCount; $column++) {
|
|
$value = trim((string) $sheet->getCellByColumnAndRow($column, $row)->getValue());
|
|
|
|
if ($value === ProducersDedupeReport::COLUMN_ID) {
|
|
$headerRow = $row;
|
|
}
|
|
}
|
|
|
|
if ($headerRow !== null) {
|
|
for ($column = 1; $column <= $columnCount; $column++) {
|
|
$value = trim((string) $sheet->getCellByColumnAndRow($column, $headerRow)->getValue());
|
|
if ($value !== '') {
|
|
$columns[$value] = $column;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($headerRow === null) {
|
|
return [];
|
|
}
|
|
|
|
$idColumn = $columns[ProducersDedupeReport::COLUMN_ID] ?? null;
|
|
$groupColumn = $columns[ProducersDedupeReport::COLUMN_GROUP] ?? null;
|
|
$nameColumn = $columns[ProducersDedupeReport::COLUMN_FINAL_NAME] ?? null;
|
|
|
|
if (! $idColumn || ! $nameColumn) {
|
|
return [];
|
|
}
|
|
|
|
$decisions = [];
|
|
|
|
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
|
|
$id = (int) $sheet->getCellByColumnAndRow($idColumn, $row)->getValue();
|
|
|
|
if ($id <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$decisions[] = [
|
|
'group' => $groupColumn ? $sheet->getCellByColumnAndRow($groupColumn, $row)->getValue() : null,
|
|
'id' => $id,
|
|
'final_name' => trim((string) $sheet->getCellByColumnAndRow($nameColumn, $row)->getValue()),
|
|
];
|
|
}
|
|
|
|
return $decisions;
|
|
}
|
|
|
|
private function summarize(array $merges): void
|
|
{
|
|
$rows = [];
|
|
$totals = ['products' => 0, 'order_archives_items' => 0, 'pricelist_file_lines' => 0];
|
|
|
|
foreach ($merges as $merge) {
|
|
foreach ($merge['from'] as $from) {
|
|
foreach ($totals as $key => $value) {
|
|
$totals[$key] += $from['counts'][$key];
|
|
}
|
|
|
|
$rows[] = [
|
|
'"' . $from['name'] . '"',
|
|
'→ #' . $merge['keeper_id'] . ' "' . $merge['final_name'] . '"'
|
|
. ($merge['rename'] ? ' (átnevezés)' : ''),
|
|
number_format($from['counts']['products']),
|
|
number_format($from['counts']['order_archives_items']),
|
|
];
|
|
}
|
|
}
|
|
|
|
$this->table(['Beolvasztandó', 'Célrekord', 'Termék', 'Rendelési tétel'], $rows);
|
|
|
|
$this->line('Csoport: ' . count($merges)
|
|
. ' | beolvasztandó rekord: ' . array_sum(array_map(fn ($m) => count($m['from']), $merges)));
|
|
$this->line('Átírandó sorok: products ' . number_format($totals['products'])
|
|
. ' · order_archives_items ' . number_format($totals['order_archives_items'])
|
|
. ' · pricelist_file_lines ' . number_format($totals['pricelist_file_lines']));
|
|
}
|
|
|
|
private function writeReport(array $report): string
|
|
{
|
|
$path = $this->option('report') ?: storage_path(
|
|
'app/private/producers-dedupe-' . now()->format('Y-m-d_His') . '.json'
|
|
);
|
|
|
|
$directory = dirname($path);
|
|
if (! is_dir($directory)) {
|
|
mkdir($directory, 0775, true);
|
|
}
|
|
|
|
file_put_contents($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function rollback(ProducerDeduplicator $deduplicator, string $path): int
|
|
{
|
|
if (! is_file($path)) {
|
|
$this->error('A jelentésfájl nem található: ' . $path);
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$report = json_decode((string) file_get_contents($path), true);
|
|
|
|
if (! is_array($report) || ! isset($report['restore'])) {
|
|
$this->error('A fájl nem érvényes producers:dedupe jelentés.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$database = DB::connection()->getDatabaseName();
|
|
|
|
if (($report['database'] ?? null) !== $database) {
|
|
$this->warn('A jelentés a(z) "' . ($report['database'] ?? '?')
|
|
. '" adatbázisról készült, most viszont "' . $database . '" az aktív.');
|
|
|
|
if (! $this->confirmed('Biztosan folytatod?')) {
|
|
return self::FAILURE;
|
|
}
|
|
}
|
|
|
|
$this->line('Visszagörgetés: ' . count($report['merges'] ?? []) . ' összevonás ('
|
|
. ($report['created_at'] ?? '?') . ')');
|
|
|
|
if (! $this->confirmed("Visszagörgetés a(z) '{$database}' adatbázison. Folytatod?")) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$restored = $deduplicator->rollback($report);
|
|
|
|
$this->info('Visszagörgetve.');
|
|
foreach ($restored as $table => $count) {
|
|
$this->line(' ' . str_pad($table, 24) . number_format($count) . ' sor visszaállítva');
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|