- A-01/A-02/A-04: archive_pending_jobs je Vorgang (ArchivePendingQueue), Depublikationsstand am Auftrag festgehalten, Nachholer ohne Schlüssel FAILURE; archive_pending_at nur noch Sammelmarker - O07: Direktweg und Nachholer beanspruchen den Auftrag unter Zeilensperre - O08: fehlende Snapshot-Datei → Verweis auf den image_removed-Eintrag oder offener Auftrag, nie ein still unvollständiger Nachweis - A-03/A-06: Sweep verschiebt in die Quarantäne, Pause beim Restore, archive:reregister; Restore-Abgleich löscht Anwendungskopien erneut - O04/O05: Restore ordnet über den Payload-Pfad zu; unsignierte Exporte nur mit --accept-unsigned, reregister bricht bei ungültigem/fehlendem Export ab - A-05, A-07 bis A-15: Altbestand als möglicherweise veröffentlicht, Hold- Historie, Zweck per POST, Nachweise gelöschter Bilder nur mit compliance:manage, Anhänge auf ihrer Disk, Schlüssel-ID und ARCHIVE_PREVIOUS_KEYS, HMAC per HKDF, archive:verify-export - 5.1 d: Depublikation im Meldeverfahren trägt legal_request_id PressReleaseService.php enthält zusätzlich K-01 (Prüfung im Publish-Lock), N19 (archive() gibt Kopien frei) und N09 (markImagesPublished nur für auslieferbare Bilder), weil die Datei ungeteilt committet ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
5.2 KiB
PHP
120 lines
5.2 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;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 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. Was
|
||
* möglicherweise veröffentlicht war, bleibt unangetastet — maßgeblich ist
|
||
* PressRelease::neverPublished(): `published_ever_at` und der Altbestand
|
||
* `rejected` (im Altsystem auch „offline genommen", Umsetzungsaudit
|
||
* 29.09.2026 A-05).
|
||
*
|
||
* Ein defektes Bild bricht den Lauf nicht ab (A-14): der Fehler wird
|
||
* gezählt, die Meldung behält ihren Status für den nächsten Lauf, und der
|
||
* Befehl endet mit Fehlerstatus.
|
||
*/
|
||
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)
|
||
->neverPublished()
|
||
->whereNull('legacy_id')
|
||
->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);
|
||
$failedPressReleaseIds = [];
|
||
$imagesRemoved = $this->removeImages((clone $drafts)->neverPublished(), $remover, $failedPressReleaseIds)
|
||
+ $this->removeImages(clone $rejected, $remover, $failedPressReleaseIds);
|
||
|
||
// Entwürfe mit gescheiterter Bildlöschung behalten ihren Status,
|
||
// sonst fielen sie aus dem nächsten Lauf heraus.
|
||
$archived = $drafts->when($failedPressReleaseIds !== [], fn (Builder $query) => $query->whereKeyNot($failedPressReleaseIds))
|
||
->update(['status' => PressReleaseStatus::Archived->value]);
|
||
|
||
$failed = count($failedPressReleaseIds);
|
||
$this->info("PM-Entwürfe archiviert: {$archived} Einträge, abgelehnte Einreichungen bereinigt: {$rejectedCount}, Bilder gelöscht: {$imagesRemoved}, Meldungen mit fehlgeschlagener Bildlöschung: {$failed}.");
|
||
|
||
if ($failed > 0) {
|
||
$this->error("Bildlöschung fehlgeschlagen: {$failed} Meldung(en) — Details im Log, sie werden beim nächsten Lauf wiederholt.");
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* @param Builder<PressRelease> $query
|
||
* @param list<int> $failedPressReleaseIds
|
||
*/
|
||
private function removeImages(Builder $query, PressReleaseImageRemover $remover, array &$failedPressReleaseIds): int
|
||
{
|
||
$removed = 0;
|
||
|
||
$query->with('images')->chunkById(200, function ($pressReleases) use ($remover, &$removed, &$failedPressReleaseIds): void {
|
||
foreach ($pressReleases as $pressRelease) {
|
||
foreach ($pressRelease->images as $image) {
|
||
try {
|
||
$remover->remove($image);
|
||
$removed++;
|
||
} catch (Throwable $exception) {
|
||
$failedPressReleaseIds[] = (int) $pressRelease->getKey();
|
||
Log::error('purge-drafts: Bild konnte nicht gelöscht werden, wird beim nächsten Lauf wiederholt.', [
|
||
'press_release_id' => $pressRelease->getKey(),
|
||
'press_release_image_id' => $image->getKey(),
|
||
'error' => $exception->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
$failedPressReleaseIds = array_values(array_unique($failedPressReleaseIds));
|
||
|
||
return $removed;
|
||
}
|
||
}
|