companies: Zusammenführung im Bestand, rückabwickelbar (Roadmap 1a.3)
companies:merge führt Firmen mit derselben Firmen-E-Mail zu einem Newsroom zusammen — nur über Adressen, die 1a.2 als Dublette oder Namensvariante klassifiziert hat. Es bleibt die Firma mit den meisten jemals veröffentlichten Meldungen; Meldungen, Kontakte, Nutzer- und Zahlungsverknüpfungen wandern per company_id, jede Bewegung steht in company_merge_moves. companies:unmerge spielt sie rückwärts ab. 77 % der Gruppen sind dieselbe Firma auf beiden Portalen (Altsystem: eine Firma je Portal). Der Überlebende wird dann `both`; die Meldungen behalten ihr Portal. Der alte Newsroom-Slug führt per 301 zum Überlebenden. Slug-Kollisionen auf dem anderen Portal werden übersprungen, nicht umbenannt. Company::slugExists() kennt jetzt `both`. Dev-DB: 14 142 Adressen, 16 194 Firmen, 13 297 Meldungen in 2 min; Rückabwicklung in 1:15 min exakt auf den Stand davor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
66d8398e97
commit
2aebb9d27e
17 changed files with 1508 additions and 7 deletions
|
|
@ -108,6 +108,8 @@ Der Kern. Wandelt vorhandenen Zulauf in Konten und Kunden, ohne neuen Traffic zu
|
|||
|
||||
Schlüssel ist die **Firmen-E-Mail**, nicht die Pressekontaktadresse. Der Pressekontakt ist der Firma untergeordnet und wechselt; die Firmenidentität darf nicht an einer Person hängen. Der Magic Link kann trotzdem an beide gehen, das ist eine Berechtigungs- und keine Identitätsfrage.
|
||||
|
||||
**Entschieden 15.09.2026 (1a.3):** Es bleibt die Firma mit den meisten jemals veröffentlichten Meldungen, bei Gleichstand die ältere; leere Stammdaten werden aufgefüllt. Meldungen, Kontakte und Verknüpfungen wandern per `company_id`, jede Bewegung protokolliert (`company_merges`/`company_merge_moves`), rückabwickelbar mit `companies:unmerge`. Die wegfallende Firma wird soft-deleted, ihr Slug führt per 301 weiter. Der Überlebende wird `both`, wenn die Hälften auf verschiedenen Portalen lagen — das ist bei 77 % der Gruppen der Fall (Altsystem: eine Firma je Portal). Slug-Kollisionen werden übersprungen, nicht umbenannt. Details in `umsetzung-protokoll.md` Teil 3.
|
||||
|
||||
Für selbst angelegte Firmendaten trägt der Nutzer die Verantwortung. Für die rückwirkende Zusammenführung im Altbestand trägt sie das Portal, weil dort nicht angelegt, sondern verschmolzen wird. Deshalb 1a.1 vor 1a.3.
|
||||
|
||||
### 1b Newsroom als Produkt
|
||||
|
|
|
|||
|
|
@ -701,3 +701,104 @@ Statuslogs des neuen Systems).
|
|||
**Deploy:** Nach dem Deploy `companies:classify-addresses` erneut laufen
|
||||
lassen — die 16 Adressen werden dabei umklassifiziert, Handentscheidungen
|
||||
bleiben.
|
||||
|
||||
## 1a.3 — Zusammenführung im Bestand
|
||||
|
||||
```
|
||||
php artisan companies:merge [--dry-run] [--limit=] [--email=] [--json]
|
||||
php artisan companies:unmerge --merge=ID | --batch=UUID | --email= [--dry-run]
|
||||
```
|
||||
|
||||
### Der Fakt, der den Entwurf bestimmt hat
|
||||
|
||||
Die meisten „Dubletten" sind **dieselbe Firma auf zwei Portalen**: Das
|
||||
Altsystem hat jede Firma je Portal einmal angelegt.
|
||||
|
||||
| | Gruppen |
|
||||
|---|---:|
|
||||
| Adressgruppen gesamt (Dev-DB) | 18 860 |
|
||||
| davon mit Firmen auf beiden Portalen | **14 519** (77 %) |
|
||||
| Zweiergruppen | 15 754 |
|
||||
| davon exakt PE + B24 | 12 605 |
|
||||
| davon zusätzlich mit identischem Slug | 12 037 |
|
||||
|
||||
`Portal::Both` existiert im Code — Newsroom-Controller, PortalScope und
|
||||
Slug-Auflösung behandeln es —, aber **keine Firma** trug es. Die
|
||||
Zusammenführung ist also weniger „Dubletten löschen" als „zwei Portalhälften
|
||||
zu einer Firma machen": Der Überlebende bekommt `both`, die Meldungen
|
||||
behalten ihr Portal, der Newsroom zeigt auf jedem Portal die Meldungen dieses
|
||||
Portals.
|
||||
|
||||
### Entscheidungen (15.09.2026, A/A/A + both)
|
||||
|
||||
1. **Es bleibt** die Firma mit den meisten jemals veröffentlichten Meldungen
|
||||
(`published` oder `hits > 0`), bei Gleichstand die ältere. Leere
|
||||
Stammdatenfelder (Website, Telefon, Fax, Adresse, Land, Logo, Boilerplate,
|
||||
Inhaber) füllt sie aus den wegfallenden Firmen; belegte bleiben. Ist der
|
||||
Überlebende inaktiv und eine wegfallende Firma aktiv, wird er aktiv.
|
||||
2. **Meldungen, Kontakte, KI-Erklärungen, Konversionsereignisse** wandern per
|
||||
`company_id`. `company_user` und Zahlungsoptionen werden zur Vereinigung;
|
||||
ein Nutzer, der schon am Überlebenden stand, wird nicht doppelt — sein
|
||||
alter Eintrag fällt weg und ist als `dropped` mit Rolle protokolliert.
|
||||
Jede Bewegung steht in `company_merge_moves`; der Vorgang in
|
||||
`company_merges` (Batch-ID, Überlebender, sein Portal vorher).
|
||||
3. **Die wegfallende Firma** wird soft-deleted. Ihr alter Slug führt per
|
||||
**301** zum Überlebenden (`NewsroomDetailPageData::mergedInto()`), sofern
|
||||
der Slug abweicht — bei gleichem Slug findet die Auflösung den `both`-
|
||||
Überlebenden direkt. Kein 410: Die Firma gibt es noch, nur woanders.
|
||||
|
||||
**Slug-Kollision:** Wird der Überlebende `both`, muss sein Slug auf dem
|
||||
anderen Portal frei sein (die öffentliche Auflösung fragt
|
||||
`portal IN (eigenes, both)`). Ist der Erstkandidat blockiert, kommt der
|
||||
nächste dran; ist keiner frei, wird die Adresse **übersprungen** und
|
||||
aufgelistet. Nicht umbenannt — die URL ist öffentlich. Vorab geschätzt 664
|
||||
Gruppen, tatsächlich **5** — die Kollisionen sind fast immer einseitig
|
||||
(Alt-Slugs sind je Portal durchnummeriert), und die andere Hälfte ist frei.
|
||||
|
||||
**410-entfernte Firmen** bleiben außen vor: Ihre Meldungen in den
|
||||
Überlebenden zu heben, würde bewusst entfernte Inhalte wieder sichtbar
|
||||
machen.
|
||||
|
||||
**Keine Schema-Änderung an bestehenden Tabellen.** Kein
|
||||
`merged_from_company_id` auf `press_releases` (1 GB, einmaliger Vorgang),
|
||||
kein `merged_into_company_id` auf `companies` (FULLTEXT) — beides kommt aus
|
||||
den zwei neuen Tabellen. `press_release_search` enthält keine Firmendaten;
|
||||
das Umhängen per Query-Builder braucht keinen Reindex.
|
||||
|
||||
### Nebenbefund und Fix: Slug-Eindeutigkeit kannte `both` nicht
|
||||
|
||||
`HasUniqueSlug` prüft bei Firmen nur `portal = X`. Eine neue PE-Firma hätte
|
||||
denselben Slug wie eine `both`-Firma bekommen können — und die Newsroom-URL
|
||||
hätte zwei Firmen gefunden. `Company::slugExists()` prüft jetzt
|
||||
`portal IN (X, both)`, und eine `both`-Firma braucht den Slug überall frei.
|
||||
Test in `HasUniqueSlugTest`.
|
||||
|
||||
### Lauf auf der Dev-DB (Kopie des Bestands)
|
||||
|
||||
| | |
|
||||
|---|---:|
|
||||
| Adressen zusammengeführt | 14 142 |
|
||||
| Firmen im Überlebenden aufgegangen | 16 194 |
|
||||
| Meldungen gewandert | 13 297 |
|
||||
| Firmen jetzt `both` | 12 926 |
|
||||
| Kontakte gewandert | 8 038 |
|
||||
| Nutzer-Verknüpfungen weggefallen (schon am Überlebenden) | 15 467 |
|
||||
| Stammdatenfelder aufgefüllt | 1 819 |
|
||||
| übersprungen (Slug-Kollision) | 5 |
|
||||
| Laufzeit merge / unmerge | 2:00 min / 1:15 min |
|
||||
|
||||
Nach `companies:unmerge --batch=…` stimmten Portalverteilung (42 287 / 66 892),
|
||||
`company_user` (107 663) und Soft-Deletes (2 155) wieder exakt mit dem
|
||||
Stand davor überein. Die Rückabwicklung ist verifiziert.
|
||||
|
||||
Zwei Performance-Punkte beim Bau: Die Kollisionsprüfung mit `portal != ?`
|
||||
kostete 24 ms je Adresse (kein Index) — mit `portal IN (…)` greift der
|
||||
Unique-Index (portal, slug), 0 ms. Und wegfallende Firmen werden per
|
||||
Query-Builder soft-deleted, nicht per `$company->delete()`: Der Observer
|
||||
würde je Firma den Admin-Cache leeren; der Lauf leert einmal am Ende.
|
||||
|
||||
**Deploy:** `migrate --force`, dann `companies:merge --dry-run`, dann
|
||||
`companies:merge --limit=50` und die 50 im Admin ansehen, dann ohne Limit.
|
||||
Erledigte Adressen zählen beim zweiten Lauf nicht gegen das Limit. Danach
|
||||
`companies:classify-addresses` — die zusammengeführten Adressen fallen aus
|
||||
`company_email_groups`, weil nur noch eine Firma darunter steht.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue