companies: Befehl zum Deaktivieren der Legacy-Linkspam-Registrierungen

4.391 aktive Firmen (2010-2022) tragen bbcode-/HTML-Linkspam in den
Stammdaten und haben nie eine Meldung veroeffentlicht. Sie werden
deaktiviert, nicht geloescht; IDs landen in storage/app, Besitzerkonten
bleiben unangetastet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-17 09:56:44 +00:00
parent 8ea5e52c84
commit 8cd76df0bb
3 changed files with 161 additions and 0 deletions

View file

@ -0,0 +1,106 @@
<?php
namespace App\Console\Commands;
use App\Enums\ActivityType;
use App\Models\Company;
use App\Scopes\PortalScope;
use App\Services\ActivityLog;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
/**
* Legacy-Registrierungen mit bbcode-/HTML-Linkspam in den Stammdaten
* (Befund 17.09.2026: 4.391 aktive Firmen, Moncler/Chanel-Outlet-Muster,
* 2010–2022, nie eine veröffentlichte Meldung) werden deaktiviert — nicht
* gelöscht. Sie haben keine Newsroom-Seite, verzerren aber Firmensuche und
* Statistik. Die Besitzerkonten bleiben unangetastet: 24 von 44 führen auch
* saubere Firmen.
*
* Reversibel: Die IDs landen in einer Datei unter storage/app, und
* `is_active` lässt sich zurücksetzen.
*/
class RetireLinkSpamCompanies extends Command
{
private const MARKERS = ['[url=', '<a href'];
protected $signature = 'companies:retire-link-spam
{--dry-run : Nur zählen und Beispiele zeigen, nichts schreiben}';
protected $description = 'Deaktiviert Firmen mit bbcode-/HTML-Linkspam in den Stammdaten, die nie eine Meldung veröffentlicht haben.';
public function handle(ActivityLog $activityLog): int
{
$query = $this->candidates();
$count = (clone $query)->count();
if ($count === 0) {
$this->info('Keine Linkspam-Firmen gefunden.');
return self::SUCCESS;
}
$this->table(
['ID', 'Name', 'Angelegt', 'Adresse (Anfang)'],
(clone $query)->limit(10)->get()->map(fn (Company $company): array => [
$company->id,
mb_strimwidth((string) $company->name, 0, 50, '…'),
$company->created_at?->format('d.m.Y'),
mb_strimwidth(str_replace(["\r", "\n"], ' ', (string) $company->address), 0, 60, '…'),
])->all(),
);
if ($this->option('dry-run')) {
$this->info(sprintf('[Dry-Run] %d Firmen würden deaktiviert (10 Beispiele oben).', $count));
return self::SUCCESS;
}
$ids = (clone $query)->pluck('id');
$file = sprintf('companies-link-spam-%s.txt', now()->format('Y-m-d-His'));
Storage::disk('local')->put($file, $ids->implode("\n"));
$retired = 0;
foreach ($ids->chunk(500) as $chunk) {
$retired += Company::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('id', $chunk->all())
->update(['is_active' => false]);
}
$activityLog->record(
ActivityType::Correction,
sprintf('%d Legacy-Firmen mit Linkspam in den Stammdaten deaktiviert (IDs in storage/app/%s).', $retired, $file),
null,
null,
['count' => $retired, 'file' => $file],
'system',
);
$this->info(sprintf('%d Firmen deaktiviert. IDs: storage/app/%s', $retired, $file));
return self::SUCCESS;
}
/**
* @return Builder<Company>
*/
private function candidates(): Builder
{
return Company::query()
->withoutGlobalScope(PortalScope::class)
->where('is_active', true)
->whereNull('gone_at')
->where(function (Builder $query): void {
foreach (['address', 'boilerplate', 'name'] as $column) {
foreach (self::MARKERS as $marker) {
$query->orWhere($column, 'like', '%'.$marker.'%');
}
}
})
->whereDoesntHave('pressReleases', fn (Builder $q) => $q->withoutGlobalScope(PortalScope::class)->published())
->orderBy('id');
}
}