Bilder (N01, N05, N07, N08): gemeinsamer Freigabezustand PressReleaseImage::isPubliclyDeliverable() für Seiten, OG und Medienroute (Bestand bleibt sichtbar, v2 nur mit Bestätigung, Admin-Upload bis zum Kundenklick nicht öffentlich); eigene Ability confirmImageRights nur für den Kontoinhaber, ohne Super-Admin-Kurzschluss, Identität und ausstehende Bestätigung auch im Service; Status-Regel unter der PM-Sperre; Cache-Flush nach dem Commit außerhalb der Dateibereinigung; Snapshot nach Bestätigung an veröffentlichter PM; Bestätigungstexte an die gezeigte Kennung gebunden. Beweisarchiv (N02, N10, N11, N12, N13, 5.1): Tombstone unter Zeilensperre mit erneuter Hold-/Fristprüfung; Dateioperationen ausgewertet, Commands mit FAILURE; record() über .tmp + move; Marker archive_pending_at mit archive:retry-pending und archive:sweep-orphans; Anonymisierung entfernt Bilder über den Remover mit Antragsbezug; Draft-Purge räumt Medien nie veröffentlichter abgelehnter Einreichungen; App-Kopien fallen mit dem Archiv-Tombstone; label_corrected nur für veröffentlichte Bilder; archive_entries.legal_request_id; FileDeletion-Helfer; Systemstatus-Checks. KI und Hintergrund (N03, N04, N14, N20, N21): Ergebnis-UPDATE und Audit in einer Transaktion; erwarteter Hash bis in reject()/holdForManualReview(); Ablaufmail per beanspruchtem Marker in einer Transaktion; strikte Schemaprüfung der Vision-Antwort; Fortschrittsmarke ungecacht. Berichte und Canonical (N15–N19, 5.2): lückenlose Alterskohorten; Feed-Identität mit Portal und Sprache; Sitemap-Rolle in der Kundenansicht; Indexverteilung auf einer Grundgesamtheit; Kopien nach Wegfall der Erstquelle neu bewertet; Kategorie-Admin leert Seiten-Caches; Dry-Run zählt alle Tabellen. Gesamtsuite: 2.122 Tests grün, einer übersprungen; Larastan und Pint sauber. Zwei Migrationen (nullable Spalten, INSTANT). N09 offen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
88 lines
3.4 KiB
PHP
88 lines
3.4 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Enums\PressReleaseStatus;
|
||
use App\Models\PressRelease;
|
||
use App\Services\PressRelease\Images\PressReleaseImageRemover;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
|
||
/**
|
||
* Archiviert Entwürfe, die seit mehr als X Tagen nicht bearbeitet wurden,
|
||
* und bereinigt die Medien abgelehnter, nie veröffentlichter Einreichungen
|
||
* nach derselben Frist (Bildkennzeichnung O1, Umsetzungsaudit N13).
|
||
* Schützt vor "Zombie-Drafts" in der DB – optional, konfigurierbar.
|
||
*
|
||
* Abgelehnte Meldungen behalten ihren Status (Ablehnungsdatensatz bleibt
|
||
* als Vertragsdokumentation, DU 4.1); nur die Medien fallen. Altbestand
|
||
* mit `published_ever_at` (im Altsystem „offline genommen") bleibt
|
||
* unangetastet.
|
||
*/
|
||
class PurgeExpiredPressReleaseDrafts extends Command
|
||
{
|
||
protected $signature = 'press-releases:purge-drafts
|
||
{--days=180 : Entwürfe älter als X Tage archivieren}
|
||
{--dry-run : Nur zählen, nichts ändern}';
|
||
|
||
protected $description = 'Archiviert inaktive PM-Entwürfe und bereinigt Medien abgelehnter Einreichungen (älter als X Tage).';
|
||
|
||
public function handle(): int
|
||
{
|
||
$days = (int) $this->option('days');
|
||
$isDryRun = $this->option('dry-run');
|
||
$cutoff = now()->subDays($days);
|
||
|
||
$drafts = PressRelease::withoutGlobalScopes()
|
||
->where('status', PressReleaseStatus::Draft->value)
|
||
->where('updated_at', '<', $cutoff);
|
||
|
||
// Abgelehnt und nie veröffentlicht: nur Meldungen, an denen noch
|
||
// Bilder hängen — sonst wäre der Lauf jede Woche derselbe Zähler.
|
||
$rejected = PressRelease::withoutGlobalScopes()
|
||
->where('status', PressReleaseStatus::Rejected->value)
|
||
->whereNull('published_ever_at')
|
||
->where('updated_at', '<', $cutoff)
|
||
->whereHas('images');
|
||
|
||
$draftCount = $drafts->count();
|
||
$rejectedCount = $rejected->count();
|
||
|
||
if ($isDryRun) {
|
||
$this->warn("[DRY-RUN] {$draftCount} Entwürfe würden archiviert, {$rejectedCount} abgelehnte Einreichungen würden bereinigt. Kein tatsächlicher Vorgang.");
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
// Bilder nie veröffentlichter Entwürfe werden mit dem Purge sofort
|
||
// und vollständig gelöscht (Bildkennzeichnung E.3, Decision-Update E4).
|
||
$remover = app(PressReleaseImageRemover::class);
|
||
$imagesRemoved = $this->removeImages((clone $drafts)->whereNull('published_ever_at'), $remover)
|
||
+ $this->removeImages(clone $rejected, $remover);
|
||
|
||
$drafts->update(['status' => PressReleaseStatus::Archived->value]);
|
||
|
||
$this->info("PM-Entwürfe archiviert: {$draftCount} Einträge, abgelehnte Einreichungen bereinigt: {$rejectedCount}, Bilder gelöscht: {$imagesRemoved}.");
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* @param Builder<PressRelease> $query
|
||
*/
|
||
private function removeImages(Builder $query, PressReleaseImageRemover $remover): int
|
||
{
|
||
$removed = 0;
|
||
|
||
$query->with('images')->chunkById(200, function ($pressReleases) use ($remover, &$removed): void {
|
||
foreach ($pressReleases as $pressRelease) {
|
||
foreach ($pressRelease->images as $image) {
|
||
$remover->remove($image);
|
||
$removed++;
|
||
}
|
||
}
|
||
});
|
||
|
||
return $removed;
|
||
}
|
||
}
|