images: Lebenszyklus — Veröffentlichungsgate, published_ever_at, Snapshot der veröffentlichten Fassung, Sofortlöschung, Legacy-Bereinigung, Beweiszugriff, Korrekturweg (Bildkennzeichnung Block E)
- publish()/changeStatusFromAdmin(): kein Bild aus Formular v2 ohne gültige Bestätigung (ImageRightsConfirmationRequiredException, Admin-Seite meldet es); published_ever_at an PM und Bildern; PublishedImageSnapshot (Cover + JSON mit Nachweis/Kennzeichnung) - deleteFromAdmin() und press-releases:purge-drafts löschen Bilder nie veröffentlichter Meldungen samt Original und Bestätigung sofort (E4, O1) - press-releases:backfill-published-ever (Status-Log), press-release-images:purge-inactive-legacy (E-B11, Trockenlauf Standard) - Admin-Route für Beweisoriginal und veröffentlichte Fassung nur mit Zweck, ActivityType EvidenceAccess; Architekturtest: original_path nur in vier Dateien - ImageCorrectionService + image_declaration_corrections (insert-only), withheld_at blendet das Bild öffentlich aus (publiclyVisible-Scope), Korrekturpanel auf der Admin-Seite - Systemstatus: fehlgeschlagene Herkunftsprüfungen der letzten 7 Tage Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9f44546082
commit
da167b713b
24 changed files with 1202 additions and 12 deletions
62
app/Console/Commands/BackfillPublishedEver.php
Normal file
62
app/Console/Commands/BackfillPublishedEver.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseStatusLog;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Bildkennzeichnung E.1: `published_ever_at` für den Bestand — jede
|
||||
* Meldung, die heute veröffentlicht ist, ein `published_at` trägt oder
|
||||
* laut Status-Log jemals auf `published` stand, war öffentlich. Ihre Bilder
|
||||
* erben den Zeitpunkt. Idempotent, nur NULL-Zeilen werden gesetzt.
|
||||
*/
|
||||
class BackfillPublishedEver extends Command
|
||||
{
|
||||
protected $signature = 'press-releases:backfill-published-ever {--dry-run : Nur zählen}';
|
||||
|
||||
protected $description = 'Setzt published_ever_at für Meldungen und Bilder, die jemals öffentlich waren';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$candidates = PressRelease::withoutGlobalScopes()
|
||||
->whereNull('published_ever_at')
|
||||
->where(function ($query): void {
|
||||
$query->whereNotNull('published_at')
|
||||
->orWhere('status', PressReleaseStatus::Published->value)
|
||||
->orWhereIn('id', PressReleaseStatusLog::query()->where('to_status', PressReleaseStatus::Published->value)->select('press_release_id'));
|
||||
});
|
||||
|
||||
$count = $candidates->count();
|
||||
$this->info("Meldungen ohne published_ever_at, die öffentlich waren: {$count}");
|
||||
|
||||
if ($this->option('dry-run') || $count === 0) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$updated = 0;
|
||||
|
||||
$candidates->select(['id', 'published_at', 'created_at'])->chunkById(1000, function ($releases) use (&$updated): void {
|
||||
foreach ($releases as $release) {
|
||||
$firstLog = PressReleaseStatusLog::query()
|
||||
->where('press_release_id', $release->id)
|
||||
->where('to_status', PressReleaseStatus::Published->value)
|
||||
->min('created_at');
|
||||
|
||||
$at = collect([$release->published_at, $firstLog, $release->created_at])->filter()->min();
|
||||
|
||||
DB::table('press_releases')->where('id', $release->id)->update(['published_ever_at' => $at]);
|
||||
DB::table('press_release_images')->where('press_release_id', $release->id)->whereNull('published_ever_at')
|
||||
->update(['published_ever_at' => DB::raw('CASE WHEN created_at > '.DB::getPdo()->quote((string) $at).' THEN created_at ELSE '.DB::getPdo()->quote((string) $at).' END')]);
|
||||
$updated++;
|
||||
}
|
||||
});
|
||||
|
||||
$this->info("Gesetzt: {$updated} Meldungen samt Bildern.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
|||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressRelease;
|
||||
use App\Services\PressRelease\Images\PressReleaseImageRemover;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
|
|
@ -36,9 +37,23 @@ class PurgeExpiredPressReleaseDrafts extends Command
|
|||
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 = 0;
|
||||
|
||||
(clone $query)->whereNull('published_ever_at')->with('images')->chunkById(200, function ($drafts) use ($remover, &$imagesRemoved): void {
|
||||
foreach ($drafts as $draft) {
|
||||
foreach ($draft->images as $image) {
|
||||
$remover->remove($image);
|
||||
$imagesRemoved++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$query->update(['status' => PressReleaseStatus::Archived->value]);
|
||||
|
||||
$this->info("PM-Entwürfe archiviert: {$count} Einträge.");
|
||||
$this->info("PM-Entwürfe archiviert: {$count} Einträge, Bilder gelöscht: {$imagesRemoved}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Services\Image\ImageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Legacy-Bereinigung (Bildkennzeichnung E.5, Entscheidung E-B11): Bilder
|
||||
* aus dem Alt-Import an Meldungen, die nicht veröffentlicht sind (offline
|
||||
* genommen, archiviert, Entwurf), werden samt Dateien gelöscht. Sie haben
|
||||
* weder Nachweis noch Protokoll; eine erneute Einreichung läuft ohne Bild.
|
||||
* Auf der Dev-Kopie: 10.768 Bilder (Stand 22.09.2026).
|
||||
*/
|
||||
class PurgeInactiveLegacyPressReleaseImages extends Command
|
||||
{
|
||||
protected $signature = 'press-release-images:purge-inactive-legacy
|
||||
{--execute : Wirklich löschen (Standard ist ein Trockenlauf)}
|
||||
{--chunk=500 : Zeilen je Durchlauf}';
|
||||
|
||||
protected $description = 'Löscht Legacy-Bilder an nicht veröffentlichten Meldungen samt Dateien (E-B11)';
|
||||
|
||||
public function __construct(private readonly ImageService $imageService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$execute = (bool) $this->option('execute');
|
||||
$inactive = [PressReleaseStatus::Rejected->value, PressReleaseStatus::Archived->value, PressReleaseStatus::Draft->value];
|
||||
|
||||
$query = PressReleaseImage::query()
|
||||
->whereNotNull('press_release_images.legacy_id')
|
||||
->whereNull('press_release_images.rules_version')
|
||||
->whereHas('pressRelease', fn ($q) => $q->withoutGlobalScopes()->whereIn('press_releases.status', $inactive));
|
||||
|
||||
$byStatus = (clone $query)
|
||||
->join('press_releases', 'press_releases.id', '=', 'press_release_images.press_release_id')
|
||||
->selectRaw('press_releases.status as status, count(*) as c')
|
||||
->groupBy('press_releases.status')
|
||||
->pluck('c', 'status')
|
||||
->all();
|
||||
|
||||
$total = array_sum($byStatus);
|
||||
$this->info(($execute ? 'Lösche' : 'Trockenlauf:').' Legacy-Bilder an inaktiven Meldungen: '.$total);
|
||||
|
||||
foreach ($byStatus as $status => $count) {
|
||||
$this->line(sprintf(' %-10s %d', $status, $count));
|
||||
}
|
||||
|
||||
if (! $execute || $total === 0) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
$bytes = 0;
|
||||
|
||||
$query->with('pressRelease')->chunkById((int) $this->option('chunk'), function ($images) use (&$deleted, &$bytes): void {
|
||||
foreach ($images as $image) {
|
||||
$bytes += $this->sizeOf($image);
|
||||
$this->imageService->deletePressReleaseImage($image->disk, $image->path, $image->variants);
|
||||
$image->forceDelete();
|
||||
$deleted++;
|
||||
}
|
||||
});
|
||||
|
||||
$this->info(sprintf('Gelöscht: %d Bilder, etwa %s MB freigegeben.', $deleted, number_format($bytes / 1048576, 1, ',', '.')));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function sizeOf(PressReleaseImage $image): int
|
||||
{
|
||||
$disk = Storage::disk($image->disk);
|
||||
$size = 0;
|
||||
|
||||
foreach (array_filter([$image->path, ...array_values($image->variants ?? [])]) as $path) {
|
||||
if (is_string($path) && $disk->exists($path)) {
|
||||
$size += (int) $disk->size($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue