oszlop. FK csak a pricelist_file_lines-on van. */ public const REFERENCING_TABLES = [ 'products' => 'producer_id', 'order_archives_items' => 'producer_id', 'pricelist_file_lines' => 'producer_id', ]; private const UPDATE_CHUNK = 5000; /** * Duplikált névcsoportok, csoportonként a tagokkal és a hivatkozás-darabszámokkal. * A tagok rendezettek: az első a javasolt megtartandó (legtöbb termék). * * @return array}>> */ public function duplicateGroups(): array { $counts = $this->referenceCounts(); $byNormalized = []; foreach (DB::table('producers')->orderBy('id')->get() as $producer) { $byNormalized[NameNormalizer::normalize($producer->name)][] = $producer; } $groups = []; foreach ($byNormalized as $normalized => $members) { if (count($members) < 2) { continue; } $decorated = array_map(fn ($producer) => [ 'producer' => $producer, 'counts' => [ 'products' => $counts['products'][$producer->id] ?? 0, 'order_archives_items' => $counts['order_archives_items'][$producer->id] ?? 0, 'pricelist_file_lines' => $counts['pricelist_file_lines'][$producer->id] ?? 0, ], ], $members); // A megtartandó a legtöbb TERMÉKKEL rendelkező rekord: a termékek a napi // működés alapja. Döntetlennél a több rendelési tétel, majd a régebbi rekord. usort($decorated, function ($a, $b) { return [$b['counts']['products'], $b['counts']['order_archives_items'], $a['producer']->id] <=> [$a['counts']['products'], $a['counts']['order_archives_items'], $b['producer']->id]; }); $groups[$normalized] = $decorated; } ksort($groups); return $groups; } /** * @return array> tábla => [producer_id => darab] */ private function referenceCounts(): array { $counts = []; foreach (self::REFERENCING_TABLES as $table => $column) { $counts[$table] = DB::table($table) ->selectRaw("{$column} as pid, COUNT(*) as c") ->whereNotNull($column) ->groupBy($column) ->pluck('c', 'pid') ->toArray(); } return $counts; } /** * Összevonási terv a döntési lap sorai alapján. * * @param array $decisions * @return array{merges: array, errors: array} */ public function buildPlan(array $decisions): array { $groups = $this->duplicateGroups(); $byId = []; foreach ($groups as $normalized => $members) { foreach ($members as $member) { $byId[$member['producer']->id] = $normalized; } } $rowsByGroup = []; $finalNames = []; $errors = []; foreach ($decisions as $row) { $id = (int) ($row['id'] ?? 0); if (! isset($byId[$id])) { $errors[] = "A(z) {$id} azonosítójú gyártó nem szerepel duplikált csoportban " . '(időközben megváltozott az adat, vagy más környezetből származik a lap).'; continue; } $normalized = $byId[$id]; $rowsByGroup[$normalized][] = $id; $name = trim((string) ($row['final_name'] ?? '')); if ($name !== '') { $finalNames[$normalized] ??= $name; } } $merges = []; foreach ($rowsByGroup as $normalized => $ids) { $members = $groups[$normalized]; $memberIds = array_map(fn ($m) => $m['producer']->id, $members); if (! isset($finalNames[$normalized])) { $errors[] = 'Hiányzó végleges név ehhez a csoporthoz: ' . implode(' / ', array_map(fn ($m) => '"' . $m['producer']->name . '"', $members)); continue; } $missing = array_diff($memberIds, $ids); if ($missing !== []) { $errors[] = 'A döntési lap nem tartalmazza a csoport minden tagját (hiányzó azonosító: ' . implode(', ', $missing) . '). Generáld újra a lapot ebben a környezetben.'; continue; } $keeper = $members[0]['producer']; $losers = array_slice($members, 1); $merges[] = [ 'normalized' => $normalized, 'keeper_id' => $keeper->id, 'keeper_name' => $keeper->name, 'final_name' => $finalNames[$normalized], 'rename' => trim((string) $keeper->name) !== $finalNames[$normalized], 'from' => array_map(fn ($m) => [ 'id' => $m['producer']->id, 'name' => $m['producer']->name, 'counts' => $m['counts'], ], $losers), ]; } return ['merges' => $merges, 'errors' => $errors]; } /** * A terv végrehajtása. Előbb visszaállítási pontot ír, csak utána módosít. * * @return array a jelentés, ami egyben a visszagörgetés bemenete is */ public function apply(array $merges): array { $report = [ 'created_at' => now()->toDateTimeString(), 'database' => DB::connection()->getDatabaseName(), 'merges' => [], 'restore' => [], 'totals' => array_fill_keys(array_keys(self::REFERENCING_TABLES), 0), ]; // producer_id leképezés: honnan => hova $map = []; foreach ($merges as $merge) { foreach ($merge['from'] as $from) { $map[$from['id']] = $merge['keeper_id']; } } if ($map === []) { return $report; } // 1. Visszaállítási pont: soronként a régi érték. Enélkül a művelet nem lenne // visszafordítható, mert a fordított leképezés azokat a sorokat is átírná, // amelyek eredetileg is a megtartott rekordra mutattak. foreach (self::REFERENCING_TABLES as $table => $column) { $report['restore'][$table] = DB::table($table) ->whereIn($column, array_keys($map)) ->pluck($column, 'id') ->toArray(); } $report['restore']['producers'] = DB::table('producers') ->whereIn('id', array_merge(array_keys($map), array_column($merges, 'keeper_id'))) ->get(['id', 'name', 'status', 'canSee', 'note']) ->keyBy('id') ->toArray(); // 2. Hivatkozások átírása - táblánként EGY menetben, mert a producer_id-n a nagy // táblákon nincs index, tehát minden külön WHERE teljes scant jelentene. foreach (self::REFERENCING_TABLES as $table => $column) { $report['totals'][$table] = $this->remap($table, $column, $map); } // 3. A gyártó rekordok rendezése foreach ($merges as $merge) { if ($merge['rename']) { DB::table('producers')->where('id', $merge['keeper_id']) ->update(['name' => $merge['final_name'], 'updated_at' => now()]); } foreach ($merge['from'] as $from) { DB::table('producers')->where('id', $from['id'])->update([ 'status' => DbStatusFieldEnum::archive, 'canSee' => 0, // Tartós nyom az adatbázisban: a jelentésfájl elveszhet, ez nem. 'note' => trim(sprintf( 'Összevonva ide: #%d (%s) – %s producers:dedupe', $merge['keeper_id'], $merge['final_name'], now()->toDateString(), )), 'updated_at' => now(), ]); } $report['merges'][] = [ 'keeper_id' => $merge['keeper_id'], 'final_name' => $merge['final_name'], 'from' => array_map(fn ($f) => ['id' => $f['id'], 'name' => $f['name']], $merge['from']), ]; } return $report; } /** * Egyetlen UPDATE táblánként, CASE leképezéssel. */ private function remap(string $table, string $column, array $map): int { $ids = array_map('intval', array_keys($map)); $updated = 0; foreach (array_chunk($ids, 200) as $chunk) { $cases = ''; foreach ($chunk as $from) { $cases .= sprintf(' WHEN %d THEN %d', $from, (int) $map[$from]); } $updated += DB::update(sprintf( 'UPDATE `%s` SET `%s` = CASE `%s`%s END WHERE `%s` IN (%s)', $table, $column, $column, $cases, $column, implode(',', $chunk), )); } return $updated; } /** * Visszagörgetés a jelentésfájlból: minden érintett sor visszakapja a saját, * eredeti producer_id-ját, a gyártó rekordok pedig az eredeti nevüket/státuszukat. */ public function rollback(array $report): array { $restored = array_fill_keys(array_keys(self::REFERENCING_TABLES), 0); foreach (self::REFERENCING_TABLES as $table => $column) { $rows = (array) ($report['restore'][$table] ?? []); foreach (array_chunk($rows, self::UPDATE_CHUNK, true) as $chunk) { $byValue = []; foreach ($chunk as $rowId => $producerId) { $byValue[(int) $producerId][] = (int) $rowId; } foreach ($byValue as $producerId => $rowIds) { $restored[$table] += DB::table($table) ->whereIn('id', $rowIds) ->update([$column => $producerId]); } } } foreach ((array) ($report['restore']['producers'] ?? []) as $id => $producer) { $producer = (array) $producer; DB::table('producers')->where('id', (int) $id)->update([ 'name' => $producer['name'], 'status' => $producer['status'], 'canSee' => $producer['canSee'], 'note' => $producer['note'], 'updated_at' => now(), ]); } return $restored; } }